关于定价的 A/B 测试协议


首页 » 资源 » 此处

优化离不开度量,定价优化既需要进行多次试验,也需要进行多次度量。但在实际上,定价往往只是关乎价格变化而已,而没有将这些变化加以利用从而对市场响应新价格的方式提供更深入的了解。制定定价策略应是一个受知识驱动的过程,大部分变化会进行事后分析,从而精细化调整现有战略。在本页面,我们将概述由 Lokad 提供支持的待执行定价的 A/B 测试协议。虽然在讨论过程中我们会运用手边的工具,但必须要提的是,协议本身并非特定于 Lokad,同样可以用于其他工具,甚至还可以用于 Excel(前提是谨慎维护电子表格)。


1. 创建参照文档

如果你能进行一次定价试验,那么很快便会执行几十次定价试验。如果不采用一种结构化的方式来收集所有这些信息,结果会一团糟,大部分潜在的洞见也将无法获取。所以,我们建议您通过创建参照文档来开始每次定价试验,该文档将收集与定价试验相关的所有信息。尽管它也可以是 Microsoft Word 文件,但在当下,我们强烈建议您使用 Web 原生协作工具,例如 Google Sites 或 hackpad,或者采用可以联机使用的任意一种内容管理系统。

创建文档后,应当花点心思为试验找一个“容易记住的名称”。实际上,只有人们能够轻松方便地就此进行沟通,并且定价团队及其后继者(因为会不断有人员流动)能够记住结论,那么试验才是有效的。如果名称太枯燥难以让人记住,那么即便是最专业的员工也会摒弃这样的信息。

2. 创建专用项目

在您的 Lokad 账户中,建议您创建一个专门用于定价试验的项目。为了清楚起见,这个项目应根据为试验选择的容易记住的名称来命名。此外,我们还建议您运用 label 磁贴的超链接行为来直接链接可以联机使用的参照文档。因此,Envision 脚本的开头通常类似于:

// Pricing Experiment: Sevilla
// Start: 2014-07-09 End: 2014-08-08
// Author: Joannes Vermorel
show label "http://example.org/my-reference-document Ref. Document"

借助这段脚本,label 磁贴将作为一个超链接显示在仪表板中,该仪表板指向详细介绍定价试验的参照文档的位置。

在定价试验的不同阶段,这个项目将用于多种用途。例如它将用于:

  1. 创建项目的样本控制组
  2. 存留样本控制组
  3. 定义修订后的定价策略
  4. 检验价格的正确部署
  5. 编译两个控制组的可视化

所有步骤将通过该项目的脚本片段实现。

3. 定义测试假说

不论测试结果是否符合预期,任何时候测试定价策略,事后重新解释结果都非常具有诱惑力。这种心理偏误属于叙述谬误,对此Taleb 进行了全面阐述:

叙述谬误指的是我们无法在不编造理由或者强加一种逻辑关系的情况下观察一系列事实。对事实的解释会与事实混在一起,使事实变得更容易被记住,更符合道理。这种倾向的坏处在于它使我们以为对事物有了更好的理解。—Nassim Nicholas Taleb,《黑天鹅》


在定价方面,这个问题显得尤其敏锐,因为市场行情不存在可控环境。不论对试验协议有多关注,许多因素仍不可避免地在我们的控制之外,首先是竞争对手的计划。

因此,试验伊始便定义好测试假说,以确保是针对这一特定假说,而不是针对在试验过程中进行了某些特定修订的假说,来证实其成立或证明其无效的。在先前提到的参照文档的开头应写明这一假说。好的假说应是这样的:造成销售量锐减的原因并不在于需求降低,而在于不太透明的竞争对手在打价格战。

4. 正向和负向控制组

注明假说后,便要设计试验。通常同一个项目不能对不同的客户群显示两种不同的价格,或者说此举不切实际 – 有时甚至非法。因此在零售业中,更实用的方法是选择两个相当的项目组来代表总目录中的一小部分。

这两个样本将分别命名为:
  • 正向控制组,该组将应用新的定价策略。
  • 负向控制组,该组保留原有的定价策略。

通过对比正向控制组与负向控制组的结果,可以评估原始假说是否成立。由于使用了哈希函数,因此通过 Envision 可以非常直接地随机挑选两个控制组:

seed := "hello world"
R = rankd(hash(concat(Id, seed)))
where R <= 1000
  // positive group here
where R > 1000 & R <= 2000
  // negative group here
在以上脚本中的第 2 行,计算了所有随机排序的项目。作为输入传递的字符串通过函数哈希生成了伪随机数。这并非真正的随机,因为相同的字符串每次生成相同的哈希值。所以,为了生成不同的样本,应更改作为种子传递的文本。再来看第 3 行,我们选择了 1000 个项目作为正向控制组。在第 5 行,则选择了 1000 个项目作为负向控制组。

对于要定义两个都符合特定条件的组的情形,可以非常容易地调整这种逻辑。例如,假定要在属于品牌 Fabrikam 的项目集合中建立控制组,可以这样实现:
where Brand == "Fabrikam" // scoping only the brand 'Fabrikam'
  seed := "hello world"
  R = rankd(hash(concat(Id, seed)))
  where R <= 50
    // positive group here
  where R > 50 & R <= 100
    // negative group here

5. 保存控制组

如果选择控制组时使用复杂条件,可能会存在相同采样逻辑随着时间的变化不返回相同项目列表的风险。举个例子,如果引入了新项目,新项目影响采样逻辑的方式将与上一节中所述的相同。因此,建议您将控制组保存在独立的文件中,这样就不会随着时间的变化发生变更。

seed := "hello world"
R = rankd(hash(concat(Id, seed)))
Control = ""
where R <= 1000
  Control = "pos" // positive group
where R > 1000 & R <= 2000
  Control = "neg" // negative group

// exporting the group
startDate := "2014-07-09"
endDate := "2014-08-09"
where Control != ""
show table "sample" export:"/exp/g-sevilla.tsv" with 
Id
startDate as "Date"
endDate, Control
Price

在上面的脚本中,建立控制组后,结果保存在 `g-sevilla.tsv` 文件中。在下一节我们将介绍使用前缀 `g`(或任何替代性的前缀)可以方便地避免样本重叠。`sevilla` 是一个容易记住的试验名称示例。该文件已格式化为事件流文件,以后在 Lokad 中可以重新加载该文件。您应当运行一次这个逻辑,然后为文件导出行添加注释,以避免重写现有文件。作为一项安全措施,您可以下载文件并将文件副本附加到参照文档中。

在后续,为了避免定义与其他正在进行的试验相重叠的组,可以重新加载已保存的文件,并使用其中的信息来排除相关项目。这个过程可以这样实现:
read "/exp/g-*" as Experiments // loading all experiments so far

today := Date(2014,7,9)
IsPartOfGroup = false
where Experiments.EndDate < today // we exclude all ongoing experiments
  // look for the existence of a matching group
  IsPartOfGroup = exists(Experiments.Date) 

where not IsPartOfGroup
  seed := "hello world"
  R = rankd(hash(concat(Id, seed)))
  Control = ""
  where R <= 1000
    // positive group here
  where R > 1000 & R <= 2000
    // negative group here

6. 定义新的定价脚本

在上面的章节中,我们介绍了多种定价策略。在定价协议的这个部分,我们将实现修订过的定价脚本。本节不会介绍有关定价逻辑的详细信息,但您应当了解一点:价格通常使用简单的 table 磁贴导出:
read "/exp/g-sevilla.tsv" as Scope

// Original logic to generate the control groups is commented out.
// We reload the control groups directly from the persisted copy.
Control = last(Scope.Control) or ""

where Control != "" // excluding items not part of the scope
  // snipped here: actual pricing logic
  show table "sample" export:"/exp/p-sevilla.tsv" with Id, today() as Date, Price
在这段脚本中,先是加载存留的控制组副本。然后重新定义范围,将范围局限于这些控制组。最后是导出新价格文件。

在实践中,存留修订价格可能没有存留控制组本身这么简单。实际上,在试验期间,价格会根据相应的基础策略不断变化。

7. 公布和观察价格

一旦生成修订价格的脚本准备就绪,便可以通过各种销售渠道公布这些价格。您应当尽量重视相对自动化的价格公布流程。Lokad 提供了 REST API,它支持自动触发项目。脚本一执行完,FTP 或 FTPS 便会从 Lokad 检索输出文件来导入生产系统。

为了把握修订价格后的销售观察,务必确保销售渠道中公布的价格实际上就是 Envision 脚本生成的价格。我们会定期观察是否发生了故障或只导入部分价格,而导致实际公布的价格与定价策略计算出的价格存在偏差。

理论上,所公布的历史价格应作为输入反馈给 Lokad。在存在此类数据循环时,可以比较公布的价格与 Lokad 中计算出的价格,以确保这些值相一致。在下面的脚本中,我们加载了从生产中检索的历史价格,然后加载了试验范围,最后加载了最初计算得出的价格。

//  ... snipped items and other data files ...
read "/prices.tsv" as Prices // from the production systems
read "/exp/g-sevilla.tsv" as Scope
read "/exp/p-sevilla.tsv" as ExpPrices

// The original logic to generate control groups is commented out.
// We reload the control groups directly from the persisted copy.
Control = last(Scope.Control) or ""

where Control != "" // excluding items not part of the scope
  // relevant date for the publish date of the prices
  when date <= date(2014,8,1) 
    where last(Prices.Price) != last(ExpPrices.Price)
      show table "Item count with price mismatch" with count(Id) 
这段脚本输出了从生产系统中检索的价格与定价脚本最初计算出的价格之间不匹配的价格数。

最后,在几天或几周后(根据定价试验适用的时间框架),可以比较收集到的这两个组的销售进展信息,以便评估初始假说是否成立。累积决定性的试验是一项宝贵资产,商户可以使用这项资产来随着时间的变化设计更有效的定价策略。