見出し画像

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便利です。




簡単ですが、以上です。

この記事が気に入ったらサポートをしてみませんか?