【问题标题】:Getting a Map of arrays by grouping the records通过对记录进行分组来获取数组的映射
【发布时间】:2019-07-03 07:17:14
【问题描述】:

我正在尝试使用 D3 转换一些数据,以获得按月分组的每天平均数量(数组映射)。数据如下所示:

data = [
    {day: "mon", amount: "4", month: "jan"},
    {day: "tue", amount: "2", month: "jan"},
    {day: "wed", amount: "3", month: "jan"},
    {day: "wed", amount: "1", month: "jan"}
]

我想要得到的输出应该是这样的:

{"jan": [
    {day:"mon", avg_amount:"4"},
    {day:"tue", avg_amount:"2"},
    {day:"wed", avg_amount:"2"}
]}

我已经尝试过使用 D3 的分组和汇总功能,但我只能得到一张地图,这并不是我想要的。

d3.rollup(data, 
    v => d3.mean(v, d => d.amount), 
    d => d.month, d => d.day);

我是 D3 的新手,所以我不确定如何获得这样的结果。有没有什么简单的方法可以用 D3 做到这一点?

【问题讨论】:

    标签: javascript arrays dictionary d3.js


    【解决方案1】:

    尽管您的问题标题是 "Map" 并且您在问题正文中多次说 "Map",但您还是将期望的结果写为 object,可能是因为在问题正文中很难写出Map。更好的方法可能是:

    Map(1) {
        "jan" => [
            {day:"mon", avg_amount:"4"},
            {day:"tue", avg_amount:"2"},
            {day:"wed", avg_amount:"2"}
        ]
    }
    

    所以我假设您实际上是在请求地图(特别是因为您使用的是d3.rollup,它返回一个地图)。不过,为了安全起见,我会提供两种解决方案,一种是d3.rollup 创建一个真实的Map,另一种是d3.nest 创建一个对象,以防万一。

    使用 d3.rollup 映射

    如果你想要的确实是一个 Map,只需更改 d3.rollup 中的 reduce 函数即可:

    d3.rollup(iterable, reduce, ...keys)
    //this part ----------^
    

    或者,在你的情况下:

    d3.rollup(data, 
        v => d3.mean(v, d => d.amount),//this is the reduce 
        d => d.month, d => d.day);//2 keys here
    

    这是我将使用的 reduce:

    v => v.reduce((a, b) => {
        const found = a.find(d => d.day === b.day);
        if (!found) {
            a.push({
                day: b.day,
                avg_amount: +b.amount
            })
        } else {
            found.avg_amount = (found.avg_amount + (+b.amount)) / 2
        };
        return a;
    }, [])
    

    顺便说一句,您在该汇总中有 2 个键,删除最后一个。最后,记住你有字符串,而不是数字。相应地强迫他们。

    这是一个演示。不要使用 sn-p 控制台(它只会显示{}),检查浏览器的控制台:

    data = [{
        day: "mon",
        amount: "4",
        month: "jan"
      },
      {
        day: "tue",
        amount: "2",
        month: "jan"
      },
      {
        day: "wed",
        amount: "3",
        month: "jan"
      },
      {
        day: "wed",
        amount: "1",
        month: "jan"
      }
    ];
    
    const nested = d3.rollup(data,
      v => v.reduce((a, b) => {
        const found = a.find(d => d.day === b.day);
        if (!found) {
          a.push({
            day: b.day,
            avg_amount: +b.amount
          })
        } else {
          found.avg_amount = (found.avg_amount + (+b.amount)) / 2
        };
        return a;
      }, []),
      d => d.month);
    
    console.log(nested)
    <script src="https://d3js.org/d3-array.v2.min.js"></script>

    带有 d3.nest 的对象

    另一方面,如果你想要的是一个使用d3.nest的对象,有两件重要的事情:

    1. 您想使用nest.object(),而不是nest.entries(),因为您需要嵌套对象,而不是嵌套数组;
    2. nest.rollup() 将替换嵌套值数组,这是预期的行为。因此,为了以您想要的方式获得平均值,您必须指定nest.rollup() 将返回的数组。在下面的演示中,我将使用上面使用的相同 reduce。

    这是使用d3.nest的演示:

    data = [{
        day: "mon",
        amount: "4",
        month: "jan"
      },
      {
        day: "tue",
        amount: "2",
        month: "jan"
      },
      {
        day: "wed",
        amount: "3",
        month: "jan"
      },
      {
        day: "wed",
        amount: "1",
        month: "jan"
      }
    ];
    
    const nested = d3.nest()
      .key(d => d.month)
      .rollup(v => v.reduce((a, b) => {
        const found = a.find(d => d.day === b.day);
        if (!found) {
          a.push({
            day: b.day,
            avg_amount: +b.amount
          })
        } else {
          found.avg_amount = (found.avg_amount + (+b.amount)) / 2
        };
        return a;
      }, []))
      .object(data);
    
    console.log(nested)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

    【讨论】:

      【解决方案2】:

      一个选项(不使用d3.js)是首先使用Array::reduce() 创建一个按月和日分组的结构,并按每个组获取总金额。完成后,我们可以使用 Object.values()Array.map() 映射生成的结构以匹配您的预期输出:

      const data = [
          {day: "mon", amount: "4", month: "jan"},
          {day: "tue", amount: "2", month: "jan"},
          {day: "wed", amount: "3", month: "jan"},
          {day: "wed", amount: "1", month: "jan"},
          {day: "mon", amount: "5", month: "feb"},
          {day: "mon", amount: "7", month: "feb"},
          {day: "tue", amount: "9", month: "feb"},
          {day: "tue", amount: "7", month: "feb"}
      ];
      
      // Reduce to group by month/day and get total amount by group.
      
      let res = data.reduce((acc, {day, amount, month}) =>
      {
          acc[month] = acc[month] || {};
          acc[month][day] = acc[month][day] || {day, amount: 0, count: 0};
          acc[month][day].amount += +amount;
          acc[month][day].count++;
          return acc;
      }, {});
      
      console.log(res);
      
      // Transfrom previous result to final structure.
      
      for (key in res)
      {
          res[key] = Object.values(res[key])
              .map(({day, amount, count}) => ({day, avg_amount: amount/count}));
      }
      
      console.log(res);

      【讨论】:

        【解决方案3】:

        您可以使用reducemap

        所以这里的基本想法是首先将数据分成月份和日期。并且对于每个月和日,我们将金额加在一起并保留一个名为count 的属性来检查天数。现在使用地图,我们将数据映射到月份和日期,并将金额更改为amount_avg

        let data = [{day: "mon", amount: "4", month: "jan"},{day: "tue", amount: "2", month: "jan"},{day: "wed", amount: "3", month: "jan"},{day: "wed", amount: "1", month: "jan"}]
        
        let output = data.reduce((op,{day,amount,month})=>{
          if(op[month] && op[month][day]){
            op[month][day]['amount'] += parseInt(amount)||0
            op[month][day]['count']++
          } else {
            if(!op[month]) {
              op[month]={}
            }
            op[month][day] = {day,amount: parseInt(amount)||0,count:1}
          } 
          return op
        },{})
        
        
        let desired = Object.keys(output).map(el=>{
          return  ({[el]: Object.values(output[el]).map(({day,amount,count})=>({day,avg_amount: (amount/count).toFixed(2)}) )
        })})
        
        
        console.log(desired[0])

        【讨论】:

          【解决方案4】:

          这是我想出来的。

          它与其他答案有点不同,因为它具有预先填充 0 平均金额的完整结构。这可能很有用,因为检查没有条目的月份和日期不会返回 undefined。它还单独存储所有条目,以防万一。如果它以某种方式破坏了事物,您当然可以将其扔掉——在计算平均值之后。

          好的,这里是条目:

          data = [
           {day: "mon", amount: "4", month: "jan"},
           {day: "tue", amount: "2", month: "jan"},
           {day: "wed", amount: "3", month: "jan"},
           {day: "wed", amount: "1", month: "jan"}
          ];
          

          然后我们构建保存平均值的 R 对象。您可以简单地手动完成此操作,但写出 84 次日期元素将非常乏味。此外,这可以很容易地更改月份和日期的名称 - 即。当您需要使用不同的语言时。

          var m, M=['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'];
          var d, D={'mon':0,'tue':1,'wed':2,'thu':3,'fri':4,'sat':5,'sun':6};
          var R = {};
          for(m=0;m<12;m++){
           R[M[m]] = []; // M[0] = jan,...
           for(d in D){ // d = mon,...
            R[M[m]][D[d]] = {day:d,avg_amount:0,all:[]}; // D[d] = 0,...
            // R = { jan:[ {day:'mon',avg_amount:0,all:[]},...
           }
          }
          

          这是剩下的,遍历 data 数组,将金额存储在 R 对象中,最后在 R 中遍历每一天> 对象,计算平均值。

          var sum,n;
          data.map( x => R[x.month][D[x.day]].all.push(parseInt(x.amount)) );
          for(m in R) for(d=0;d<7;d++){
           sum = R[m][d].all.reduce((total,x)=>total+x,0);
           n = R[m][d].all.length;
           R[m][d].avg_amount = ((n==0)? 0 : sum/n);
           // delete R[m][d].all;
           // in case the structure can't have any extra fields
           // we can dispose of the list of all entries belonging
           // to a particular month and day
          }
          

          您还可以创建一个包含新数据的函数,如下所示:

          function processData(data){
           var sum,n;
           data.map( x => R[x.month][D[x.day]].all.push(parseInt(x.amount)) );
           for(m in R) for(d=0;d<7;d++){
            sum = R[m][d].all.reduce((total,x)=>total+x,0);
            n = R[m][d].all.length;
            R[m][d].avg_amount = ((n==0)? 0 : sum/n);
           }
          }
          

          最后,你有没有考虑更换

          {"jan":["mon",avg_amount:"4"},{day:"tue", avg_amount:"2"}]}
          

          {"jan":{"mon":5,"tue":3}}
          

          因为除非这种特定的结构是强加给你的,否则像这样混合对象和数组对我来说似乎有点笨拙。它可以做得更整洁。只是一个建议。

          R.jan[0].avg_value; // avg_amount of january, monday
          

          R.jan.mon; // avg_amount of january, monday
          

          【讨论】:

            猜你喜欢
            • 2022-06-14
            • 1970-01-01
            • 2022-07-25
            • 1970-01-01
            • 2021-12-17
            • 2019-11-21
            • 2022-12-04
            • 1970-01-01
            相关资源
            最近更新 更多