演習:Credit Card Fraudデータを分析しよう
「理論は十分だ。実際のデータで手を動かして、不均衡データの感覚を身につけてくれ。」
田中VPoEがKaggleのページを開く。
「まずはEDAから始めて、サンプリング手法の効果を実験的に確かめよう。」
ミッション概要
Kaggle Credit Card Fraud Detectionデータセットを使い、EDA、サンプリング手法の比較、評価指標の実践を行う。
Mission 1: EDA(30分)
データセットの全体像を把握し、不正取引の特徴を発見する。
タスク:
- データの基本統計量を確認し、欠損値の有無を報告する
- クラス分布を確認し、不均衡比率を計算する
- 正常/不正それぞれの取引金額(Amount)の分布を可視化し、特徴を述べる
- 時間帯(Time)ごとの不正取引件数を可視化し、パターンを分析する
- V1〜V28のうち、正常/不正で分布が大きく異なる特徴量を3つ以上特定する
解答例
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
df = pd.read_csv('creditcard.csv')
# 1. 基本統計量
print(df.describe())
print(f"\n欠損値:\n{df.isnull().sum().sum()}") # 0件
# 2. クラス分布
print(f"\nクラス分布:\n{df['Class'].value_counts()}")
ratio = df['Class'].value_counts()[0] / df['Class'].value_counts()[1]
print(f"不均衡比率: 1:{ratio:.0f}")
# 3. 取引金額の分布
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
df[df['Class'] == 0]['Amount'].hist(bins=50, ax=axes[0], alpha=0.7)
axes[0].set_title('正常取引の金額分布')
axes[0].set_xlim(0, 500)
df[df['Class'] == 1]['Amount'].hist(bins=50, ax=axes[1], alpha=0.7, color='red')
axes[1].set_title('不正取引の金額分布')
plt.tight_layout()
plt.show()
# 特徴: 不正取引は比較的小額に集中(平均122ドル vs 正常88ドル)
# ただし、高額の不正取引も存在
# 4. 時間帯分析
df['Hour'] = (df['Time'] / 3600).astype(int) % 24
fraud_by_hour = df[df['Class'] == 1].groupby('Hour').size()
fraud_by_hour.plot(kind='bar', color='crimson')
plt.title('時間帯別不正取引件数')
plt.show()
# 5. 特徴量の分布比較
from scipy import stats
significant_features = []
for col in [f'V{i}' for i in range(1, 29)]:
stat, p_value = stats.mannwhitneyu(
df[df['Class'] == 0][col],
df[df['Class'] == 1][col]
)
if p_value < 0.001:
effect_size = abs(
df[df['Class'] == 0][col].mean() - df[df['Class'] == 1][col].mean()
)
significant_features.append((col, effect_size, p_value))
significant_features.sort(key=lambda x: x[1], reverse=True)
print("分布が大きく異なる特徴量(効果量上位):")
for feat, effect, p in significant_features[:5]:
print(f" {feat}: 効果量={effect:.3f}, p値={p:.2e}")
# 典型的にはV14, V12, V10, V17等が有意
Mission 2: サンプリング手法の比較(30分)
複数のサンプリング手法を適用し、ベースラインモデル(ロジスティック回帰)での性能を比較する。
タスク:
- データを学習/テストに分割する(8、層化分割)
- 以下の5つの条件でロジスティック回帰を学習し、テストデータで評価する
- サンプリングなし
- ランダムオーバーサンプリング
- SMOTE
- ランダムアンダーサンプリング
- SMOTE + Tomek Links
- 各条件でPR-AUC、F1-Score、Recall、Precisionを計算する
- 結果を比較し、最も効果的なサンプリング手法を選定する
解答例
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import (
average_precision_score, f1_score,
recall_score, precision_score
)
from imblearn.over_sampling import RandomOverSampler, SMOTE
from imblearn.under_sampling import RandomUnderSampler
from imblearn.combine import SMOTETomek
X = df.drop('Class', axis=1)
y = df['Class']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
samplers = {
'なし': None,
'Random Over': RandomOverSampler(random_state=42),
'SMOTE': SMOTE(random_state=42),
'Random Under': RandomUnderSampler(random_state=42),
'SMOTE+Tomek': SMOTETomek(random_state=42),
}
results = []
for name, sampler in samplers.items():
if sampler:
X_res, y_res = sampler.fit_resample(X_train_scaled, y_train)
else:
X_res, y_res = X_train_scaled, y_train
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_res, y_res)
y_prob = model.predict_proba(X_test_scaled)[:, 1]
y_pred = model.predict(X_test_scaled)
results.append({
'Method': name,
'PR-AUC': average_precision_score(y_test, y_prob),
'F1': f1_score(y_test, y_pred),
'Recall': recall_score(y_test, y_pred),
'Precision': precision_score(y_test, y_pred),
})
results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))
# SMOTEがPR-AUCとF1のバランスが良い傾向
# Random Underはデータ量が減るため性能が不安定
Mission 3: 評価指標の実践(30分)
最もPR-AUCが高かったモデルを使い、評価指標を深掘りする。
タスク:
- PR曲線とROC曲線を並べて可視化する
- 閾値を0.1刻みで変化させ、Recall/Precision/F1の変化をプロットする
- ビジネスコスト関数(FN=50,000円、FP=500円)を定義し、コスト最小化閾値を求める
- F1最適化閾値とコスト最適化閾値の違いを分析し、どちらを採用すべきか提案する
解答例
from sklearn.metrics import (
precision_recall_curve, roc_curve,
average_precision_score, roc_auc_score
)
# 最良モデルの予測確率を使用(例: SMOTE + LR)
y_prob = best_model_prob
# 1. PR曲線とROC曲線
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
precision, recall, pr_thresholds = precision_recall_curve(y_test, y_prob)
axes[0].plot(recall, precision)
axes[0].set_title(f'PR Curve (AUC={average_precision_score(y_test, y_prob):.4f})')
axes[0].set_xlabel('Recall')
axes[0].set_ylabel('Precision')
fpr, tpr, roc_thresholds = roc_curve(y_test, y_prob)
axes[1].plot(fpr, tpr)
axes[1].plot([0, 1], [0, 1], 'r--')
axes[1].set_title(f'ROC Curve (AUC={roc_auc_score(y_test, y_prob):.4f})')
axes[1].set_xlabel('FPR')
axes[1].set_ylabel('TPR')
plt.tight_layout()
plt.show()
# 2-3. 閾値探索
thresholds = np.arange(0.01, 1.0, 0.01)
metrics = {'threshold': [], 'recall': [], 'precision': [],
'f1': [], 'cost': []}
for t in thresholds:
y_pred_t = (y_prob >= t).astype(int)
fn = ((y_test == 1) & (y_pred_t == 0)).sum()
fp = ((y_test == 0) & (y_pred_t == 1)).sum()
metrics['threshold'].append(t)
metrics['recall'].append(recall_score(y_test, y_pred_t))
metrics['precision'].append(precision_score(y_test, y_pred_t, zero_division=0))
metrics['f1'].append(f1_score(y_test, y_pred_t))
metrics['cost'].append(fn * 50000 + fp * 500)
metrics_df = pd.DataFrame(metrics)
# F1最適化閾値
best_f1_idx = metrics_df['f1'].idxmax()
best_f1_threshold = metrics_df.loc[best_f1_idx, 'threshold']
# コスト最適化閾値
best_cost_idx = metrics_df['cost'].idxmin()
best_cost_threshold = metrics_df.loc[best_cost_idx, 'threshold']
print(f"F1最適化閾値: {best_f1_threshold:.2f}")
print(f"コスト最適化閾値: {best_cost_threshold:.2f}")
# 4. 分析
# コスト最適化閾値の方がF1閾値より低い傾向
# → 見逃しコストが高いため、Recall重視の閾値が選ばれる
# → ビジネス判断としてはコスト最適化閾値を採用すべき
達成度チェック
- データのEDAを実施し、不正取引の特徴を3つ以上発見できた
- 5つのサンプリング条件で学習・評価を実施できた
- サンプリング手法間の性能差を定量的に比較できた
- PR曲線とROC曲線を正しく可視化できた
- ビジネスコスト最小化閾値を算出できた
- F1閾値とコスト閾値の違いを分析・説明できた
推定所要時間: 90分