Search code examples
javascriptarraysdata-structureslodash

Calculate rolling incremental of properties from an array of objects


I have an array of objects of this structure

- day
- dimension
- sub-dimension
  ...
- sub-sub-dimension
- measure1, measure2,...

and want to convert into this structure to feed into a dashboard template, with the "diff" is DoD difference.

- day
- dimension
- sub-dimension
  ...
- sub-sub-dimension
- measure1: [value,#diff,%diff]
- measure2: [value,#diff,%diff]
  ...

My best logic so far is

  • Loop through the data and group by every value combinations of dimensionList.
  • Populate an array of values for each measure combination in the measureList with all days.
  • Calculate rolling difference for each measure.
  • Update back to the original data.

My best attemp failed at the first step at generating the array for each combinations and my logic isn't neat as well. Hope someone could help, it's okay to use packages.

    const dimensionList = ["city","category"];
    const measureList = ["order","buyer","gmv"];
    const newData = [];
    measureList.forEach(measure => {
      newData.push(
        {
          measure: measure,
          values: _.chain(data)
                   .groupBy(data,item => dimensionList.map(dim => item[dim]).join('-'))
                   .mapValues(group => _.map(group, measure))
                   .value()
        }
      )
    });

Example data and the expected results

[
    {
        "day": "2024-01-01",
        "city": "A",
        "category": "X",
        "order": 100,
        "buyer": 50,
        "gmv": 1000
    },
    {
        "day": "2024-01-01",
        "city": "A",
        "category": "Y",
        "order": 110,
        "buyer": 160,
        "gmv": 1200
    },
    {
        "day": "2024-01-02",
        "city": "A",
        "category": "X",
        "order": 125,
        "buyer": 60,
        "gmv": 1400
    },
    ...
]
[
    {
        "day": "2024-01-01",
        "city": "A",
        "category": "X",
        "order": [100,null,null]
        "buyer": [50,null,null]
        "gmv": [1000,null,null]
    },
    {
        "day": "2024-01-01",
        "city": "A",
        "category": "Y",
        "order": [110,null,null]
        "buyer": [160,null,null]
        "gmv": [1200,null,null]
    },
    {
        "day": "2024-01-02",
        "city": "A",
        "category": "X",
        "order": [125,25,0.25]
        "buyer": [60,10,0.2]
        "gmv": [1400,400,0.4]
    },
    ...
]

Solution

  • Iterate the array with Array.map(), and use a Map to hold the previous item with the same dimensions. On each item take the previous from the Map (or default null), and replace it with the current item. Return the new object after calculating the diffs.

    const { mapValues, pick } = _;
    
    const dimensionList = ["city","category"];
    const measureList = ["order","buyer","gmv"];
    
    const getDimensions = item => dimensionList.map(dim => item[dim]).join('-');
    
    const getMeasures = (curr, prev) => prev === null 
      ? [curr, null, null]
      : [curr, curr - prev, (curr - prev) / prev];
      
    const updateItem = (curr, prev) => ({
      ...curr,
      ...mapValues(pick(curr, measureList), (v, k) => 
        getMeasures(v, prev?.[k] ?? null)
      )
    });
    
    const mapDiffs = arr => {
      const dimMap = new Map();
    
      return arr.map(item => {
        const dimensions = getDimensions(item);
        
        const prev = dimMap.get(dimensions) ?? null; 
        
        dimMap.set(dimensions, item);
        
        return updateItem(item, prev);
      });
    };
    
    const arr = [{"day":"2024-01-01","city":"A","category":"X","order":100,"buyer":50,"gmv":1000},{"day":"2024-01-01","city":"A","category":"Y","order":110,"buyer":160,"gmv":1200},{"day":"2024-01-02","city":"A","category":"X","order":125,"buyer":60,"gmv":1400}];
    
    const result = mapDiffs(arr);
    
    console.log(result);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>