演習:Telco Churnデータを徹底分析しよう
「理論は十分だ。ここからはKaggleのデータに向き合う時間だ。」
田中VPoEがJupyter Notebookを指す。
「EDAから前処理まで、一連の流れを自分の手で完成させてくれ。特に仮説検証の結果をビジネス視点でまとめることを意識してほしい。」
ミッション概要
Kaggle Telco Customer Churnデータセットに対して、EDAから前処理までを一貫して実施する。各Missionの成果物はNotebookのセクションとしてまとめること。
Mission 1: データ概要レポートの作成(30分)
以下の項目を含むデータ概要レポートを作成せよ。
- データサイズ、カラム数、データ型の一覧
- 各カラムの基本統計量(数値)またはユニーク値(カテゴリ)
- 欠損値の有無と対処方針
- 目的変数(Churn)の分布と不均衡度
- 全カテゴリカル変数の値分布を棒グラフで可視化
解答例
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
# 1. データ概要
print(f"データサイズ: {df.shape}")
print(f"\nデータ型:\n{df.dtypes}")
# 2. 基本統計量
print(f"\n数値変数の統計量:\n{df.describe()}")
for col in df.select_dtypes(include='object').columns:
print(f"\n{col}: {df[col].nunique()}種類")
print(df[col].value_counts())
# 3. 欠損値
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
print(f"\n欠損値:\n{df.isnull().sum()[df.isnull().sum() > 0]}")
# 対処: tenure=0の新規顧客 → TotalCharges=0で補完
# 4. 目的変数の分布
print(f"\n離反分布:\n{df['Churn'].value_counts()}")
print(f"離反率: {(df['Churn'] == 'Yes').mean():.3f}")
# 5. カテゴリカル変数の可視化
cat_cols = df.select_dtypes(include='object').columns.drop(['customerID', 'Churn'])
fig, axes = plt.subplots(3, 4, figsize=(20, 12))
axes = axes.flatten()
for i, col in enumerate(cat_cols):
df[col].value_counts().plot(kind='bar', ax=axes[i])
axes[i].set_title(col)
axes[i].tick_params(axis='x', rotation=45)
for j in range(i+1, len(axes)):
axes[j].set_visible(False)
plt.tight_layout()
plt.savefig('categorical_distributions.png', dpi=150)
Mission 2: 仮説検証レポートの作成(30分)
Step 1で構築した仮説ツリーから最低5つの仮説を選び、データで検証せよ。
各仮説について以下を含めること:
- 仮説の記述
- 検証に使用した分析手法(グラフ/統計量)
- 結果の数値
- 仮説が支持されたかの判定
- ビジネスへの示唆
解答例
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce').fillna(0)
results = []
# H1: Month-to-month契約は離反率が高い
h1_rates = df.groupby('Contract')['Churn'].apply(lambda x: (x=='Yes').mean())
results.append({
'仮説': 'H1: Month-to-month契約は離反率が高い',
'結果': f"M-t-M: {h1_rates['Month-to-month']:.1%}, 1yr: {h1_rates['One year']:.1%}, 2yr: {h1_rates['Two year']:.1%}",
'判定': '支持',
'示唆': '長期契約への移行インセンティブが有効'
})
# H2: 月額料金が高い顧客は離反しやすい
churned = df[df['Churn']=='Yes']['MonthlyCharges']
retained = df[df['Churn']=='No']['MonthlyCharges']
t_stat, p_val = stats.ttest_ind(churned, retained)
results.append({
'仮説': 'H2: 月額料金が高い顧客は離反しやすい',
'結果': f"離反者中央値: ¥{churned.median():.0f}, 非離反: ¥{retained.median():.0f}, p={p_val:.2e}",
'判定': '支持',
'示唆': '高額プラン顧客に割引や付加価値の提供を検討'
})
# H4: Fiber optic顧客は離反率が高い
h4_rates = df.groupby('InternetService')['Churn'].apply(lambda x: (x=='Yes').mean())
results.append({
'仮説': 'H4: Fiber optic顧客は離反率が高い',
'結果': f"Fiber: {h4_rates['Fiber optic']:.1%}, DSL: {h4_rates['DSL']:.1%}",
'判定': '支持',
'示唆': 'Fiber opticの品質/価格に対する不満を調査'
})
# H7: 利用期間が短い顧客は離反しやすい
df['tenure_group'] = pd.cut(df['tenure'], bins=[0,12,24,48,72], labels=['0-12','13-24','25-48','49-72'])
h7_rates = df.groupby('tenure_group')['Churn'].apply(lambda x: (x=='Yes').mean())
results.append({
'仮説': 'H7: 利用期間が短い顧客は離反しやすい',
'結果': f"0-12M: {h7_rates['0-12']:.1%}, 49-72M: {h7_rates['49-72']:.1%}",
'判定': '支持',
'示唆': '入会後12ヶ月のオンボーディング強化が必要'
})
# H10: Electronic check支払いは離反率が高い
h10_rates = df.groupby('PaymentMethod')['Churn'].apply(lambda x: (x=='Yes').mean())
results.append({
'仮説': 'H10: Electronic check支払いは離反率が高い',
'結果': f"E-check: {h10_rates['Electronic check']:.1%}, 他: {h10_rates.drop('Electronic check').mean():.1%}",
'判定': '支持',
'示唆': '自動引き落とし/カード決済への移行を促進'
})
report = pd.DataFrame(results)
print(report.to_string(index=False))
Mission 3: 前処理パイプラインの構築(30分)
モデル構築に使えるレベルの前処理パイプラインを構築せよ。
- TotalChargesの型変換と欠損値処理
- customerIDの削除
- 目的変数のエンコーディング
- カテゴリカル変数のエンコーディング(手法を選択し理由を述べる)
- 数値変数のスケーリング(手法を選択し理由を述べる)
- 訓練/検証/テストの分割(比率と層化抽出を指定)
- 各セットの件数と離反率を確認
解答例
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
# 1. 型変換と欠損値処理
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce').fillna(0)
# 2. customerID削除
df = df.drop('customerID', axis=1)
# 3. 目的変数エンコーディング
df['Churn'] = (df['Churn'] == 'Yes').astype(int)
# 4. カテゴリカル変数のエンコーディング
# 2値変数: LabelEncoding(シンプルで次元が増えない)
binary_cols = ['gender', 'Partner', 'Dependents', 'PhoneService', 'PaperlessBilling']
le = LabelEncoder()
for col in binary_cols:
df[col] = le.fit_transform(df[col])
# "No internet/phone service" → "No" に統合
service_cols = ['MultipleLines', 'OnlineSecurity', 'OnlineBackup',
'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies']
for col in service_cols:
df[col] = df[col].replace({'No internet service': 'No', 'No phone service': 'No'})
# 多値変数: One-Hotエンコーディング(順序がないため)
multi_cols = service_cols + ['InternetService', 'Contract', 'PaymentMethod']
df = pd.get_dummies(df, columns=multi_cols, drop_first=True)
# 5. スケーリング(ロジスティック回帰のベースラインモデル用)
numeric_cols = ['tenure', 'MonthlyCharges', 'TotalCharges']
scaler = StandardScaler()
# 6. データ分割 (60:20:20、層化抽出)
X = df.drop('Churn', axis=1)
y = df['Churn']
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.4, random_state=42, stratify=y)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp)
# スケーリングは訓練データでfitし、検証・テストにtransform
X_train[numeric_cols] = scaler.fit_transform(X_train[numeric_cols])
X_val[numeric_cols] = scaler.transform(X_val[numeric_cols])
X_test[numeric_cols] = scaler.transform(X_test[numeric_cols])
# 7. 確認
print(f"訓練: {X_train.shape[0]}件 (離反率: {y_train.mean():.3f})")
print(f"検証: {X_val.shape[0]}件 (離反率: {y_val.mean():.3f})")
print(f"テスト: {X_test.shape[0]}件 (離反率: {y_test.mean():.3f})")
print(f"特徴量数: {X_train.shape[1]}")
達成度チェック
- データ概要レポートを正しく作成できた
- 最低5つの仮説をデータで検証し、判定を記述できた
- 前処理パイプラインを構築し、再現可能な状態にした
- 層化抽出でデータを分割し、各セットの離反率が同程度であることを確認した
- エンコーディング/スケーリング手法の選択理由を述べられた
推定所要時間: 90分