Velvet Theory AIとデータの話を、やわらかく、筋道立てて。

機械学習の全体地図(7)Pythonで書く

scikit-learnを中心に、すべての推定器に共通の使い方と、回帰・分類・クラスタリング・異常検知などのコードのレシピ、地図の項目とPythonのクラスの対応表をまとめます。

連載 機械学習の全体地図 第7回/全8回
  1. 第1回全体像と「学び方」
  2. 第2回タスクとモデル
  3. 第3回作って確かめる手順
  4. 第4回評価指標と落とし穴
  5. 第5回深層学習とLLM
  6. 第6回運用と責任あるAI
  7. 第7回Pythonで書く
  8. 第8回通しチュートリアル:解約予測

Pythonで書く

地図の各項目を、Pythonの機械学習ライブラリ scikit-learn で使う方法です。scikit-learn 1.5以上が必要で、下のコードはすべて 1.8 で実行して確認しています。バージョンは import sklearn; print(sklearn.__version__) で確認できます。

すべての推定器に共通の使い方

scikit-learn では、モデルや前処理の部品を「推定器」(Estimator)と呼び、どれも同じ3つの操作で使えます。fit で学習し、predict で予測し、前処理なら transform で変換します。教師ありは fit(X, y)、教師なしは fit(X) と、①学び方の違いがそのまま引数の違いになっています。

レシピ

そのままコピーして動かせます。データは scikit-learn に同梱のものか、コード内で作るものなので、ダウンロードは要りません。出力の数値はバージョンや環境によって少し変わることがあります。

基本の型:分類を最後まで

④手順の「分割→前処理→学習→評価」を最小限で書いた型です。前処理とモデルを Pipeline(1つにまとめる仕組み)でまとめると、標準化の平均などが学習用データだけで計算され、データリークを防げます。 地図の該当箇所へ

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, classification_report

# 乳がん診断データ(0=悪性, 1=良性)
X, y = load_breast_cancer(return_X_y=True)

# 最初にテスト用を取り分ける。stratify でクラスの比率を保つ
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=0)

# 前処理とモデルを1つにまとめる
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
model.fit(X_train, y_train)

# テスト用データで1回だけ評価する
pred = model.predict(X_test)
print(confusion_matrix(y_test, pred))  # [[TN, FP], [FN, TP]] の並び
print(classification_report(y_test, pred, digits=3))

出力例

[[40  2]
 [ 0 72]]
              precision    recall  f1-score   support

           0      1.000     0.952     0.976        42
           1      0.973     1.000     0.986        72

    accuracy                          0.982       114
   macro avg      0.986     0.976     0.981       114
weighted avg      0.983     0.982     0.982       114
回帰:ベースラインと比べる

まず「平均値を返すだけ」のベースラインを作り、それを超えるかで判断します。このデータ(約440件)では線形モデルが勾配ブースティングに勝ちます。表形式のデータでも、件数が少ないと単純なモデルが強いことがあるため、複数を比べるのが基本です。 地図の該当箇所へ

from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Ridge
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.metrics import mean_absolute_error, root_mean_squared_error, r2_score

X, y = load_diabetes(return_X_y=True)  # 1年後の病気の進行度(数値)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

models = {
    "ベースライン(平均値)": DummyRegressor(),
    "線形(Ridge)": Ridge(),
    "勾配ブースティング": HistGradientBoostingRegressor(max_iter=200, learning_rate=0.05, random_state=0),
}
for name, m in models.items():
    pred = m.fit(X_train, y_train).predict(X_test)
    print(f"{name}: MAE={mean_absolute_error(y_test, pred):.1f} "
          f"RMSE={root_mean_squared_error(y_test, pred):.1f} R2={r2_score(y_test, pred):.3f}")

出力例

ベースライン(平均値): MAE=59.2 RMSE=71.7 R2=-0.001
線形(Ridge): MAE=46.6 RMSE=58.1 R2=0.341
勾配ブースティング: MAE=50.7 RMSE=65.8 R2=0.155
表データの前処理:列ごとに処理を分ける

数値列とカテゴリ列で別々の前処理を ColumnTransformer でまとめます。欠損値や、学習時になかったカテゴリが来ても止まりません(書き方を示すための8行だけのデータです)。 地図の該当箇所へ

import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import make_pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression

df = pd.DataFrame({
    "年齢": [25, 32, np.nan, 51, 46, 29, 38, np.nan],
    "月額": [3000, 5000, 4500, np.nan, 8000, 3500, 6000, 7000],
    "プラン": ["A", "B", "B", "C", "C", "A", None, "B"],
    "解約": [1, 0, 0, 1, 0, 1, 0, 0],
})
X, y = df.drop(columns="解約"), df["解約"]

# 数値列: 中央値で補完+欠損フラグ → 標準化
num = make_pipeline(SimpleImputer(strategy="median", add_indicator=True), StandardScaler())
# カテゴリ列: 最頻値で補完 → One-hot(未知のカテゴリは無視)
cat = make_pipeline(SimpleImputer(strategy="most_frequent"), OneHotEncoder(handle_unknown="ignore"))
pre = ColumnTransformer([("num", num, ["年齢", "月額"]), ("cat", cat, ["プラン"])])

model = make_pipeline(pre, LogisticRegression()).fit(X, y)
print(pre.get_feature_names_out())

new = pd.DataFrame({"年齢": [40], "月額": [np.nan], "プラン": ["D"]})  # 欠損と未知のカテゴリ
print("解約確率:", model.predict_proba(new)[:, 1].round(3))

出力例

['num__年齢' 'num__月額' 'num__missingindicator_年齢' 'num__missingindicator_月額'
 'cat__プラン_A' 'cat__プラン_B' 'cat__プラン_C']
解約確率: [0.802]
交差検証:データに合った分割を選ぶ

評価の分割方法はデータの性質で選びます。時系列分割では、学習用が常に検証用より過去になることを確認できます。 地図の該当箇所へ

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import cross_val_score, StratifiedKFold, TimeSeriesSplit
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

X, y = load_breast_cancer(return_X_y=True)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))

# 分類: 層化k分割(各分割でクラス比率をそろえる)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
scores = cross_val_score(model, X, y, cv=cv, scoring="f1")
print("F1:", scores.round(3), "平均", scores.mean().round(3))

# 時系列: 常に過去で学び、直後で検証する(シャッフルしない)
for train_idx, test_idx in TimeSeriesSplit(n_splits=3).split(np.arange(12)):
    print("学習", train_idx, "→ 検証", test_idx)

# 同じ顧客の行が複数ある場合は GroupKFold を使い、
# cross_val_score(model, X, y, cv=GroupKFold(5), groups=顧客ID) とする

出力例

F1: [0.966 0.979 0.986 1.    0.986] 平均 0.983
学習 [0 1 2] → 検証 [3 4 5]
学習 [0 1 2 3 4 5] → 検証 [6 7 8]
学習 [0 1 2 3 4 5 6 7 8] → 検証 [ 9 10 11]
偏ったデータ:クラスの重みと閾値

陽性が5%のデータです。重みなしでは正解率94%でも再現率は2%で、陽性をほぼ見逃しています。クラスの重み(少数派の誤りを重く数える設定)を付けると再現率は75%に上がり、代わりに適合率と正解率が下がります。PR-AUCはむしろ少し下がっています(0.25→0.21)。重み付けは主に「判定の境目」を動かすもので、陽性を見分ける力そのものを高めるわけではないためです。 地図の該当箇所へ

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, TunedThresholdClassifierCV
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, average_precision_score

X, y = make_classification(n_samples=3000, weights=[0.95], class_sep=0.5,
                           flip_y=0.02, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)

for cw in [None, "balanced"]:
    m = LogisticRegression(class_weight=cw).fit(X_train, y_train)
    p = m.predict(X_test)
    print(f"class_weight={cw}: 正解率={accuracy_score(y_test, p):.3f} "
          f"適合率={precision_score(y_test, p):.3f} 再現率={recall_score(y_test, p):.3f} "
          f"PR-AUC={average_precision_score(y_test, m.predict_proba(X_test)[:, 1]):.3f}")

# F1が最大になる閾値を交差検証で選ぶ(scikit-learn 1.5以上)
tuned = TunedThresholdClassifierCV(LogisticRegression(), scoring="f1").fit(X_train, y_train)
print("選ばれた閾値:", round(tuned.best_threshold_, 3))

出力例

class_weight=None: 正解率=0.941 適合率=0.500 再現率=0.023 PR-AUC=0.250
class_weight=balanced: 正解率=0.684 適合率=0.127 再現率=0.750 PR-AUC=0.205
選ばれた閾値: 0.11
クラスタリングと次元削減

正解ラベルを使わずにグループ分けします。シルエット係数が最大のグループ数が、本当のグループ数と一致するとは限らない点に注意してください。 地図の該当箇所へ

from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, adjusted_rand_score

X, y = load_iris(return_X_y=True)  # アヤメ3品種。y はクラスタリングには使わない
Xs = StandardScaler().fit_transform(X)

for k in [2, 3, 4, 5]:
    labels = KMeans(n_clusters=k, n_init=10, random_state=0).fit_predict(Xs)
    print(f"k={k}: シルエット={silhouette_score(Xs, labels):.3f} "
          f"正解との一致ARI={adjusted_rand_score(y, labels):.3f}")

pca = PCA(n_components=2).fit(Xs)
print("2次元に圧縮して残る情報の割合:", pca.explained_variance_ratio_.sum().round(3))

出力例

k=2: シルエット=0.582 正解との一致ARI=0.568
k=3: シルエット=0.460 正解との一致ARI=0.620
k=4: シルエット=0.387 正解との一致ARI=0.473
k=5: シルエット=0.346 正解との一致ARI=0.420
2次元に圧縮して残る情報の割合: 0.958
異常検知

正常データの中に混ぜた珍しい点を、ラベルなしで見つけます。predict は 1=正常、−1=異常を返します。 地図の該当箇所へ

import numpy as np
from sklearn.ensemble import IsolationForest

rng = np.random.default_rng(0)
normal = rng.normal(0, 1, size=(300, 2))    # 正常データ
odd = rng.uniform(4, 6, size=(10, 2))       # 珍しいデータ
X = np.vstack([normal, odd])
is_odd = np.r_[np.zeros(300), np.ones(10)]

iso = IsolationForest(contamination=0.03, random_state=0).fit(X)
flag = iso.predict(X) == -1                  # 異常と判定したもの
print("異常と判定:", flag.sum(), "件")
print("そのうち本当に珍しいデータ:", int((flag & (is_odd == 1)).sum()), "/ 10件")

出力例

異常と判定: 9 件
そのうち本当に珍しいデータ: 8 / 10件
予測の根拠を確かめる

ある特徴量をシャッフルしたときに精度がどれだけ落ちるかで、その特徴量の重要さを測ります(Permutation Importance)。このデータのように似た特徴量が多いと、1つを並べ替えて無効にしても他が補うため、値が小さく出ます。 地図の該当箇所へ

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance

data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.2, stratify=data.target, random_state=0)

model = RandomForestClassifier(random_state=0).fit(X_train, y_train)
r = permutation_importance(model, X_test, y_test, n_repeats=10, random_state=0)

for i in r.importances_mean.argsort()[::-1][:5]:
    print(f"{data.feature_names[i]:<25} {r.importances_mean[i]:.4f}")

出力例

area error                0.0061
worst radius              0.0035
mean texture              0.0026
worst texture             0.0026
worst area                0.0026
過学習を目で確かめる

決定木の深さを変えながら、学習データと検証データの精度を比べます。学習精度は上がり続けても、検証精度はどこかで頭打ちになります。 地図の該当箇所へ

from sklearn.datasets import make_classification
from sklearn.model_selection import validation_curve
from sklearn.tree import DecisionTreeClassifier

X, y = make_classification(n_samples=500, n_features=20, flip_y=0.15, random_state=0)
depths = [1, 2, 3, 5, 8, 12, None]
train, valid = validation_curve(DecisionTreeClassifier(random_state=0), X, y,
                                param_name="max_depth", param_range=depths, cv=5)
for d, a, b in zip(depths, train.mean(1), valid.mean(1)):
    print(f"深さ={str(d):>4}: 学習={a:.3f} 検証={b:.3f}")

出力例

深さ=   1: 学習=0.799 検証=0.784
深さ=   2: 学習=0.814 検証=0.792
深さ=   3: 学習=0.855 検証=0.836
深さ=   5: 学習=0.919 検証=0.822
深さ=   8: 学習=0.978 検証=0.798
深さ=  12: 学習=0.998 検証=0.780
深さ=None: 学習=1.000 検証=0.784

対応表:地図の項目とPythonのクラス

「—」は scikit-learn に該当する機能がないものです。追加ライブラリは pip install で入れて使います。

① 学び方

項目scikit-learn追加ライブラリ
教師あり
Supervised learning
fit(X, y)—
教師なし
Unsupervised learning
fit(X)—
半教師あり
Semi-supervised learning
SelfTrainingClassifier、LabelSpreading—
自己教師あり
Self-supervised learning
—PyTorch、Hugging Face Transformers
強化学習
Reinforcement learning
—Gymnasium、Stable-Baselines3
オンライン学習
Online learning
SGDClassifier.partial_fit—
転移学習・ファインチューニング
Transfer learning / Fine-tuning
—PyTorch、Hugging Face Transformers

② タスク

項目scikit-learn追加ライブラリ
回帰
Regression
LinearRegression、HistGradientBoostingRegressor—
分類
Classification
LogisticRegression、HistGradientBoostingClassifier—
マルチラベル分類
Multi-label classification
OneVsRestClassifier、MultiOutputClassifier—
順序回帰
Ordinal regression
—statsmodels(OrderedModel)
ランキング
Learning to rank
—LightGBM(LGBMRanker)
系列ラベリング
Sequence labeling
—sklearn-crfsuite、Transformers
時系列予測
Time series forecasting
TimeSeriesSplitstatsmodels(ARIMA)
生存時間分析
Survival analysis
—lifelines、scikit-survival
クラスタリング
Clustering
KMeans、AgglomerativeClustering、DBSCAN、HDBSCAN、GaussianMixture—
次元削減
Dimensionality reduction
PCA、TSNEumap-learn
異常検知
Anomaly detection
IsolationForest、OneClassSVM、LocalOutlierFactor—
密度推定
Density estimation
KernelDensity、GaussianMixture—
相関ルール
Association rule mining
—mlxtend(apriori)
生成
Generative modeling
GaussianMixture.samplePyTorch、diffusers

③ モデル

項目scikit-learn追加ライブラリ
線形モデル
Linear models
LinearRegression、Ridge、Lasso、LogisticRegression、LinearSVCstatsmodels(ARIMA)
カーネル法
Kernel methods
SVC、SVR、GaussianProcessRegressor—
決定木・アンサンブル
Trees / Ensembles
DecisionTreeClassifier、RandomForestClassifier、HistGradientBoostingClassifierLightGBM、XGBoost、CatBoost
近傍ベース
Nearest neighbors
KNeighborsClassifier—
確率モデル
Probabilistic models
GaussianNB、MultinomialNB、GaussianMixturehmmlearn(隠れマルコフ)
ニューラルネット
Neural networks
MLPClassifierPyTorch
アンサンブル全般
Ensembles
BaggingClassifier、StackingClassifier、VotingClassifier—

④ 手順・評価

項目scikit-learn追加ライブラリ
欠損値
Missing values
SimpleImputer、KNNImputer、IterativeImputer—
尺度の変換
Scaling
StandardScaler、MinMaxScaler—
カテゴリ変数
Categorical encoding
OneHotEncoder、OrdinalEncoder、TargetEncoder—
テキスト
Text features
TfidfVectorizerfugashi・Janome(日本語の分かち書き)
クラスの偏り
Class imbalance
class_weight、TunedThresholdClassifierCVimbalanced-learn(SMOTE)
列ごとの前処理
Column-wise preprocessing
ColumnTransformer、Pipeline—
データ分割
Data splitting
train_test_split、StratifiedKFold、GroupKFold、TimeSeriesSplit、LeaveOneOut—
ハイパーパラメータ探索
Hyperparameter tuning
GridSearchCV、RandomizedSearchCVOptuna
予測根拠
Model explanation
permutation_importance、PartialDependenceDisplaySHAP
評価指標
Metrics
sklearn.metricstorchmetrics(mAP・FID)、sacrebleu(BLEU)

Spark