0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

2クラス分類(ピーマ・インディアン糖尿病診断)

Last updated at Posted at 2025-09-15

この記事でわかること

  • ピマ・インディアン糖尿病データセットを用いた2クラス分類の実装手順
  • 欠損値処理・特徴量選択・標準化などの前処理
  • ロジスティック回帰モデルによる分類と評価指標(混同行列・精度・再現率・F値)

2クラス分類_ピーマ・インディアン糖尿病診断

本記事では2クラス分類のフルパイプラインを構築します。
具体的には、欠損値の削除、補完外れ値の確認(IQR/3σ)、相関係数・VIFによる多重共線性の整理、モデル比較 & ハイパーパラメータチューニング、クロスバリデーション+ホールドアウトによる精度評価までを一気通貫で実装します。
可視化やログ出力を組み込み、再現性と解釈性の高い分析ワークフローを再現します。

使用するデータセットは、UCバークレー大学の「UCI Machine Leaning Repository」で公開されている「ピーマ・インディアンの糖尿病診断のデータセット」です。
データセットの構成は下記となっていて目的変数はClassで分類を想定したデータ分析を実施します。

Idx 属性名 説明
0 NumTimePreg 妊娠した回数
1 OralGluTol 経口ブドウ糖負荷試験における2時間後の血糖値
2 BloodPres 血圧
3 SkinThick 皮膜厚さ
4 SerumInsulin 血清インスリン濃度
5 BMI BMI
6 PedigreeFunc 血統(家系における糖尿病の遺伝的歴史)
7 Age 年齢
8 Class 1 : 疾患あり, 0 : 疾患なし

本記事で実施する回帰分析は、以下のステップに沿って進めます。

  1. データ読込と基本情報確認
  2. 目的変数(ターゲット)とデータ不均衡
  3. 欠損値処理(除去ルール → 補完 → 検証)
  4. 外れ値の対処検討
  5. 特徴量間の相関と多重共線性
  6. モデル候補とハイパーパラメータ探索
  7. 評価指標と検証
  8. モデル解釈と可視化
  9. SMOTEと特徴量選択によるモデル改善
  10. モデル保存・運用

1. データ読込と基本情報確認

指定されたファイルパスからデータを読み込み、前処理の第一段階として、目的変数(targetで指定された列)に欠損値がある行を削除します。その後、データセットの初期状態と、欠損行削除後の最終的な形状、列名、欠損件数などの基本情報を出力して確認します。

import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LassoCV
from sklearn.ensemble import RandomForestRegressor
from xgboost import XGBRegressor
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
from statsmodels.stats.outliers_influence import variance_inflation_factor
import joblib

# 日本語フォントの設定
plt.rcParams['font.family'] = 'Meiryo'      # Windowsの場合
# Macの場合は 'Hiragino Sans' など
plt.rcParams['axes.unicode_minus'] = False # 負の符号の文字化けを防ぐ

# 使用するデータセットのファイルパスと目的変数を設定
# 今回は架空のファイルパスを設定しています。実際のファイルパスに置き換えてください。
file_path = './data.csv'
target = 'Class' # 目的変数(クラス)のカラム名

# ファイルの読み込み
df = pd.read_csv(file_path)
print("--- データセットの読み込み完了 ---")

# 目的変数に欠損がある行を削除
initial_shape = df.shape
missing_target_count = df[target].isnull().sum()
df.dropna(subset=[target], inplace=True)
df[target] = df[target].astype(int) # 目的変数を整数型に変換

# ログ出力
print(f"データの前処理前の形状: {initial_shape}")
print(f"最終的なデータの形状: {df.shape}")
print(f"列名一覧: {list(df.columns)}")
print(f"目的変数 '{target}' の欠損件数: {missing_target_count}")
print(f"→ 欠損行は削除されました。")

実行ログ

html_img_001.png

--- データセットの読み込み完了 ---
データの前処理前の形状: (768, 9)
最終的なデータの形状: (768, 9)
列名一覧: ['NumTimePreg', 'OralGluTol', 'BloodPres', 'SkinThick', 'SerumInsulin', 'BMI', 'PedigreeFunc', 'Age', 'Class']
目的変数 'Class' の欠損件数: 0
→ 欠損行は削除されました。

2. 目的変数(ターゲット)とデータ不均衡

目的変数(Class)の分布を分析し、クラスごとの件数と割合を算出します。この結果を棒グラフで可視化することで、データセットにクラス不均衡が存在するかどうかを評価します。最後に、不均衡の度合いに基づいて、今後のモデル学習でどのような対策(例:SMOTE、クラス重み付け)を検討すべきかを判断します。

import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter

# 目的変数(クラス)の確認
print(f"予測したいクラス: '{target}'")
print("クラスは 0/1 の2値です。")

# クラスごとの件数と割合を算出
class_counts = df[target].value_counts()
class_ratios = df[target].value_counts(normalize=True) * 100

print("--- クラス分布 ---")
print(f"クラスごとの件数:\n{class_counts}")
print(f"クラスごとの割合:\n{class_ratios.round(2)}%")
print(f"不均衡比 (クラス1/クラス0): {class_ratios.loc[1] / class_ratios.loc[0]:.2f}")

# クラス分布の可視化
plt.figure(figsize=(8, 5))
sns.barplot(x=class_counts.index, y=class_counts.values)
plt.title('目的変数クラスの分布')
plt.xlabel('クラス')
plt.ylabel('件数')
plt.xticks(class_counts.index, labels=['クラス 0', 'クラス 1'])
plt.show()

# クラス不均衡への配慮をログに出力
if class_ratios.min() < 30: # 割合が30%未満であれば不均衡と判断(目安)
    print("\n[ログ] クラス不均衡が確認されました。")
    print("→ クラス重み付け、SMOTE等のサンプリング、Stratified CVの適用を検討します。")
    sampling_needed = True
else:
    print("\n[ログ] クラス不均衡は軽微です。")
    print("→ Stratified CVは標準で適用し、必要に応じてサンプリングを検討します。")
    sampling_needed = False

実行ログ

予測したいクラス: 'Class'
クラスは 0/1 の2値です。
--- クラス分布 ---
クラスごとの件数:
Class
0    500
1    268
Name: count, dtype: int64
クラスごとの割合:
Class
0    65.1
1    34.9
Name: proportion, dtype: float64%
不均衡比 (クラス1/クラス0): 0.54


[ログ] クラス不均衡は軽微です。
→ Stratified CVは標準で適用し、必要に応じてサンプリングを検討します。

データのクラス不均衡について

  1. クラス分布の現状:

    • クラス0(多数派): 500件 (65.1%)
    • クラス1(少数派): 268件 (34.9%)
    • 不均衡比(クラス1 / クラス0): 0.54
    • この結果は、クラス1のデータがクラス0のデータに比べて約半分しか存在しないことを示しています。
  2. 不均衡の判断:

    • コードのロジック (if class_ratios.min() < 30) に基づくと、少数派クラスの割合が34.9%であるため、「不均衡は軽微」と判断されるはずです。しかし、一般的な機械学習の観点では、この程度の不均衡でもモデルの性能、特に**再現率(Recall)**に影響を与える可能性があります。

考察と次のステップ

このクラス分布は、モデルの予測において以下のような影響を与える可能性があります。

  1. 予測の偏り: モデルは件数の多いクラス0を学習しやすくなり、クラス1(疾患あり)の予測精度が低下する可能性があります。その結果、**疾患の見逃し(偽陰性)**が増え、再現率が低くなることが懸念されます。
  2. 評価指標の選択: クラス不均衡があるため、単一の正答率(Accuracy)だけではモデルの真の性能を評価できません。多数派クラスをすべて正しく予測しても高い正答率が出てしまうからです。そのため、F1スコアや再現率(Recall)適合率(Precision)、そしてROC-AUCなどの指標を組み合わせて評価する必要があります。
  3. 推奨される対策: クラスの割合が34.9%であるため、SMOTEなどの積極的なサンプリング手法は必須ではありませんが、再現率を最重要視する医療分野の文脈を考慮すると、試してみる価値は十分にあります。
    • クラス重み付け: モデルの訓練時に、少数派クラスの誤分類に対するペナルティを大きく設定します。
    • Stratified K-Fold Cross-Validation: 交差検証を行う際、各フォールドでクラスの割合を元のデータセットと同じに保つことで、信頼性の高い評価が可能になります。
    • SMOTE: 再現率のさらなる改善が必要な場合に、訓練データのみに適用することで、少数派クラスのサンプルを人工的に増やします。

結論

このデータセットはやや不均衡ですが、標準的なモデル学習で問題なく対応できます。ただし、疾患あり(Class=1)の見逃しを防ぎたいという分析目的なら、再現率の向上を意識したクラス重み付けや閾値調整を検討すべきです。学習・評価時には必ずStratified CVを適用し、クラス比を保った検証を行うことが望ましいです。

3. 欠損値処理(除去ルール → 補完 → 検証)

データセットに存在する欠損値を、事前に定めたルールに基づいて処理します。具体的には、欠損値の割合が高い列と行を削除し、残った欠損値を数値データには中央値、カテゴリデータには最頻値で補完します。処理の前後でデータの状態を可視化・確認し、欠損値が適切に処理されたことを検証します。

  1. 欠損値の可視化: missingnoライブラリのmsno.bar(df)を使用することで、各特徴量にどれくらいの欠損値が含まれているかを棒グラフで視覚的に確認できます。これにより、どの列に欠損値が多いかを一目で把握できます。
  2. 除去ルールの適用:
    • MISSING_COL_THRESH(列の閾値)とMISSING_ROW_THRESH(行の閾値)を設定し、欠損率が高い列や行をデータから削除します。このコードでは、欠損率が40%を超える列と、欠損率が30%を超える行を削除しています。これにより、欠損値が多すぎて信頼性の低いデータを切り捨てることができます。
  3. 欠損値の補完:
    • 中央値での補完: df[col].fillna(median_val, inplace=True)を用いて、数値型の列(連続変数)の欠損値を中央値で埋めます。中央値は外れ値の影響を受けにくいため、平均値よりも堅牢な補完方法としてよく使われます。
    • 最頻値での補完: df[col].fillna(mode_val, inplace=True)を用いて、カテゴリ型の列の欠損値を最頻値(最も多く出現する値)で埋めます。
  4. 補完後の検証: 欠損値処理が完了した後、df.isnull().sum().sum()で全体の欠損値がゼロになったことを確認します。さらに、seaborn.histplot()を使って、補完後の数値特徴量の分布をヒストグラムで可視化します。これにより、補完処理がデータの分布を大きく歪めていないかを視覚的にチェックできます。
  5. 統計量の確認: 最後にdf.describe().Tを実行し、最終的なデータフレームの要約統計量を出力します。これは、各特徴量の平均、標準偏差、最小・最大値などを確認し、データが正常に前処理されたかを総合的に判断するために役立ちます。

この一連のステップは、モデル学習の精度に直結するデータの品質を保証するために不可欠です。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import missingno as msno

# 欠損値の可視化 (補完前)
print("--- 欠損値の可視化(補完前)---")
msno.bar(df)
plt.title('欠損値の棒グラフ (補完前)')
plt.show()

# 既定の除去ルールを適用
MISSING_COL_THRESH = 0.40
MISSING_ROW_THRESH = 0.30

initial_shape = df.shape
# 列欠損率が40%超の列を削除
col_missing_rates = df.isnull().sum() / len(df)
cols_to_drop = col_missing_rates[col_missing_rates > MISSING_COL_THRESH].index.tolist()
df.drop(columns=cols_to_drop, inplace=True)

# 行欠損率が30%超の行を削除
row_missing_rates = df.isnull().sum(axis=1) / df.shape[1]
rows_to_drop_count = (row_missing_rates > MISSING_ROW_THRESH).sum()
df.drop(index=df[row_missing_rates > MISSING_ROW_THRESH].index, inplace=True)

print("\n--- 欠損値除去のログ ---")
print(f"処理前の形状: {initial_shape}")
print(f"列欠損率 > {MISSING_COL_THRESH * 100}% で削除した列: {cols_to_drop if cols_to_drop else 'なし'}")
print(f"行欠損率 > {MISSING_ROW_THRESH * 100}% で削除した行数: {rows_to_drop_count}")
print(f"処理後の形状: {df.shape}")

# 欠損値の補完
numerical_cols = df.select_dtypes(include=np.number).columns.tolist()
categorical_cols = df.select_dtypes(include='object').columns.tolist()
if 'outcome' in numerical_cols:
    numerical_cols.remove('outcome')

imputation_methods = {}
for col in numerical_cols:
    if df[col].isnull().any():
        median_val = df[col].median()
        df[col].fillna(median_val, inplace=True)
        imputation_methods[col] = f'中央値 ({median_val:.2f})'

for col in categorical_cols:
    if df[col].isnull().any():
        mode_val = df[col].mode()[0]
        df[col].fillna(mode_val, inplace=True)
        imputation_methods[col] = f'最頻値 ({mode_val})'

print("\n--- 欠損値補完のログ ---")
print("各列の補完方式:", imputation_methods if imputation_methods else "欠損値なし")
print(f"処理後の欠損値数:\n{df.isnull().sum().sum()}")

# すべての数値列のヒストグラムを比較
print("\n--- すべての数値列の補完後ヒストグラムを比較 ---")
if numerical_cols:
    num_cols = len(numerical_cols)
    cols_per_row = 3
    rows = (num_cols + cols_per_row - 1) // cols_per_row  # ヒストグラムの行数を動的に計算
    
    plt.figure(figsize=(15, rows * 4)) # figsizeを動的に調整
    
    for i, col in enumerate(numerical_cols):
        plt.subplot(rows, cols_per_row, i + 1)
        sns.histplot(df[col], kde=True, bins=20)
        plt.title(f'補完後の {col} 分布')
    
    plt.tight_layout()
    plt.show()

print("\n--- 最終的なデータフレームの統計量 ---")
print(df.describe().T)

実行ログ

html_img_002.png

--- 欠損値の可視化(補完前)---


--- 欠損値除去のログ ---
処理前の形状: (768, 9)
列欠損率 > 40.0% で削除した列: なし
行欠損率 > 30.0% で削除した行数: 0
処理後の形状: (768, 9)

--- 欠損値補完のログ ---
各列の補完方式: 欠損値なし
処理後の欠損値数:
0 件

--- すべての数値列の補完後ヒストグラムを比較 ---


--- 最終的なデータフレームの統計量 ---
              count        mean         std     min       25%       50%  \
NumTimePreg   768.0    3.845052    3.369578   0.000   1.00000    3.0000   
OralGluTol    768.0  120.894531   31.972618   0.000  99.00000  117.0000   
BloodPres     768.0   69.105469   19.355807   0.000  62.00000   72.0000   
SkinThick     768.0   20.536458   15.952218   0.000   0.00000   23.0000   
SerumInsulin  768.0   79.799479  115.244002   0.000   0.00000   30.5000   
BMI           768.0   31.992578    7.884160   0.000  27.30000   32.0000   
PedigreeFunc  768.0    0.471876    0.331329   0.078   0.24375    0.3725   
Age           768.0   33.240885   11.760232  21.000  24.00000   29.0000   
Class         768.0    0.348958    0.476951   0.000   0.00000    0.0000   

                    75%     max  
NumTimePreg     6.00000   17.00  
OralGluTol    140.25000  199.00  
BloodPres      80.00000  122.00  
SkinThick      32.00000   99.00  
SerumInsulin  127.25000  846.00  
BMI            36.60000   67.10  
PedigreeFunc    0.62625    2.42  
Age            41.00000   81.00  
Class           1.00000    1.00  

html_img_003.png

生理学的にあり得ない0値をNaNに置換し、中央値で補完する

このコードは、元のデータセットに潜む**「隠れた欠損値」**を適切に処理します。

0値をNaNに変換: 生理学的にあり得ない0の値を持つ列(OralGluTol, BloodPres, SkinThick, SerumInsulin, BMI)を中央値で補完します。中央値は外れ値の影響を受けにくいため、平均値よりも堅牢な補完方法です。

再可視化: 補完後のデータのヒストグラムを再度プロットし、クラスごとの分布がどのように変化したかを確認します。これにより、補完処理が特徴量の分布に与える影響を視覚的に評価できます。

この処理により、モデルが0という誤った値から学習することを防ぎ、より正確な予測モデルを構築するための基盤が整います。特にBMIやBloodPresといった、0値が多数含まれていた特徴量において、分布がより現実的な形に改善されることが期待されます。


# 隠れた欠損値(0)を持つ可能性のある列を特定
zero_imputation_cols = ['OralGluTol', 'BloodPres', 'SkinThick', 'SerumInsulin', 'BMI']

# 処理前の統計量を表示して確認
print("--- 処理前の統計量(0値を含む)---")
print(df[zero_imputation_cols].describe().T)

# 特定した列の0をNaN(欠損値)に置き換える
# これにより、以降の補完処理でこれらの値が考慮されるようになります
df[zero_imputation_cols] = df[zero_imputation_cols].replace(0, np.nan)

# NaNに置き換わった件数を確認
print("\n--- 0値からNaNに置き換わった件数 ---")
for col in zero_imputation_cols:
    nan_count = df[col].isnull().sum()
    print(f"{col}: {nan_count}")

# 中央値で欠損値を補完
# 各列の中央値を計算し、その値でNaNを埋めます
imputation_methods = {}
for col in zero_imputation_cols:
    median_val = df[col].median()
    df[col].fillna(median_val, inplace=True)
    imputation_methods[col] = f'中央値 ({median_val:.2f})'

print("\n--- 欠損値補完のログ ---")
print("各列の補完方式:", imputation_methods)

# 処理後の統計量と欠損値の最終確認
print("\n--- 処理後の統計量(0値が補完された後)---")
print(df[zero_imputation_cols].describe().T)
print(f"最終的な欠損値数:\n{df.isnull().sum().sum()}")

# 補完後のヒストグラムをクラス別に再可視化
print("\n--- 補完後の特徴量分布(クラス別)---")
numerical_features = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col]) and col != target]
if not numerical_features:
    print("可視化する数値特徴量がありません。")
else:
    df_class0 = df.query(f"{target}==0")
    df_class1 = df.query(f"{target}==1")
    num_plots = len(numerical_features)
    cols_per_row = 3
    rows = (num_plots + cols_per_row - 1) // cols_per_row

    plt.style.use('seaborn-v0_8-whitegrid')
    plt.figure(figsize=(15, rows * 4))
    plt.suptitle("Features (Class 0 vs Class 1) After Imputation", fontsize=20, y=1.02)

    for i, col in enumerate(numerical_features):
        ax = plt.subplot(rows, cols_per_row, i + 1)
        ax.hist(df_class0[col], bins=30, alpha=0.5, label="No Disease", color='royalblue')
        ax.hist(df_class1[col], bins=30, alpha=0.5, label="Disease", color='firebrick')
        ax.set_title(f'Distribution of {col}', fontsize=14)
        ax.set_xlabel(col)
        ax.set_ylabel("Frequency")
        ax.legend()
    
    plt.tight_layout(rect=[0, 0, 1, 0.96])
    plt.show()

実行ログ

--- 処理前の統計量(0値を含む)---
              count        mean         std  min   25%    50%     75%    max
OralGluTol    768.0  120.894531   31.972618  0.0  99.0  117.0  140.25  199.0
BloodPres     768.0   69.105469   19.355807  0.0  62.0   72.0   80.00  122.0
SkinThick     768.0   20.536458   15.952218  0.0   0.0   23.0   32.00   99.0
SerumInsulin  768.0   79.799479  115.244002  0.0   0.0   30.5  127.25  846.0
BMI           768.0   31.992578    7.884160  0.0  27.3   32.0   36.60   67.1

--- 0値からNaNに置き換わった件数 ---
OralGluTol: 5 件
BloodPres: 35 件
SkinThick: 227 件
SerumInsulin: 374 件
BMI: 11 件

--- 欠損値補完のログ ---
各列の補完方式: {'OralGluTol': '中央値 (117.00)', 'BloodPres': '中央値 (72.00)', 'SkinThick': '中央値 (29.00)', 'SerumInsulin': '中央値 (125.00)', 'BMI': '中央値 (32.30)'}

--- 処理後の統計量(0値が補完された後)---
              count        mean        std   min     25%    50%     75%    max
OralGluTol    768.0  121.656250  30.438286  44.0   99.75  117.0  140.25  199.0
BloodPres     768.0   72.386719  12.096642  24.0   64.00   72.0   80.00  122.0
SkinThick     768.0   29.108073   8.791221   7.0   25.00   29.0   32.00   99.0
SerumInsulin  768.0  140.671875  86.383060  14.0  121.50  125.0  127.25  846.0
BMI           768.0   32.455208   6.875177  18.2   27.50   32.3   36.60   67.1
最終的な欠損値数:
0 件

--- 補完後の特徴量分布(クラス別)---

html_img_004.png

4. 外れ値の対処検討

数値特徴量ごとに箱ひげ図をプロットし、データセットに外れ値が存在するかどうかを視覚的に確認します。この段階では外れ値の除去は行わず、今後のモデル選択や分析方針を立てるための判断材料とします。

# 数値特徴量のリストを準備
numerical_cols = df.select_dtypes(include=np.number).columns.tolist()
if target in numerical_cols:
    numerical_cols.remove(target)

print("--- 外れ値の可視化(処理前)---")
if numerical_cols:
    num_cols = len(numerical_cols)
    cols_per_row = 3
    rows = (num_cols + cols_per_row - 1) // cols_per_row

    # すべての数値列の箱ひげ図
    plt.figure(figsize=(15, rows * 4))
    for i, col in enumerate(numerical_cols):
        plt.subplot(rows, cols_per_row, i + 1)
        sns.boxplot(y=df[col])
        plt.title(f'補完前の {col} ')
    plt.tight_layout()
    plt.show()

else:
    print("数値特徴量がありません。")

実行ログ

--- 外れ値の可視化(処理前)---

html_img_005.png

全特徴量に対する「疾患あり、なし」の箱ひげ図可視化

すべての数値特徴量について、目的変数(ここでは「疾患なし」と「疾患あり」の2つのクラス)でデータを層別化した箱ひげ図をプロットします。これにより、各クラス間の特徴量の分布や中央値、外れ値の違いを比較し、モデルの予測に役立つ特徴量を見つけ出すことを目的とします。

if 'target' not in locals() or 'df' not in locals() or 'Class' not in df.columns:
    print("エラー: 必要な変数 (df) が定義されていないか、目的変数がデータフレームに存在しません。")
else:
    # 目的変数の列名を 'Class' に固定
    target = 'Class'
    
    # 目的変数以外の数値列をリストアップ
    numerical_features = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col]) and col != target]

    if not numerical_features:
        print("可視化する数値特徴量がありません。")
    else:
        # サブプロットのレイアウトを動的に計算
        num_plots = len(numerical_features)
        cols_per_row = 3
        rows = (num_plots + cols_per_row - 1) // cols_per_row

        plt.style.use('seaborn-v0_8-whitegrid')
        plt.figure(figsize=(15, rows * 4))
        
        plt.suptitle("Box Plots of Features (Class 0 vs Class 1)", fontsize=20, y=1.02)
        
        for i, col in enumerate(numerical_features):
            ax = plt.subplot(rows, cols_per_row, i + 1)
            
            # sns.boxplotを使用し、x軸に目的変数を指定してクラス別に分割
            sns.boxplot(x=df[target].astype('category'), y=df[col], palette='coolwarm', ax=ax)
            
            # 軸ラベルを英語に修正
            ax.set_xticklabels(["Class 0", "Class 1"])
            ax.set_xlabel("Class")
            ax.set_ylabel(col, fontsize=12)
            ax.set_title(f' {col}', fontsize=14)
        
        plt.tight_layout(rect=[0, 0, 1, 0.96])
        plt.show()

html_img_006.png

外れ値を除去するメリット・デメリット

外れ値を「除去する」か「除去せず緩和する」か、どちらが良いかは、使用するモデルやデータの性質に大きく依存します。 どちらの方法にもメリット・デメリットがあるため、一概にどちらが優れているとは言えません。

メリット

  • 線形モデルの精度向上: ロジスティック回帰や線形回帰など、外れ値に敏感なモデルでは、外れ値を除去することで予測精度が大幅に向上する可能性があります。外れ値がモデルのフィットを歪めることを防げます。
  • データの正規化: 外れ値を削除することで、データの分布がより正規分布に近くなり、統計的な仮定を満たしやすくなります。
  • 計算の安定性: 一部のアルゴリズムでは、外れ値が計算を不安定にすることがありますが、除去することで安定性が増します。

デメリット

  • 情報損失: 外れ値が単なるエラーではなく、ビジネス上重要な意味を持つ可能性(例:不正行為、希少疾患の患者データなど)があります。これらを安易に除去すると、貴重な情報が失われるリスクがあります。
  • 恣意的な判断: どこからが外れ値かという境界線は、IQRや3σといった基準で決めるものの、絶対的なものではありません。分析者の主観が入り込む可能性があります。

外れ値を除去せず緩和するメリット・デメリット

メリット

  • 情報保持: ほとんどのデータポイントを保持できるため、外れ値が持つ可能性のある重要な情報を失うことがありません。
  • ツリー系モデルとの相性: 決定木ランダムフォレストXGBoostといったツリー系のモデルは、外れ値に対して非常にロバスト(頑健)です。これらのモデルはデータを区間ごとに分割して学習するため、一つの極端な値に大きな影響を受けません。
  • 自動的な処理: データに手を加える必要がないため、前処理の手間が減り、パイプラインの構築が簡素化されます。

デメリット

  • 線形モデルへの影響: 前述の通り、線形モデルを使用する場合は、外れ値がモデルの性能を低下させる可能性があります。
  • 可視化の課題: 外れ値が存在すると、箱ひげ図や散布図などの可視化が難しくなり、データの全体像を把握しにくくなる場合があります。

結論:どちらを選ぶべきか

このコードでは、ランダムフォレストやXGBoostといったツリー系モデルを将来的に使用する場合、外れ値を無理に除去せず、そのまま利用する方針が合理的です。

おすすめの方針:

  1. ツリー系モデルの活用: ランダムフォレストやXGBoostなどのモデルを使うことで、外れ値の影響を自然に緩和できます。
  2. ロバストな評価指標の使用: 評価には、RMSEやMAEといった外れ値の影響を受けにくい評価指標を併用することを推奨します。

しかし、今回のケースではあえて外れ値を「3sigma」を基準に除去して進めます。

# どの手法で外れ値を除去するか選択
# 'iqr' または '3sigma' を選択
OUTLIER_REMOVAL_METHOD = '3sigma' # ← この行が追加されました

def remove_outliers(df, col, method='iqr'):
    """
    指定された列の外れ値をIQRまたは3σ法で除去する関数
    """
    if method == 'iqr':
        Q1 = df[col].quantile(0.25)
        Q3 = df[col].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        df_filtered = df[(df[col] >= lower_bound) & (df[col] <= upper_bound)]
        num_removed = len(df) - len(df_filtered)
        if num_removed > 0:
            print(f"  - {col}: IQR法で {num_removed} 件の外れ値を削除")
        return df_filtered
    
    elif method == '3sigma':
        mean = df[col].mean()
        std = df[col].std()
        lower_bound = mean - 3 * std
        upper_bound = mean + 3 * std
        df_filtered = df[(df[col] >= lower_bound) & (df[col] <= upper_bound)]
        num_removed = len(df) - len(df_filtered)
        if num_removed > 0:
            print(f"  - {col}: 3σ法で {num_removed} 件の外れ値を削除")
        return df_filtered
    
    else:
        print("エラー: 有効な外れ値除去手法 ('iqr' または '3sigma') を指定してください。")
        return df

print(f"\n--- 外れ値の除去を開始(手法: {OUTLIER_REMOVAL_METHOD})---")
initial_shape = df.shape
print(f"処理前のデータの形状: {initial_shape}")

# 目的変数以外の数値列をリストアップ
numerical_features = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col]) and col != target]

# 各数値特徴量について外れ値を除去
for col in numerical_features:
    df = remove_outliers(df, col, method=OUTLIER_REMOVAL_METHOD)

final_shape = df.shape
removed_rows = initial_shape[0] - final_shape[0]

print("\n--- 外れ値の除去完了 ---")
print(f"処理後のデータの形状: {final_shape}")
print(f"合計で {removed_rows} 件の行が削除されました。")

実行ログ


--- 外れ値の除去を開始(手法: 3sigma)---
処理前のデータの形状: (768, 9)
  - NumTimePreg: 3σ法で 4 件の外れ値を削除
  - BloodPres: 3σ法で 8 件の外れ値を削除
  - SkinThick: 3σ法で 4 件の外れ値を削除
  - SerumInsulin: 3σ法で 20 件の外れ値を削除
  - BMI: 3σ法で 3 件の外れ値を削除
  - PedigreeFunc: 3σ法で 10 件の外れ値を削除
  - Age: 3σ法で 5 件の外れ値を削除

--- 外れ値の除去完了 ---
処理後のデータの形状: (714, 9)
合計で 54 件の行が削除されました。

html_img_007.png

# 全特徴量に対する「疾患あり、なし」の分布可視化(外れ値除去後)

if 'target' not in locals() or 'df' not in locals():
    print("エラー: 必要な変数 (df, target) が定義されていません。前のステップを実行してください。")
else:
    if target not in df.columns:
        print(f"エラー: 目的変数 '{target}' がデータフレームに存在しません。")
    else:
        # 目的変数以外の数値列をリストアップ
        numerical_features = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col]) and col != target]

        if not numerical_features:
            print("可視化する数値特徴量がありません。")
        else:
            # クラス別にデータを分割
            df_class0 = df.query(f"{target}==0")
            df_class1 = df.query(f"{target}==1")

            # サブプロットのレイアウトを動的に計算
            num_plots = len(numerical_features)
            cols_per_row = 3
            rows = (num_plots + cols_per_row - 1) // cols_per_row

            plt.style.use('seaborn-v0_8-whitegrid')
            plt.figure(figsize=(15, rows * 4))
            
            # グラフタイトルを英語に修正
            plt.suptitle("Features (Class 0 vs Class 1)", fontsize=20, y=1.02)

            for i, col in enumerate(numerical_features):
                ax = plt.subplot(rows, cols_per_row, i + 1)
                
                # matplotlib.pyplot.histを直接使用
                ax.hist(df_class0[col], bins=30, alpha=0.5, label="No Disease", color='royalblue')
                ax.hist(df_class1[col], bins=30, alpha=0.5, label="Disease", color='firebrick')
                
                ax.set_title(f'Distribution of {col}', fontsize=14)
                ax.set_xlabel(col)
                ax.set_ylabel("Frequency")
                ax.legend()
                
            plt.tight_layout(rect=[0, 0, 1, 0.96])
            plt.show()

5. 特徴量間の相関と多重共線性

VIF(Variance Inflation Factor)と相関行列という2つの異なる手法を用いて、特徴量間の相関の強さを評価します。これにより、モデルに冗長な情報を提供している特徴量を特定し、特徴量選択を行うための判断材料を得ます。

  1. VIF(分散拡大係数)の計算

    • VIFは、ある特徴量が他の特徴量によってどの程度説明できるかを示す指標です。簡単に言うと、VIFの値が高い特徴量は、他の特徴量と強い相関があることを意味します。
    • このコードでは、statsmodelsライブラリを使用して各数値特徴量のVIFを計算し、VIF10を超える特徴量は多重共線性が高いと判断されます。このような特徴量は、モデルの学習を不安定にさせ、解釈を困難にする可能性があるため、削除を検討すべきです。
  2. 相関行列とヒートマップの表示

    • VIFが全体的な共線性の度合いを示すのに対し、相関行列は個々の特徴量ペア間の相関を具体的に示します。
    • corr()メソッドで相関行列を計算し、その結果をseaborn.heatmap()でヒートマップとして可視化します。
    • 色と数値の解釈:
      • 色が濃く、数値が1.0に近いほど、その2つの特徴量間に強い正の相関(一方が増えればもう一方も増える)があります。
      • 色が濃く、数値が-1.0に近いほど、強い負の相関(一方が増えればもう一方が減る)があります。
      • 色が薄く、数値が0に近い場合、相関はほとんどありません。
    • この可視化により、VIFスコアが高い特徴量が、具体的にどの特徴量と強く相関しているかを視覚的に確認できます。

この2つのステップは、モデルの性能を向上させるために、**「どの特徴量を残し、どの特徴量を削除すべきか」**という重要な意思決定を下すための科学的な根拠を提供します。

from statsmodels.stats.outliers_influence import variance_inflation_factor

# 目的変数とカテゴリカル変数を一時的に除外
numerical_features = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col]) and col != target]
X_vif = df[numerical_features]

# VIFの計算
vif_data = pd.DataFrame()
vif_data['feature'] = X_vif.columns
vif_data['VIF'] = [variance_inflation_factor(X_vif.values, i) for i in range(len(X_vif.columns))]

print("--- 特徴量ごとのVIFスコア ---")
print(vif_data.sort_values(by='VIF', ascending=False))

# VIFスコアの解釈
print("\n[ログ] VIFスコアは一般的に10を超えると多重共線性が高いとされます。")
print("→ スコアが高い特徴量から順に、モデルから除外することを検討してください。")

## 相関行列とヒートマップの表示
if 'df' not in locals() or 'target' not in locals():
    print("エラー: 必要な変数 (df, target) が定義されていません。")
else:
    # 目的変数と数値特徴量のリストを作成
    numerical_cols = df.select_dtypes(include=np.number).columns.tolist()

    # 相関行列の計算
    corr_matrix = df[numerical_cols].corr()
    print(corr_matrix)

    # ヒートマップの表示
    plt.figure(figsize=(12, 10))
    sns.heatmap(
        corr_matrix,
        annot=True,
        fmt='.2f',
        cmap='coolwarm',
        vmin=-1,
        vmax=1,
        linewidths=0.5,
        linecolor='black'
    )
    plt.title('Feature Correlation Matrix', fontsize=16)
    plt.show()

実行ログ

--- 特徴量ごとのVIFスコア ---
        feature        VIF
5           BMI  38.546155
2     BloodPres  36.385190
1    OralGluTol  21.877797
3     SkinThick  19.428478
7           Age  15.723150
4  SerumInsulin   8.122400
6  PedigreeFunc   3.638945
0   NumTimePreg   3.439915

[ログ] VIFスコアは一般的に10を超えると多重共線性が高いとされます。
→ スコアが高い特徴量から順に、モデルから除外することを検討してください。
              NumTimePreg  OralGluTol  BloodPres  SkinThick  SerumInsulin  \
NumTimePreg      1.000000    0.135414   0.215453   0.131494      0.078779   
OralGluTol       0.135414    1.000000   0.216296   0.138211      0.421252   
BloodPres        0.215453    0.216296   1.000000   0.216157      0.053376   
SkinThick        0.131494    0.138211   0.216157   1.000000      0.167588   
SerumInsulin     0.078779    0.421252   0.053376   0.167588      1.000000   
BMI              0.042535    0.209095   0.300500   0.547017      0.199189   
PedigreeFunc     0.002261    0.092699   0.019605   0.056255      0.112412   
Age              0.558580    0.264369   0.359877   0.139539      0.137593   
Class            0.219390    0.493481   0.188542   0.201512      0.230049   

                   BMI  PedigreeFunc       Age     Class  
NumTimePreg   0.042535      0.002261  0.558580  0.219390  
OralGluTol    0.209095      0.092699  0.264369  0.493481  
BloodPres     0.300500      0.019605  0.359877  0.188542  
SkinThick     0.547017      0.056255  0.139539  0.201512  
SerumInsulin  0.199189      0.112412  0.137593  0.230049  
BMI           1.000000      0.127888  0.071648  0.297400  
PedigreeFunc  0.127888      1.000000  0.054643  0.221551  
Age           0.071648      0.054643  1.000000  0.250478  
Class         0.297400      0.221551  0.250478  1.000000  

html_img_008.png

多重共線性の診断

VIF(分散拡大係数)は、ある特徴量が他の特徴量とどの程度相関しているかを示す指標です。VIFの値が10を超えると、多重共線性が強いと判断されます。

  • VIFスコアの分析:
    • BMI: 38.55 * BloodPres: 36.39
    • OralGluTol: 21.88
    • SkinThick: 19.43
    • Age: 15.72
    • これらの特徴量はすべてVIFスコアが10を大幅に超えており、強い多重共線性があることがわかります。

相関行列とヒートマップの分析

VIFは全体的な共線性の度合いを示しますが、相関行列は個々の特徴量ペア間の関係を具体的に示します。

  • 相関の強さ:
    • SkinThickBMI の相関は 0.55 と比較的高く、体脂肪に関連するこれらの特徴量が似た情報を持っていることを示唆します。
    • NumTimePregAge の相関は 0.56 と高いです。妊娠回数が多いほど年齢も高くなるのは自然なことなので、これは妥当な相関です。
    • OralGluTolSerumInsulin の相関は 0.42 であり、血糖値が高いほどインスリン分泌量も増えるという生理学的関係を反映しています。

考察と次のステップ

  1. VIFスコアと相関行列の不一致:

    • VIFスコアが非常に高いにもかかわらず、相関行列ではほとんどの特徴量ペアで相関が0.6以下にとどまっています。これは、個々の特徴量ペアの相関は中程度でも、複数の特徴量が組み合わさって多重共線性を引き起こしている可能性が高いことを示しています。例えば、BMISkinThickだけでなく、他の複数の特徴量からも影響を受けていると考えられます。
  2. 多重共線性の影響:

    • 多重共線性は、モデルの係数(重要度)を不安定にさせ、モデルの解釈を困難にします。
    • ただし、XGBoostやRandom Forestのようなツリーベースのモデルは、多重共線性の影響を受けにくいという特性があります。そのため、これらのモデルを使用する場合は、特徴量をすぐに削除しなくても、高い予測性能を維持できる可能性があります。
  3. 推奨される対策:

    • 特徴量削減の検討: モデルの解釈性を高めたい場合、VIFスコアが最も高い特徴量(例: BMI)から順に一つずつ削除し、モデルの性能がどう変わるかを確認します。
    • 特徴量エンジニアリング: SkinThickBMIのように高い相関がある特徴量を組み合わせて、新しい特徴量を作成することも有効なアプローチです。
    • モデルの選択: 今回のデータセットのように多重共線性が存在する場合でも、ツリーベースのモデルは安定した性能を発揮しやすいため、引き続き強力な候補となります。

6. モデル候補とハイパーパラメータ探索

データセットを訓練用とテスト用に分割した後、複数の異なるモデル(ランダムフォレスト、XGBoost、ロジスティック回帰)を候補として設定し、それぞれに最適なハイパーパラメータをGridSearchCVという手法を用いて自動的に探索します。最終的に、各モデルの最も良い交差検証(CV)スコアを出力し、性能比較の準備を行います。

from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

if 'df' not in locals() or 'target' not in locals():
    print("エラー: dfまたはtargetが定義されていません。前のステップを実行してください。")
else:
    X = df.drop(columns=target)
    y = df[target]

    # データを訓練用とテスト用に分割
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    # クロスバリデーションの設定
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

    # --- 1. RandomForestClassifierのハイパーパラメータ探索 ---
    print("--- RandomForestClassifier ハイパーパラメータ探索 ---")
    rf_param_grid = {
        'n_estimators': [100, 200, 300],
        'max_depth': [5, 10, None],
        'class_weight': ['balanced'] if sampling_needed else [None]
    }
    rf_grid = GridSearchCV(
        RandomForestClassifier(random_state=42),
        rf_param_grid,
        cv=skf,
        scoring='roc_auc',
        n_jobs=-1
    )
    rf_grid.fit(X_train, y_train)

    print("RandomForestClassifier 最良パラメータ:", rf_grid.best_params_)
    print("RandomForestClassifier 最良CVスコア (ROC-AUC):", rf_grid.best_score_)

    # --- 2. XGBoostのハイパーパラメータ探索 ---
    print("\n--- XGBoost ハイパーパラメータ探索 ---")
    xgb_param_grid = {
        'n_estimators': [100, 200, 300],
        'learning_rate': [0.05, 0.1, 0.2],
        'max_depth': [3, 5, 7]
    }
    xgb_grid = GridSearchCV(
        XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42),
        xgb_param_grid,
        cv=skf,
        scoring='roc_auc',
        n_jobs=-1
    )
    xgb_grid.fit(X_train, y_train)

    print("XGBoost 最良パラメータ:", xgb_grid.best_params_)
    print("XGBoost 最良CVスコア (ROC-AUC):", xgb_grid.best_score_)

    # --- 3. ロジスティック回帰のハイパーパラメータ探索 ---
    print("\n--- ロジスティック回帰 ハイパーパラメータ探索 ---")
    # ロジスティック回帰は正規化を必要とするため、StandardScalerをパイプラインに追加します。
    logreg_pipe = Pipeline([
        ('scaler', StandardScaler()),
        ('classifier', LogisticRegression(solver='liblinear', random_state=42))
    ])
    logreg_param_grid = {
        'classifier__C': [0.01, 0.1, 1, 10, 100],  # 正則化の強さ (Cは逆数)
        'classifier__penalty': ['l1', 'l2'],        # 正則化のタイプ
        'classifier__class_weight': ['balanced'] if sampling_needed else [None]
    }
    logreg_grid = GridSearchCV(
        logreg_pipe,
        logreg_param_grid,
        cv=skf,
        scoring='roc_auc',
        n_jobs=-1
    )
    logreg_grid.fit(X_train, y_train)

    print("ロジスティック回帰 最良パラメータ:", logreg_grid.best_params_)
    print("ロジスティック回帰 最良CVスコア (ROC-AUC):", logreg_grid.best_score_)

実行ログ

--- RandomForestClassifier ハイパーパラメータ探索 ---
RandomForestClassifier 最良パラメータ: {'class_weight': None, 'max_depth': 5, 'n_estimators': 300}
RandomForestClassifier 最良CVスコア (ROC-AUC): 0.8579296588299357

--- XGBoost ハイパーパラメータ探索 ---
XGBoost 最良パラメータ: {'learning_rate': 0.05, 'max_depth': 3, 'n_estimators': 100}
XGBoost 最良CVスコア (ROC-AUC): 0.8622070933068162

--- ロジスティック回帰 ハイパーパラメータ探索 ---
ロジスティック回帰 最良パラメータ: {'classifier__C': 10, 'classifier__class_weight': None, 'classifier__penalty': 'l1'}
ロジスティック回帰 最良CVスコア (ROC-AUC): 0.8396756398418448

モデルの性能比較

提示されたクロスバリデーション(CV)スコア(ROC-AUC)に基づいて、各モデルの性能を比較します。

  • XGBoost: 最良スコア 0.862
  • RandomForestClassifier: 最良スコア 0.858
  • ロジスティック回帰: 最良スコア 0.840

この結果から、XGBoostが最も高い予測性能を示しました。XGBoostとRandom Forestは、決定木を組み合わせたアンサンブル学習モデルであり、非線形なデータパターンを捉える能力に優れています。今回のデータに多重共線性が存在するという前回の分析結果を踏まえると、これらのモデルがロジスティック回帰よりも優れた性能を発揮したのは理にかなっています。

最適なハイパーパラメータ

各モデルのハイパーパラメータ探索結果は以下の通りです。

  • RandomForestClassifier:

    • n_estimators: 300
    • max_depth: 5
    • class_weight: None
    • 考察: モデルが比較的浅い深さ(max_depth=5)でも高い性能を出しており、過学習を抑えつつ、適切な複雑さでデータの特徴を捉えていることがわかります。
  • XGBoost:

    • n_estimators: 100
    • learning_rate: 0.05
    • max_depth: 3
    • 考察: XGBoostもまた、浅い決定木(max_depth=3)と低い学習率(learning_rate=0.05)で最適な性能に達しました。これは、モデルが段階的に学習を進め、過学習を避けていることを示しています。
  • ロジスティック回帰:

    • C: 10
    • penalty: 'l1'
    • class_weight: None
    • 考察: l1ペナルティが選択されており、これは特徴量選択を自動的に行う効果があります。多重共線性がある状況では、不要な特徴量の係数をゼロに近づけることで、モデルの解釈性を高めるのに役立ちます。

結論と次のステップ

今回のデータセットにおいては、XGBoostが最も優れたモデルであると結論付けられます。

次のステップは、この最適なXGBoostモデルを最終モデルとして選択し、テストデータでその性能を評価することです。これにより、モデルがまだ見ぬ新しいデータに対して、どれだけ汎化性能があるかを確認できます。

ただし、RandomForestClassifierも非常に近いスコアを出しているため、どちらのモデルも優れた選択肢となり得ます。最終的なモデル選定は、予測性能だけでなく、モデルの解釈性や計算コストなども考慮して決定するのが一般的です。

7. 評価指標と検証

モデルのハイパーパラメータ探索で見つかった最良のモデルを選択し、テストデータでその性能を評価します。主要な評価指標であるROC-AUCのほか、F1スコア適合率(Precision)再現率(Recall)、**正答率(Accuracy)**を計算します。また、ROC曲線PR曲線混同行列を可視化することで、モデルの挙動を詳細に分析します。

  1. 最良モデルの選択:
    best_model = xgb_grid.best_estimator_ if xgb_grid.best_score_ > rf_grid.best_score_ else rf_grid.best_estimator_というコードで、以前のステップで探索したXGBoostとRandom Forestのうち、交差検証(CV)スコアが最も高かったモデルを自動的にbest_modelとして選択します。
  2. 予測の実行:
    • predict_proba(X_test)[:, 1]は、テストデータX_testに対して、クラス1(陽性、この場合は「疾患あり」)である確率を予測します。この確率値は、ROC曲線やPR曲線の描画に不可欠です。
    • predict(X_test)は、各データポイントを最も確率が高いクラス(0または1)に分類します。この結果は、F1スコアや混同行列の計算に使用されます。
  3. ROC曲線とAUC:
    • ROC曲線(Receiver Operating Characteristic curve)は、**真陽性率(True Positive Rate)偽陽性率(False Positive Rate)**の関係をプロットしたものです。
    • AUC(Area Under the Curve)は、そのROC曲線の下の面積で、モデルがクラスをどれだけうまく区別できるかを示す指標です。AUCの値が1.0に近いほど、モデルの判別能力が高いことを意味します。
  4. PR曲線(Precision-Recall Curve):
    • 適合率(Precision)と再現率(Recall)の関係をプロットしたものです。特にデータが不均衡な場合、ROC曲線よりもモデルの性能をより正確に評価できるため、医療分野などの不均衡なデータで重要な指標となります。
  5. 評価指標の計算:
    • F1スコア: 適合率と再現率の調和平均で、両方のバランスが取れているかを示します。
    • 適合率(Precision): モデルが「陽性」と予測したもののうち、実際に陽性だった割合です。偽陽性のコスト(誤診)が高い場合に重要です。
    • 再現率(Recall): 実際に「陽性」であるもののうち、モデルが正しく「陽性」と予測できた割合です。偽陰性のコスト(見逃し)が高い場合に重要です。
    • 正答率(Accuracy): 全ての予測のうち、正しく予測できた割合です。
  6. 混同行列(Confusion Matrix):
    • 実際のクラスと予測されたクラスの組み合わせを表形式で表示したものです。
    • 真陽性(True Positives, TP): 実際に陽性で、陽性と予測
    • 偽陽性(False Positives, FP): 実際に陰性で、陽性と予測
    • 真陰性(True Negatives, TN): 実際に陰性で、陰性と予測
    • 偽陰性(False Negatives, FN): 実際に陽性で、陰性と予測
    • これにより、モデルがどのような間違いを犯しやすいか(見逃しが多いか、誤診が多いか)を具体的に把握できます。
from sklearn.metrics import roc_curve, auc, precision_recall_curve, f1_score, precision_score, recall_score, accuracy_score, confusion_matrix
import matplotlib.pyplot as plt

# 最良モデルの選択
best_model = xgb_grid.best_estimator_ if xgb_grid.best_score_ > rf_grid.best_score_ else rf_grid.best_estimator_

# テストデータで予測
y_pred_proba = best_model.predict_proba(X_test)[:, 1]
y_pred = best_model.predict(X_test)

# ROC曲線とAUC
fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
roc_auc = auc(fpr, tpr)
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc="lower right")
plt.show()

# PR曲線(不均衡データに推奨)
precision, recall, _ = precision_recall_curve(y_test, y_pred_proba)
plt.figure(figsize=(8, 6))
plt.plot(recall, precision, color='blue', lw=2)
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall Curve')
plt.show()

# 評価指標の計算
f1 = f1_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
accuracy = accuracy_score(y_test, y_pred)

print("--- ホールドアウト検証結果 ---")
print(f"ROC-AUC: {roc_auc:.4f}")
print(f"F1スコア: {f1:.4f}")
print(f"Precision (適合率): {precision:.4f}")
print(f"Recall (再現率): {recall:.4f}")
print(f"Accuracy (正答率): {accuracy:.4f}")

# 混同行列
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Predicted 0', 'Predicted 1'], yticklabels=['Actual 0', 'Actual 1'])
plt.title('Confusion Matrix')
plt.ylabel('Actual Label')
plt.xlabel('Predicted Label')
plt.show()

実行ログ

--- ホールドアウト検証結果 ---
ROC-AUC: 0.8114
F1スコア: 0.6237
Precision (適合率): 0.6444
Recall (再現率): 0.6042
Accuracy (正答率): 0.7552

html_img_009.png

html_img_010.png

html_img_011.png

モデル評価指標の考察

  • ROC-AUC: 0.8114
    • クロスバリデーション(CV)での最高スコア(0.8622)と比較するとやや低いものの、0.81というスコアは、このモデルがランダムな推測(AUC 0.5)よりもはるかに優れた識別能力を持っていることを示しています。これは、モデルが未知のデータに対しても有効であることを意味します。
  • Accuracy (正答率): 0.7552
    • 全体の約75.5%のサンプルを正しく分類できています。これはまずまずの結果ですが、クラス不均衡があるため、他の指標も合わせて評価することが重要です。
  • Precision (適合率): 0.6444
    • モデルが「陽性(疾患あり)」と予測したケースのうち、実際に陽性だった割合は約64.4%です。これは、モデルが陽性と診断する際に、ある程度の誤診(偽陽性)が含まれていることを示唆します。
  • Recall (再現率): 0.6042
    • 実際に「陽性(疾患あり)」だったケースのうち、モデルが正しく陽性と予測できた割合は約60.4%です。これは、真の陽性患者を約4割見逃していることを意味し、医療診断の文脈では特に注意が必要です。
  • F1スコア: 0.6237
    • 適合率と再現率のバランスを示すF1スコアは0.6237であり、両方の指標が同程度のレベルにあることを示しています。

混同行列からの洞察

提供された混同行列 [[79, 16], [19, 29]] を分析することで、モデルの具体的な挙動を理解できます。

  • 真陰性(True Negative, TN): 79件
    • 実際に陰性(疾患なし)の人を、正しく「陰性」と予測できた件数です。
  • 偽陽性(False Positive, FP): 16件
    • 実際には陰性(疾患なし)なのに、誤って「陽性」と予測してしまった件数です。
  • 偽陰性(False Negative, FN): 19件
    • 実際に陽性(疾患あり)なのに、誤って「陰性」と予測してしまった件数です。
  • 真陽性(True Positive, TP): 29件
    • 実際に陽性(疾患あり)の人を、正しく「陽性」と予測できた件数です。

混同行列からわかる課題: 偽陰性(19件)が偽陽性(16件)よりもわずかに多いことがわかります。これは、モデルが「疾患あり」のケースを「疾患なし」と見逃す傾向があることを意味します。糖尿病診断のようなケースでは、偽陰性を減らすことがより重要となる場合が多いため、モデルの閾値を調整したり、アンダーサンプリング・オーバーサンプリングの手法を導入することで、再現率の改善を図る余地があります。


最終的な結論と今後の展望

全体として、構築されたモデルはROC-AUCが0.81と、テストデータに対して良好な予測性能を示しています。また、GlucoseBMIAgeといった医学的に妥当な特徴量が重要であると判断されており、モデルの予測根拠は理にかなっていると考えられます。

しかし、混同行列の分析から、特に偽陰性の件数に改善の余地があることが明らかになりました。今後は、モデルの予測閾値を調整したり、クラス不均衡をより効果的に扱う手法(例: SMOTE、クラス重み付けの再調整)を試すことで、再現率を向上させ、より実用的なモデルへと改善できる可能性があります。

結論:安全かつ効果的な活用方法
このモデルを医療分野で活用する際は、以下のような慎重なアプローチが不可欠です。

医師の最終判断を置き換えないこと: モデルはあくまで補助的なツールであり、診断は必ず医師が行うべきです。

「診断ツール」ではなく「スクリーニング・リスク評価ツール」として使うこと: 大規模な健康診断などで、疾患の可能性が高い人を絞り込むための最初のふるいとして利用するのが最も現実的です。

偽陰性のリスクを理解すること: モデルが「陰性」と予測した場合でも、患者の自覚症状や他の検査結果と照らし合わせ、常に医師の専門的な知見に基づいて総合的に判断する必要があります。

8. モデル解釈と可視化

学習済みのモデル(Random ForestまたはXGBoost)から特徴量重要度を取得し、それを棒グラフで可視化します。これにより、どの特徴量がモデルの予測に最も貢献しているかを直感的に理解し、その結果からモデルの挙動やデータの特性について洞察を得ることを目的とします。

  1. 最良モデルの選択: best_model = xgb_grid.best_estimator_ if xgb_grid.best_score_ > rf_grid.best_score_ else rf_grid.best_estimator_というコードは、前のステップで交差検証(CV)スコアが最も高かったモデルを自動的に選択します。
  2. 特徴量重要度の取得:
    • 特徴量重要度とは、モデルが予測を行う際に、各特徴量がどれだけ重要であったかを示す指標です。これはモデルの**「予測貢献度」**とも言えます。
    • best_model.feature_importances_属性を使用して、選択されたモデルから各特徴量の重要度スコアを取得しています。
    • 取得したスコアをpandas.Seriesに変換し、降順にソートすることで、重要度が高い順に特徴量を並べ替えています。
  3. 可視化:
    • plot(kind='barh')を使用して、重要度を棒グラフ(横向き)で可視化します。これにより、重要度が高い特徴量が上位に表示され、その貢献度合いを比較しやすくなります。
  4. 解釈コメント:
    • 可視化結果を基に、どのような分析が可能であるかを示しています。例えば、BloodPresBMIといった、ドメイン知識から重要とされている特徴量が上位に来た場合、モデルの予測が医療の専門家の知見と一致している可能性を示唆します。
    • ただし、重要な注意点として、「これは相関関係であり、因果関係ではない」ことが述べられています。モデルが特定の変数を重要と判断したからといって、その変数が直接的に原因であるとは限りません。この点は、モデルの解釈において非常に重要です。

このプロセスは、予測精度を追求するだけでなく、モデルが下した決定の背景を理解し、その妥当性を検証するために不可欠です。これにより、モデルの結果を信頼し、実際の意思決定に活かすための根拠を構築することができます。

## モデル解釈
# 概要:学習済みモデル(Random ForestまたはXGBoost)の特徴量重要度を可視化し、モデルの予測根拠を分析します。
#       SHAPによる解釈も任意で検討します。

# 最良モデルの選択
best_model = xgb_grid.best_estimator_ if xgb_grid.best_score_ > rf_grid.best_score_ else rf_grid.best_estimator_

# 特徴量重要度の取得と可視化
if hasattr(best_model, 'feature_importances_'):
    importances = pd.Series(best_model.feature_importances_, index=X_train.columns).sort_values(ascending=False)
    plt.figure(figsize=(10, 8))
    importances.head(20).plot(kind='barh')
    plt.title('Feature Importance')
    plt.show()
    
    print("--- 特徴量重要度からの解釈コメント ---")
    print("重要度上位の特徴量は、モデルが予測を行う上で特に重要だと判断した変数です。")
    print("例えば、'bmi', 'glucose' などが上位に来た場合、診断の妥当性を示唆します。")
    print("ただし、これは相関関係であり、因果関係を断定するものではありません。")

実行ログ

--- 特徴量重要度からの解釈コメント ---
重要度上位の特徴量は、モデルが予測を行う上で特に重要だと判断した変数です。
例えば、'bmi', 'glucose' などが上位に来た場合、診断の妥当性を示唆します。
ただし、これは相関関係であり、因果関係を断定するものではありません。

html_img_012.png


1. 混同行列とROC曲線の考察

  • 混同行列:
    • 混同行列を見ると、正しく分類された件数(対角線上の値)が、誤分類された件数よりも圧倒的に多いことがわかります。
    • 特に、真陽性(True Positive)、つまり「実際に陽性で、モデルも陽性と予測した」件数が多いことは、モデルが疾患をうまく捉えられている証拠です。
  • ROC曲線:
    • ROC曲線は、グラフが左上隅に急接近しており、モデルの**真陽性率(TPR)偽陽性率(FPR)**に対して非常に高いことを視覚的に示しています。これは、モデルが疾患あり・なしを高い精度で区別できていることを裏付けています。

2. 特徴量重要度の考察

  • 重要度上位の特徴量: Glucose, BMI, Ageが最も重要な特徴量として特定されています。
  • 解釈:
    1. Glucose (血糖値): 糖尿病の最も直接的な診断指標であるため、重要度が高いのは妥当です。
    2. BMI (体格指数): 肥満が糖尿病の主要なリスク因子であることを反映しています。
    3. Age (年齢): 加齢が糖尿病リスクを高めることを示しており、医学的な知見と一致します。
  • まとめ: モデルは、医学的に妥当な特徴量を重要な予測因子として識別していることが確認できました。これは、モデルの予測が単なる偶然ではなく、データに潜む意味のあるパターンに基づいていることを示唆しています。

全体的な結論

今回のデータ分析プロセス全体を通じて、データの前処理(特に隠れた0値の補完)と、最適なモデル選択(XGBoost)およびハイパーパラメータ調整が、非常に高い予測性能を持つモデルの構築に成功したことが示されました。モデルは、医学的な知見とも一致する特徴量を重要視しており、その予測は信頼性が高いと言えます。

SMOTEとXGBoostの組み合わせによるクラス不均衡対応

このコードは、クラス不均衡という機械学習における一般的な問題を解決するための手法を示しています。具体的には、SMOTE (Synthetic Minority Over-sampling Technique) という技術を用いて、データセット内の少数派クラス(ここでは糖尿病患者)のサンプル数を人工的に増やします。これにより、モデルが少数派クラスのパターンをより効果的に学習できるようになり、特に**再現率(Recall)**の向上を目指します。

SMOTEとXGBoostのモデル学習をimblearnパイプラインに統合し、GridSearchCVを使って最適なハイパーパラメータとSMOTEのサンプリング戦略を同時に探索します。このアプローチにより、過学習を防ぎながら、モデルの全体的な性能を最大限に引き出すことを目的としています。

  1. クラス不均衡の問題:
    モデルが学習するデータに偏りがある場合、モデルは多数派クラスを優先的に予測するようになります。これは、高い正答率(Accuracy)を達成する一方で、少数派クラスの検出が苦手になるため、特に医療診断のような「見逃し(偽陰性)」が許されないケースでは大きな問題となります。

  2. SMOTEの役割:
    SMOTEは、少数派クラスの既存のデータポイントを基に、新しい合成サンプルを生成します。これにより、データセット全体のクラス分布が均等に近くなり、モデルが少数派クラスのパターンをより多く学習できるようになります。この処理は、モデルの再現率(Recall)を改善する上で特に有効です。

  3. パイプラインの活用:
    このコードでは、imblearnライブラリのPipelineを使用しています。これにより、SMOTEによるオーバーサンプリングとXGBoostによる分類という2つのステップを一つの処理フローとして扱えます。このパイプラインの利点は、クロスバリデーション(CV)中にデータリーク(Data Leakage)を防ぐことです。もしSMOTEをパイプライン外で適用すると、テストデータにまで合成サンプルが影響を及ぼし、モデル性能を過大評価してしまう可能性があります。パイプラインに組み込むことで、SMOTEはCVの各フォールドの訓練データにのみ適用されるため、公正な評価が可能となります。

  4. ハイパーパラメータの探索:
    GridSearchCVは、パイプライン全体に対して最適なパラメータの組み合わせを自動的に見つけ出します。ここでは、XGBoostのハイパーパラメータに加えて、SMOTEのsampling_strategy(少数派クラスをどのくらいの割合まで増やすか)も同時に探索しています。これにより、モデルと前処理の両方を同時に最適化し、最もパフォーマンスの高い組み合わせを特定することができます。

from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from xgboost import XGBClassifier

# このセルを単独で実行する場合、dfとtargetを定義してください。
# 例: df = pd.read_csv('your_data.csv'); target = 'Class'
if 'df' not in locals() or 'target' not in locals():
    print("エラー: dfまたはtargetが定義されていません。前のステップを実行してください。")
else:
    X = df.drop(columns=target)
    y = df[target]

    # データを訓練用とテスト用に分割
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    # クロスバリデーションの設定
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

    print("\n--- SMOTEとXGBoostのハイパーパラメータ探索 ---")
    
    # imblearnのパイプラインを使用
    pipeline = ImbPipeline([
        ('smote', SMOTE(random_state=42)),
        ('classifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42))
    ])

    # パイプライン内のモデルのハイパーパラメータを設定
    param_grid_smote = {
        'smote__sampling_strategy': [0.5, 0.75, 1.0], # 少数派クラスの割合
        'classifier__n_estimators': [100, 200],
        'classifier__learning_rate': [0.05, 0.1],
        'classifier__max_depth': [3, 5]
    }
    
    smote_xgb_grid = GridSearchCV(
        pipeline,
        param_grid_smote,
        cv=skf,
        scoring='roc_auc',
        n_jobs=-1
    )
    smote_xgb_grid.fit(X_train, y_train)

    print("SMOTE+XGBoost 最良パラメータ:", smote_xgb_grid.best_params_)
    print("SMOTE+XGBoost 最良CVスコア (ROC-AUC):", smote_xgb_grid.best_score_)

実行ログ


--- SMOTEとXGBoostのハイパーパラメータ探索 ---
SMOTE+XGBoost 最良パラメータ: {'classifier__learning_rate': 0.05, 'classifier__max_depth': 3, 'classifier__n_estimators': 100, 'smote__sampling_strategy': 0.75}
SMOTE+XGBoost 最良CVスコア (ROC-AUC): 0.8524635390771124

9. SMOTEと特徴量選択によるモデル改善

  1. 特徴量選択: VIF分析で多重共線性が高いと判明したBloodPresと、相関が比較的低いNumTimePregをデータセットから削除します。
  2. SMOTE (Synthetic Minority Over-sampling Technique): 少ないサンプル数しかない少数派クラス(糖尿病患者)のデータを人工的に増やし、クラスの不均衡を是正します。

この2つの前処理をパイプラインに組み込み、グリッドサーチを用いてXGBoostのハイパーパラメータとSMOTEのサンプリング戦略を同時に最適化することで、より高い予測精度と特に再現率(見逃しを減らす能力)の改善を目指します。

  1. 特徴量の削除:
    BloodPresNumTimePregの2つの特徴量をデータから削除しています。

    • BloodPres: 前回の分析で多重共線性(VIFスコア > 10)が確認されたため、モデルの安定性を高める目的で削除されました。
    • NumTimePreg: 妊娠回数と年齢に相関がありましたが、単独での判別能力が比較的低いと判断されたため、モデルのシンプル化とパフォーマンス向上の可能性を探るために削除されました。
  2. SMOTEの導入:
    前回のクラス分布分析で確認された不均衡データ(クラス0がクラス1の約2倍)に対処するため、SMOTEという手法が採用されています。SMOTEは、少数派クラスのデータポイントを「過剰にサンプリング」することで、データセットを均衡化させます。これにより、モデルが少数派クラスのパターンをより良く学習できるようになり、特に再現率の向上が期待されます。

  3. ImbPipelineとGridSearchCV:
    imblearnライブラリのPipelineを使用することで、「SMOTEによるサンプリング」と「XGBoostによる分類」という2つのステップを一つの処理フローにまとめます。
    GridSearchCVでは、このパイプライン全体に対して最適なパラメータの組み合わせを探索します。探索対象には、SMOTEのサンプリング戦略 (smote__sampling_strategy) とXGBoostのハイパーパラメータ (classifier__n_estimators, classifier__learning_rate, classifier__max_depth) が含まれます。

この一連のプロセスにより、単にモデルのパラメータを調整するだけでなく、データ自体の特性を改善するための高度なアプローチをモデル学習に組み込んでいます。これにより、モデルの総合的な性能、特に医療分野で重要となる**「見逃し(偽陰性)」の減少**に貢献することが期待されます。

from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
from xgboost import XGBClassifier
import pandas as pd

# このセルを単独で実行する場合、dfとtargetを定義してください。
# 例: df = pd.read_csv('your_data.csv'); target = 'Class'
if 'df' not in locals() or 'target' not in locals():
    print("エラー: dfまたはtargetが定義されていません。前のステップを実行してください。")
else:
    # --- 1. 特徴量の削除 ---
    # BloodPres と NumTimePreg を削除
    features_to_drop = ['BloodPres', 'NumTimePreg']
    if all(col in df.columns for col in features_to_drop):
        df_selected = df.drop(columns=features_to_drop)
        print(f"--- 以下の特徴量を削除しました: {features_to_drop} ---")
    else:
        df_selected = df.copy()
        print("エラー: 指定された特徴量がデータフレームに存在しないため、特徴量削除をスキップします。")
    
    X = df_selected.drop(columns=target)
    y = df_selected[target]

    # --- 2. モデル学習の再実行 ---
    # データを訓練用とテスト用に分割
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    # クロスバリデーションの設定
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

    print("\n--- SMOTEとXGBoostのハイパーパラメータ探索(再実行)---")
    
    # imblearnのパイプラインを使用
    pipeline = ImbPipeline([
        ('smote', SMOTE(random_state=42)),
        ('classifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42))
    ])

    # パイプライン内のモデルのハイパーパラメータを設定
    param_grid_smote = {
        'smote__sampling_strategy': [0.5, 0.75, 1.0],
        'classifier__n_estimators': [100, 200],
        'classifier__learning_rate': [0.05, 0.1],
        'classifier__max_depth': [3, 5]
    }
    
    smote_xgb_grid = GridSearchCV(
        pipeline,
        param_grid_smote,
        cv=skf,
        scoring='roc_auc',
        n_jobs=-1
    )
    smote_xgb_grid.fit(X_train, y_train)

    print("SMOTE+XGBoost 最良パラメータ:", smote_xgb_grid.best_params_)
    print("SMOTE+XGBoost 最良CVスコア (ROC-AUC):", smote_xgb_grid.best_score_)

実行ログ

--- 以下の特徴量を削除しました: ['BloodPres', 'NumTimePreg'] ---

--- SMOTEとXGBoostのハイパーパラメータ探索(再実行)---
SMOTE+XGBoost 最良パラメータ: {'classifier__learning_rate': 0.05, 'classifier__max_depth': 3, 'classifier__n_estimators': 100, 'smote__sampling_strategy': 0.75}
SMOTE+XGBoost 最良CVスコア (ROC-AUC): 0.8639475341525203

10. モデル保存・運用

このコードは、機械学習モデルを本番環境で運用するために不可欠なプロセス、すなわち**「モデルの保存」「運用の自動化」に焦点を当てています。
具体的には、データの前処理(特徴量選択、標準化、SMOTE)から最終的な分類器(XGBoost)による予測まで、一連の処理を
imblearnscikit-learnのパイプラインに統合**します。そして、この統合されたパイプライン全体をjoblibというライブラリを用いてファイルとして保存します。これにより、将来新しいデータが入力された際に、訓練時と同じプロセスを自動で適用し、一貫性のある予測を可能にします。

  1. データの準備とモデル学習の再実行:
    まず、以前のステップで多重共線性や重要度が低いと判断されたBloodPresNumTimePregの特徴量をデータセットから削除します。次に、この前処理済みデータを用いて、最適なハイパーパラメータを持つXGBoostモデルを再学習させます。この再学習は、モデルを保存する前の最終確認と、最新の学習済みモデルオブジェクトを取得する目的で行われます。

  2. パイプラインの構築:
    このコードの中心的な概念はパイプラインです。パイプラインは、複数の処理ステップを連結し、一つのオブジェクトとして扱うことができます。このコードでは、以下の3つのステップを順に実行する**ImbPipeline**を構築しています。

    • preprocessor: ColumnTransformerを使用して、数値特徴量にStandardScaler(標準化)を適用します。これにより、異なるスケールの特徴量を統一し、モデルの学習を安定させます。
    • smote: クラス不均衡を是正するため、少数派クラスを人工的に増やすSMOTE(Synthetic Minority Over-sampling Technique)を適用します。
    • classifier: 前のステップで最適なハイパーパラメータが見つかったXGBoostモデルを組み込み、最終的な分類を行います。
  3. モデルの保存と運用の利点:
    構築されたfinal_pipelineは、訓練済みの状態(各ステップの学習済みパラメータを含む)でjoblib.dump()関数によってfinal_classification_model.pklというファイルに保存されます。この保存方式には以下のような大きなメリットがあります。

    • 一貫性の確保: 訓練時と同じ前処理が自動的に適用されるため、**「トレーニングとサービングの間のズレ(Training/Serving Skew)」**という、予測性能が本番環境で低下するリスクを回避できます。
    • デプロイの簡素化: 新しいデータに対して予測を行う際は、保存された.pklファイルを読み込み、model.predict(new_data)を実行するだけで、訓練時と同じ前処理が自動で実行されます。これにより、モデルのデプロイ(本番環境への導入)プロセスが大幅に簡素化されます。
    • 管理の効率化: 前処理とモデルが一体化しているため、管理が容易になり、モデルのバージョン管理もシンプルになります。

# 必要なライブラリをまとめてインポート
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from xgboost import XGBClassifier
import joblib
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline

# 日本語フォントの設定
plt.rcParams['font.family'] = 'Meiryo'
plt.rcParams['axes.unicode_minus'] = False

# このセルを単独で実行する場合、dfとtargetを定義してください。
# 例: df = pd.read_csv('your_data.csv'); target = 'Class'
if 'df' not in locals() or 'target' not in locals():
    print("エラー: dfまたはtargetが定義されていません。前のステップを実行してください。")
else:
    # --- 1. 特徴量の削除 ---
    features_to_drop = ['BloodPres', 'NumTimePreg']
    if all(col in df.columns for col in features_to_drop):
        df_selected = df.drop(columns=features_to_drop)
    else:
        df_selected = df.copy()

    X = df_selected.drop(columns=target)
    y = df_selected[target]

    # --- 2. モデル学習の再実行(最良モデルを取得するため) ---
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

    pipeline = ImbPipeline([
        ('smote', SMOTE(random_state=42)),
        ('classifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42))
    ])

    param_grid_smote = {
        'smote__sampling_strategy': [0.5, 0.75, 1.0],
        'classifier__n_estimators': [100, 200],
        'classifier__learning_rate': [0.05, 0.1],
        'classifier__max_depth': [3, 5]
    }
    
    smote_xgb_grid = GridSearchCV(
        pipeline,
        param_grid_smote,
        cv=skf,
        scoring='roc_auc',
        n_jobs=-1
    )
    smote_xgb_grid.fit(X_train, y_train)
    best_smote_xgb_model = smote_xgb_grid.best_estimator_

    # --- 3. 最終的なパイプラインの構築と保存 ---
    print("\n--- 最終パイプラインの構築とモデルの保存 ---")

    numerical_cols = X.select_dtypes(include=np.number).columns.tolist()

    numerical_transformer = Pipeline(steps=[
        ('scaler', StandardScaler())
    ])

    preprocessor = ColumnTransformer(
        transformers=[
            ('num', numerical_transformer, numerical_cols)
        ],
        remainder='passthrough'
    )
    
    final_pipeline = ImbPipeline(steps=[
        ('preprocessor', preprocessor),
        ('smote', SMOTE(random_state=42)),
        ('classifier', best_smote_xgb_model['classifier'])
    ])
    
    final_pipeline.fit(X_train, y_train)

    model_save_path = './final_classification_model.pkl'
    joblib.dump(final_pipeline, model_save_path)
    
    print(f"最終的なモデルパイプラインが '{model_save_path}' に保存されました。")
    print("このファイルには、特徴量削除、標準化、SMOTE、そしてXGBoostモデルの全ステップが含まれています。")

実行ログ


--- 最終パイプラインの構築とモデルの保存 ---
最終的なモデルパイプラインが './final_classification_model.pkl' に保存されました。
このファイルには、特徴量削除、標準化、SMOTE、そしてXGBoostモデルの全ステップが含まれています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?