「データサイエンス100本ノック(構造化データ加工編)」の R と SQL をオリジナルに実装したコードを丁寧に解説するシリーズです。 本編にはない、R によるデータベース操作の方法についても、あわせて紹介しています。
R のデータ加工は dplyr をベースとしており、データフレーム操作とデータベース操作の両面から学べる構成になっています。
RStudio や VSCode など、一般的な R の開発環境でそのまま実行できるようにしているので、実践的なスキルを身につけたい方におすすめです。
参考情報:
[R & SQL] データサイエンス100本ノック+α - 概要・導入
- 設問文は データサイエンス100本ノック(構造化データ加工編) より引用しています。
- すべてのコードはコピー&ペーストでそのまま実行できます。
演習問題
出力イメージ:
age_range gender_cd sum_amount
1 0-9 00 0
2 10-19 00 1591
3 20-29 00 72940
4 30-39 00 177322
5 40-49 00 19355
6 50-59 00 54320
7 60-69 00 272469
8 70-79 00 13435
9 80-89 00 46360
10 90-99 00 0
11 100+ 00 0
12 0-9 01 0
13 10-19 01 149836
14 20-29 01 1363724
...
33 100+ 99 0実行環境の構築について
R コード実行環境の構築に必要な手順は こちらから確認できます 。 セットアップが完了すると、必要なパッケージとデータを使用できるようになります。
ここでは、元のデータから縦持ちさせ、年代、性別コード、売上金額の 3 項目に変換する解答例を紹介します。
R (データフレーム操作)
利用するデータ
以下のデータを利用します。
主要なカラムを抜き出してデータの一部を確認します。
df_customerの顧客ID (customer_id)、性別コード (gender_cd)、年齢 (age)
df_customer %>% select(customer_id, gender_cd, age)# A tibble: 21,971 × 3
customer_id gender_cd age
<chr> <chr> <int>
1 CS021313000114 1 37
2 CS037613000071 9 66
3 CS031415000172 1 42
4 CS028811000001 1 86
5 CS001215000145 1 24
6 CS020401000016 0 44
7 CS015414000103 1 41
...df_receiptの顧客ID (customer_id) と売上金額 (amount)
df_receipt %>% select(customer_id, amount)# A tibble: 104,681 × 2
customer_id amount
<chr> <dbl>
1 CS006214000001 158
2 CS008415000097 81
3 CS028414000014 170
4 ZZ000000000000 25
5 CS025415000050 90
6 CS003515000195 138
7 CS024514000042 30
...解答例と実行結果
max_age = df_customer$age %>% max(na.rm = TRUE)
df_sales = df_customer %>%
inner_join(
df_receipt, by = "customer_id"
) %>%
mutate(
age_range =
epikit::age_categories(
age,
lower = 0,
upper = floor(max_age / 10) * 10 + 10,
by = 10
)
) %>%
summarise(
sum_amount = sum(amount),
.by = c("gender_cd", "age_range")
) %>%
mutate(
across(
gender_cd,
~ forcats::lvls_revalue(.x, c("00", "01", "99"))
)
)
df_sales %>%
tidyr::complete(
age_range, gender_cd,
fill = list(sum_amount = 0.0)
) %>%
arrange(gender_cd, age_range)# A tibble: 33 × 3
age_range gender_cd sum_amount
<fct> <fct> <dbl>
1 0-9 00 0
2 10-19 00 1591
3 20-29 00 72940
4 30-39 00 177322
5 40-49 00 19355
6 50-59 00 54320
7 60-69 00 272469
8 70-79 00 13435
9 80-89 00 46360
10 90-99 00 0
11 100+ 00 0
12 0-9 01 0
13 10-19 01 149836
14 20-29 01 1363724
15 30-39 01 693047
16 40-49 01 9320791
17 50-59 01 6685192
18 60-69 01 987741
19 70-79 01 29764
20 80-89 01 262923
21 90-99 01 6260
22 100+ 01 0
23 0-9 99 0
24 10-19 99 4317
25 20-29 99 44328
26 30-39 99 50441
27 40-49 99 483512
28 50-59 99 342923
29 60-69 99 71418
30 70-79 99 2427
31 80-89 99 5111
32 90-99 99 0
33 100+ 99 0解説
年齢の最大値を取得
max_age = df_customer$age %>% max(na.rm = TRUE)df_customer データフレームの age 列の最大値を取得します。
df_customer と df_receipt を結合
df_sales = df_customer %>%
inner_join(
df_receipt, by = "customer_id"
)customer_id をキーに df_customer と df_receipt を内部結合 (inner_join) し、顧客ごとの購買データを作成します。
年齢を 10 歳ごとのカテゴリに変換
mutate(
age_range =
epikit::age_categories(
age,
lower = 0,
upper = floor(!!max_age / 10) * 10 + 10,
by = 10
)
)年齢を 10 歳ごとの範囲に分類し、age_range 列としてデータフレームに追加します。
age_categories()は、指定された年齢 (age) を範囲ごとに分類します。lower = 0で年齢範囲の下限を 0 歳に設定します。upper = floor(!!max_age / 10) * 10 + 10で、max_ageに基づいた上限年齢を計算し、年齢範囲の上限に設定します。by = 10で、10 歳ごとに年齢を分類します。
性別と年齢範囲別に売上金額を集計
summarise(
sum_amount = sum(amount),
.by = c("gender_cd", "age_range")
)gender_cd と age_range ごとに売上金額 amount を合計します。
性別コードをラベルに変換
mutate(
across(
gender_cd,
~ forcats::lvls_revalue(.x, c("00", "01", "99"))
)
)gender_cd 列に対して forcats::lvls_revalue() を適用し、0 を 00、1 を 01、9 を 99 に変換します。
ここまでの処理により、df_sales は次のようなデータフレームになります。
# A tibble: 25 × 3
gender_cd age_range sum_amount
<fct> <fct> <dbl>
1 01 40-49 9320791
2 01 20-29 1363724
3 01 50-59 6685192
4 01 30-39 693047
5 99 40-49 483512
6 99 30-39 50441
7 99 20-29 44328
8 00 60-69 272469
...
25 99 10-19 4317性別・年齢範囲の全パターンの作成と並び替え
df_sales %>%
tidyr::complete(
age_range, gender_cd,
fill = list(sum_amount = 0.0)
) %>%
arrange(gender_cd, age_range)tidyr::complete()により、全てのage_range×gender_cdの組み合わせを作成します。元のデータに存在しない組み合わせには、
sum_amountを0.0で埋めます。arrange()で、gender_cdとage_rangeの昇順に並び替えます。
R (データベース操作)
利用するデータ
以下のデータを利用します。
主要なカラムを抜き出してデータの一部を確認します。
db_customerの顧客ID (customer_id)、性別コード (gender_cd)、年齢 (age)
db_customer %>% select(customer_id, gender_cd, age) %>% collect() customer_id gender_cd age
<chr> <chr> <int>
1 CS021313000114 1 37
2 CS037613000071 9 66
3 CS031415000172 1 42
4 CS028811000001 1 86
5 CS001215000145 1 24
6 CS020401000016 0 44
7 CS015414000103 1 41
...db_receiptの顧客ID (customer_id) と売上金額 (amount)
db_receipt %>% select(customer_id, amount) %>% collect() customer_id amount
<chr> <dbl>
1 CS006214000001 158
2 CS008415000097 81
3 CS028414000014 170
4 ZZ000000000000 25
5 CS025415000050 90
6 CS003515000195 138
7 CS024514000042 30
...解答例と実行結果
dbplyr は epikit::age_categories() に対応していないため、年齢を用いた計算により 10 歳単位に分類します。
db_sales = db_customer %>%
inner_join(
db_receipt %>% select(customer_id, amount),
by = "customer_id"
) %>%
mutate(
age_range =
(floor(age / 10) * 10) %>% as.integer()
) %>%
summarise(
sum_amount = sum(amount),
.by = c("gender_cd", "age_range")
) %>%
mutate(
gender_cd = case_match(
gender_cd,
"0" ~ "00",
"1" ~ "01",
.default = "99"
)
)
db_result = db_sales %>%
tidyr::complete(
age_range, gender_cd,
fill = list(sum_amount = 0.0)
) %>%
arrange(gender_cd, age_range)
db_result %>% collect()# A tibble: 27 × 3
age_range gender_cd sum_amount
<int> <chr> <dbl>
1 10 00 1591
2 20 00 72940
3 30 00 177322
4 40 00 19355
5 50 00 54320
6 60 00 272469
7 70 00 13435
8 80 00 46360
9 90 00 0
10 10 01 149836
11 20 01 1363724
12 30 01 693047
13 40 01 9320791
14 50 01 6685192
15 60 01 987741
16 70 01 29764
17 80 01 262923
18 90 01 6260
19 10 99 4317
20 20 99 44328
21 30 99 50441
22 40 99 483512
23 50 99 342923
24 60 99 71418
25 70 99 2427
26 80 99 5111
27 90 99 0解説
inner_join()db_customerとdb_receiptをcustomer_idをキーとして内部結合します。db_receiptからcustomer_idとamountのみを選択しています。
mutate(age_range ...)- 年齢 (
age) を 10 歳単位に切り捨てて、age_range列を作成します。 floor(age / 10) * 10で年齢を 10 歳ごとに分類し、それを整数型に変換しています。
- 年齢 (
summarise()gender_cdとage_rangeごとにamountの合計 (sum(amount)) を求めます。
mutate(gender_cd ...)gender_cd列に対してcase_match()を使用し、0を00、1を01、9を99に変換します。
ここまでの処理により、
db_salesは次のようなデータになります。gender_cd age_range sum_amount <chr> <int> <dbl> 1 01 20 1363724 2 99 20 44328 3 00 80 46360 4 01 10 149836 5 99 60 71418 6 01 60 987741 7 99 10 4317 ...tidyr::complete()- 全ての
age_range×gender_cdの組み合わせを作成します。 - 元のデータに存在しない組み合わせには、
sum_amountを0.0で埋めます。
- 全ての
arrange()gender_cdとage_rangeの昇順に並び替えます。
db_result %>% collect()- データベース操作の結果を R のデータフレーム (tibble) として取得します。
SQL
利用するデータ
以下のデータを利用します。
主要なカラムを抜き出してデータの一部を確認します。
customerテーブルの顧客ID (customer_id)、性別コード (gender_cd)、年齢 (age)
"SELECT customer_id, gender_cd, age FROM customer" %>% db_get_query(con)# A tibble: 21,971 × 3
customer_id gender_cd age
<chr> <chr> <int>
1 CS021313000114 1 37
2 CS037613000071 9 66
3 CS031415000172 1 42
4 CS028811000001 1 86
5 CS001215000145 1 24
6 CS020401000016 0 44
7 CS015414000103 1 41
...receiptテーブルの顧客ID (customer_id) と売上金額 (amount)
"SELECT customer_id, amount FROM receipt" %>% db_get_query(con)# A tibble: 104,681 × 2
customer_id amount
<chr> <dbl>
1 CS006214000001 158
2 CS008415000097 81
3 CS028414000014 170
4 ZZ000000000000 25
5 CS025415000050 90
6 CS003515000195 138
7 CS024514000042 30
...自動生成された SQL クエリ
データベース操作による結果 (db_result) に基づき、自動生成された SQLクエリを show_query() で確認できます。
db_result %>% show_query(cte = TRUE)WITH q01 AS (
SELECT customer.*, amount
FROM customer
INNER JOIN receipt
ON (customer.customer_id = receipt.customer_id)
),
q02 AS (
SELECT q01.*, CAST((FLOOR(age / 10.0) * 10.0) AS INTEGER) AS age_range
FROM q01
),
q03 AS (
SELECT DISTINCT age_range
FROM q02 q01
GROUP BY gender_cd, age_range
),
q04 AS (
SELECT customer.*, amount
FROM customer
INNER JOIN receipt
ON (customer.customer_id = receipt.customer_id)
),
q05 AS (
SELECT q01.*, CAST((FLOOR(age / 10.0) * 10.0) AS INTEGER) AS age_range
FROM q04 q01
),
q06 AS (
SELECT DISTINCT gender_cd
FROM q05 q01
GROUP BY gender_cd, age_range
),
q07 AS (
SELECT age_range, gender_cd
FROM q03 LHS
CROSS JOIN q06 RHS
),
q08 AS (
SELECT customer.*, amount
FROM customer
INNER JOIN receipt
ON (customer.customer_id = receipt.customer_id)
),
q09 AS (
SELECT q01.*, CAST((FLOOR(age / 10.0) * 10.0) AS INTEGER) AS age_range
FROM q08 q01
),
q10 AS (
SELECT gender_cd, age_range, SUM(amount) AS sum_amount
FROM q09 q01
GROUP BY gender_cd, age_range
),
q11 AS (
SELECT
COALESCE(LHS.age_range, RHS.age_range) AS age_range,
COALESCE(LHS.gender_cd, RHS.gender_cd) AS gender_cd,
sum_amount
FROM q07 LHS
FULL JOIN q10 RHS
ON (LHS.age_range = RHS.age_range AND LHS.gender_cd = RHS.gender_cd)
),
q12 AS (
SELECT age_range, gender_cd, COALESCE(sum_amount, 0.0) AS sum_amount
FROM q11 q01
)
SELECT
age_range,
CASE
WHEN (gender_cd IN ('0')) THEN '00'
WHEN (gender_cd IN ('1')) THEN '01'
ELSE '99'
END AS gender_cd,
sum_amount
FROM q12 q01
ORDER BY gender_cd, age_range解答例
このクエリを参考にして、次の解答例が得られます。
WITH joined_data AS (
SELECT
c.customer_id,
CAST((FLOOR(age / 10.0) * 10.0) AS INTEGER) AS age_range,
CASE c.gender_cd
WHEN '0' THEN '00'
WHEN '1' THEN '01'
ELSE '99'
END AS gender_cd,
r.amount
FROM
customer c
INNER JOIN
receipt r
USING(customer_id)
),
all_combinations AS (
SELECT
jd.age_range, g.gender_cd
FROM
(SELECT DISTINCT age_range FROM joined_data) AS jd
CROSS JOIN
(VALUES ('00'), ('01'), ('99')) AS g(gender_cd)
),
sales_summary AS (
SELECT
gender_cd,
age_range,
SUM(amount) AS sum_amount
FROM
joined_data
GROUP BY
gender_cd, age_range
)
SELECT
ac.age_range,
ac.gender_cd,
COALESCE(ss.sum_amount, 0.0) AS sum_amount
FROM
all_combinations ac
LEFT JOIN
sales_summary ss
USING
(age_range, gender_cd)
ORDER BY
ac.gender_cd, ac.age_range解説
1. joined_data の作成
WITH joined_data AS (
SELECT
c.customer_id,
CAST((FLOOR(age / 10.0) * 10.0) AS INTEGER) AS age_range,
CASE c.gender_cd
WHEN '0' THEN '00'
WHEN '1' THEN '01'
ELSE '99'
END AS gender_cd,
r.amount
FROM
customer c
INNER JOIN
receipt r
USING(customer_id)
)customer テーブルと receipt テーブルを結合し、age と gender_cd を変換します。
INNER JOINによる結合customer_idをキーにcustomerテーブルとreceiptテーブルを内部結合します。
年齢範囲 (
age_range) の計算ageを 10 歳刻みの年齢範囲に変換します。(例:ageが 23 の場合、age_rangeは 20。)FLOOR(age / 10.0) * 10.0により、年齢を 10 の倍数に切り下げます。
性別 (
gender_cd) の変換gender_cdを'0'→'00'、'1'→'01'、それ以外 →'99'に変換します。
2. all_combinations の作成
all_combinations AS (
SELECT
jd.age_range, g.gender_cd
FROM
(SELECT DISTINCT age_range FROM joined_data) AS jd
CROSS JOIN
(VALUES ('00'), ('01'), ('99')) AS g(gender_cd)
)CROSS JOINによる全組み合わせの作成age_rangeのユニークな値とgender_cdの 3 種類 ('00','01','99') の全組み合わせを作成します。age_rangeはjoined_dataから取得し、gender_cdはVALUES句で指定しています。
3. sales_summary の作成
sales_summary AS (
SELECT
gender_cd,
age_range,
SUM(amount) AS sum_amount
FROM
joined_data
GROUP BY
gender_cd, age_range
)- 売上金額 (
sum_amount) の集計SUM(amount)とGROUP BYにより、(gender_cd,age_range) の各組み合わせごとに売上金額を集計します。
4. メインの SELECT 文と結合
SELECT
ac.age_range,
ac.gender_cd,
COALESCE(ss.sum_amount, 0.0) AS sum_amount
FROM
all_combinations ac
LEFT JOIN
sales_summary ss
USING
(age_range, gender_cd)
ORDER BY
ac.gender_cd, ac.age_rangeLEFT JOINとUSINGall_combinationsを基準に、(age_range,gender_cd) をキーとしてsales_summaryをLEFT JOIN(左外部結合) します。
COALESCE()COALESCE(ss.sum_amount, 0.0)により、売上金額がNULLの場合は0.0に置き換えます。
ORDER BY- 結果を
gender_cdとage_rangeの昇順に並び替えます。
- 結果を
実行結果の確認
この SQLクエリの実行結果は、次のようにして確認できます。
query = sql("
WITH joined_data AS (
SELECT
c.customer_id,
CAST((FLOOR(age / 10.0) * 10.0) AS INTEGER) AS age_range,
CASE c.gender_cd
WHEN '0' THEN '00'
WHEN '1' THEN '01'
ELSE '99'
END AS gender_cd,
r.amount
FROM
customer c
INNER JOIN
receipt r
USING(customer_id)
),
all_combinations AS (
SELECT
jd.age_range, g.gender_cd
FROM
(SELECT DISTINCT age_range FROM joined_data) AS jd
CROSS JOIN
(VALUES ('00'), ('01'), ('99')) AS g(gender_cd)
),
sales_summary AS (
SELECT
gender_cd,
age_range,
SUM(amount) AS sum_amount
FROM
joined_data
GROUP BY
gender_cd, age_range
)
SELECT
ac.age_range,
ac.gender_cd,
COALESCE(ss.sum_amount, 0.0) AS sum_amount
FROM
all_combinations ac
LEFT JOIN
sales_summary ss
USING
(age_range, gender_cd)
ORDER BY
ac.gender_cd, ac.age_range
"
)
query %>% db_get_query(con)# A tibble: 27 × 3
age_range gender_cd sum_amount
<int> <chr> <dbl>
1 10 00 1591
2 20 00 72940
3 30 00 177322
4 40 00 19355
5 50 00 54320
6 60 00 272469
7 70 00 13435
8 80 00 46360
9 90 00 0
10 10 01 149836
11 20 01 1363724
12 30 01 693047
13 40 01 9320791
14 50 01 6685192
15 60 01 987741
16 70 01 29764
17 80 01 262923
18 90 01 6260
19 10 99 4317
20 20 99 44328
21 30 99 50441
22 40 99 483512
23 50 99 342923
24 60 99 71418
25 70 99 2427
26 80 99 5111
27 90 99 0![Featured image of post [R & SQL] データサイエンス100本ノック+α - R-044 (解説)](/images/ds-drills/free/cover-drills-free_R-044.png)