【Python×LightGBM入門】第2回:基本的な分類問題への適用

26分で読めます
エンハンスド技術チーム
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を使った分類問題の実装方法を詳しく学びました。重要なポイントは:

  1. データの前処理: カテゴリ変数の適切な処理が重要
  2. クラス不均衡への対処: is_unbalanceパラメータやクラスウェイトの使用
  3. モデルの評価: 単一の指標だけでなく、複数の観点から評価
  4. 閾値の最適化: ビジネス要件に応じた調整
  5. 特徴量の重要度: モデルの解釈性を高める

次回は「回帰問題とパラメータチューニング」について解説します。LightGBMのパラメータを最適化する方法を詳しく見ていきます。

演習問題

  1. 異なる評価指標(例:auc)を使ってモデルを学習し、結果を比較してみましょう
  2. カテゴリ変数をOne-Hot エンコーディングした場合と比較してみましょう
  3. scale_pos_weightパラメータを使ってクラス不均衡に対処してみましょう

ご質問やフィードバックがございましたら、ぜひコメント欄でお知らせください。次回もお楽しみに!

技術的な課題をお持ちですか?

記事でご紹介した技術や実装について、
より詳細なご相談やプロジェクトのサポートを承ります

無料技術相談を申し込む