演習:購買データを前処理しよう
田中VPoE「いよいよNetShop社の実データを触る時が来た。マーケティング部門から購買データ、顧客マスタ、商品マスタの3つのCSVファイルが届いている。」
あなた「早速分析を始めたいです!」
田中VPoE「焦るな。まずはデータの品質チェックと前処理だ。データ分析の時間の60-80%はここに費やされる。丁寧にやっていこう。」
ミッション概要
NetShop社から提供された3つのデータセットを結合・前処理し、分析可能な状態に整えます。以下のサンプルデータを使って、Jupyter Notebook上で作業してください。
サンプルデータの作成
まず、以下のコードでサンプルデータを作成します:
import pandas as pd
import numpy as np
np.random.seed(42)
# 購買履歴データ
n_orders = 1000
orders = pd.DataFrame({
'order_id': [f'ORD{str(i).zfill(5)}' for i in range(1, n_orders + 1)],
'customer_id': np.random.choice([f'C{str(i).zfill(4)}' for i in range(1, 201)], n_orders),
'product_id': np.random.choice([f'P{str(i).zfill(3)}' for i in range(1, 51)], n_orders),
'amount': np.random.lognormal(mean=8, sigma=1, size=n_orders).round(0),
'quantity': np.random.randint(1, 5, n_orders),
'order_date': pd.date_range('2024-01-01', periods=n_orders, freq='8h').strftime('%Y-%m-%d'),
'status': np.random.choice(['completed', 'completed', 'completed', 'cancelled', 'returned'], n_orders)
})
# 意図的に欠損値・異常値を追加
orders.loc[np.random.choice(n_orders, 50, replace=False), 'amount'] = np.nan
orders.loc[np.random.choice(n_orders, 10, replace=False), 'amount'] = -500
orders.loc[np.random.choice(n_orders, 5, replace=False), 'order_id'] = orders['order_id'].iloc[:5].values # 重複
# 顧客マスタ
customers = pd.DataFrame({
'customer_id': [f'C{str(i).zfill(4)}' for i in range(1, 201)],
'age': np.random.randint(18, 70, 200).astype(float),
'gender': np.random.choice(['M', 'F', 'Other'], 200),
'region': np.random.choice(['関東', '関西', '中部', '北海道', '九州'], 200),
'registration_date': pd.date_range('2020-01-01', periods=200, freq='5D').strftime('%Y-%m-%d')
})
customers.loc[np.random.choice(200, 30, replace=False), 'age'] = np.nan
customers.loc[np.random.choice(200, 10, replace=False), 'gender'] = np.nan
# 商品マスタ
products = pd.DataFrame({
'product_id': [f'P{str(i).zfill(3)}' for i in range(1, 51)],
'category': np.random.choice(['家電', '書籍', '食品', 'ファッション', 'スポーツ'], 50),
'price': np.random.lognormal(mean=7.5, sigma=0.8, size=50).round(0),
'brand': np.random.choice(['BrandA', 'BrandB', 'BrandC', 'BrandD', None], 50)
})
Mission 1: データ品質チェック
3つのデータセットの品質を確認し、問題点をリストアップしてください。
要件
- 各データセットの基本情報を確認(shape, dtypes, describe)
- 欠損値の確認と欠損率の算出
- 重複レコードの検出
- 異常値の検出(金額がマイナスなど)
- 品質レポートとしてまとめる
解答例
# === 購買履歴の品質チェック ===
print("=" * 50)
print("購買履歴データ")
print("=" * 50)
print(f"Shape: {orders.shape}")
print(f"\n欠損値:\n{orders.isnull().sum()}")
print(f"\n欠損率(%):\n{(orders.isnull().mean() * 100).round(1)}")
# 重複チェック
dup_orders = orders[orders.duplicated(subset=['order_id'], keep=False)]
print(f"\n重複order_id: {len(dup_orders)}件")
# 異常値チェック
negative_amounts = orders[orders['amount'] < 0]
print(f"マイナス金額: {len(negative_amounts)}件")
# ステータス分布
print(f"\nステータス分布:\n{orders['status'].value_counts()}")
# === 顧客マスタの品質チェック ===
print("\n" + "=" * 50)
print("顧客マスタ")
print("=" * 50)
print(f"Shape: {customers.shape}")
print(f"\n欠損値:\n{customers.isnull().sum()}")
print(f"\n年齢の範囲: {customers['age'].min()} - {customers['age'].max()}")
# === 商品マスタの品質チェック ===
print("\n" + "=" * 50)
print("商品マスタ")
print("=" * 50)
print(f"Shape: {products.shape}")
print(f"\n欠損値:\n{products.isnull().sum()}")
# === 品質レポート ===
print("\n" + "=" * 50)
print("品質レポートまとめ")
print("=" * 50)
quality_issues = pd.DataFrame({
'問題': [
'amount欠損', 'amount異常値(マイナス)', 'order_id重複',
'age欠損', 'gender欠損', 'brand欠損'
],
'件数': [
orders['amount'].isna().sum(),
(orders['amount'] < 0).sum(),
orders.duplicated(subset=['order_id']).sum(),
customers['age'].isna().sum(),
customers['gender'].isna().sum(),
products['brand'].isna().sum()
],
'対策': [
'中央値で補完',
'除外(キャンセル・返品処理の可能性)',
'重複排除(最新を保持)',
'地域別中央値で補完',
'「不明」カテゴリで補完',
'「不明」で補完'
]
})
print(quality_issues.to_string(index=False))
Mission 2: データクレンジングと結合
品質チェックで見つかった問題を解決し、3つのデータセットを結合してください。
要件
- 重複レコードの排除
- 異常値(マイナス金額)の処理
- 欠損値の適切な補完
- 3テーブルの結合
- 結合結果の検証
解答例
# === Step 1: 重複排除 ===
orders_clean = orders.drop_duplicates(subset=['order_id'], keep='first')
print(f"重複排除: {len(orders)} -> {len(orders_clean)}")
# === Step 2: 異常値処理 ===
# マイナス金額を除外
orders_clean = orders_clean[orders_clean['amount'] >= 0].copy()
# NaN(まだ残っている)はこの時点ではNaNのまま
print(f"異常値除外後: {len(orders_clean)}")
# === Step 3: 欠損値補完 ===
# amount: 完了注文の中央値で補完
median_amount = orders_clean.loc[
orders_clean['status'] == 'completed', 'amount'
].median()
orders_clean['amount_was_missing'] = orders_clean['amount'].isna().astype(int)
orders_clean['amount'] = orders_clean['amount'].fillna(median_amount)
# 顧客age: 地域別中央値で補完
customers_clean = customers.copy()
customers_clean['age_was_missing'] = customers_clean['age'].isna().astype(int)
customers_clean['age'] = customers_clean.groupby('region')['age'].transform(
lambda x: x.fillna(x.median())
)
# gender: 不明で補完
customers_clean['gender'] = customers_clean['gender'].fillna('不明')
# brand: 不明で補完
products_clean = products.copy()
products_clean['brand'] = products_clean['brand'].fillna('不明')
# === Step 4: データ結合 ===
df = pd.merge(orders_clean, customers_clean, on='customer_id', how='left')
df = pd.merge(df, products_clean, on='product_id', how='left')
print(f"\n結合後のshape: {df.shape}")
print(f"結合後の欠損値:\n{df.isnull().sum()}")
# === Step 5: 結合検証 ===
print(f"\n結合前のorders: {len(orders_clean)}")
print(f"結合後のdf: {len(df)}")
assert len(df) == len(orders_clean), "結合で行数が変化しています"
print("結合検証OK: 行数は一致しています")
Mission 3: 分析用データセットの作成
結合したデータに特徴量を追加し、分析しやすい形に整えてください。
要件
- 日付カラムの型変換と年月・曜日の抽出
- 完了注文のみに絞り込み
- 顧客ごとのRFM指標の計算
- カテゴリ別・月別の売上サマリーの作成
- 最終データセットのCSV出力
解答例
# === Step 1: 日付処理 ===
df['order_date'] = pd.to_datetime(df['order_date'])
df['year_month'] = df['order_date'].dt.to_period('M')
df['day_of_week'] = df['order_date'].dt.day_name()
df['is_weekend'] = df['order_date'].dt.dayofweek >= 5
# === Step 2: 完了注文のみ ===
df_completed = df[df['status'] == 'completed'].copy()
print(f"完了注文数: {len(df_completed)}")
# === Step 3: RFM指標 ===
reference_date = df_completed['order_date'].max() + pd.Timedelta(days=1)
rfm = df_completed.groupby('customer_id').agg(
recency=('order_date', lambda x: (reference_date - x.max()).days),
frequency=('order_id', 'nunique'),
monetary=('amount', 'sum')
).reset_index()
# RFMスコア(四分位数でスコアリング)
rfm['r_score'] = pd.qcut(rfm['recency'], 4, labels=[4, 3, 2, 1]).astype(int)
rfm['f_score'] = pd.qcut(rfm['frequency'].rank(method='first'), 4, labels=[1, 2, 3, 4]).astype(int)
rfm['m_score'] = pd.qcut(rfm['monetary'], 4, labels=[1, 2, 3, 4]).astype(int)
rfm['rfm_score'] = rfm['r_score'] * 100 + rfm['f_score'] * 10 + rfm['m_score']
print(f"\nRFM分析結果:")
print(rfm.describe())
# === Step 4: カテゴリ別月別サマリー ===
monthly_summary = df_completed.pivot_table(
values='amount',
index='year_month',
columns='category',
aggfunc=['sum', 'count'],
fill_value=0
)
print(f"\n月別カテゴリサマリー:")
print(monthly_summary.head())
# === Step 5: 出力 ===
# df_completed.to_csv('netshop_cleaned.csv', index=False)
# rfm.to_csv('netshop_rfm.csv', index=False)
print("\nデータセット作成完了!")
print(f" - クレンジング済みデータ: {df_completed.shape}")
print(f" - RFMデータ: {rfm.shape}")
達成度チェック
- 3つのデータセットの品質問題を洗い出せた
- 重複排除と異常値処理ができた
- 欠損値を適切な手法で補完できた
- 3テーブルを結合し、結合結果を検証できた
- RFM指標を計算し、分析用データセットを作成できた
- 各処理の理由を説明できる
推定所要時間:90分