📈

データサイエンス100本ノック(構造化データ加工編) を Ruby の RedAmber で書く。

に公開

100本ノックについて

データサイエンス100本ノック(構造化データ加工編) というのがあります。

https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess

模範回答は、SQL、R、Pythonの3つが用意されています。

で、以前にそれを Julia で解いたのですが、
https://zenn.dev/tkmfujise/articles/8c7e5b0a1caa6b

今回は、Ruby の red-data-tools プロジェクトの1つである RedAmber がおもしろそうだったので、それで解いてみました。
https://github.com/red-data-tools/red_amber

と言っても、↓ RedAmber の作者様御自身が100本ノックを解いたリポジトリがあるので、それを見ながら試してみたという感じです。詳細はそちらをご覧になったほうが断然いいとは思うんですが、
https://github.com/heronshoes/100knocks-preprocess-ruby

ここでは以前に Julia で解いた問題をかいつまんで記事にします。

CSVの読み込み

require 'red_amber'
include RedAmber

dir = 'path/to/dir'

df_customer = DataFrame.load "#{dir}/customer.csv"
df_category = DataFrame.load "#{dir}/category.csv"
df_product  = DataFrame.load "#{dir}/product.csv"
df_receipt  = DataFrame.load "#{dir}/receipt.csv"
df_store    = DataFrame.load "#{dir}/store.csv"
df_geocode  = DataFrame.load "#{dir}/geocode.csv"

001

レシート明細データ(df_receipt)から全項目の先頭10件を表示し、どのようなデータを保有しているか目視で確認せよ。

df_receipt.head 10

or

df_receipt.first 10

エイリアスが用意されているのが、Ruby らしいなと思いました。

004

レシート明細データ(df_receipt)から売上日(sales_ymd)、顧客ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の条件を満たすデータを抽出せよ。
顧客ID(customer_id)が"CS018205000001"

df_receipts.pick(:sales_ymd, :customer_id, :product_cd, :amount)
  .slice{ customer_id == 'CS018205000001' }

or

df_receipts.pick(:sales_ymd, :customer_id, :product_cd, :amount)
  .filter{ customer_id == 'CS018205000001' }
sales_ymd customer_id product_cd amount
20180911 CS018205000001 P071401012 2200
20180414 CS018205000001 P060104007 600
20170614 CS018205000001 P050206001 990

列の抽出は pick
条件の指定は slicefilter のどちらでも同じ結果です。
条件をブロックで書けるのがすっきりしていて気持ちいいです。

010

店舗データ(df_store)から、店舗コード(store_cd)が"S14"で始まるものだけ全項目抽出し、10件表示せよ。

#df_store.slice{ store_cd =~ /^S14/ }.fisrt 10 # undefined method `=~'
df_store.slice{ store_cd.start_with? 'S14' }.first 10

=~ でいけるかなと思ったんですが、それはエラーでした。

018

顧客データ(df_customer)を生年月日(birth_day)で若い順にソートし、先頭から全項目を10件表示せよ。

df_customer.sort('-birth_day').first 10

sort('birth_day') で昇順、sort('-birth_day') で降順になります。

019

レシート明細データ(df_receipt)に対し、1件あたりの売上金額(amount)が高い順にランクを付与し、先頭から10件表示せよ。項目は顧客ID(customer_id)、売上金額(amount)、付与したランクを表示させること。なお、売上金額(amount)が等しい場合は同一順位を付与するものとする。

df_receipt.sort('-amount')
  .assign(:amount_rank){ amount.rank(:descending, tie: :min) }
  .pick(:customer_id, :amount, :amount_rank)
  .first(10)
customer_id amount amount_rank
CS011415000006 10925 1
ZZ000000000000 6800 2
CS028605000002 5780 3
CS015515000034 5480 4
ZZ000000000000 5480 4
ZZ000000000000 5480 4
ZZ000000000000 5440 7
CS021515000089 5440 7
CS015515000083 5280 9
CS017414000114 5280 9

列の追加は assign

rank の使い方はちょっと初見だと見慣れない感じのメソッドでしたが、
REPL (pry) から ? df_receipt.amount.rank で使い方を確認。

param order [:ascending, '+', :descending, '-']
  the order of the elements should be ranked in.
  - :ascending or '+' : rank is computed in ascending order.
  - :descending or '-' : rank is computed in descending order.
param tie [:first, :min, :max, :dense]
  configure how ties between equal values are handled.
  - first: Ranks are assigned in order of when ties appear in the input.
  - min: Ties get the smallest possible rank in the sorted order.
  - max: Ties get the largest possible rank in the sorted order.
  - dense: The ranks span a dense [1, M] interval where M is the number
    of distinct values in the input.

ascending+, descending- で指定できるらしい。
ということで試してみたら同じ結果でした。

df_receipt.sort('-amount')
  .assign(:amount_rank){ amount.rank(:-, tie: :min) }
  .pick(:customer_id, :amount, :amount_rank)
  .first(10)

慣れ親しんだ pry で探索できるのは楽しい。

023

レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)と売上数量(quantity)を合計せよ。

df_receipt.group(:store_cd)
  .summarize(:amount, :quantity){ [sum(:amount), sum(:quantity)] }
store_cd amount quantity
S14006 712839 2284
S13008 809288 2491
S14028 786145 2458

初見のメソッドに遭遇したら、とりあえず pry

df_receipt.group(:store_cd).class
=> RedAmber::Group
> ls df_receipt.group(:store_cd)
Enumerable#methods: 
  all?            detect            filter      group_by  minmax_by     slice_before  to_set
  any?            drop              filter_map  include?  none?         slice_when    uniq  
  chain           drop_while        find        inject    one?          sort          zip   
  chunk           each_cons         find_all    lazy      partition     sort_by     
  chunk_while     each_entry        find_index  map       reduce        take        
  collect         each_slice        first       max_by    reject        take_while  
  collect_concat  each_with_index   flat_map    member?   reverse_each  tally       
  compact         each_with_object  grep        min_by    select        to_a        
  cycle           entries           grep_v      minmax    slice_after   to_h        
RedAmber::Group#methods: 
  agg_sum             count           dataframe    group_keys     mean    one      summarize
  all                 count_all       each         grouped_frame  median  product  variance 
  any                 count_distinct  filters      inspect        min     stddev 
  approximate_median  count_uniq      group_count  max            none    sum    
instance variables: @dataframe  @group  @group_keys

$ df_receipt.group(:store_cd).summarize でメソッドを調べると、instance_eval していた。

試しに group した状態で summarize せずに、sum してみる。

df_receipt.group(:store_cd).sum(:amount)
store_cd sum(amount)
S14006 712839
S13008 809288
S14028 786145

1列の結果が取れた。
じゃあ Array 渡したらどうなるんだろうと自然と試してみる。

df_receipt.group(:store_cd).sum([:amount, :quantity])
store_cd sum(amount) sum(quantity)
S14006 712839 2284
S13008 809288 2491
S14028 786145 2458

summarize したのと同じ結果が取れた。
動いてほしいとおりに書いたら動きました。書き方はいろいろありそうです。
summarize イズなんなのかはとりあえず飛ばして進めます。
(列名が summarize すると与えられてるっぽい?)

024

レシート明細データ(df_receipt)に対し、顧客ID(customer_id)ごとに最も新しい売上年月日(sales_ymd)を求め、10件表示せよ。

df_receipt.group(:customer_id).max(:sales_ymd).sort(:customer_id).first 10
customer_id max(sales_ymd)
CS001113000004 20190308
CS001114000005 20190731
CS001115000010 20190405

多分 max だろうなと思って解答見ずに書いたら動いた。気持ちいい。

031

レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)の標準偏差を計算し、降順で5件表示せよ。

df_receipt.group(:store_cd).methods.grep /std/
=> [:stddev]

stddev が標準偏差を求めるメソッドらしいので試す。

df_receipt.group(:store_cd)
  .summarize(:amount_std){ stddev(:amount) }
  .sort('-amount_std')
store_cd amount_std
S13052 663.392
S14011 553.457
S14034 544.904
S13001 543.537

動いた。

感想

とりあえず前半部分を解いてみましたが、触っていて気持ちよかったです。

気になった方は、RedAmber 作者の heronshoes さんのリポジトリに 100 問全部あるのでそちらをどうぞ。
https://github.com/heronshoes/100knocks-preprocess-ruby/blob/main/doc/qmd/preprocess_knock_Ruby-RedAmber.qmd

RedAmber はいいぞ。

Discussion