使用 Envision 执行时序计算

使用 Envision 执行时序计算


首页 » 资源 » 此处

任何一种商业贸易的大部分业务运作都可以通过时序准确呈现:销售历史记录、采购订单历史记录、价格历史记录等等。鉴于时序处理对于公司至关重要,Envision 针对此类应用方案提供广泛的原生支持。具体而言,Envision 支持根据每天、每周和每月聚合数据。此外还支持更复杂的时序分析,例如滞后数据或计算不断变化的平均值。本页将对这些功能加以说明。

示例

我们将再一次使用样本数据集来说明 Envision 的功能。考虑到本页介绍的是 Envision 较为高级的用法,因此建议您先阅读执行计算聚合数据,然后再继续浏览本页。
read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO

show label "Time-series calculations with Envision" a1f1 tomato

end := max(date)

Week.sold := sum(Orders.NetAmount)
when date >= end - 52 * 7
show linechart "Weekly sales{$}" a2f4 tomato with 
Week.sold as "This year"
Week.sold[-52] as "Last year"

Week.ma := sum(Orders.NetAmount / 4) over [-3 .. 0]
when date >= end - 52 * 7
show linechart "Weekly sales with 4 weeks moving average{$}" a5f7 tomato with 
Week.ma as "This year"
Week.ma[-52] as "Last year"

Day.cashIn := sum(Orders.NetAmount)
Day.cashOut := sum(PO.NetAmount)
Day.balance := avg(Day.cashIn - Day.cashOut) over [-13 .. 0]
when date >= end - 6 * 7 & date < monday(end)
show linechart "Cash flow over the last 6 weeks{$}" a8f10 tomato with 
Day.balance as "Balance"

PO.Price = PO.NetAmount / PO.Quantity
Orders.PurchasePrice = latest(PO.Price)
Orders.Cost = Orders.PurchasePrice * Orders.Quantity
Week.profitability := sum(Orders.NetAmount - Orders.Cost) / sum(Orders.Cost) or 1
when date >= monday(end) - 13 * 7 & date < monday(end)
show linechart "Profitability over the last 13 weeks{%}" a11f13 tomato with 
Week.profitability
使用样本数据集运行这段脚本后,将生成以下仪表板。

Image

虚拟日历表

由于每日、每周和每月聚合非常普遍,因此 Envision 被设计为针对这些周期日历模式提供原生支持。具体地说,Envision 提供三个虚拟表,分别为 DayWeekMonth。之所以称这些表为“虚拟”表,是因为它们不具备相应的表格文件:换言之,这些表仅在执行脚本期间存在。上述脚本就是利用这些虚拟表来显示线形图的。我们来看一下重新复制到下面的相关代码行。
Week.sold := sum(Orders.NetAmount)
end := max(date)
when date >= end - 52 * 7
show linechart "Weekly sales{$}" a2f4 tomato with 
Week.sold as "This year"
Week.sold[-52] as "Last year"
在第 1 行,Orders 表的内容求和为 Week 表。由于这里使用了标量赋值 :=,因此每周计算一个单一值。第 2 行定义了一个过滤器用于排除时间超过 52 周的数据。最后在第 3 - 5 行,定义了一个具有两个时序的线形图。第二个时序 Week.sold[-52] 附带有一个滞后算子,下一节将对此进行说明。 这段脚本可以很容易地修改为按每天或每月聚合。例如,可以在脚本末尾添加以下代码行:
Day.sold := sum(Orders.NetAmount)
show linechart "Daily sales{$}" a14f16 tomato with Day.sold
Month.sold := sum(Orders.NetAmount)
show linechart "Monthly sales{$}" a17f19 tomato with Month.sold
该代码块显示另外两个线形图,这两个线形图分别按每天和每月进行时序聚合。 变量 Day.sold 可解读为 Day 表中 sold 列的内容,也可解读为 “1天”的周期内等距的时序,这与包含 Date 列的常规 Envision 表(例如 Orders 表,可解读为非等距时序 相反。

显示滞后的时序

在时序分析中,“滞后”是指涉及按时序元素移动时间的操作。时序滞后的基本用途之一便是比较两个不同的时间周期。Envision 支持专门用于处理这种情形的滞后算子
Week.sold := sum(Orders.NetAmount)
when date >= end - 52 * 7
  show linechart "Weekly sales{$}" a2f4 tomato with
    Week.sold as "This year", Week.sold[-52] as "Last year"
第 4、5 行有两个时序。第一个时序 Week.sold 是按每周聚合的原始销售额。第二个时序包含一个额外后缀 [-52]。这个后缀就是滞后算子。它表示过去 52 周的数据将向前移动并最终显示在线形图中。在将滞后算子应用于 Week 表时,滞后参数是一个以每周为单位的整数。同样,DayMonth 表的适用单位分别为“天”和“月”。

滞后算子得益于过滤算子 when 的智能协作行为。实际上,如果没有这种协作行为,过滤器 when 将排除所有 52 周之前的数据,因此滞后 52 周的时序将导致零移动。然而,从上面仪表板的屏幕截图可以看出,滞后算子正确前移了具有一年之久的数据,而不只是移动零值。这种行为是通过 when 过滤器和滞后算子之间的内置协作来实现的。

如果将鼠标放在仪表板中的线形图上,您将发现会显示日期和值。具体地说,运行本页顶部的脚本后,与名称为“今年”的时序所报告的日期相比,针对名称为“去年”的时序所报告的日期不到一年。值得一提的是,滞后算子实际上并不保留时序的初始日期。相反,Envision 只是使用一种约定来实现这一点:只有在线形图的声明中定义了滞后算子时,才仍会保留初始日期。

我们对这段脚本加以修改,从而在线形图外部应用滞后算子。通过引入变量 Week.lastYear 可以实现这一点。
Week.sold := sum(Orders.NetAmount)
Week.lastYear := Week.sold[-52]
when date >= end - 52 * 7
show linechart "Weekly sales{$}" a2f4 tomato with 
Week.sold as "This year"
Week.lastYear as "Last year" // date display issue
如果运行这段修改后的脚本,并将鼠标放在时序点上,会发现这两个时序都报告相同的日期。在本例中,对于所要显示的日期不存在模糊不清的语义。例如,Week.lastYear 时序实际上可以解读为逐年预测;因此针对本例中我们所考察的这两个时序生成了相同的日期。总而言之,如果要保留线形图中的初始日期以便并排比较滞后的时序,则应在 show 语句中定义滞后算子。

聚合时间窗口内的数据

在之前的说明中,我们了解了如何通过 Envision聚合数据。对于时序,还有另外一种类型的聚合非常合乎需要:对时间窗口聚合。本页顶部的脚本说明了如何计算 4 周内关于销售额的不断变化的平均值。为了清楚起见,我们将脚本的相关代码行重新复制到下面。
Week.ma := sum(Orders.NetAmount / 4) over [-3 .. 0]
when date >= end - 52 * 7
  show linechart "Weekly sales with 4 weeks moving average{$}" a5f7 tomato with 
Week.ma as "This year"
Week.ma[-52] as "Last year"
第 1 行使用了 sum() 聚合器执行聚合,该聚合附带一个语句,它以末尾的 over 关键字开始。在第 2 行,专门针对过去 52 周的数据进行了过滤。最后来看第 3 - 5 行,线形图中显示有两个时序。这两个时序很“平滑”,因为二者为 4 周的平均值。

算子 over 用于定义适用的时间窗口,其编写格式应为 [a .. b],其中 ab 为整数,且 a 小于或等于 bab 所使用的单位取决于赋值左边的表达式。在本例中,赋值左边为 Week 表,因此 -30 按周表示。

注意:索引为 -3 的周与索引为 0 的周之间有 4 周,而不是 3 周。因此,周数索引为 -3、-2、-1 和 0。
over 选项可用于所有聚合器。在使用此选项时,赋值左边通常为虚拟日历表,例如 DayWeekMonth。但这并非硬性要求,任何按 Date 索引的表都可以使用。此外根据定义,在使用日历表的情况下,通过使用 over [0 .. 0] 选项可以获得与默认聚合相同的结果:
Week.sold := sum(Orders.NetAmount)
Week.same := sum(Orders.NetAmount) over [0 .. 0] // same result!

更为复杂的时序聚合

Envision 的语法为执行更精细的时序计算提供了可能。例如,可以计算时序,然后根据这些初始时序执行进一步计算。本页顶部脚本中的第三个代码块说明了这一点。脚本的相关代码行复制在下面。
Day.cashIn := sum(Orders.NetAmount)
Day.cashOut := sum(PO.NetAmount)
Day.balance := avg(Day.cashIn - Day.cashOut) over [-13 .. 0]
when date >= monday(end) - 6 * 7 & date < monday(end)
show linechart "Cash flow over the last 6 weeks{$}" a8f10 tomato with 
Day.balance as "Balance"
在第 1 行和第 2 行,Day.cashInDay.cashOut 这两个时序分别定义为总销售额和总采购额。在第 3 行,时序 Day.balance 被作为 cashIn 减去 cashOut 之差在 14 天内不断变化的平均值进行计算。在第 4 行,定义了一个具有两个特定条件的过滤器:数据存在的时间不得超过 7 周 – 从最后一个星期一开始算起,且数据不得“晚于”最后一个星期一。在这里使用了monday() 函数,来确保范围刚好涵盖 7 个星期。最后,时序 Day.balance 作为第 5、6 行定义的线形图的唯一时序来显示。

在第 3 行中执行的聚合运用了上一节中详述的 over 选项。在这里,对这两个时序在 14 天的时间窗口中执行求平均值 – 实际上,从索引 -13 到索引 0 刚好有 14 个不同的索引。这种聚合与我们到目前为止所了解的其他聚合略有不同,因为表 Day 同时显示在赋值的左边和右边,而较为常用的不含 over 后缀的聚合通常是将从一个表的数据聚合到另一个表。

每天的时序也可以聚合成每周的时序。下面的脚本说明了计算每周现金流而非每天现金流的过程。
Day.cashIn := sum(Orders.NetAmount)
Day.cashOut := sum(O.NetAmount)
Week.balance := sum((Day.cashIn - Day.cashOut) / 2) over [-1 .. 0]
when date >= monday(end) - 6 * 7 & date < monday(end)
  show linechart "Cash flow over the last 6 weeks{$}" a8f10 tomato with 
    Week.balance as "Balance"

处理事件驱动的数据

事件驱动的数据是指侧重于“变化”的历史数据的特定呈现。例如,相比收集数据历史记录中每一天的所有历史价格,只收集历史价格变化列表更实用:每次价格变化产生新的价格和日期,且假定在观察到新价格变化前保持新价格不变。Envision 支持将历史数据作为变化列表来呈现的应用方案。

尽管样本数据集不包含有关历史价格的表,但查看过去的采购交易,并假定在观察到下次交易前价格保持不变,可以大概推断出此类数据。本页顶部示例中的最后一个脚本块正是这样的。我们来更详细地看一下复制到下面的相关代码行。
PO.Price = PO.NetAmount / PO.Quantity
Orders.PurchasePrice = latest(PO.Price)
Orders.Cost = Orders.PurchasePrice * Orders.Quantity
Week.profitability := sum(Orders.NetAmount - Orders.Cost) / sum(Orders.Cost) or 1
when date >= monday(end) - 13 * 7 & date < monday(end)
show linechart "Profitability over the last 13 weeks{%}" a11f13 tomato with 
Week.profitability
在第 1 行,通过 Envision 中普通的矢量运算针对 O 表中的每一行计算了采购单价。第 2 行使用了 latest 函数,该函数具有十分具体的行为:针对被赋给 latest 函数的 Orders 表的每一行,查找最近的 O.Price(不晚于所考量的 Orders 行),并将此值复制到赋值的左边。第 3 行和第 4 行为更多涉及矢量与聚合的计算。第 5 行定义了一个过滤器,用于将运算范围限制为过去 13 周,在该过滤器块中显示了一个线形图,该线形图使用了之前在第 4 行计算的 Week.profitability 时序。

支持此处计算的要素正是 latest 函数。该函数专门用于捕捉事件流的语义,其中一个值假定为常量,除非它被新事件覆盖。具体地说,使用以下脚本还可以“密化”采购价格,来计算历史记录中每一天的价格:
Day.PurchasePrice = latest(O.Price)
在实践中,latest 函数可用于处理许多情形,例如缺货、促销、产品生命周期等。