来自 Crossfilter 数据集的年度统计数据

Year over Year Stats from a Crossfilter Dataset

总结

我想在 Crossfilter-DC 驱动的仪表板中提取同比统计数据

同比 (YoY) 定义

2017 YoY 是 2017 年的总单位数除以 2016 年的总单位数。

详情

我正在使用 DC.js(因此 D3.jsCrossfilter)创建一个交互式仪表板,该仪表板也可用于更改其呈现的数据。

我有数据,虽然更广泛(除了日期和数量之外还有 ~6 个其他属性:尺寸、颜色等……销售数据),归结为以下对象:

[
 { date: 2017-12-7, quantity: 56,  color: blue  ...},
 { date: 2017-2-17, quantity: 104, color: red   ...},
 { date: 2016-12-7, quantity: 60,  color: red   ...},
 { date: 2016-4-15, quantity: 6,   color: blue  ...},
 { date: 2017-2-17, quantity: 10,  color: green ...},
 { date: 2016-12-7, quantity: 12,  color: green ...}
  ...
]

我为每个属性显示一个行图,以便您可以按颜色、大小等查看总计。人们会使用这些图表中的每一个来查看该属性的总计,并通过仅按一种颜色、一种颜色和一种尺寸、或一种尺寸等进行过滤。此设置(相对)简单明了,类似于 DC 的用途。

但是,现在我想添加一些 YoY 统计数据,这样我就可以显示一个条形图,其中 x 轴为年份,y 轴为 YoY 值(例如 YoY-2019 = Units- 2019 / 单位 - 2018)。我还想按季度和月份做同样的事情,这样我就可以看到 YoY Mar-2019 = Units-Mar-2019 / Units-Mar-2018(季度也一样)。

我有年份维度和总数量

var yearDim = crossfilterObject.dimension(_ => _.date.getFullYear());
var quantityGroup = yearDim.group.reduceSum(_ => _.quantity);

我无法弄清楚如何进行年复一年的计算,尽管以漂亮、漂亮的方式 DC.js。

尝试的解决方案

年+1

添加另一个维度,即年 + 1。不过我并没有真正深入,因为我得到的只是两个维度,我想划分它们的年份组......但我不确定如何划分。

var yearPlusOneDim = crossfilterObject.dimension(_ => _.date.getFullYear() + 1);

在视觉上,我可以分别绘制两者的图表,并且从概念上讲,我知道我想做什么:将 yearDim 中的 2017 年数字除以 YearPlusOneDim 中的 2017 年数字(实际上是 2016 年的数字)。但是“作为一个概念,就我所了解的而言。

放弃 DC 绘图

我总是可以使用 yearDim 的数量组来获取值数组,然后我可以将其输入到正常的 D3.js 图表中。

var annualValues = quantityGroup.all();
console.log(annualValues);
// output = [{key: 2016, value: 78}, {key: 2017, value: 170}]
// example data from the limited rows listed above

但这感觉就像一个 hacky 解决方案,注定会失败,并且无法从所有快速和动态的 DC 更新中受益。

为了一次解决这个问题,我会使用一个假的组。

正如@Ethan 所说,您也可以使用值访问器,但是每次访问值时您都必须查找前一年 - 因此您可能必须保留一个额外的 table 左右。对于假组,您只需要在 .all() 函数的主体中使用此 table。

以下是假冒组的概览:

function yoy_group(group) {
    return {
        all: function() {
            // index all values by date
            var bydate = group.all().reduce(function(p, kv) {
                p[kv.key.getTime()] = kv.value;
                return p;
            }, {});
            // for any key/value pair which had a value one year earlier,
            // produce a new pair with the ratio between this year and last
            return group.all().reduce(function(p, kv) {
                var date = d3.timeYear.offset(kv.key, -1);
                if(bydate[date.getTime()])
                    p.push({key: kv.key, value: kv.value / bydate[date.getTime()]});
                return p;
            }, []);
        }
    };
}

想法很简单:首先按日期索引所有值。然后在生成 key/value 对的数组时,查找每一对,看看它是否在一年前有值。如果是这样,将一对推入结果(否则将其丢弃)。

这应该适用于日期四舍五入的任何日期键组。

请注意在几个地方使用 Array.reduce。这是 crossfilter 的精神祖先 group.reduce - 它采用与 reduce-add 函数具有相同签名的函数和初始值(不是函数)并生成单个值。它不像 crossfilter 那样对变化做出反应,它只是在数组上循环一次。当您想从数组生成对象,或生成与原始数组大小不同的数组时,它很有用。

此外,当按日期索引对象时,我使用 Date.getTime() 来获取日期的数字表示。否则,日期会强制转换为可能不准确的字符串表示形式。对于此应用程序,跳过 .getTime() 可能没问题,但我习惯于始终准确比较日期。

Demo fiddle dc.js 主页上股票示例使用的数据集中的 YOY 交易量。

我在下面重写了@Gordon 的代码。所有的功劳都归功于他的解决方案(上面已经回答),我刚刚写下了我自己的代码版本(更长并且可能只对像我这样的初学者有用)(更冗长!)和解释(也更多冗长)来复制我的想法,将我几乎一无所有的起点与@Gordon 的真正聪明的答案联系起来。

yoyGroup = function(group) {
  return { all: function() {
    // For every key-value pair in the group, iterate across it, indexing it by it's time-value
    var valuesByDate = group.all().reduce(function(outputArray, thisKeyValuePair) {
      outputArray[thisKeyValuePair.key.getTime()] = thisKeyValuePair.value;
      return outputArray;
    }, []);
    return group.all().reduce(function(newAllArray, thisKeyValuePair) {
        var dateLastYear = d3.timeYear.offset(thisKeyValuePair.key, -1);
        if (valuesByDate[dateLastYear.getTime()]) {
          newAllArray.push({
              key: thisKeyValuePair.key, 
            value: thisKeyValuePair.value / valuesByDate[dateLastYear.getTime()] - 1
          });
        }
        return newAllArray;
      }, []); // closing reduce() and a function(...)
  }}; // closing the return object & a function
};

¿我们为什么要覆盖 all() 函数? 当 DC.js 基于分组创建图形时,它使用的唯一来自 Crossfilter 的函数是 all() 函数。因此,如果我们想对分组进行一些自定义操作以影响 DC 图,我们只需重写一个函数:all().

¿ all() 函数需要 return 什么? 一个组的 all 函数必须 return 一个 objectarray 并且每个 object 必须有两个属性:key & value .

¿那么我们到底在做什么? 我们从一个现有的组开始,它随时间显示一些值(重要假设:键是日期对象),然后围绕它创建一个包装器,以便我们可以利用工作crossfilter 已经在某个级别(例如年、月等)进行聚合。

我们首先使用 reduce 将对象数组操作成一个更简单的数组,其中对象中的键和值现在直接位于数组中。我们这样做是为了让按键查找值变得更容易。

before / output structure of group.all()
[ {key: k1, value: v1},
  {key: k2, value: v2},
  {key: k3, value: v3}
]

after
[ k1: v1,
  k2: v2,
  k3: v3
]

然后我们继续创建正确的 all() 结构:objectsarray 每个结构都有 keyvalue 属性。我们从现有组的 all() 数组开始(再次),但这次我们有 valuesByDate 数组的优势,这将使查找其他日期变得容易。

因此我们迭代(通过 reduce)原始 group.all() 输出并在我们之前生成的数组中查找 (valuesByDate),如果存在一年前的条目 ( valuesByDate[dateLastYear.getTime()])。 (我们使用 getTime() 所以它是简单的整数而不是我们索引的对象。)如果数组中有一个元素来自一年前,那么我们添加一个键值对象对到我们的很快 - to-be-returned array with the current key (date) and for the value we divide the "now" value (thisKeyValuePair.value) by the value 1 ago: valuesByDate[dateLastYear.getTime()] .最后我们减去 1,这样它就是(最传统的定义)YoY。前任。今年 = 110,去年 = 100 ... YoY = +10% = 110/100 - 1.