参考記事:
[R & SQL] データサイエンス100本ノック+α - 概要・導入
- 設問文は データサイエンス100本ノック(構造化データ加工編) より引用しています。
- すべてのコードはコピー&ペーストでそのまま実行できます。
演習問題
出力イメージ:
customer_id n_date sum_amount
1 CS040214000008 23 NA
2 CS015415000185 22 20153
3 CS010214000010 22 18585
4 CS028415000007 21 19127
5 CS010214000002 21 NA
6 CS017415000097 20 23086
7 CS016415000141 20 18372
8 CS031414000051 19 19202
9 CS014214000023 19 NA
10 CS021514000045 19 NA
...
32 CS007514000094 NA 15735
33 CS009414000059 NA 15492
34 CS030415000034 NA 15468
35 CS015515000034 NA 15300
実行環境の構築について
R コード実行環境の構築に必要な手順は こちら から確認できます。
セットアップが完了すると、必要なパッケージとデータを使用できるようになります。
以下に 2 通りの解答例を示します。
各コードの出力結果を統一させるため、n_date
の降順、sum_amount
の降順、customer_id
の昇順でソートしています。
R (データフレーム操作)
利用するデータ
以下のデータを利用します。
df_receipt
の顧客ID (customer_id
)、売上年⽉⽇ (sales_ymd
)、売上金額 (amount
)
主要なカラムを抜き出してデータの一部を確認します。
df_receipt %>% select(customer_id, sales_ymd, amount)
# A tibble: 104,681 × 3
customer_id sales_ymd amount
<chr> <int> <dbl>
1 CS006214000001 20181103 158
2 CS008415000097 20181118 81
3 CS028414000014 20170712 170
4 ZZ000000000000 20190205 25
5 CS025415000050 20180821 90
6 CS003515000195 20190605 138
7 CS024514000042 20181205 30
...
解答例 (1)
slice_max()
を使用し、同じスコアの顧客を全て抽出する場合の解答例です。
解答コードと実行結果
df_rec = df_receipt %>%
filter(!str_detect(customer_id, "^Z")) %>%
select(customer_id, sales_ymd, amount) %>%
group_by(customer_id)
df_date = df_rec %>%
summarise(n_date = n_distinct(sales_ymd)) %>%
slice_max(n_date, n = 20, with_ties = TRUE)
df_amount = df_rec %>%
summarise(sum_amount = sum(amount)) %>%
slice_max(sum_amount, n = 20, with_ties = TRUE)
df_date %>%
full_join(df_amount, by = "customer_id") %>%
arrange(desc(n_date), desc(sum_amount), customer_id)
# A tibble: 35 × 3
customer_id n_date sum_amount
<chr> <int> <dbl>
1 CS040214000008 23 NA
2 CS015415000185 22 20153
3 CS010214000010 22 18585
4 CS028415000007 21 19127
5 CS010214000002 21 NA
6 CS017415000097 20 23086
7 CS016415000141 20 18372
8 CS031414000051 19 19202
9 CS014214000023 19 NA
10 CS021514000045 19 NA
11 CS021515000172 19 NA
12 CS022515000226 19 NA
13 CS039414000052 19 NA
14 CS007515000107 18 NA
15 CS014415000077 18 NA
16 CS021515000056 18 NA
17 CS021515000211 18 NA
18 CS022515000028 18 NA
19 CS030214000008 18 NA
20 CS031414000073 18 NA
21 CS032415000209 18 NA
22 CS001605000009 NA 18925
...
34 CS030415000034 NA 15468
35 CS015515000034 NA 15300
解説
df_rec
df_rec = df_receipt %>%
filter(!str_detect(customer_id, "^Z")) %>%
select(customer_id, sales_ymd, amount) %>%
group_by(customer_id)
ここでは、顧客データのフィルタリングとグループ化を行います。
df_receipt
から、customer_id
が"Z"
で始まらないレコードを抽出します。- 必要な列 (
customer_id
、sales_ymd
、amount
) を選択し、customer_id
でグループ化します。
df_date
df_date = df_rec %>%
summarise(n_date = n_distinct(sales_ymd)) %>%
slice_max(n_date, n = 20, with_ties = TRUE)
ここでは、顧客ごとの売上日数の集計と上位 20 件の抽出を行います。
df_rec
から各顧客ごとの売上日数(n_date
)を計算します。n_distinct(sales_ymd)
は、ユニークな売上日をカウントします。slice_max(n_date, n = 20, with_ties = TRUE)
により、売上日数が多い上位 20 顧客を抽出します。 同順位に複数の顧客が該当すると、20 件を超える場合があります。
出力結果から、n_date = 18
に該当する顧客が全て選択されたことにより、全体として 21 件分が抽出されたことが分かります。
df_amount
df_amount = df_rec %>%
summarise(sum_amount = sum(amount)) %>%
slice_max(sum_amount, n = 20, with_ties = TRUE)
ここでは、顧客ごとの総売上金額の集計と上位 20 件の抽出を行います。
df_rec
から各顧客ごとの売上金額合計(sum_amount
)を計算します。slice_max(sum_amount, n = 20, with_ties = TRUE)
により、売上金額が高い上位 20 顧客を抽出します。同順位に複数の顧客が該当すると、20 件を超える場合があります。
df_date
と df_amount
の統合およびソート
df_date %>%
full_join(df_amount, by = "customer_id") %>%
arrange(desc(n_date), desc(sum_amount), customer_id)
full_join()
を使用して、df_date
とdf_amount
をcustomer_id
をキーとして完全外部結合します。これにより、売上日数と売上金額合計の各カラムを持つデータフレームが得られます。- 最後に、
arrange()
により、売上日数と売上金額合計の降順およびcustomer_id
の昇順でデータを並べ替えます。
解答例 (2)
arrange() + head(20)
により、単純に上位 20 顧客ずつを抽出する場合の解答例です。
同スコアの一部の顧客が結果に含まれない場合もありますが、slice_max()
を使う処理に比べて計算コストが低く、パフォーマンス面で有利になります。
解答コードと実行結果
df_rec = df_receipt %>%
filter(!str_detect(customer_id, "^Z")) %>%
select(customer_id, sales_ymd, amount) %>%
group_by(customer_id)
df_date = df_rec %>%
summarise(n_date = n_distinct(sales_ymd)) %>%
arrange(desc(n_date), customer_id) %>%
head(20)
df_amount = df_rec %>%
summarise(sum_amount = sum(amount)) %>%
arrange(desc(sum_amount), customer_id) %>%
head(20)
df_date %>%
full_join(df_amount, by = "customer_id") %>%
arrange(desc(n_date), desc(sum_amount), customer_id)
# A tibble: 34 × 3
customer_id n_date sum_amount
<chr> <int> <dbl>
1 CS040214000008 23 NA
2 CS015415000185 22 20153
3 CS010214000010 22 18585
4 CS028415000007 21 19127
5 CS010214000002 21 NA
6 CS017415000097 20 23086
7 CS016415000141 20 18372
8 CS031414000051 19 19202
9 CS014214000023 19 NA
10 CS021514000045 19 NA
11 CS021515000172 19 NA
12 CS022515000226 19 NA
13 CS039414000052 19 NA
14 CS007515000107 18 NA
15 CS014415000077 18 NA
16 CS021515000056 18 NA
17 CS021515000211 18 NA
18 CS022515000028 18 NA
19 CS030214000008 18 NA
20 CS031414000073 18 NA
21 CS001605000009 NA 18925
...
33 CS030415000034 NA 15468
34 CS015515000034 NA 15300
解説
解答例 (1)
より、df_date
と df_amount
の処理を変更しています。
df_date
df_date = df_rec %>%
summarise(n_date = n_distinct(sales_ymd)) %>%
arrange(desc(n_date), customer_id) %>%
head(20)
ここでは、売上日数の上位 20 顧客を抽出します。
df_rec
から顧客ごとの売上日数(n_date
)をカウントします。arrange(desc(n_date), customer_id)
で売上日数の降順に並べ替え、head(20)
で上位 20 顧客を抽出します。
df_amount
df_amount = df_rec %>%
summarise(sum_amount = sum(amount)) %>%
arrange(desc(sum_amount), customer_id) %>%
head(20)
ここでは、売上金額の上位 20 顧客を抽出します。
df_rec
から顧客ごとの売上金額合計(sum_amount
)を計算します。arrange(desc(sum_amount), customer_id)
で売上金額合計の降順に並べ替え、head(20)
で上位 20 顧客を抽出します。
R (データベース操作)
解答例 (1)
データフレーム操作の 解答例 (1) をデータベース操作に適用した解答例です。
df_receipt
をテーブル参照db_receipt
に置き換えます。n_date = n_distinct(sales_ymd) %>% as.integer()
により、売上日数を整数型に変換しています (デフォルトでは DOUBLE 型になるため)。filter(!str_detect(...))
の代わりに%LIKE%
を使用し、構文を簡潔にしています。
db_rec = db_receipt %>%
filter(!(customer_id %LIKE% "Z%")) %>%
select(customer_id, sales_ymd, amount) %>%
group_by(customer_id)
db_date = db_rec %>%
summarise(
n_date = n_distinct(sales_ymd) %>% as.integer()
) %>%
slice_max(n_date, n = 20, with_ties = TRUE)
db_amount = db_rec %>%
summarise(sum_amount = sum(amount)) %>%
slice_max(sum_amount, n = 20, with_ties = TRUE)
db_result = db_date %>%
full_join(db_amount, by = "customer_id") %>%
arrange(desc(n_date), desc(sum_amount), customer_id)
db_result %>% collect()
# A tibble: 35 × 3
customer_id n_date sum_amount
<chr> <int> <dbl>
1 CS040214000008 23 NA
2 CS015415000185 22 20153
3 CS010214000010 22 18585
4 CS028415000007 21 19127
5 CS010214000002 21 NA
6 CS017415000097 20 23086
...
%LIKE%
は dbplyr が認識できないインフィックス関数なので、慣例に従い大文字で記述します。
(詳しくは dbplyr が認識できない式 を参照してください。)db_result %>% collect()
データベース操作の結果を R のデータフレーム (tibble) として取得します。
解答例 (2)
同様に、データフレーム操作の 解答例 (2) をデータベース操作に適用した解答例です。
db_rec = db_receipt %>%
filter(!(customer_id %LIKE% "Z%")) %>%
select(customer_id, sales_ymd, amount) %>%
group_by(customer_id)
db_date = db_rec %>%
summarise(
n_date = n_distinct(sales_ymd) %>% as.integer()
) %>%
arrange(desc(n_date), customer_id) %>%
head(20)
db_amount = db_rec %>%
summarise(sum_amount = sum(amount)) %>%
arrange(desc(sum_amount), customer_id) %>%
head(20)
db_result = db_date %>%
full_join(db_amount, by = "customer_id") %>%
arrange(desc(n_date), desc(sum_amount), customer_id)
db_result %>% collect()
# A tibble: 34 × 3
customer_id n_date sum_amount
<chr> <int> <dbl>
1 CS040214000008 23 NA
2 CS015415000185 22 20153
3 CS010214000010 22 18585
4 CS028415000007 21 19127
5 CS010214000002 21 NA
6 CS017415000097 20 23086
...
SQL
利用するデータ
以下のデータを利用します。
receipt
テーブルの顧客ID (customer_id
)、売上年⽉⽇ (sales_ymd
)、売上金額 (amount
)
主要なカラムを抜き出してデータの一部を確認します。
"SELECT customer_id, sales_ymd, amount FROM receipt" %>% db_get_query(con)
# A tibble: 104,681 × 3
customer_id sales_ymd amount
<chr> <int> <dbl>
1 CS006214000001 20181103 158
2 CS008415000097 20181118 81
3 CS028414000014 20170712 170
4 ZZ000000000000 20190205 25
5 CS025415000050 20180821 90
6 CS003515000195 20190605 138
7 CS024514000042 20181205 30
...
解答例 (1)
自動生成された SQL クエリ
データベース操作の 解答例 (1)
による操作結果 (db_result
) に基づき、自動生成された SQLクエリを show_query()
で確認できます。
db_result %>% show_query(cte = TRUE)
WITH q01 AS (
SELECT customer_id, sales_ymd, amount
FROM receipt
WHERE (NOT((customer_id LIKE 'Z%')))
),
q02 AS (
SELECT customer_id, CAST(COUNT(DISTINCT row(sales_ymd)) AS INTEGER) AS n_date
FROM q01
GROUP BY customer_id
),
q03 AS (
SELECT q01.*, RANK() OVER (ORDER BY n_date DESC) AS col01
FROM q02 q01
),
q04 AS (
SELECT customer_id, n_date
FROM q03 q01
WHERE (col01 <= 20)
),
q05 AS (
SELECT customer_id, SUM(amount) AS sum_amount
FROM q01
GROUP BY customer_id
),
q06 AS (
SELECT q01.*, RANK() OVER (ORDER BY sum_amount DESC) AS col02
FROM q05 q01
),
q07 AS (
SELECT customer_id, sum_amount
FROM q06 q01
WHERE (col02 <= 20)
),
q08 AS (
SELECT
COALESCE(LHS.customer_id, RHS.customer_id) AS customer_id,
n_date,
sum_amount
FROM q04 LHS
FULL JOIN q07 RHS
ON (LHS.customer_id = RHS.customer_id)
)
SELECT q01.*
FROM q08 q01
ORDER BY n_date DESC, sum_amount DESC, customer_id
解答クエリ
このクエリをより簡潔な形に書き直すと、次のようになります。
WITH purchase_data AS (
SELECT
customer_id, sales_ymd, amount
FROM receipt
WHERE customer_id NOT LIKE 'Z%'
),
customer_purchase_dates AS (
SELECT
customer_id,
CAST(COUNT(DISTINCT sales_ymd) AS INTEGER) AS n_date
FROM purchase_data
GROUP BY customer_id
),
ranked_purchase_dates AS (
SELECT
customer_id,
n_date,
RANK() OVER (ORDER BY n_date DESC) AS rank_n_date
FROM customer_purchase_dates
),
customer_total_sales AS (
SELECT
customer_id,
SUM(amount) AS sum_amount
FROM purchase_data
GROUP BY customer_id
),
ranked_total_sales AS (
SELECT
customer_id,
sum_amount,
RANK() OVER (ORDER BY sum_amount DESC) AS rank_sum_amount
FROM customer_total_sales
),
top_customers_by_dates AS (
SELECT customer_id, n_date
FROM ranked_purchase_dates
WHERE rank_n_date <= 20
),
top_customers_by_sales AS (
SELECT customer_id, sum_amount
FROM ranked_total_sales
WHERE rank_sum_amount <= 20
)
SELECT
COALESCE(d.customer_id, s.customer_id) AS customer_id,
d.n_date,
s.sum_amount
FROM top_customers_by_dates d
FULL JOIN top_customers_by_sales s
USING (customer_id)
ORDER BY n_date DESC, sum_amount DESC, customer_id
解説
purchase_data
の作成
WITH purchase_data AS (
SELECT
customer_id, sales_ymd, amount
FROM receipt
WHERE customer_id NOT LIKE 'Z%'
)
receipt
テーブルから、customer_id
が'Z'
で始まる顧客を除外したデータを取得します。customer_id
、sales_ymd
、amount
の3つのカラムを選択します。
customer_purchase_dates
の作成
customer_purchase_dates AS (
SELECT
customer_id,
CAST(COUNT(DISTINCT sales_ymd) AS INTEGER) AS n_date
FROM purchase_data
GROUP BY customer_id
)
purchase_data
から、COUNT(DISTINCT sales_ymd)
により、ユニークな売上日をカウントします。GROUP BY customer_id
で、顧客ごとに集計を行います。
ranked_purchase_dates
の作成
ranked_purchase_dates AS (
SELECT
customer_id,
n_date,
RANK() OVER (ORDER BY n_date DESC) AS rank_n_date
FROM customer_purchase_dates
)
customer_purchase_dates
に対しRANK() OVER(...)
ウィンドウ関数を使用して、売上日数n_date
に基づいてランク付けを行います。売上日数が多い顧客に低いランク (1位〜) を付けます。- 売上日数が同じ顧客には同じランクが付けられます。
customer_total_sales
の作成
customer_total_sales AS (
SELECT
customer_id,
SUM(amount) AS sum_amount
FROM purchase_data
GROUP BY customer_id
)
purchase_data
から売上金額合計 (SUM(amount)
) を計算します。GROUP BY customer_id
で、顧客ごとに集計します。
ranked_total_sales
の作成
ranked_total_sales AS (
SELECT
customer_id,
sum_amount,
RANK() OVER (ORDER BY sum_amount DESC) AS rank_sum_amount
FROM customer_total_sales
)
customer_total_sales
に対しRANK() OVER(...)
ウィンドウ関数を使用して、 売上金額sum_amount
に基づいてランク付けを行います。売上金額が多い顧客に低いランク (1位〜) を付けます。- 売上金額が同じ顧客には同じランクが付けられます。
top_customers_by_dates
の作成
top_customers_by_dates AS (
SELECT customer_id, n_date
FROM ranked_purchase_dates
WHERE rank_n_date <= 20
)
ranked_purchase_dates
から、売上日数の上位 20 顧客 (rank_n_date <= 20
) を抽出します。
top_customers_by_sales
の作成
top_customers_by_sales AS (
SELECT customer_id, sum_amount
FROM ranked_total_sales
WHERE rank_sum_amount <= 20
)
ranked_total_sales
から、売上金額の上位 20 顧客(rank_sum_amount <= 20
)を抽出します。
メインクエリ
SELECT
COALESCE(d.customer_id, s.customer_id) AS customer_id,
d.n_date,
s.sum_amount
FROM top_customers_by_dates d
FULL JOIN top_customers_by_sales s
USING (customer_id)
ORDER BY n_date DESC, sum_amount DESC, customer_id
top_customers_by_dates
とtop_customers_by_sales
をcustomer_id
をキーとしてFULL JOIN
で結合します。COALESCE(d.customer_id, s.customer_id)
により、片方のテーブルにしか存在しないcustomer_id
を適切に処理します。ORDER BY
により、売上日数 (n_date
) の降順、売上金額 (sum_amount
) の降順、customer_id
の昇順で並べ替えます。
実行結果の確認
この SQLクエリの実行結果は、次のようにして確認できます。
query = sql("
WITH purchase_data AS (
SELECT
customer_id, sales_ymd, amount
FROM receipt
WHERE customer_id NOT LIKE 'Z%'
),
customer_purchase_dates AS (
SELECT
customer_id,
CAST(COUNT(DISTINCT sales_ymd) AS INTEGER) AS n_date
FROM purchase_data
GROUP BY customer_id
),
ranked_purchase_dates AS (
SELECT
customer_id,
n_date,
RANK() OVER (ORDER BY n_date DESC) AS rank_n_date
FROM customer_purchase_dates
),
customer_total_sales AS (
SELECT
customer_id,
SUM(amount) AS sum_amount
FROM purchase_data
GROUP BY customer_id
),
ranked_total_sales AS (
SELECT
customer_id,
sum_amount,
RANK() OVER (ORDER BY sum_amount DESC) AS rank_sum_amount
FROM customer_total_sales
),
top_customers_by_dates AS (
SELECT customer_id, n_date
FROM ranked_purchase_dates
WHERE rank_n_date <= 20
),
top_customers_by_sales AS (
SELECT customer_id, sum_amount
FROM ranked_total_sales
WHERE rank_sum_amount <= 20
)
SELECT
COALESCE(d.customer_id, s.customer_id) AS customer_id,
d.n_date,
s.sum_amount
FROM top_customers_by_dates d
FULL JOIN top_customers_by_sales s
USING (customer_id)
ORDER BY n_date DESC, sum_amount DESC, customer_id
"
)
query %>% db_get_query(con)
# A tibble: 35 × 3
customer_id n_date sum_amount
<chr> <int> <dbl>
1 CS040214000008 23 NA
2 CS015415000185 22 20153
3 CS010214000010 22 18585
4 CS028415000007 21 19127
5 CS010214000002 21 NA
6 CS017415000097 20 23086
...
解答例 (2)
自動生成された SQL クエリ
データベース操作の 解答例 (2)
による操作結果 (db_result
) に基づき、自動生成された SQLクエリを show_query()
で確認できます。
db_result %>% show_query(cte = TRUE)
WITH q01 AS (
SELECT customer_id, sales_ymd, amount
FROM receipt
WHERE (NOT((customer_id LIKE 'Z%')))
),
q02 AS (
SELECT customer_id, CAST(COUNT(DISTINCT row(sales_ymd)) AS INTEGER) AS n_date
FROM q01
GROUP BY customer_id
ORDER BY n_date DESC, customer_id
LIMIT 20
),
q03 AS (
SELECT customer_id, SUM(amount) AS sum_amount
FROM q01
GROUP BY customer_id
ORDER BY sum_amount DESC, customer_id
LIMIT 20
),
q04 AS (
SELECT
COALESCE(LHS.customer_id, RHS.customer_id) AS customer_id,
n_date,
sum_amount
FROM q02 LHS
FULL JOIN q03 RHS
ON (LHS.customer_id = RHS.customer_id)
)
SELECT q01.*
FROM q04 q01
ORDER BY n_date DESC, sum_amount DESC, customer_id
解答クエリ
このクエリをより簡潔な形に書き直すと、次のようになります。
WITH purchase_data AS (
SELECT
customer_id,
sales_ymd,
amount
FROM
receipt
WHERE
customer_id NOT LIKE 'Z%'
),
top_customers_by_days AS (
SELECT
customer_id,
CAST(COUNT(DISTINCT sales_ymd) AS INTEGER) AS n_date
FROM
purchase_data
GROUP BY
customer_id
ORDER BY
n_date DESC, customer_id
LIMIT 20
),
top_customers_by_sales AS (
SELECT
customer_id,
SUM(amount) AS sum_amount
FROM
purchase_data
GROUP BY
customer_id
ORDER BY
sum_amount DESC, customer_id
LIMIT 20
)
SELECT
COALESCE(d.customer_id, s.customer_id) AS customer_id,
d.n_date,
s.sum_amount
FROM
top_customers_by_days d
FULL JOIN
top_customers_by_sales s
USING (customer_id)
ORDER BY
n_date DESC, sum_amount DESC, customer_id
解説
解答例 (1)のクエリ
との相違点は、top_customers_by_dates
と top_customers_by_sales
の作成方法です。
top_customers_by_dates
の作成- 顧客ごとにユニークな売上日 (
n_date
) をカウントします。 n_date
の降順で並べ替え、上位 20 人の顧客をLIMIT 20
で抽出します。
- 顧客ごとにユニークな売上日 (
top_customers_by_sales
の作成- 顧客ごとに売上金額合計 (
sum_amount
) を集計します。 sum_amount
の降順で並べ替え、上位 20 人の顧客をLIMIT 20
で抽出します。
- 顧客ごとに売上金額合計 (
実行結果の確認
この SQLクエリの実行結果は、次のようにして確認できます。
query = sql("
WITH purchase_data AS (
SELECT
customer_id,
sales_ymd,
amount
FROM
receipt
WHERE
customer_id NOT LIKE 'Z%'
),
top_customers_by_dates AS (
SELECT
customer_id,
CAST(COUNT(DISTINCT sales_ymd) AS INTEGER) AS n_date
FROM
purchase_data
GROUP BY
customer_id
ORDER BY
n_date DESC, customer_id
LIMIT 20
),
top_customers_by_sales AS (
SELECT
customer_id,
SUM(amount) AS sum_amount
FROM
purchase_data
GROUP BY
customer_id
ORDER BY
sum_amount DESC, customer_id
LIMIT 20
)
SELECT
COALESCE(d.customer_id, s.customer_id) AS customer_id,
d.n_date,
s.sum_amount
FROM
top_customers_by_dates d
FULL JOIN
top_customers_by_sales s
USING (customer_id)
ORDER BY
n_date DESC, sum_amount DESC, customer_id
"
)
query %>% db_get_query(con)
# A tibble: 34 × 3
customer_id n_date sum_amount
<chr> <int> <dbl>
1 CS040214000008 23 NA
2 CS015415000185 22 20153
3 CS010214000010 22 18585
4 CS028415000007 21 19127
5 CS010214000002 21 NA
6 CS017415000097 20 23086
...