создание топ-5 агрегации с помощью ramdajs - PullRequest
4 голосов
/ 08 мая 2019

Я хотел бы преобразовать этот вход

[
        { country: 'France', value: 100 },
        { country: 'France', value: 100 },
        { country: 'Romania', value: 500 },
        { country: 'England', value: 400 },
        { country: 'England', value: 400 },
        { country: 'Spain', value: 130 },
        { country: 'Albania', value: 4 },
        { country: 'Hungary', value: 3 }
]

на выходе

[
      { country: 'England', value: 800 },
      { country: 'Romania', value: 500 },
      { country: 'France', value: 200 },
      { country: 'Spain', value: 130 },
      { country: 'Other', value: 8 }
]

Который в основном делает сумму значений для топ-4 + других стран .

Я использую javascript с ramdajs , и мне удалось сделать это только как-то громоздко .

Я ищу элегантное решение: любой функциональный программист может предоставить свое решение? Или какая-нибудь идея методов ramda, которые могли бы помочь?

Ответы [ 9 ]

4 голосов
/ 08 мая 2019

(Каждый шаг получает выходные данные предыдущего шага. Все будет сведено в конец.)

Шаг 1: Получить карту сумм

Вы можете преобразовать это:

[
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
]

в это:

{
  Albania: 4,
  England: 800,
  France: 200,
  Hungary: 3,
  Romania: 500,
  Spain: 130
}

С этим:

const reducer = reduceBy((sum, {value}) => sum + value, 0);
const reduceCountries = reducer(prop('country'));

Шаг 2:Преобразовать это обратно в отсортированный массив

[
  { country: "Hungary", value: 3 },
  { country: "Albania", value: 4 },
  { country: "Spain", value: 130 },
  { country: "France", value: 200 },
  { country: "Romania", value: 500 },
  { country: "England", value: 800 }
]

Вы можете сделать это с помощью:

const countryFromPair = ([country, value]) => ({country, value});
pipe(toPairs, map(countryFromPair), sortBy(prop('value')));

Шаг 3: Создайте две подгруппы, не верхние4 страны и топ-4 страны

[
  [
    { country: "Hungary", value: 3},
    { country: "Albania", value: 4}
  ],
  [
    { country: "Spain", value: 130 },
    { country: "France", value: 200 },
    { country: "Romania", value: 500 },
    { country: "England", value: 800 }
  ]
]

Что вы можете сделать с этим:

splitAt(-4)

Шаг 4: Объедините первую подгруппу

[
  [
    { country: "Others", value: 7 }
  ],
  [
    { country: "Spain", value: 130 },
    { country: "France", value: 200 },
    { country: "Romania", value: 500 },
    { country: "England", value: 800 }
  ]
]

При этом:

over(lensIndex(0), compose(map(countryFromPair), toPairs, reduceOthers));

Шаг 5: Свести весь массив

[
  { country: "Others", value: 7 },
  { country: "Spain", value: 130 },
  { country: "France", value: 200 },
  { country: "Romania", value: 500 },
  { country: "England", value: 800 }
]

С

flatten

Полный рабочий пример

const data = [
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
];

const reducer = reduceBy((sum, {value}) => sum + value, 0);
const reduceOthers = reducer(always('Others'));
const reduceCountries = reducer(prop('country'));
const countryFromPair = ([country, value]) => ({country, value});

const top5 = pipe(
  reduceCountries,
  toPairs,
  map(countryFromPair),
  sortBy(prop('value')),
  splitAt(-4),
  over(lensIndex(0), compose(map(countryFromPair), toPairs, reduceOthers)),
  flatten
);

top5(data)
3 голосов
/ 08 мая 2019

Вот подход:

const combineAllBut = (n) => pipe(drop(n), pluck(1), sum, of, prepend('Others'), of)

const transform = pipe(
  groupBy(prop('country')),
  map(pluck('value')),
  map(sum),
  toPairs,
  sort(descend(nth(1))),
  lift(concat)(take(4), combineAllBut(4)),
  map(zipObj(['country', 'value']))
)

const countries = [{ country: 'France', value: 100 }, { country: 'France', value: 100 }, { country: 'Romania', value: 500 }, { country: 'England', value: 400 }, { country: 'England', value: 400 }, { country: 'Spain', value: 130 }, { country: 'Albania', value: 4 }, { country: 'Hungary', value: 3 }]

console.log(transform(countries))
<script src="https://bundle.run/ramda@0.26.1"></script>
<script>
const {pipe, groupBy, prop, map, pluck, sum, of, prepend, toPairs, sort, descend, nth, lift, concat, take, drop, zipObj} = ramda
</script>

За исключением одной сложной строки (lift(concat)(take(4), combineAllBut(4))) и связанной вспомогательной функции (combineAllBut), это набор простых преобразований.Эта вспомогательная функция, вероятно, бесполезна вне этой функции, поэтому было бы вполне приемлемо встроить ее как lift(concat)(take(4), pipe(drop(4), pluck(1), sum, of, prepend('Others'), of)), но я нахожу результирующую функцию слишком сложной для чтения.

Обратите внимание, что эта функция вернетчто-то вроде [['Other', 7]], что является бессмысленным форматом, за исключением того факта, что мы собираемся затем concat это с массивом из первых четырех.Так что есть хотя бы какой-то аргумент для удаления окончательного of и замены concat на flip(append).Я этого не делал, поскольку эта вспомогательная функция ничего не значит, кроме как в контексте этого конвейера.Но я бы понял, если бы кто-то выбрал другое.

Мне нравится остальная часть этой функции, и она, кажется, хорошо подходит для стиля конвейера Ramda.Но эта вспомогательная функция в некоторой степени портит его.Я хотел бы услышать предложения по его упрощению.

Обновление

Затем ответ от customcommander продемонстрировал упрощение, которое я мог бы сделать, используя reduceBy вместо танца groupBy -> map(pluck) -> map(sum) в приведенном выше.подход.Это делает для определенного улучшения.

const combineAllBut = (n) => pipe(drop(n), pluck(1), sum, of, prepend('Others'), of)

const transform = pipe(
  reduceBy((a, {value}) => a + value, 0, prop('country')),
  toPairs,
  sort(descend(nth(1))),
  lift(concat)(take(4), combineAllBut(4)),
  map(zipObj(['country', 'value']))
)

const countries = [{ country: 'France', value: 100 }, { country: 'France', value: 100 }, { country: 'Romania', value: 500 }, { country: 'England', value: 400 }, { country: 'England', value: 400 }, { country: 'Spain', value: 130 }, { country: 'Albania', value: 4 }, { country: 'Hungary', value: 3 }]

console.log(transform(countries))
<script src="https://bundle.run/ramda@0.26.1"></script>
<script>
const {pipe, reduceBy, prop, map, pluck, sum, of, prepend, toPairs, sort, descend, nth, lift, concat, take, drop, zipObj} = ramda
</script>
2 голосов
/ 08 мая 2019

Я попробую и попробую использовать его функцию для большинства вещей. и держи его в покое pipe

const f = pipe(
  groupBy(prop('country')),
  map(map(prop('value'))),
  map(sum),
  toPairs(),
  sortBy(prop(1)),
  reverse(),
  addIndex(map)((val, idx) => idx<4?val:['Others',val[1]]),
  groupBy(prop(0)),
  map(map(prop(1))),
  map(sum),
  toPairs(),
  map(([a,b])=>({'country':a,'value':b}))
)

Ramda REPL


Тем не менее, я не думаю, что это каким-либо образом читается.

1 голос
/ 08 мая 2019

Я бы, наверное, сделал что-то вроде этого:

const aggregate = R.pipe(
  R.groupBy(R.prop('country')),
  R.toPairs,
  R.map(
    R.applySpec({ 
      country: R.head, 
      value: R.pipe(R.last, R.pluck('value'), R.sum),
    }),
  ),
  R.sort(R.descend(R.prop('value'))),
  R.splitAt(4),
  R.over(
    R.lensIndex(1), 
    R.applySpec({ 
      country: R.always('Others'), 
      value: R.pipe(R.pluck('value'), R.sum),
    }),
  ),
  R.unnest,
);

const data = [
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
];

console.log('result', aggregate(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
1 голос
/ 08 мая 2019

Я бы группировал по стране, объединял каждую группу стран в один объект, суммируя значение, сортировал, разделял на два массива [наивысшее 4] и [другие], объединял другие в один объект и объединял с наивысший 4.

const { pipe, groupBy, prop, values, map, converge, merge, head, pluck, sum, objOf, sort, descend, splitAt, concat, last, of, assoc } = R

const sumProp = key => pipe(pluck(key), sum, objOf(key))

const combineProp = key => converge(merge, [head, sumProp(key)])

const getTop5 = pipe(
  groupBy(prop('country')),
  values, // convert to array of country arrays
  map(combineProp('value')), // merge each sub array to a single object
  sort(descend(prop('value'))), // sort descebdubg by the value property
  splitAt(4), // split to two arrays [4 highest][the rest]
  converge(concat, [ // combine the highest and the others object
    head,
    // combine the rest to the others object wrapped in an array
    pipe(last, combineProp('value'), assoc('country', 'others'), of)
  ])
)

const countries = [{ country: 'France', value: 100 }, { country: 'France', value: 100 }, { country: 'Romania', value: 500 }, { country: 'England', value: 400 }, { country: 'England', value: 400 }, { country: 'Spain', value: 130 }, { country: 'Albania', value: 4 }, { country: 'Hungary', value: 3 }]

const result = getTop5(countries)

console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
1 голос
/ 08 мая 2019

Вот мои два цента.

const a = [
    { country: 'France', value: 100 },
    { country: 'France', value: 100 },
    { country: 'Romania', value: 500 },
    { country: 'England', value: 400 },
    { country: 'England', value: 400 },
    { country: 'Spain', value: 130 },
    { country: 'Albania', value: 4 },
    { country: 'Hungary', value: 3 }
];

const diff = (a, b) => b.value - a.value;
const addValues = (acc, {value}) => R.add(acc,value);
const count = R.reduce(addValues, 0);
const toCountry = ({country}) => country;
const toCountryObj = (x) => ({'country': x[0], 'value': x[1] });
const reduceC = R.reduceBy(addValues, [], toCountry);

const [countries, others] = R.compose(
    R.splitAt(4), 
    R.sort(diff), 
    R.chain(toCountryObj), 
    R.toPairs, 
    reduceC)(a);

const othersArray = [{ 'country': 'Others', 'value': count(others) }];

R.concat(countries, othersArray);

Ramda REPL

1 голос
/ 08 мая 2019

Использование большего количества функций ramda, но не уверен, что лучше:

let country = pipe(
  groupBy(prop('country')),
  map(pluck('value')),
  map(sum)
)([
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
]);

let splitCountry = pipe(
  map((k) => ({country: k, value: country[k]})),
  sortBy(prop('value')),
  reverse,
  splitAt(4)
)(keys(country));

splitCountry[0].push({country: 'Others', value: sum(map(prop('value'))(splitCountry[1]))});
splitCountry[0]
1 голос
/ 08 мая 2019

Я думаю, вы можете немного упростить groupOthersKeeping, разделив массив перед тем, как уменьшить его, с точки зрения ramda, который может выглядеть следующим образом:

const groupOthersKeeping = contriesToKeep => arr => [
    ...slice(0, contriesToKeep, arr),
    reduce(
      (acc, i) => ({ ...acc, value: acc.value + i.value }),
      { country: 'Others', value: 0 },
      slice(contriesToKeep, Infinity, arr)
    )
 ]
0 голосов
/ 14 июня 2019

Вот два решения

Я думаю, что второе легче понять, даже если оно длиннее

Функция «mergeAllWithKeyBy» объединяет функциональность «R.mergeAll», «R.mergeWithKey»и "R.groupBy".

const mergeAllWithKeyBy = R.curry((mergeFn, keyFn, objs) =>
  R.values(R.reduceBy(R.mergeWithKey(mergeFn), {}, keyFn, objs)))

const addValue = (k, l, r) => 
  k === 'value' ? l + r : r

const getTop = 
  R.pipe(
    mergeAllWithKeyBy(addValue, R.prop('country')),
    R.sort(R.descend(R.prop('value'))),
    R.splitAt(4),
    R.adjust(-1, R.map(R.assoc('country', 'Others'))),
    R.unnest,
    mergeAllWithKeyBy(addValue, R.prop('country')),
  )
  
const data = [
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
]

console.log(getTop(data))
<script src="//cdn.jsdelivr.net/npm/ramda@latest/dist/ramda.min.js"></script>

const getTop = (data) => {
  const getCountryValue =
    R.prop(R.__, R.reduceBy((y, x) => y + x.value, 0, R.prop('country'), data))
    
  const countries = 
    R.uniq(R.pluck('country', data))
  
  const [topCounties, bottomCountries] = 
    R.splitAt(4, R.sort(R.descend(getCountryValue), countries))
  
  const others = {
    country: 'Others', 
    value: R.sum(R.map(getCountryValue, bottomCountries))
  }
  
  const top =
    R.map(R.applySpec({country: R.identity, value: getCountryValue}), topCounties)
  
  return R.append(others, top)
}

const data = [
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
]

console.log(getTop(data))
<script src="//cdn.jsdelivr.net/npm/ramda@latest/dist/ramda.min.js"></script>
...