参考記事:
[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_range
LEFT JOIN
とUSING
all_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