【问题标题】:Percentage Change in dc.js/crossfilterdc.js/crossfilter 中的百分比变化
【发布时间】:2017-08-29 21:16:15
【问题描述】:

我刚开始使用 dc.js,正在查看主站点上的 NASDAQ 示例:https://dc-js.github.io/dc.js/

我创建了a Fiddle,其中包含一些示例虚拟数据以及该问题的两个相关图表。

与 NASDAQ 示例类似,我想要一个气泡图,其中 Y 轴是由不同图表中的画笔控制的时间跨度内的值变化百分比。纳斯达克示例的代码执行以下操作:

    var yearlyPerformanceGroup = yearlyDimension.group().reduce(
    /* callback for when data is added to the current filter results */
    function (p, v) {
        ++p.count;
        p.absGain += v.close - v.open;
        p.fluctuation += Math.abs(v.close - v.open);
        p.sumIndex += (v.open + v.close) / 2;
        p.avgIndex = p.sumIndex / p.count;
        p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
        p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
        return p;
    },
    /* callback for when data is removed from the current filter results */
    function (p, v) {
        --p.count;
        p.absGain -= v.close - v.open;
        p.fluctuation -= Math.abs(v.close - v.open);
        p.sumIndex -= (v.open + v.close) / 2;
        p.avgIndex = p.count ? p.sumIndex / p.count : 0;
        p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
        p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
        return p;
    },
    /* initialize p */
    function () {
        return {
            count: 0,
            absGain: 0,
            fluctuation: 0,
            fluctuationPercentage: 0,
            sumIndex: 0,
            avgIndex: 0,
            percentageGain: 0
        };
    }
);

我目前将其解释为对所有数据求和(关闭-打开)并除以平均每日指数的平均值。但这不是我熟悉的百分比变化公式。 (例如(新旧)/旧 x 100)

虽然它似乎适用于 NASDAQ 示例,但我的数据更像是以下内容:

country_id,received_week,product_type,my_quantity,my_revenue,country_other_quantity
3,2017-04-02,1,1,361,93881
1,2017-04-02,4,45,140,93881
2,2017-04-02,4,2,30,93881
3,2017-04-02,3,1,462,93881
2,2017-04-02,3,48,497,93881

等.. 多月和 product_types。

假设我对计算特定国家的百分比变化感兴趣。如何获取给定国家/地区的开始和结束数量,以便将变化计算为 end-start/start * 100?

我正在考虑以下内容(假设我设置了正确的尺寸和所有内容)

var country_dim = ndx.dimension(function (d) { return d['country_id']; })
var received_day_dim = ndx.dimension(function (d) { return d['received_day']; })
var date_min = received_day_dim.bottom(1)[0]['received_day']
var date_max = received_day_dim.top(1)[0]['received_day']

然后在我的自定义reduce函数中,目前在示例的脉络中(错误):

var statsByCountry = country_dim.group().reduce(
          function (p, v) {
              ++p.count;
              p.units += +v["my_units"];
              p.example_rate = +v['my_units']/(v['quantity_unpacked']*90) //place holder for total units per day per country
              p.sumRate +=  p.opp_buy_rate;
              p.avgRate = p.opp_buy_rate/p.count;
              p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
              p.dollars += +v["quantity_unpacked"]/2;
              // p.max_date = v['received_week'].max();
              // p.min_date
              //dateDimension.top(Infinity)[dateDimension.top(Infinity).length - 1]['distance'] - dateDimension.top(Infinity)[0]['distance']


              return p;
          },
          function (p, v) {
              --p.count;
              if (v.region_id > 2) {
                p.test -= 100;
              }
              p.units -= +v["quantity_unpacked"];
              p.opp_buy_rate = +v['quantity_unpacked']/(v['quantity_unpacked']*90) //place holder for total units per day per country
              p.sumRate -=  p.opp_buy_rate;
              p.avgRate = p.count ? p.opp_buy_rate/p.count : 0;
              p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
              p.dollars -= +v["quantity_unpacked"]/2;
              // p.max_date = v['received_week'].max();
              return p;
          },
          function () {
              return {quantity_unpacked: 0,
                      count: 0,
                      units: 0,
                      opp_buy_rate: 0,
                      sumRate: 0,
                      avgRate: 0,
                      percentageGain: 0,
                      dollars: 0,
                      test: 0
              };//, dollars: 0}
          }
  );

还有我的图表:

country_bubble
    .width(990)
    .height(250)
    .margins({top:10, right: 50, bottom: 30, left:80})
    .dimension(country_dim)
    .group(statsByCountry)
    .keyAccessor(function (p) {
      return p.value.units;
    })
    .valueAccessor(function (p) { //y alue
      return p.value.percentageGain;
    })
    .radiusValueAccessor(function (p) { //radius
        return p.value.dollars/10000000;
    })
    .maxBubbleRelativeSize(0.05)
    .elasticX(true)
    .elasticY(true)        
    .elasticRadius(true)
    .x(d3.scale.linear())
    .y(d3.scale.linear())
    // .x(d3.scale.linear().domain([0, 1.2*bubble_xmax]))
    // .y(d3.scale.linear().domain([0, 10000000]))
    .r(d3.scale.linear().domain([0, 10]))
    .yAxisPadding('25%')
    .xAxisPadding('15%')
    .renderHorizontalGridLines(true)

    .renderVerticalGridLines(true)        

    .on('renderlet', function(chart, filter){
    chart.svg().select(".chart-body").attr("clip-path",null);
 });

本来想在statsbycountry里有类似下面的东西:

          if (v.received_day == date_min) {
            p.start_value += v.my_quantity;
          }
          if (v.received_day == date_max) {
            p.end_value += v.my_quantity;
          }

这似乎有点笨拙?但是如果我这样做,我认为这不会随着其他过滤器的变化(比如时间或产品)而不断更新? Ethan 建议我使用假组,但我有点迷茫。

【问题讨论】:

  • Crossfilter 在这种计算上不会很擅长,因此尝试将其嵌入 Crossfilter 范式中并不值得。进行过滤,然后使用dimension.top(Infinity) 提取当前过滤器中的数据,然后使用该数据计算您在时间跨度内的变化。如果您需要在 dc.js 图表中显示此内容,请使用假组 (github.com/dc-js/dc.js/wiki/FAQ#fake-groups)
  • @Ethan,所以你是在建议我不要使用你对 dimension.top(infinity) 的建议吗?那么有什么替代方法来进行计算,以便在我更改时间范围时它会更新?抱歉,我有点糊涂了。
  • 我是说使用维度而不是组。请改用假组。抱歉,这几周我不太在线,希望其他人可以帮忙
  • 感谢 Ethan 的帮助,不幸的是,通过不乏您的尝试,我仍然不明白我的自定义 reduce 函数、我原来的 country_dim 维度、statsByCountry 和我的假组是如何组合在一起的以及我放了什么在哪里。
  • @Ethan,也许您可​​以再次提供一些快速指导。再考虑一下,我想我的主要问题是如何按国家/地区分组,但按日期排序,以便 top(Infinity)[0] 和 top(Infinity)[array.length - 1] 给我想要的结果?我首先需要在最早日期和最晚日期获取每个国家/地区的单位。例如开始日期, 1, 100| end_date, 1, 200|开始日期, 2, 50| start_date,2,50 才能开始计算。

标签: javascript d3.js dc.js crossfilter


【解决方案1】:

使用工作小提琴,我们可以演示一种方法来做到这一点。我真的不认为这是最好的方法,但它是 Crossfilter 方法。

首先,您需要使用自定义 reduce 函数将组中所有数据的有序数组作为组的一部分:

var statsByCountry = country_dim.group().reduce(
  function(p, v) {
    ++p.count;
    p.units += +v["my_quantity"];
    p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
    p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
    p.dollars += +v["my_revenue"];

    i = bisect(p.data, v, 0, p.data.length);
    p.data.splice(i, 0, v);
    return p;
  },
  function(p, v) {
    --p.count;
    p.units -= +v["my_quantity"];
    p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
    p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
    p.dollars -= +v["my_revenue"];

    i = bisect(p.data, v, 0, p.data.length);
    p.data.splice(i, 1)
    return p;
  },
  function() {
    return {
      data: [],
      count: 0,
      units: 0,
      country_rate: 0,
      dollars: 0,
      percent_change: 0
    }; //, dollars: 0}
  }
);

上面,我已经更新了您的 reduce 函数,以在 .data 属性下维护这个有序数组(由 received_week 排序)。它使用 Crossfilter 的bisect 函数来有效地维护订单。

然后在您的valueAccessor 中,您想根据这些数据实际计算您的价值变化:

  .valueAccessor(function(p) { //y alue
    // Calculate change in units/day from first day to last day.
    var firstDay = p.value.data[p.value.data.length-1].received_week.toString();
    var lastDay = p.value.data[0].received_week.toString();
    var firstDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === firstDay ? d.my_quantity : 0 })
    var lastDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === lastDay ? d.my_quantity : 0 })
    return lastDayUnits - firstDayUnits;
  })

您在 value 访问器中执行此操作,因为它每次过滤器更改只运行一次,而 reduce 函数在添加或删除每个记录时运行一次,每个过滤器可能运行数千次。

如果您想计算 % 变化,您也可以在此处执行此操作,但 % 计算的关键问题始终是“% of what?”从您的问题中,我并不清楚该问题的答案。

值得注意的是,使用这种方法,您的组结构将变得非常大,因为您将整个数据集存储在组中。如果您在过滤时遇到性能问题,我仍然建议您放弃这种方法,转而采用基于假组的方法。

工作更新的小提琴:https://jsfiddle.net/vysbxd1h/1/

【讨论】:

  • 超级乐于助人的伊桑!谢谢!正如您所预测的那样,我确实注意到使用这种方法的性能下降(尽管作为第一步,它仍然很有效)。将其移入假组的建议?另外,您对调试 valueAccessor 和控制台中的所有内容而不是从图表中读取它有什么建议吗?更容易仔细检查确切的数字。再次感谢!
  • 它出现在我更完整的仪表板上,数据数组没有正确排序。嗯
  • 一切正常!再次感谢伊桑!在我真正的仪表板中,当将参数传递给 bisect 函数时,我必须输入 v.received_week 。快速的问题,虽然我很好奇你是否能解释一下。如果我在 valueAccessor 中放入一个 console.log (或逐步执行),它会多次遍历国家组,似乎在最终停止之前再次重复计算。例如console.log(p.key + firstDay) 给出 1firstDay, 2firstDay, 3firstDay 8 次或更少。这与我总共拥有的周组数量有关吗?
  • 很高兴它的工作!每次更新图表时,valueAccessor 将每组运行一次​​。因此,您似乎看到它每组运行一次​​,这对我来说很有意义。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-21
  • 1970-01-01
相关资源
最近更新 更多