Crossfilter数据集的年度统计数据

时间:2018-06-29 22:11:11

标签: dc.js crossfilter

摘要

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

年度(YoY)定义

2017年同比是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的用途。

但是,现在,我想添加一些同比数据,以便可以显示以年为x轴,以年值为y轴的条形图(例如,Yo-2019 = Units-2019 /单位-2018)。我还想按季度和月份进行相同操作,以便可以看到2019年3月同比= 2019年3月单位/ 2018年3月单位(季度也是如此)。

我有一个年份维度和总数

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

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

2 个答案:

答案 0 :(得分:1)

我会使用一个伪造的小组,以便一次性解决这个问题。

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

下面是假组的外观的简短概述:

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;
            }, []);
        }
    };
}

这个想法很简单:首先按日期索引所有值。然后,在生成键/值对数组时,向上查找每个键/值对是否一年前有值。如果是这样,将一对插入结果(否则将其放下)。

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

请注意在几个地方使用Array.reduce。这是crossfilter group.reduce的精神祖先-它采用了具有与reduce-add函数相同的签名的函数,以及一个初始值(而不是函数)并产生单个值。它没有像交叉过滤器那样对更改做出反应,而是仅循环遍历该数组一次。当您要从数组中生成对象或生成与原始对象大小不同的数组时,这很有用。

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

dc.js主页上的股票示例所使用的数据集中的

Demo fiddle同比交易量。

答案 1 :(得分:0)

我在下面重写了@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()函数需要返回什么? 组的all函数必须返回arrayobject,每个object必须具有两个属性:keyvalue

¿那么我们到底在这里做什么? 我们从一个现有的组开始,该组显示一段时间内的一些值(重要假设:键是日期对象),然后围绕它创建一个包装器,以便我们可以利用交叉过滤器已经拥有的工作完成以某个级别(例如年,月等)进行汇总。

我们首先使用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()结构:array中的objects每个都具有keyvalue属性。我们从现有组的all()数组开始(再一次)开始,但是这次我们有了valuesByDate数组的优势,它将使查找其他日期变得容易。

因此,如果存在一年前的条目({{},我们(通过reduce)对原始group.all()的输出进行迭代,并在我们之前生成的数组(valuesByDate)中进行查找。 1}})。 (我们使用valuesByDate[dateLastYear.getTime()],所以它是简单的整数,而不是我们要为其编制索引的对象。)如果一年前存在数组的元素,那么我们会在不久的将来为我们添加键值对象对,包含当前键(日期)的待返回数组,对于该值,我们将“现在”值(getTime())除以1年前的值:thisKeyValuePair.value。最后,我们减去1,以使其为YoY(最传统的定义)。例如今年= 110,去年= 100 ...同比​​= + 10%= 110/100-1。