来自 Crossfilter 数据集的年度统计数据
Year over Year Stats from a Crossfilter Dataset
总结
我想在 Crossfilter-DC 驱动的仪表板中提取同比统计数据
同比 (YoY) 定义
2017 YoY 是 2017 年的总单位数除以 2016 年的总单位数。
详情
我正在使用 DC.js
(因此 D3.js
和 Crossfilter
)创建一个交互式仪表板,该仪表板也可用于更改其呈现的数据。
我有数据,虽然更广泛(除了日期和数量之外还有 ~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 一个 object
的 array
并且每个 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()
结构:objects
的 array
每个结构都有 key
和 value
属性。我们从现有组的 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.
总结
我想在 Crossfilter-DC 驱动的仪表板中提取同比统计数据
同比 (YoY) 定义
2017 YoY 是 2017 年的总单位数除以 2016 年的总单位数。
详情
我正在使用 DC.js
(因此 D3.js
和 Crossfilter
)创建一个交互式仪表板,该仪表板也可用于更改其呈现的数据。
我有数据,虽然更广泛(除了日期和数量之外还有 ~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 一个 object
的 array
并且每个 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()
结构:objects
的 array
每个结构都有 key
和 value
属性。我们从现有组的 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.