tidyjsを使ったデータ操作20選
はじめに
こんにちわ。nap5です。
Typescriptを通したデータ操作問題20選をChatGPTに作ってもらってTidyjsで回答してみました。
demo code.
デモデータ
export type SaleItem = {
date: Date
product: string
category: string
value: number
}
export const salesData: SaleItem[] = [
{
date: new Date('2023-01-01'),
product: 'T-Shirt',
category: 'Clothing',
value: 100
},
{
date: new Date('2023-01-01'),
product: 'Sneakers',
category: 'Footwear',
value: 150
},
{
date: new Date('2023-01-01'),
product: 'Watch',
category: 'Accessories',
value: 200
},
{
date: new Date('2023-01-02'),
product: 'Dress',
category: 'Clothing',
value: 110
},
{
date: new Date('2023-01-02'),
product: 'Boots',
category: 'Footwear',
value: 140
},
{
date: new Date('2023-01-02'),
product: 'Necklace',
category: 'Accessories',
value: 220
},
{
date: new Date('2023-01-02'),
product: 'Hat',
category: 'Headwear',
value: 250
},
{
date: new Date('2023-01-03'),
product: 'T-Shirt',
category: 'Clothing',
value: 105
},
{
date: new Date('2023-01-03'),
product: 'Watch',
category: 'Accessories',
value: 215
},
{
date: new Date('2023-01-03'),
product: 'Beanie',
category: 'Headwear',
value: 230
},
{
date: new Date('2023-01-04'),
product: 'Jacket',
category: 'Clothing',
value: 155
},
{
date: new Date('2023-01-04'),
product: 'Earrings',
category: 'Accessories',
value: 225
},
{
date: new Date('2023-01-05'),
product: 'Blouse',
category: 'Clothing',
value: 100
},
{
date: new Date('2023-01-05'),
product: 'Watch',
category: 'Accessories',
value: 210
},
{
date: new Date('2023-01-05'),
product: 'Cap',
category: 'Headwear',
value: 245
},
{
date: new Date('2023-01-06'),
product: 'Sneakers',
category: 'Footwear',
value: 150
},
{
date: new Date('2023-01-06'),
product: 'Scarf',
category: 'Accessories',
value: 220
},
{
date: new Date('2023-01-07'),
product: 'Trousers',
category: 'Clothing',
value: 115
},
{
date: new Date('2023-01-07'),
product: 'Bracelet',
category: 'Accessories',
value: 205
},
{
date: new Date('2023-01-08'),
product: 'Sweater',
category: 'Clothing',
value: 160
},
{
date: new Date('2023-01-08'),
product: 'Hat',
category: 'Headwear',
value: 235
},
{
date: new Date('2023-01-09'),
product: 'T-Shirt',
category: 'Clothing',
value: 105
},
{
date: new Date('2023-01-09'),
product: 'Gloves',
category: 'Accessories',
value: 230
},
{
date: new Date('2023-01-10'),
product: 'Sandals',
category: 'Footwear',
value: 165
},
{
date: new Date('2023-01-10'),
product: 'Ring',
category: 'Accessories',
value: 200
}
]
カテゴリごとの平均販売価格を計算
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { groupBy, mean, n, sum, summarize, tidy } from '@tidyjs/tidy'
import { salesData } from '@/data'
import { omit } from 'radash'
describe('カテゴリごとの平均販売価格を計算', () => {
test('averageSaleByCategory', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) =>
tidy(
input,
groupBy('category', [
summarize({ total: sum('value'), count: n(), avg: mean('value') })
])
)
),
map(({ output }) => output.map((d) => omit(d, ['count', 'total'])))
)
expect(result).toStrictEqual([
{ category: 'Clothing', avg: 118.75 },
{ category: 'Footwear', avg: 151.25 },
{ category: 'Accessories', avg: 213.88888888888889 },
{ category: 'Headwear', avg: 240 }
])
})
})
指定された期間内の最も売れた日を取得
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import {
filter,
groupBy,
max,
mutateWithSummary,
select,
sum,
summarize,
tidy,
} from '@tidyjs/tidy'
import { salesData } from '@/data'
import dayjs from 'dayjs'
describe('指定された期間内の最も売れた日を取得', () => {
test('bestSellingDayInPeriod', () => {
const startDate = new Date('2023-01-01')
const endDate = new Date('2023-01-31')
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) => {
const [item] = tidy(
input,
filter(
(d) =>
(dayjs(d.date).isAfter(dayjs(startDate)) ||
dayjs(d.date).isSame(dayjs(startDate))) &&
(dayjs(d.date).isBefore(dayjs(endDate)) ||
dayjs(d.date).isSame(dayjs(endDate)))
),
map((data) =>
data.map((d) => ({ ...d, day: dayjs(d.date).get('date') }))
),
groupBy(['day'], summarize({ total: sum('value') })),
mutateWithSummary({
maxTotal: max('total'),
}),
filter((d) => d.total === d.maxTotal),
select('day')
)
return item.day
}),
map(({ output }) => output)
)
expect(result).toStrictEqual(2)
})
})
各カテゴリの商品で最も売れた商品を取得
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { groupBy, sum, summarize, tidy } from '@tidyjs/tidy'
import { salesData } from '@/data'
import { partitionBy } from '@/utils/dataUtil'
import { omit } from 'radash'
describe('各カテゴリの商品で最も売れた商品を取得', () => {
test('bestSellingProductByCategory', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('summarized', ({ input }) =>
tidy(
input,
groupBy(
['category', 'product'],
summarize({ total: sum('value') })
)
)
),
bind('partitioned', ({ summarized }) =>
partitionBy(summarized, ['category'])
),
bind('convolved', ({ partitioned }) =>
partitioned.map((d) =>
d.reduce<{
category: string
product: string
total: number
}>(
(acc, cur) => {
if (acc.total < cur.total) {
acc = cur
}
return acc
},
{ category: '', product: '', total: 0 }
)
)
),
bind('output', ({ convolved }) =>
convolved.map((d) => ({ ...omit(d, ['total']), total: d.total }))
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ category: 'Clothing', product: 'T-Shirt', total: 310 },
{ category: 'Footwear', product: 'Sneakers', total: 300 },
{ category: 'Accessories', product: 'Watch', total: 625 },
{ category: 'Headwear', product: 'Hat', total: 485 }
])
})
})
連続して売上が上昇した日数を計算する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { type SaleItem, salesData } from '@/data'
import {
groupBy,
max,
n,
summarize,
tidy
} from '@tidyjs/tidy'
import { yyyymmdd } from '@/utils/dateUtil'
import { partitionByWithPredicate } from '@/utils/dataUtil'
describe('連続して売上が上昇した日数を計算する', () => {
test('calculateConsecutiveGrowingDays', () => {
const partitionByPositiveSales = (data: SaleItem[]): SaleItem[][] => {
return partitionByWithPredicate(data, (prev, curr) => {
return prev.value < curr.value
})
}
const result = pipe(
salesData,
bindTo('input'),
bind('partitioned', ({ input }) => partitionByPositiveSales(input)),
bind('formattedDate', ({ partitioned }) =>
partitioned.map((g) => g.map((d) => ({ ...d, date: yyyymmdd(d.date) })))
),
bind('consecutived', ({ formattedDate }) =>
formattedDate
.map((g) =>
tidy(
g,
groupBy(
['date'],
summarize({
count: n(),
detail: (d) => d
})
)
)
)
.flat()
),
bind('maxed', ({ consecutived }) => {
const [maxed] = tidy(
consecutived,
summarize({
maxConsecutivedDates: max((d) => d.count)
})
)
return maxed.maxConsecutivedDates
}),
bind('output', ({ maxed }) => maxed),
map(({ output }) => output)
)
expect(result).toStrictEqual(4)
})
})
商品の日毎の売上の成長率を計算する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import {
arrange,
asc,
groupBy,
lag,
mutateWithSummary,
sum,
summarize,
tidy
} from '@tidyjs/tidy'
import { salesData } from '@/data'
import { yyyymmdd } from '@/utils/dateUtil'
import { Decimal } from '@/utils/decimal'
describe('商品の日毎の売上の成長率を計算する', () => {
test('calculateDailyGrowthRate', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('sorted', ({ input }) => tidy(input, arrange([asc((d) => d.date)]))),
bind('mappedDate', ({ sorted }) =>
sorted.map((d) => ({ ...d, date: yyyymmdd(d.date) }))
),
bind('summarized', ({ mappedDate }) =>
tidy(
mappedDate,
groupBy(
'date',
summarize({
total: sum('value')
})
)
)
),
bind('output', ({ summarized }) =>
tidy(
summarized,
mutateWithSummary({
prevTotal: lag('total', { n: 1, default: -Infinity })
})
).map((d) => ({
...d,
percent: new Decimal(d.total)
.minus(d.prevTotal)
.div(new Decimal(d.total))
.mul(new Decimal(100))
}))
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{
date: '2023-01-01',
total: 450,
prevTotal: -Infinity,
percent: new Decimal(Infinity)
},
{
date: '2023-01-02',
total: 720,
prevTotal: 450,
percent: new Decimal(37.5)
},
{
date: '2023-01-03',
total: 550,
prevTotal: 720,
percent: new Decimal(-30.91)
},
{
date: '2023-01-04',
total: 380,
prevTotal: 550,
percent: new Decimal(-44.74)
},
{
date: '2023-01-05',
total: 555,
prevTotal: 380,
percent: new Decimal(31.53)
},
{
date: '2023-01-06',
total: 370,
prevTotal: 555,
percent: new Decimal(-50)
},
{
date: '2023-01-07',
total: 320,
prevTotal: 370,
percent: new Decimal(-15.63)
},
{
date: '2023-01-08',
total: 395,
prevTotal: 320,
percent: new Decimal(18.99)
},
{
date: '2023-01-09',
total: 335,
prevTotal: 395,
percent: new Decimal(-17.91)
},
{
date: '2023-01-10',
total: 365,
prevTotal: 335,
percent: new Decimal(8.219)
}
])
})
})
日ごとの売上の移動平均を計算する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { salesData } from '@/data'
import { mean, mutateWithSummary, roll, tidy } from '@tidyjs/tidy'
import { yyyymmdd } from '@/utils/dateUtil'
describe('日ごとの売上の移動平均を計算する', () => {
test('calculateMovingAverage', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('withMovingAvg', ({ input }) =>
tidy(
input,
mutateWithSummary({
movingAvg: roll(3, mean('value'), { partial: true })
})
)
),
bind('output', ({ withMovingAvg }) =>
withMovingAvg.map((d) => ({
...d,
date: yyyymmdd(d.date)
}))
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{
date: '2023-01-01',
product: 'T-Shirt',
category: 'Clothing',
value: 100,
movingAvg: 100
},
{
date: '2023-01-01',
product: 'Sneakers',
category: 'Footwear',
value: 150,
movingAvg: 125
},
{
date: '2023-01-01',
product: 'Watch',
category: 'Accessories',
value: 200,
movingAvg: 150
},
{
date: '2023-01-02',
product: 'Dress',
category: 'Clothing',
value: 110,
movingAvg: 153.33333333333334
},
{
date: '2023-01-02',
product: 'Boots',
category: 'Footwear',
value: 140,
movingAvg: 150
},
{
date: '2023-01-02',
product: 'Necklace',
category: 'Accessories',
value: 220,
movingAvg: 156.66666666666666
},
{
date: '2023-01-02',
product: 'Hat',
category: 'Headwear',
value: 250,
movingAvg: 203.33333333333334
},
{
date: '2023-01-03',
product: 'T-Shirt',
category: 'Clothing',
value: 105,
movingAvg: 191.66666666666666
},
{
date: '2023-01-03',
product: 'Watch',
category: 'Accessories',
value: 215,
movingAvg: 190
},
{
date: '2023-01-03',
product: 'Beanie',
category: 'Headwear',
value: 230,
movingAvg: 183.33333333333334
},
{
date: '2023-01-04',
product: 'Jacket',
category: 'Clothing',
value: 155,
movingAvg: 200
},
{
date: '2023-01-04',
product: 'Earrings',
category: 'Accessories',
value: 225,
movingAvg: 203.33333333333334
},
{
date: '2023-01-05',
product: 'Blouse',
category: 'Clothing',
value: 100,
movingAvg: 160
},
{
date: '2023-01-05',
product: 'Watch',
category: 'Accessories',
value: 210,
movingAvg: 178.33333333333334
},
{
date: '2023-01-05',
product: 'Cap',
category: 'Headwear',
value: 245,
movingAvg: 185
},
{
date: '2023-01-06',
product: 'Sneakers',
category: 'Footwear',
value: 150,
movingAvg: 201.66666666666666
},
{
date: '2023-01-06',
product: 'Scarf',
category: 'Accessories',
value: 220,
movingAvg: 205
},
{
date: '2023-01-07',
product: 'Trousers',
category: 'Clothing',
value: 115,
movingAvg: 161.66666666666666
},
{
date: '2023-01-07',
product: 'Bracelet',
category: 'Accessories',
value: 205,
movingAvg: 180
},
{
date: '2023-01-08',
product: 'Sweater',
category: 'Clothing',
value: 160,
movingAvg: 160
},
{
date: '2023-01-08',
product: 'Hat',
category: 'Headwear',
value: 235,
movingAvg: 200
},
{
date: '2023-01-09',
product: 'T-Shirt',
category: 'Clothing',
value: 105,
movingAvg: 166.66666666666666
},
{
date: '2023-01-09',
product: 'Gloves',
category: 'Accessories',
value: 230,
movingAvg: 190
},
{
date: '2023-01-10',
product: 'Sandals',
category: 'Footwear',
value: 165,
movingAvg: 166.66666666666666
},
{
date: '2023-01-10',
product: 'Ring',
category: 'Accessories',
value: 200,
movingAvg: 198.33333333333334
}
])
})
})
特定のカテゴリ内での商品別の市場シェアを計算する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { filter, groupBy, sum, summarize, tidy } from '@tidyjs/tidy'
import { salesData } from '@/data'
import { Decimal } from '@/utils/decimal'
describe('特定のカテゴリ内での商品別の市場シェアを計算する', () => {
test('calculateProductShare', () => {
const focusedCategory = 'Clothing'
const result = pipe(
salesData,
bindTo('input'),
bind('filtered', ({ input }) =>
tidy(
input,
filter((d) => d.category === focusedCategory)
)
),
bind('withTotal', ({ filtered }) => {
const [result] = tidy(
filtered,
summarize({
total: sum('value'),
})
)
return result.total
}),
bind('withSubTotal', ({ filtered, withTotal }) =>
tidy(
filtered,
groupBy('product', [
summarize({
subTotal: sum('value'),
total: () => withTotal,
}),
])
)
),
bind('output', ({ withSubTotal }) =>
withSubTotal.map((d) => ({
...d,
share: new Decimal(d.subTotal).div(d.total).mul(new Decimal(100)),
}))
),
map(({ output }) => output)
)
const totalShare = result.reduce(
(acc, cur) => (acc = acc.add(cur.share)),
new Decimal(0)
)
expect(totalShare).toStrictEqual(new Decimal(100))
expect(result).toStrictEqual([
{
product: 'T-Shirt',
subTotal: 310,
total: 950,
share: new Decimal(32.63),
},
{
product: 'Dress',
subTotal: 110,
total: 950,
share: new Decimal(11.58),
},
{
product: 'Jacket',
subTotal: 155,
total: 950,
share: new Decimal(16.32),
},
{
product: 'Blouse',
subTotal: 100,
total: 950,
share: new Decimal(10.53),
},
{
product: 'Trousers',
subTotal: 115,
total: 950,
share: new Decimal(12.11),
},
{
product: 'Sweater',
subTotal: 160,
total: 950,
share: new Decimal(16.84),
},
])
})
})
特定の日におけるカテゴリ別の売上の割合を計算する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { salesData } from '@/data'
import {
groupBy,
mutateWithSummary,
summarize,
filter,
tidy,
sum
} from '@tidyjs/tidy'
import {Decimal} from '@/utils/decimal'
import dayjs from 'dayjs'
describe('特定の日におけるカテゴリ別の売上の割合を計算する', () => {
test('calculateSalesRatioOnSpecificDay', () => {
const focusedDate = new Date('2023-01-02')
const result = pipe(
salesData,
bindTo('input'),
bind('filtered', ({ input }) =>
tidy(
input,
filter((d) => dayjs(d.date).isSame(focusedDate))
)
),
bind('summarized', ({ filtered }) =>
tidy(
filtered,
groupBy(
['product'],
summarize({
subTotal: sum('value')
})
),
mutateWithSummary({ total: sum('subTotal') }),
map((d) =>
d.map((d) => ({
...d,
percent: new Decimal(d.subTotal)
.div(new Decimal(d.total))
.mul(new Decimal(100))
}))
),
mutateWithSummary({
totalPercent: (datum) =>
datum.reduce((acc, cur) => acc.add(cur.percent), new Decimal(0))
})
)
),
bind('output', ({ summarized }) => summarized),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{
product: 'Dress',
subTotal: 110,
total: 720,
percent: new Decimal(15.28),
totalPercent: new Decimal(100)
},
{
product: 'Boots',
subTotal: 140,
total: 720,
percent: new Decimal(19.44),
totalPercent: new Decimal(100)
},
{
product: 'Necklace',
subTotal: 220,
total: 720,
percent: new Decimal(30.56),
totalPercent: new Decimal(100)
},
{
product: 'Hat',
subTotal: 250,
total: 720,
percent: new Decimal(34.72),
totalPercent: new Decimal(100)
}
])
})
})
特定のカテゴリの週末の平均売上を計算する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import {
filter,
mean,
n,
select,
sum,
summarize,
tidy
} from '@tidyjs/tidy'
import { salesData } from '@/data'
import dayjs from 'dayjs'
describe('特定のカテゴリの週末の平均売上を計算する', () => {
test('calculateWeekendAverageSalesForCategory', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) => {
const [item] = tidy(
input,
filter((d) => d.category === 'Accessories'),
filter((d) => [6, 0].includes(dayjs(d.date).day())),
summarize({
sum: sum('value'),
count: n(),
avg: mean('value')
}),
select('avg')
)
return item.avg
}),
map(({ output }) => output)
)
expect(result).toStrictEqual(202.5)
})
})
週ごとの総売上の変動を計算する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { distinct, groupBy, max, min, sum, summarize, tidy } from '@tidyjs/tidy'
import { salesData } from '@/data'
import dayjs from 'dayjs'
import { yyyymmdd } from '@/utils/dateUtil'
describe('週ごとの総売上の変動を計算する', () => {
test('calculateWeeklySalesChange', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('grouped', ({ input }) =>
tidy(
input,
map((data) =>
data.map((d) => ({ ...d, week: dayjs(d.date).week() }))
),
groupBy(
['week'],
summarize({
startDate: min('date'),
endDate: max('date'),
total: sum('value'),
categories: (data) =>
tidy(data, distinct(['category'])).map((d) => d.category)
})
)
)
),
bind('output', ({ grouped }) =>
grouped.map((d) => ({
...d,
startDate: yyyymmdd(d.startDate),
endDate: yyyymmdd(d.endDate)
}))
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{
week: 1,
startDate: '2023-01-01',
endDate: '2023-01-07',
total: 3345,
categories: ['Clothing', 'Footwear', 'Accessories', 'Headwear']
},
{
week: 2,
startDate: '2023-01-08',
endDate: '2023-01-10',
total: 1095,
categories: ['Clothing', 'Headwear', 'Accessories', 'Footwear']
}
])
})
})
各カテゴリごとの最も売れた日を特定する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { salesData } from '@/data'
import { groupBy, summarize, tidy } from '@tidyjs/tidy'
import { yyyymmdd } from '@/utils/dateUtil'
describe('各カテゴリごとの最も売れた日を特定する', () => {
test('findBestDayPerCategory', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('summarized', ({ input }) =>
tidy(
input,
groupBy(
['category'],
summarize({
bestDay: (data) =>
data.reduce((acc, cur) => {
if (acc.value < cur.value) {
acc = cur
}
return acc
}, data[0]).date
})
)
)
),
bind('output', ({ summarized }) =>
summarized.map((d) => ({
...d,
bestDay: yyyymmdd(d.bestDay)
}))
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ category: 'Clothing', bestDay: '2023-01-08' },
{ category: 'Footwear', bestDay: '2023-01-10' },
{ category: 'Accessories', bestDay: '2023-01-09' },
{ category: 'Headwear', bestDay: '2023-01-02' }
])
})
})
最も高い売上を記録した3日間の日付を特定する
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import {
arrange,
desc,
select,
sliceHead,
tidy
} from '@tidyjs/tidy'
import { salesData } from '@/data'
import { yyyymmdd } from '@/utils/dateUtil'
describe('最も高い売上を記録した3日間の日付を特定する', () => {
test('findTop3SalesDays', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) =>
tidy(
input,
arrange([desc((d) => d.value)]),
sliceHead(3),
select('date'),
map((data) => data.map((d) => yyyymmdd(d.date)))
)
),
map(({ output }) => output)
)
expect(result).toStrictEqual(['2023-01-02', '2023-01-05', '2023-01-08'])
})
})
特定の年度の成長率を計算
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { filter, tidy } from '@tidyjs/tidy'
import { salesData } from '@/data'
import { yyyy } from '@/utils/dateUtil'
describe('特定の年度の成長率を計算', () => {
test('growthRateForYear', () => {
const focusedYear = 2023
const result = pipe(
salesData,
bindTo('input'),
bind('currentYears', ({ input }) =>
tidy(
input,
filter((d) => parseInt(yyyy(d.date)) === focusedYear)
)
),
bind('currentYearTotal', ({ currentYears }) =>
currentYears.reduce((sum, sale) => sum + sale.value, 0)
),
bind('previousYears', ({ input }) =>
tidy(
input,
filter((d) => parseInt(yyyy(d.date)) === focusedYear - 1)
)
),
bind('previousYearTotal', ({ previousYears }) =>
previousYears.reduce((sum, sale) => sum + sale.value, 0)
),
bind(
'output',
({
previousYears,
currentYears,
previousYearTotal,
currentYearTotal
}) => {
if (previousYears.length === 0 && currentYears.length === 0) { return NaN }
if (previousYearTotal === 0) return 100
return (
((currentYearTotal - previousYearTotal) / previousYearTotal) * 100
)
}
),
map(({ output }) => output)
)
expect(result).toStrictEqual(100)
})
})
商品ごとに売上が特定の閾値を超えた日をリストアップする
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { salesData } from '@/data'
import { groupBy, summarize, tidy } from '@tidyjs/tidy'
import { yyyymmdd } from '@/utils/dateUtil'
describe('商品ごとに売上が特定の閾値を超えた日をリストアップする', () => {
test('listUpHighSalesDaysByProduct', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('summarized', ({ input }) =>
tidy(
input,
groupBy(
['product'],
summarize({
highSalesDays: (data) =>
data.filter((d) => d.value > 150).map((d) => d.date)
})
)
)
),
bind('output', ({ summarized }) =>
summarized.map((d) => ({
...d,
highSalesDays: d.highSalesDays.map(yyyymmdd)
}))
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ product: 'T-Shirt', highSalesDays: [] },
{ product: 'Sneakers', highSalesDays: [] },
{
product: 'Watch',
highSalesDays: ['2023-01-01', '2023-01-03', '2023-01-05']
},
{ product: 'Dress', highSalesDays: [] },
{ product: 'Boots', highSalesDays: [] },
{ product: 'Necklace', highSalesDays: ['2023-01-02'] },
{ product: 'Hat', highSalesDays: ['2023-01-02', '2023-01-08'] },
{ product: 'Beanie', highSalesDays: ['2023-01-03'] },
{ product: 'Jacket', highSalesDays: ['2023-01-04'] },
{ product: 'Earrings', highSalesDays: ['2023-01-04'] },
{ product: 'Blouse', highSalesDays: [] },
{ product: 'Cap', highSalesDays: ['2023-01-05'] },
{ product: 'Scarf', highSalesDays: ['2023-01-06'] },
{ product: 'Trousers', highSalesDays: [] },
{ product: 'Bracelet', highSalesDays: ['2023-01-07'] },
{ product: 'Sweater', highSalesDays: ['2023-01-08'] },
{ product: 'Gloves', highSalesDays: ['2023-01-09'] },
{ product: 'Sandals', highSalesDays: ['2023-01-10'] },
{ product: 'Ring', highSalesDays: ['2023-01-10'] }
])
})
})
カテゴリごとの商品の中央値価格を計算
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { filter, groupBy, median, summarize, tidy } from '@tidyjs/tidy'
import { salesData } from '@/data'
describe('カテゴリごとの商品の中央値価格を計算', () => {
test('medianPriceByProductInCategory -> Accessories', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) =>
tidy(
input,
filter((d) => d.category === 'Accessories'),
groupBy('product', [summarize({ median: median('value') })])
)
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ product: 'Watch', median: 210 },
{ product: 'Necklace', median: 220 },
{ product: 'Earrings', median: 225 },
{ product: 'Scarf', median: 220 },
{ product: 'Bracelet', median: 205 },
{ product: 'Gloves', median: 230 },
{ product: 'Ring', median: 200 }
])
})
})
特定のカテゴリ内の商品ごとの販売割合を計算
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import {
filter,
groupBy,
mutateWithSummary,
sum,
summarize,
tidy
} from '@tidyjs/tidy'
import { salesData } from '@/data'
import { Decimal } from '@/utils/decimal'
import { omit } from 'radash'
describe('特定のカテゴリ内の商品ごとの販売割合を計算', () => {
test('salesPercentageByProductInCategory -> Accessories', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) =>
tidy(
input,
filter((d) => d.category === 'Accessories'),
groupBy('product', [summarize({ subTotal: sum('value') })]),
mutateWithSummary({ total: sum('subTotal') }),
map((d) =>
d.map((d) => ({
...d,
percentage: new Decimal(d.subTotal).div(d.total).mul(new Decimal(100))
}))
)
)
),
map(({ output }) => output.map((d) => omit(d, ['subTotal', 'total'])))
)
const total = result.reduce(
(acc, cur) => (acc = acc.add(cur.percentage)),
new Decimal(0)
)
expect(total).toStrictEqual(new Decimal(100))
expect(result).toStrictEqual([
{ product: 'Watch', percentage: new Decimal(32.47) },
{ product: 'Necklace', percentage: new Decimal(11.43) },
{ product: 'Earrings', percentage: new Decimal(11.69) },
{ product: 'Scarf', percentage: new Decimal(11.43) },
{ product: 'Bracelet', percentage: new Decimal(10.65) },
{ product: 'Gloves', percentage: new Decimal(11.95) },
{ product: 'Ring', percentage: new Decimal(10.39) }
])
})
})
カテゴリごとの合計を計算し、上位 N カテゴリを取得
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import {
arrange,
desc,
groupBy,
sliceHead,
sum,
summarize,
tidy
} from '@tidyjs/tidy'
import { salesData } from '@/data'
describe('カテゴリごとの合計を計算し、上位 N カテゴリを取得', () => {
test('topNCategoriesByValue', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) =>
tidy(
input,
groupBy('category', [summarize({ total: sum('value') })]),
arrange([desc((d) => d.total)]),
sliceHead(3)
)
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ category: 'Accessories', total: 1925 },
{ category: 'Headwear', total: 960 },
{ category: 'Clothing', total: 950 }
])
})
})
特定の期間内のトップNカテゴリを取得
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import {
arrange,
desc,
filter,
groupBy,
sliceHead,
sum,
summarize,
tidy
} from '@tidyjs/tidy'
import { salesData } from '@/data'
import dayjs from 'dayjs'
describe('特定の期間内のトップNカテゴリを取得', () => {
test('topNCategoriesInPeriod', () => {
const startDate = new Date('2023-01-01')
const endDate = new Date('2023-01-02')
const result = pipe(
salesData,
bindTo('input'),
bind('output', ({ input }) =>
tidy(
input,
filter(
(d) =>
(dayjs(d.date).isAfter(dayjs(startDate)) ||
dayjs(d.date).isSame(dayjs(startDate))) &&
(dayjs(d.date).isBefore(dayjs(endDate)) ||
dayjs(d.date).isSame(dayjs(endDate)))
),
groupBy('category', [summarize({ total: sum('value') })]),
arrange([desc((d) => d.total)]),
sliceHead(3)
)
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ category: 'Accessories', total: 420 },
{ category: 'Footwear', total: 290 },
{ category: 'Headwear', total: 250 }
])
})
})
カテゴリ別・月単位の合計を計算
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { salesData } from '@/data'
import { arrange, asc, groupBy, sum, summarize, tidy } from '@tidyjs/tidy'
import { yyyymm } from '@/utils/dateUtil'
describe('カテゴリ別・月単位の合計を計算', () => {
test('totalByCategoryAndMonth', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('mapping', ({ input }) =>
input.map((d) => ({ ...d, month: yyyymm(d.date) }))
),
bind('output', ({ mapping }) =>
tidy(
mapping,
groupBy(['category', 'month'], [summarize({ total: sum('value') })]),
arrange([asc((d) => d.month)])
)
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ category: 'Clothing', month: '2023-01', total: 950 },
{ category: 'Footwear', month: '2023-01', total: 605 },
{ category: 'Accessories', month: '2023-01', total: 1925 },
{ category: 'Headwear', month: '2023-01', total: 960 },
])
})
})
特定の月ごとの合計を計算
import { test, expect, describe } from 'vitest'
import { pipe } from 'fp-ts/lib/function'
import { bind, bindTo, map } from 'fp-ts/lib/Identity'
import { groupBy, sum, summarize, tidy } from '@tidyjs/tidy'
import { salesData } from '@/data'
import { yyyymm } from '@/utils/dateUtil'
describe('特定の月ごとの合計を計算', () => {
test('totalByMonth', () => {
const result = pipe(
salesData,
bindTo('input'),
bind('mapping', ({ input }) =>
input.map((d) => ({ ...d, month: yyyymm(d.date) }))
),
bind('output', ({ mapping }) =>
tidy(mapping, groupBy('month', [summarize({ total: sum('value') })]))
),
map(({ output }) => output)
)
expect(result).toStrictEqual([
{ month: '2023-01', total: 4440 },
])
})
})
おわりに
Tidyjs便利です。
簡単ですが、以上です。
この記事が気に入ったらサポートをしてみませんか?