第4章 顧客の分析
In [44]:
Copied!
import polars as pl
import numpy as np
import datetime
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly.express as px
import polars as pl
import numpy as np
import datetime
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly.express as px
In [2]:
Copied!
url_data_4_1 = 'https://raw.githubusercontent.com/asakura-data-science/marketing/main/Chapter_4/in/sec4-1data.csv'
df = (
pl.read_csv(url_data_4_1)
.with_columns(pl.col('日付').cast(pl.Utf8).str.to_date('%Y%m%d'))
)
df.head()
url_data_4_1 = 'https://raw.githubusercontent.com/asakura-data-science/marketing/main/Chapter_4/in/sec4-1data.csv'
df = (
pl.read_csv(url_data_4_1)
.with_columns(pl.col('日付').cast(pl.Utf8).str.to_date('%Y%m%d'))
)
df.head()
Out[2]:
shape: (5, 16)
モニタ | 日付 | 購入数量 | 単価 | 金額 | 大分類 | 中分類 | 小分類 | 細分類 | 性別 | 年代 | 未既婚 | 大分類名 | 中分類名 | 小分類名 | 細分類名 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
i64 | date | i64 | i64 | i64 | i64 | i64 | i64 | i64 | i64 | i64 | i64 | str | str | str | str |
15 | 2013-07-15 | 1 | 118 | 118 | 1 | 14 | 1403 | 140397 | 2 | 10 | 3 | "食品" | "飲料・酒類" | "清涼飲料" | "その他清涼飲料" |
15 | 2013-07-15 | 1 | 128 | 128 | 1 | 11 | 1107 | 110707 | 2 | 10 | 3 | "食品" | "加工食品" | "冷凍食品" | "冷凍調理" |
15 | 2013-07-15 | 1 | 140 | 140 | 2 | 24 | 2404 | 240413 | 2 | 10 | 3 | "日用品" | "家庭用品" | "台所用品" | "キッチンペーパー" |
15 | 2013-07-15 | 1 | 78 | 78 | 1 | 12 | 1203 | 120397 | 2 | 10 | 3 | "食品" | "生鮮食品" | "農産" | "その他農産" |
15 | 2013-07-15 | 2 | 83 | 166 | 2 | 26 | 2622 | 262201 | 2 | 10 | 3 | "日用品" | "ペット用品" | "猫" | "猫フード" |
In [3]:
Copied!
# モニタ別金額集計
(
df.group_by('モニタ').agg(pl.col('金額').sum())
.sort(by='金額', descending=True)
.head(10)
)
# モニタ別金額集計
(
df.group_by('モニタ').agg(pl.col('金額').sum())
.sort(by='金額', descending=True)
.head(10)
)
Out[3]:
shape: (10, 2)
モニタ | 金額 |
---|---|
i64 | i64 |
38 | 582936 |
57 | 491733 |
98 | 261842 |
257 | 242446 |
37 | 203218 |
276 | 109111 |
127 | 97732 |
575 | 89702 |
361 | 87538 |
350 | 78426 |
In [33]:
Copied!
# デシル分析の各ランクの閾値とモニタ別のランクの計算
df_decile = (
df.group_by('モニタ').agg(pl.col('金額').sum())
.select(
pl.col('金額'),
pl.col('金額').qcut(
np.linspace(0, 1, 11)[1:],
labels=[str(i) for i in np.arange(1,12)]
).alias('rank')
)
.group_by('rank').agg(pl.col('金額').sum(), pl.col('金額').min().alias('閾値'))
.sort(by='閾値', descending=True)
.with_columns(pl.col('金額').cum_sum().alias('累積金額'))
.with_columns((pl.col('累積金額')/pl.col('金額').sum()).alias('累積構成比率'))
)
display(df_decile)
# plot
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(
go.Bar(x=df_decile['rank'], y=df_decile['金額'], name='金額'),
secondary_y=False
)
fig.add_trace(
go.Scatter(x=df_decile['rank'], y=df_decile['累積構成比率'], name='累積構成比率'),
secondary_y=True
)
fig.update_xaxes(title_text='rank')
fig.update_yaxes(range=[0, 1], secondary_y=True)
fig.update_layout(width=800, height=600)
fig.show()
# デシル分析の各ランクの閾値とモニタ別のランクの計算
df_decile = (
df.group_by('モニタ').agg(pl.col('金額').sum())
.select(
pl.col('金額'),
pl.col('金額').qcut(
np.linspace(0, 1, 11)[1:],
labels=[str(i) for i in np.arange(1,12)]
).alias('rank')
)
.group_by('rank').agg(pl.col('金額').sum(), pl.col('金額').min().alias('閾値'))
.sort(by='閾値', descending=True)
.with_columns(pl.col('金額').cum_sum().alias('累積金額'))
.with_columns((pl.col('累積金額')/pl.col('金額').sum()).alias('累積構成比率'))
)
display(df_decile)
# plot
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(
go.Bar(x=df_decile['rank'], y=df_decile['金額'], name='金額'),
secondary_y=False
)
fig.add_trace(
go.Scatter(x=df_decile['rank'], y=df_decile['累積構成比率'], name='累積構成比率'),
secondary_y=True
)
fig.update_xaxes(title_text='rank')
fig.update_yaxes(range=[0, 1], secondary_y=True)
fig.update_layout(width=800, height=600)
fig.show()
shape: (10, 5)
rank | 金額 | 閾値 | 累積金額 | 累積構成比率 |
---|---|---|---|---|
cat | i64 | i64 | i64 | f64 |
"10" | 2309497 | 64813 | 2309497 | 0.686537 |
"9" | 409666 | 31976 | 2719163 | 0.808317 |
"8" | 302953 | 24420 | 3022116 | 0.898375 |
"7" | 138430 | 9978 | 3160546 | 0.939525 |
"6" | 89835 | 6597 | 3250381 | 0.96623 |
"5" | 54395 | 4154 | 3304776 | 0.9824 |
"4" | 32745 | 2444 | 3337521 | 0.992134 |
"3" | 15105 | 1123 | 3352626 | 0.996624 |
"2" | 8250 | 564 | 3360876 | 0.999077 |
"1" | 3106 | 36 | 3363982 | 1.0 |
In [42]:
Copied!
# 例題4.1
df_decile = (
df.filter(pl.col('中分類名')=='加工食品')
.group_by('細分類名').agg(pl.col('購入数量').sum())
.select(
pl.col('購入数量'),
pl.col('購入数量').qcut(
np.linspace(0, 1, 11)[1:],
labels=[str(i) for i in np.arange(1,12)]
).alias('rank')
)
.group_by('rank').agg(pl.col('購入数量').sum(), pl.col('購入数量').min().alias('閾値'))
.sort(by='閾値', descending=True)
.with_columns(pl.col('購入数量').cum_sum().alias('累積購入数量'))
.with_columns((pl.col('累積購入数量')/pl.col('購入数量').sum()).alias('累積構成比率'))
)
# plot
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(
go.Bar(x=df_decile['rank'], y=df_decile['購入数量'], name='購入数量'),
secondary_y=False
)
fig.add_trace(
go.Scatter(x=df_decile['rank'], y=df_decile['累積構成比率'], name='累積構成比率'),
secondary_y=True
)
fig.update_xaxes(title_text='rank')
fig.update_yaxes(range=[0, 1], secondary_y=True)
fig.update_layout(width=800, height=600)
fig.show()
# 例題4.1
df_decile = (
df.filter(pl.col('中分類名')=='加工食品')
.group_by('細分類名').agg(pl.col('購入数量').sum())
.select(
pl.col('購入数量'),
pl.col('購入数量').qcut(
np.linspace(0, 1, 11)[1:],
labels=[str(i) for i in np.arange(1,12)]
).alias('rank')
)
.group_by('rank').agg(pl.col('購入数量').sum(), pl.col('購入数量').min().alias('閾値'))
.sort(by='閾値', descending=True)
.with_columns(pl.col('購入数量').cum_sum().alias('累積購入数量'))
.with_columns((pl.col('累積購入数量')/pl.col('購入数量').sum()).alias('累積構成比率'))
)
# plot
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(
go.Bar(x=df_decile['rank'], y=df_decile['購入数量'], name='購入数量'),
secondary_y=False
)
fig.add_trace(
go.Scatter(x=df_decile['rank'], y=df_decile['累積構成比率'], name='累積構成比率'),
secondary_y=True
)
fig.update_xaxes(title_text='rank')
fig.update_yaxes(range=[0, 1], secondary_y=True)
fig.update_layout(width=800, height=600)
fig.show()
RFM分析¶
- Recency: 基準日からの直近の来店日までの期間
- Frequency: 期間内の累積来店回数(もしくは累積来店日数)
- Monetary: 期間内の累積購買金額
In [99]:
Copied!
# RFM分析
# スコア計算期間: 11月30日まで
# R
df_rval = (
df.filter(pl.col('日付') <= datetime.date(2013, 11, 30))
.group_by('モニタ').agg(pl.col('日付').max())
.select(
pl.col('モニタ'),
(datetime.date(2013, 12, 1) - pl.col('日付')).dt.total_days().alias('R値')
)
)
# F
df_fval = (
df.unique(subset=['日付', 'モニタ'])
.group_by('モニタ').agg(pl.col('日付').len().alias('F値'))
)
# M
df_mval = df.group_by('モニタ').agg(pl.col('金額').sum().alias('M値'))
# RFM分析
# スコア計算期間: 11月30日まで
# R
df_rval = (
df.filter(pl.col('日付') <= datetime.date(2013, 11, 30))
.group_by('モニタ').agg(pl.col('日付').max())
.select(
pl.col('モニタ'),
(datetime.date(2013, 12, 1) - pl.col('日付')).dt.total_days().alias('R値')
)
)
# F
df_fval = (
df.unique(subset=['日付', 'モニタ'])
.group_by('モニタ').agg(pl.col('日付').len().alias('F値'))
)
# M
df_mval = df.group_by('モニタ').agg(pl.col('金額').sum().alias('M値'))
In [100]:
Copied!
# RFMクラス
df_rfm = (
df_rval.join(df_fval, on='モニタ', how='left').join(df_mval, on='モニタ', how='left')
.with_columns(
# R
pl.when(pl.col('R値') < pl.col('R値').median())
.then(pl.lit('H'))
.otherwise(pl.lit('L'))
.alias('Rclass'),
# F
pl.when(pl.col('F値') > pl.col('F値').median())
.then(pl.lit('H'))
.otherwise(pl.lit('L'))
.alias('Fclass'),
# M
pl.when(pl.col('M値') > pl.col('M値').median())
.then(pl.lit('H'))
.otherwise(pl.lit('L'))
.alias('Mclass'),
)
.with_columns((pl.col('Rclass')+pl.col('Fclass')+pl.col('Mclass')).alias('RFMclass'))
.sort('モニタ')
)
df_rfm.head()
# RFMクラス
df_rfm = (
df_rval.join(df_fval, on='モニタ', how='left').join(df_mval, on='モニタ', how='left')
.with_columns(
# R
pl.when(pl.col('R値') < pl.col('R値').median())
.then(pl.lit('H'))
.otherwise(pl.lit('L'))
.alias('Rclass'),
# F
pl.when(pl.col('F値') > pl.col('F値').median())
.then(pl.lit('H'))
.otherwise(pl.lit('L'))
.alias('Fclass'),
# M
pl.when(pl.col('M値') > pl.col('M値').median())
.then(pl.lit('H'))
.otherwise(pl.lit('L'))
.alias('Mclass'),
)
.with_columns((pl.col('Rclass')+pl.col('Fclass')+pl.col('Mclass')).alias('RFMclass'))
.sort('モニタ')
)
df_rfm.head()
Out[100]:
shape: (5, 8)
モニタ | R値 | F値 | M値 | Rclass | Fclass | Mclass | RFMclass |
---|---|---|---|---|---|---|---|
i64 | i64 | u32 | i64 | str | str | str | str |
14 | 78 | 1 | 1306 | "L" | "L" | "L" | "LLL" |
15 | 139 | 1 | 2880 | "L" | "L" | "L" | "LLL" |
16 | 169 | 17 | 38163 | "L" | "H" | "H" | "LHH" |
20 | 1 | 35 | 45981 | "H" | "H" | "H" | "HHH" |
21 | 281 | 1 | 1949 | "L" | "L" | "L" | "LLL" |
In [101]:
Copied!
# RFMセグメント別の12月の購入状況
(
df_rfm.join(
df.filter(pl.col('日付') > datetime.date(2013, 11, 30))
.group_by('モニタ').agg(pl.col('購入数量').sum()),
on='モニタ', how='left'
)
.fill_null(0)
.group_by('RFMclass')
.agg(
(pl.col('購入数量') == 0).sum().alias('再購入なし'),
(pl.col('購入数量') > 0).sum().alias('再購入あり'),
pl.col('購入数量').mean().alias('平均購入数量')
)
.with_columns(
(pl.col('再購入あり')/(pl.col('再購入あり')+pl.col('再購入なし'))).alias('再購入率')
)
.select('RFMclass', '再購入なし', '再購入あり', '再購入率', '平均購入数量')
.sort('RFMclass')
)
# RFMセグメント別の12月の購入状況
(
df_rfm.join(
df.filter(pl.col('日付') > datetime.date(2013, 11, 30))
.group_by('モニタ').agg(pl.col('購入数量').sum()),
on='モニタ', how='left'
)
.fill_null(0)
.group_by('RFMclass')
.agg(
(pl.col('購入数量') == 0).sum().alias('再購入なし'),
(pl.col('購入数量') > 0).sum().alias('再購入あり'),
pl.col('購入数量').mean().alias('平均購入数量')
)
.with_columns(
(pl.col('再購入あり')/(pl.col('再購入あり')+pl.col('再購入なし'))).alias('再購入率')
)
.select('RFMclass', '再購入なし', '再購入あり', '再購入率', '平均購入数量')
.sort('RFMclass')
)
Out[101]:
shape: (8, 5)
RFMclass | 再購入なし | 再購入あり | 再購入率 | 平均購入数量 |
---|---|---|---|---|
str | u32 | u32 | f64 | f64 |
"HHH" | 9 | 26 | 0.742857 | 38.428571 |
"HHL" | 2 | 1 | 0.333333 | 1.333333 |
"HLH" | 2 | 0 | 0.0 | 0.0 |
"HLL" | 9 | 2 | 0.181818 | 1.818182 |
"LHH" | 6 | 2 | 0.25 | 12.375 |
"LHL" | 2 | 1 | 0.333333 | 3.333333 |
"LLH" | 5 | 1 | 0.166667 | 1.333333 |
"LLL" | 32 | 2 | 0.058824 | 0.794118 |
健康志向の消費者に特徴的な購買行動を探る¶
In [ ]:
Copied!