通过 Envision 执行计算

通过 Envision 执行计算


首页 » 资源 » 此处

与使用 Excel 执行计算一样,通过 Envision 几乎可以执行任何计算。就这个方面而言,进行此类计算所使用的语法与Excel 公式所使用的语法很相似。但 Envision 重视矢量计算。基于矢量的运算可一次处理多个值,而不是一次只处理一个值。本页将详细介绍表和矢量,以帮助您自行使用 Envision 执行计算。

为了更加有效地理解本页内容,建议您设置样本数据集。虽然尚未详细介绍 Envision 在加载输入数据方面的功能,但我们的示例将为您展示需要放在脚本最上方的行。

表和矢量

Envision 的数据模式围绕表和矢量。Envision 表实际上类似于关系数据库中的表。就 Excel 而言,所谓表,就是形状规整的电子表格,其中第一行包含列标题,下面的(许多)行包含与标题相一致的数据。在 Envision 脚本中,表还会命名,表名通常取决于基础表格文件的名称。矢量与表中的列相关,同样矢量也会命名。Envision 之所以使用“矢量”而不是“列”,是为了突显可以同时对所有矢量值执行运算,也即可对原始表中的所有行执行运算。

我们可以用几行脚本(这些脚本可以应用于样本数据集)来说明这一点。在下面的脚本中,我们针对每个订单行计算税率,即计算税额与向客户收取的税额之比。
read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO

Orders.TaxRate = Orders.TaxAmount / Orders.NetAmount
其中 Orders 是指包含整个销售历史记录的表,每个交易通过给定交易中的项目行数呈现。变量 Orders.TaxAmount 指向与 Orders 表中的 TaxAmount 列相关的矢量。从语法中可以看出,表名与矢量名之间使用了点 (.),这种模式在 Envision 中很常用。

涉及到等号 = 的运算称为“赋值”:赋值运算在 = 号右边进行,其结果被赋给语句的左边。在上例中,右边为除法运算。由于脚本中既未定义 Orders.TaxAmount 也未定义 Orders.NetAmount,因此 Envision 尝试从输入数据集直接加载数据。因为表数据集的 Orders 表包含 NetAmountTaxAmount 这两列,所以脚本执行成功。等式左边为 Orders.TaxRate,它被赋予新计算得出的税率。赋值在逻辑上相当于根据赋值变量在 Excel 中创建一个新列,本例中创建的新列为 TaxRate

在以下脚本片段中,为了简洁起见,所有示例中都忽略了 read “/sample/Lokad_XYZ.tsv” 行,因为这些行原本就应当始终位于每段脚本的顶部。

在 Envision 中进行计算的语法与 Excel 公式中使用的语法很相似。在下面的脚本中,我们执行了一系列(非任意)计算来诠释语法。
Orders.A = 42
Orders.B = 5 * (1 + Orders.A)
Orders.C = (Orders.A + Orders.B) * (1 + Orders.A)
这段脚本定义了三个矢量,分别为 ABC;这三个矢量均附在 Orders 表中。第一行为简单的赋值运算,其中值 42 被赋给 Orders.A。但由于 Orders.A 为矢量,因此并非只是单个值 42,而是原始表中的每一行都被赋予该值。Envision 非常重视矢量,大部分关于矢量的运算可以同时针对所有值进行。

再来看 Orders,它并非样本数据集中唯一可用的表。例如,样本数据集还包含 PO 表,对该表同样可以执行非常类似的运算。
PO.A = 42
PO.B = 5 * (1 + PO.A)
PO.C = PO.A + PO.B
由于我们刚刚引入了第二个表,于是就有了这样一个问题:Envision 可以在表与表之间执行运算吗?当然可以,但会稍微复杂一点。请看下面的脚本:
Orders.A = 1
PO.A = Orders.A + 1 // WRONG!
由于 OrdersPO 这两个表完全没必要进行对齐 – 这两个表甚至连行数都不一样 – 此类运算涉及的语义会非常不清晰。因此,这类运算对于 Envision 无效,如果试图执行这样的脚本,执行会失败,并且会显示错误消息。

尽管如此,Envision 仍提供了多种途径来组合来自不同表的数据,下文将对此进行介绍。

特殊“项目”表

Envision 使用的表会进行命名,但针对该约定有一个例外:“项目”表。实际上,在商业中,我们注意到对于绝大多数的计算,有一个表会支配其他所有表:产品列表/变量/SKU/…具体取决于所考量的实际商业环境。譬如在关系数据库中,所有表会进行同等处理,而 Envision 会对“项目”表进行特殊处理,从而更便于应对商业中的大多数情况。

每个零售商,不论其规模是大是小,都会不同程度地使用电子表格来列出所有产品,一个产品占据一行,表中还包含其他许多列用于提供额外信息,要么提供产品类别等静态信息,要么提供过去 5 周的总销售额等动态信息。根据具体情况,这些行将关联产品或 SKU,或是对所售产品的任何类似的细分呈现。构建此类综合性的表在很多情况下非常实用:例如辨别积压库存、更新价格、确定畅销产品等。作为零售业的从业者,您或许已经在很多场合中处理了此类电子表格。

在 Lokad,我们认识到此类相似度较高的表格在零售业界普遍存在,因此我们决定将其纳入我们的技术。于是我们设计了 Envision 来深度把握这一模式,从而尽可能地顺应这种在商业中普遍存在的惯例。

来回顾一下样本数据集。该数据集包含项目列表。假设我们要计算每个项目的库存值。如果“项目”表被命名为 Items,那么这个过程可以这样实现:
Items.StockValue = (Items.StockOnHand + Items.StockOnOrder) * BuyPrice
然而,对于“项目”表以及这个表,表名可以忽略。因此,实际上可以这样编写脚本:
StockValue = (StockOnHand + StockOnOrder) * BuyPrice
删除了前缀 Items.,按照约定,任何名称中不含点 (.) 的变量隐式指向与“项目”表相关联的矢量。由于涉及“项目”的计算在商业中非常常见,因此这种约定使得 Envision 在实际中的可读性更高。

“项目”电子表格之所以使用如此普遍,其中一个关键原因就在于商业中遇到的大部分历史数据可以很方便地作为事件列表来呈现,每个事件附在一个项目上。比方说,对于销售历史记录,一个表中至少包含三列:项目标识符、采购日期、采购数量。同样,采购历史记录也可以用相同的三列呈现,当然最好涵盖到达日期,以说明订购和交付之间的前置时间。客户退货也可以通过表来展示,该表中包含的列涵盖项目标识符、日期、数量,如果用户对分析老客户的行为有兴趣,还可以包含客户标识符。

这样的例子不胜枚举。商业与流息息相关:商品流、供应商-客户流、资金流、客户-供应商流。所有这样的流都可以分解为多个基础行,每行关联一个特定项目。因此在商业中,我们考量的并非是各种各样的表,而是所有主要围绕项目进行的表,而这正是 Envision 所把握的模式。

“项目”表只有一个必须具备的列:项目标识符列。按照 Envision 的约定,该列应被命名为“Id”。几乎所有加载到 Envision 中的表也应当具有自己的“Id”列。从上文可以看到,Orders 表和 PO 表无法进行组合,因为这两个表没有对齐。而“项目”表以及其他任何表中的 Id 列正是实现这种组合所必备的“桥梁”。

通过现金计算可以说明这一点。对于每个项目,假定要计算与完整销售历史记录相关的现金收入,以及计算与完整采购历史记录相关的现金支出。使用以下脚本可以实现这个过程。
CashIn = sum(Orders.NetAmount)
CashOut = sum(PO.NetAmount) 
CashFlow = CashIn - CashOut
在这里,我们可以看到“项目”位于赋值运算的左边,其他表位于赋值运算的右边,至少对于第 1 行和第 2 行是这样。由于使用了 sum() 聚合器,因此在这种情况下可以同时使用不同的表。在本文中我们不会详细介绍聚合器,顾名思义,sum() 聚合器即计算每个项目的 NetAmount 之和。项目和订单的匹配过程很透明,因为 Orders 表也具有 Id 列。

为了清楚起见,我们将计算分解成三行。这段脚本可以重新编写成更紧凑的形式,即不对中间变量命名。脚本如下:
CashFlow = sum(Orders.NetAmount) - sum(PO.NetAmount)
除了求和,Envision 还支持各种各样的典型聚合器:平均值、最小值、最大值、中间值…这些聚合器为完善项目表提供了多种可能,这些描述性的属性对于解决商业中发现的诸多挑战十分有用。但是,尽管可以在“项目”中创建新矢量,但也可以利用其它表和聚合器执行逆向操作,并且在执行与其他表相关的计算时也可以使用“项目”表中的矢量。

我们不妨考虑这样一种情形:用户想重新计算与每个销售订单行相关的 VAT(增值税)。为了简单起见,我们假定所有项目和整个历史记录以 20% 的单一税率计算 VAT,脚本可以这样编写:
VatRate = 0.2 // hypothesis
Orders.Vat = Orders.NetAmount * VatRate / (1 - VatRate)
VatRate 矢量自然扩展到与 Orders 表对齐的矢量,因为每个订单行关联一个原始项目。下面这段实际代码可以更清楚地展示扩展:
VatRate = 0.2 // hypothesis
Orders.VatRate = VatRate
Orders.F = Orders.VatRate / (1 - Orders.VatRate)
Orders.Vat = Orders.NetAmount * Orders.F
在上例中,为了对计算进行分解,我们在 Orders 表中引入了 F 矢量。

顺带说明一下,尽管可以加载任意表到 Envision 中(即使此类表不包含 Id 列),但在目前的讨论中我们不会涉及这样的高级应用方案。

特殊日期状态

“项目”表在 Envision 中属于特殊案例,因为我们可以看出,商业中的许多基础运算是围绕“项目”的概念进行的。但是,此类运算也通常与特定日期有关:销售历史记录的每一行都有相应的日期;采购订单历史记录同样如此,几乎所有可以作为历史记录的数据都是如此。鉴于历史数据在商业中的重要性,几乎所有商业运作都可以通过注明日期、用于说明库存变化或支付的条目列表来说明,因此 Envision 以一种完全受商业驱动的方式实现了特定日期案例。

除了典型的 Id 列,任何表都有一个 日期 列。在提供有 日期 列的情况下,表不仅可以按项目标识符索引,也可以按日期索引。按日期索引非常实用,因为如果用户想对计算应用某种类型的时间窗口,则不论涉及的条目类型如何,整个历史记录都会进行同样过滤。

为了说明这一点,我们来回顾一下现金流计算。假定我们不计算整个历史记录的值,而只计算每个项目的现金流,并且只采用去年的数据历史记录。则可以这样实现:
end := max(date)
when date > end - 365
CashFlow = sum(Orders.NetAmount) - sum(PO.NetAmount)
end 变量定义为在整个输入数据集中观察到的最近日期。假定 end 为日期,这说明 Envision 提供了执行“日期运算”的功能,从上面的脚本中可以看出这一点。根据 Envision 中的任意日期 +1 的约定,最后得到的就是该特定日期加一天。再减去 365 天,就可以移到一年前。

这段脚本以 when 过滤器开始,它表示给定脚本块中处理的所有行为 true 时实施的条件。至于项目,由于它们没有按日期索引,所以日期过滤器对它们没有影响,所有项目将继续在 when 块中呈现。但是,订单和采购订单都有自己的 日期 列,因此所有不满足 when 过滤器条件的行均会从块中过滤出去。

因此,when 块中的 sum() 聚合仅处理非过滤的行,即时间在一年以内的行。使用中间变量也可以对相同的计算进行分解,这与刚开始时在完整历史记录示例中的一样。
end := max(date)
when date > end - 365
CashIn = sum(Orders.NetAmount)
CashOut = sum(PO.NetAmount)
CashFlow = CashIn - CashOut
通过上面的示例,我们可能更清楚地了解了将 when 语句作为过滤块的起始部分的原因:实际上,块中的所有行(明显可以看到第 2、3、4 行的起始处保留了 2 个额外的空格)都会针对日期采用相同的环境过滤器。

这段脚本也诠释了 Envision 将来自于其他表中的复杂数据与“项目”表进行重新对齐的功能。从 Excel 的角度说,即可以将其他表(例如订单历史记录表)转换为使用主表的列,且该列包含产品列表。Envision 大大简化了该过程。