【Python×LightGBM入門】第2回:基本的な分類問題への適用
Python機械学習LightGBM分類問題データサイエンス
LightGBMを使った分類問題の実装方法を、実践的なデータセットを用いて詳しく解説します。前処理から評価まで、ステップバイステップで学びます。
はじめに
前回はLightGBMの基本概念と環境構築について学びました。今回は、実践的なデータセットを使って分類問題に取り組みます。データの前処理から、モデルの構築、評価まで、実務で使えるテクニックを交えながら解説していきます。
今回使用するデータセット
今回は、顧客の離脱予測(チャーン予測)を題材にします。これは実務でもよく遭遇する二値分類問題です。
import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
import matplotlib.pyplot as plt
import seaborn as sns
# サンプルデータを生成(実際はCSVファイルなどから読み込む)
np.random.seed(42)
n_samples = 10000
# 顧客データの生成
data = pd.DataFrame({
'customer_id': range(n_samples),
'age': np.random.randint(18, 70, n_samples),
'tenure': np.random.randint(0, 72, n_samples), # 契約月数
'monthly_charges': np.random.uniform(20, 120, n_samples),
'total_charges': np.random.uniform(100, 8000, n_samples),
'payment_method': np.random.choice(['Electronic check', 'Mailed check', 'Bank transfer', 'Credit card'], n_samples),
'contract': np.random.choice(['Month-to-month', 'One year', 'Two year'], n_samples),
'internet_service': np.random.choice(['DSL', 'Fiber optic', 'No'], n_samples),
'online_security': np.random.choice(['Yes', 'No', 'No internet service'], n_samples),
'tech_support': np.random.choice(['Yes', 'No', 'No internet service'], n_samples),
'streaming_tv': np.random.choice(['Yes', 'No', 'No internet service'], n_samples),
'streaming_movies': np.random.choice(['Yes', 'No', 'No internet service'], n_samples),
})
# ターゲット変数(離脱フラグ)の生成
# 実際のビジネスロジックに基づいて生成
churn_probability = 0.2 + 0.3 * (data['contract'] == 'Month-to-month') - 0.2 * (data['tenure'] > 24)
data['churn'] = np.random.binomial(1, churn_probability)
print(f"データセットのサイズ: {data.shape}")
print(f"離脱率: {data['churn'].mean():.2%}")
print("\nデータの先頭5行:")
print(data.head())
データの探索的分析(EDA)
モデルを構築する前に、データの特徴を理解することが重要です。
# 基本統計量の確認
print("数値変数の統計量:")
print(data.describe())
# カテゴリ変数の分布
categorical_cols = ['payment_method', 'contract', 'internet_service',
'online_security', 'tech_support', 'streaming_tv', 'streaming_movies']
fig, axes = plt.subplots(2, 4, figsize=(20, 10))
axes = axes.ravel()
for idx, col in enumerate(categorical_cols):
data[col].value_counts().plot(kind='bar', ax=axes[idx])
axes[idx].set_title(f'{col}の分布')
axes[idx].set_xlabel('')
plt.tight_layout()
plt.show()
# 離脱率とカテゴリ変数の関係
fig, axes = plt.subplots(2, 4, figsize=(20, 10))
axes = axes.ravel()
for idx, col in enumerate(categorical_cols):
churn_rate = data.groupby(col)['churn'].mean().sort_values(ascending=False)
churn_rate.plot(kind='bar', ax=axes[idx])
axes[idx].set_title(f'{col}ごとの離脱率')
axes[idx].set_ylabel('離脱率')
axes[idx].set_xlabel('')
plt.tight_layout()
plt.show()
データの前処理
LightGBMは欠損値を自動的に処理できますが、カテゴリ変数は適切に処理する必要があります。
# カテゴリ変数の処理
# LightGBMは整数型のカテゴリ変数を直接扱える
label_encoders = {}
for col in categorical_cols:
le = LabelEncoder()
data[col + '_encoded'] = le.fit_transform(data[col])
label_encoders[col] = le
# 特徴量とターゲットの分離
feature_cols = ['age', 'tenure', 'monthly_charges', 'total_charges'] + \
[col + '_encoded' for col in categorical_cols]
X = data[feature_cols]
y = data['churn']
# カテゴリ変数のインデックスを記録(LightGBMに渡すため)
categorical_indices = [i for i, col in enumerate(feature_cols) if col.endswith('_encoded')]
print(f"特徴量の数: {len(feature_cols)}")
print(f"カテゴリ変数のインデックス: {categorical_indices}")
訓練データとテストデータの分割
# データの分割(訓練:検証:テスト = 60:20:20)
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.25, random_state=42, stratify=y_temp
)
print(f"訓練データ: {X_train.shape}")
print(f"検証データ: {X_val.shape}")
print(f"テストデータ: {X_test.shape}")
# クラスバランスの確認
print(f"\n訓練データの離脱率: {y_train.mean():.2%}")
print(f"検証データの離脱率: {y_val.mean():.2%}")
print(f"テストデータの離脱率: {y_test.mean():.2%}")
LightGBMモデルの構築
1. 基本的なモデル
# LightGBMデータセットの作成
train_data = lgb.Dataset(
X_train,
label=y_train,
categorical_feature=categorical_indices
)
valid_data = lgb.Dataset(
X_val,
label=y_val,
categorical_feature=categorical_indices,
reference=train_data
)
# 基本パラメータの設定
params = {
'objective': 'binary', # 二値分類
'metric': 'binary_logloss', # 評価指標
'boosting_type': 'gbdt', # 勾配ブースティング
'num_leaves': 31, # 葉の数
'learning_rate': 0.05, # 学習率
'feature_fraction': 0.9, # 各木で使用する特徴量の割合
'bagging_fraction': 0.8, # 各木で使用するデータの割合
'bagging_freq': 5, # バギングの頻度
'verbose': 0,
'random_state': 42
}
# モデルの学習
print("モデルの学習を開始...")
model = lgb.train(
params,
train_data,
valid_sets=[train_data, valid_data],
num_boost_round=1000,
callbacks=[
lgb.early_stopping(stopping_rounds=50),
lgb.log_evaluation(period=100)
]
)
print(f"\n最適な反復回数: {model.best_iteration}")
2. クラス不均衡への対処
離脱予測のような問題では、クラスの不均衡がよく発生します。
# クラスウェイトを考慮したモデル
from sklearn.utils.class_weight import compute_class_weight
# クラスウェイトの計算
class_weights = compute_class_weight(
'balanced',
classes=np.unique(y_train),
y=y_train
)
weight_dict = {0: class_weights[0], 1: class_weights[1]}
# is_unbalanceパラメータを使用
params_balanced = params.copy()
params_balanced['is_unbalance'] = True # クラス不均衡を考慮
# 再学習
model_balanced = lgb.train(
params_balanced,
train_data,
valid_sets=[train_data, valid_data],
num_boost_round=1000,
callbacks=[
lgb.early_stopping(stopping_rounds=50),
lgb.log_evaluation(period=100)
]
)
モデルの評価
1. 予測と基本的な評価指標
# 予測
y_pred_proba = model.predict(X_test, num_iteration=model.best_iteration)
y_pred = (y_pred_proba >= 0.5).astype(int)
# 評価指標の計算
metrics = {
'Accuracy': accuracy_score(y_test, y_pred),
'Precision': precision_score(y_test, y_pred),
'Recall': recall_score(y_test, y_pred),
'F1-Score': f1_score(y_test, y_pred),
'AUC-ROC': roc_auc_score(y_test, y_pred_proba)
}
print("モデルの評価指標:")
for metric, value in metrics.items():
print(f"{metric}: {value:.4f}")
2. 混同行列の可視化
from sklearn.metrics import confusion_matrix
import seaborn as sns
# 混同行列の計算
cm = confusion_matrix(y_test, y_pred)
# 可視化
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.title('混同行列')
plt.ylabel('実際のラベル')
plt.xlabel('予測ラベル')
plt.show()
# 詳細な分析
tn, fp, fn, tp = cm.ravel()
print(f"\n真陰性 (TN): {tn}")
print(f"偽陽性 (FP): {fp}")
print(f"偽陰性 (FN): {fn}")
print(f"真陽性 (TP): {tp}")
3. ROC曲線とPR曲線
from sklearn.metrics import roc_curve, auc, precision_recall_curve
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# ROC曲線
fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
roc_auc = auc(fpr, tpr)
ax1.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.2f})')
ax1.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
ax1.set_xlim([0.0, 1.0])
ax1.set_ylim([0.0, 1.05])
ax1.set_xlabel('偽陽性率 (FPR)')
ax1.set_ylabel('真陽性率 (TPR)')
ax1.set_title('ROC曲線')
ax1.legend(loc="lower right")
# PR曲線
precision, recall, _ = precision_recall_curve(y_test, y_pred_proba)
pr_auc = auc(recall, precision)
ax2.plot(recall, precision, color='green', lw=2, label=f'PR curve (AUC = {pr_auc:.2f})')
ax2.set_xlim([0.0, 1.0])
ax2.set_ylim([0.0, 1.05])
ax2.set_xlabel('再現率 (Recall)')
ax2.set_ylabel('適合率 (Precision)')
ax2.set_title('PR曲線')
ax2.legend(loc="lower left")
plt.tight_layout()
plt.show()
特徴量の重要度分析
# 特徴量の重要度を取得
importance_df = pd.DataFrame({
'feature': feature_cols,
'importance': model.feature_importance(importance_type='gain')
}).sort_values('importance', ascending=False)
# 可視化
plt.figure(figsize=(10, 8))
plt.barh(importance_df['feature'][:15], importance_df['importance'][:15])
plt.xlabel('重要度')
plt.title('特徴量の重要度(上位15個)')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()
print("重要度上位5つの特徴量:")
print(importance_df.head())
閾値の最適化
ビジネス要件に応じて、予測の閾値を調整することが重要です。
# 様々な閾値での評価
thresholds = np.arange(0.1, 0.9, 0.05)
results = []
for threshold in thresholds:
y_pred_thresh = (y_pred_proba >= threshold).astype(int)
results.append({
'threshold': threshold,
'precision': precision_score(y_test, y_pred_thresh),
'recall': recall_score(y_test, y_pred_thresh),
'f1': f1_score(y_test, y_pred_thresh)
})
results_df = pd.DataFrame(results)
# 可視化
plt.figure(figsize=(10, 6))
plt.plot(results_df['threshold'], results_df['precision'], label='Precision', marker='o')
plt.plot(results_df['threshold'], results_df['recall'], label='Recall', marker='s')
plt.plot(results_df['threshold'], results_df['f1'], label='F1-Score', marker='^')
plt.xlabel('閾値')
plt.ylabel('スコア')
plt.title('閾値による評価指標の変化')
plt.legend()
plt.grid(True)
plt.show()
# 最適な閾値の選択(F1スコアが最大となる閾値)
optimal_threshold = results_df.loc[results_df['f1'].idxmax(), 'threshold']
print(f"F1スコアが最大となる閾値: {optimal_threshold:.2f}")
scikit-learn APIの使用
LightGBMはscikit-learn互換のAPIも提供しています。
from lightgbm import LGBMClassifier
# scikit-learn APIを使用
lgb_clf = LGBMClassifier(
n_estimators=1000,
learning_rate=0.05,
num_leaves=31,
random_state=42,
n_jobs=-1
)
# 学習
lgb_clf.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
eval_metric='logloss',
callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)]
)
# 予測
y_pred_sklearn = lgb_clf.predict(X_test)
y_pred_proba_sklearn = lgb_clf.predict_proba(X_test)[:, 1]
# 評価
print(f"Accuracy (sklearn API): {accuracy_score(y_test, y_pred_sklearn):.4f}")
print(f"AUC-ROC (sklearn API): {roc_auc_score(y_test, y_pred_proba_sklearn):.4f}")
まとめ
今回は、LightGBMを使った分類問題の実装方法を詳しく学びました。重要なポイントは:
- データの前処理: カテゴリ変数の適切な処理が重要
- クラス不均衡への対処:
is_unbalance
パラメータやクラスウェイトの使用 - モデルの評価: 単一の指標だけでなく、複数の観点から評価
- 閾値の最適化: ビジネス要件に応じた調整
- 特徴量の重要度: モデルの解釈性を高める
次回は「回帰問題とパラメータチューニング」について解説します。LightGBMのパラメータを最適化する方法を詳しく見ていきます。
演習問題
- 異なる評価指標(例:
auc
)を使ってモデルを学習し、結果を比較してみましょう - カテゴリ変数をOne-Hot エンコーディングした場合と比較してみましょう
scale_pos_weight
パラメータを使ってクラス不均衡に対処してみましょう
ご質問やフィードバックがございましたら、ぜひコメント欄でお知らせください。次回もお楽しみに!