データサイエンス100本ノック(構造化データ加工編) を Ruby の RedAmber で書く。
100本ノックについて
データサイエンス100本ノック(構造化データ加工編) というのがあります。
模範回答は、SQL、R、Pythonの3つが用意されています。
で、以前にそれを Julia で解いたのですが、
今回は、Ruby の red-data-tools プロジェクトの1つである RedAmber がおもしろそうだったので、それで解いてみました。
と言っても、↓ RedAmber の作者様御自身が100本ノックを解いたリポジトリがあるので、それを見ながら試してみたという感じです。詳細はそちらをご覧になったほうが断然いいとは思うんですが、
ここでは以前に 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
条件の指定は slice と filter のどちらでも同じ結果です。
条件をブロックで書けるのがすっきりしていて気持ちいいです。
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 問全部あるのでそちらをどうぞ。
RedAmber はいいぞ。
Discussion