Skip to content

第 7 章 演習解答

第 7 章「分類」の演習問題の解答例です。問題は 第 7 章 演習問題 を参照してください。

演習 7-1: ロジスティック回帰

問題を見る

import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 1. データの読み込みと先頭5行の表示
tips = sns.load_dataset("tips")
print(tips.head())

# 2. 特徴量 X とラベル y の設定
X = tips[["total_bill", "tip", "size"]]
y = tips["time"]

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

# 4. モデルの作成と学習
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)

# 5. テストデータで予測し、正解率を表示
y_pred = model.predict(X_test)
print(f"正解率: {accuracy_score(y_test, y_pred):.4f}")

# 6. 係数と切片の表示
for name, coef in zip(X.columns, model.coef_[0]):
    print(f"{name}: {coef:.4f}")
print(f"切片: {model.intercept_[0]:.4f}")

# 7. tip の1列だけを特徴量にした場合との比較
X1 = tips[["tip"]]
X1_train, X1_test, y1_train, y1_test = train_test_split(
    X1, y, test_size=0.2, random_state=42
)
model1 = LogisticRegression(max_iter=1000)
model1.fit(X1_train, y1_train)
y1_pred = model1.predict(X1_test)
print(f"正解率(tip のみ): {accuracy_score(y1_test, y1_pred):.4f}")
   total_bill   tip     sex smoker  day    time  size
0       16.99  1.01  Female     No  Sun  Dinner     2
1       10.34  1.66    Male     No  Sun  Dinner     3
2       21.01  3.50    Male     No  Sun  Dinner     3
3       23.68  3.31    Male     No  Sun  Dinner     2
4       24.59  3.61  Female     No  Sun  Dinner     4
正解率: 0.6735
total_bill: -0.0883
tip: 0.0555
size: 0.0572
切片: 0.3732
正解率(tip のみ): 0.6939

考察:2値分類の係数は、アルファベット順で後のクラス(ここでは Lunch)への寄与を表します。total_bill の係数が負なので、会計金額が大きいほど Dinner と予測されやすくなります。ただし標準化をしていないため、係数の絶対値を特徴量の重要度として比べることはできません。また、tip のみの正解率 0.6939 は、テストデータに占める Dinner の割合(34/49 件)と同じ値です。3 特徴量に増やすと 0.6735 に下がっており、特徴量を増やせば性能が上がるとは限りません。予測の内訳は演習 7-3 の混同行列で確認します。

演習 7-2: k-近傍法の基礎

問題を見る

import seaborn as sns
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report

# 1. データの読み込みと特徴量 X・ラベル y の設定
iris = sns.load_dataset("iris")
X = iris[["sepal_length", "sepal_width",
          "petal_length", "petal_width"]]
y = iris["species"]

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

# 3. StandardScaler で標準化
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

# 4. k-NN モデル(n_neighbors=5)の学習
model = KNeighborsClassifier(n_neighbors=5)
model.fit(X_train_s, y_train)

# 5. テストデータで予測し、正解率を表示
y_pred = model.predict(X_test_s)
print(f"正解率: {accuracy_score(y_test, y_pred):.4f}")

# 6. classification_report の表示
print(classification_report(y_test, y_pred))

# 7. k を変えて訓練・テスト正解率を比較
for k in [1, 3, 5, 7, 9, 11, 15, 21, 31]:
    m = KNeighborsClassifier(n_neighbors=k)
    m.fit(X_train_s, y_train)
    train_acc = accuracy_score(y_train, m.predict(X_train_s))
    test_acc = accuracy_score(y_test, m.predict(X_test_s))
    print(f"k={k:2d}: 訓練 {train_acc:.4f}, テスト {test_acc:.4f}")
正解率: 1.0000
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        10
  versicolor       1.00      1.00      1.00         9
   virginica       1.00      1.00      1.00        11

    accuracy                           1.00        30
   macro avg       1.00      1.00      1.00        30
weighted avg       1.00      1.00      1.00        30

k= 1: 訓練 1.0000, テスト 1.0000
k= 3: 訓練 0.9417, テスト 1.0000
k= 5: 訓練 0.9583, テスト 1.0000
k= 7: 訓練 0.9583, テスト 1.0000
k= 9: 訓練 0.9500, テスト 1.0000
k=11: 訓練 0.9583, テスト 1.0000
k=15: 訓練 0.9583, テスト 1.0000
k=21: 訓練 0.9500, テスト 1.0000
k=31: 訓練 0.9250, テスト 0.9667

考察:k=5 では 30 件のテストデータをすべて正しく分類できました。アヤメはクラスがよく分かれているデータです。k=1 では訓練正解率が 1.0000 になります。訓練データでは自分自身が最近傍になるためで、過学習の兆候です。k=3〜21 ではテスト正解率が 1.0000 のままですが、k=31 まで大きくすると広い範囲の多数決になって決定境界がなめらかになりすぎ、訓練・テストとも正解率が下がります(未学習)。

演習 7-3: 混同行列と評価指標

問題を見る

(a)手計算

与えられた混同行列は次のとおりです(TN=85, FP=10, FN=5, TP=50)。

  1. 正解率(Accuracy)
\[\mathrm{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN} = \frac{50 + 85}{150} = 0.90\]
  1. 適合率(Precision)
\[\mathrm{Precision} = \frac{TP}{TP + FP} = \frac{50}{50 + 10} = \frac{50}{60} \approx 0.833\]
  1. 再現率(Recall)
\[\mathrm{Recall} = \frac{TP}{TP + FN} = \frac{50}{50 + 5} = \frac{50}{55} \approx 0.909\]
  1. F1 スコア
\[\mathrm{F1} = 2 \times \frac{0.833 \times 0.909}{0.833 + 0.909} \approx 0.870\]
  1. 評価:正解率90%でありF1スコアも0.87と比較的高く、バランスの取れた分類性能を持つモデルといえます。適合率(0.833)より再現率(0.909)のほうが高いため、「Positive の取りこぼしは少ないが、Negative を Positive と誤判定するケース(FP=10)がやや多い」傾向があります。

(b)実装での確認

import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import (confusion_matrix, precision_score,
                             recall_score, f1_score, classification_report)

# 1. 演習 7-1 と同じモデルを学習し、テストデータを予測
tips = sns.load_dataset("tips")
X = tips[["total_bill", "tip", "size"]]
y = tips["time"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# 2. confusion_matrix の計算と表示
cm = confusion_matrix(y_test, y_pred, labels=["Dinner", "Lunch"])
print("混同行列:")
print(cm)

# 3. ヒートマップで混同行列を可視化
plt.figure(figsize=(5, 4))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
            xticklabels=["Dinner", "Lunch"],
            yticklabels=["Dinner", "Lunch"])
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()

# 4. Lunch を Positive とした適合率・再現率・F1 スコア
precision = precision_score(y_test, y_pred, pos_label="Lunch")
recall = recall_score(y_test, y_pred, pos_label="Lunch")
f1 = f1_score(y_test, y_pred, pos_label="Lunch")
print(f"適合率: {precision:.4f}")
print(f"再現率: {recall:.4f}")
print(f"F1スコア: {f1:.4f}")

# 5. classification_report の表示
print(classification_report(y_test, y_pred))
混同行列:
[[33  1]
 [15  0]]
適合率: 0.0000
再現率: 0.0000
F1スコア: 0.0000
              precision    recall  f1-score   support

      Dinner       0.69      0.97      0.80        34
       Lunch       0.00      0.00      0.00        15

    accuracy                           0.67        49
   macro avg       0.34      0.49      0.40        49
weighted avg       0.48      0.67      0.56        49

実行すると、混同行列の各セルの件数を色の濃さで表したヒートマップも表示されます。左上(Dinner を Dinner と予測した 33 件)だけが濃く、右下(Lunch を Lunch と予測できた件数)が 0 であることがひと目でわかります。

考察:間違えやすいのは Lunch です。テストデータの Lunch 15 件はすべて Dinner と判定され、Lunch の再現率と F1 スコアは 0 です。tips データは Dinner が 176 件、Lunch が 68 件と Dinner に偏っています。さらに会計金額・チップ・人数の分布が両者で大きく重なっていて、この 3 特徴量では区別がつきません。正解率 0.67 はそれなりに見えますが、ほぼすべてを Dinner と答えるモデルになっていて、全部を Dinner と答えた場合(34/49 = 0.69)より低くなっています。

演習 7-4: 4 モデルの比較

問題を見る

import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, f1_score
import seaborn as sns

# 1. 全特徴量を使用し、標準化を行う
iris = sns.load_dataset("iris")
X = iris[["sepal_length", "sepal_width",
          "petal_length", "petal_width"]].values
y = iris["species"].values

# 2. 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

# 3. 4つのモデルの正解率と F1スコアを求める
models = {
    "ロジスティック回帰": LogisticRegression(
        max_iter=200, random_state=42),
    "ナイーブベイズ": GaussianNB(),
    "k-NN (k=5)": KNeighborsClassifier(n_neighbors=5),
    "SVM (RBF)": SVC(kernel="rbf", random_state=42),
}
results = []
for name, model in models.items():
    model.fit(X_train_s, y_train)
    y_pred = model.predict(X_test_s)
    acc = accuracy_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred, average="macro")
    results.append({
        "モデル": name,
        "正解率": f"{acc:.2%}",
        "F1スコア": f"{f1:.4f}"
    })

# 4. 結果を表にまとめて表示
result_df = pd.DataFrame(results)
print(result_df.to_string(index=False))
      モデル     正解率  F1スコア
ロジスティック回帰 100.00% 1.0000
  ナイーブベイズ 100.00% 1.0000
k-NN (k=5) 100.00% 1.0000
SVM (RBF) 100.00% 1.0000

考察:この分割(random_state=42)では4つのモデルすべてが正解率100%で、差は出ませんでした。アヤメはクラスがよく分かれた易しいデータだからです。random_state を変えたり特徴量を減らしたりすると、モデルの差が見えてきます。

演習 7-5: SVM とハイパーパラメータ

問題を見る

import seaborn as sns
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 1. データの読み込みと欠損値の削除
penguins = sns.load_dataset("penguins").dropna()

# 2. 特徴量 X とラベル y の設定 + 訓練・テストに分割
X = penguins[["bill_length_mm", "bill_depth_mm", "body_mass_g"]]
y = penguins["species"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 3. StandardScaler で標準化
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

# 4. 複数の C で SVM(RBF)を学習し、精度を比較
for C in [0.01, 0.1, 1, 10, 100]:
    model = SVC(kernel="rbf", gamma="scale", C=C, random_state=42)
    model.fit(X_train_s, y_train)
    y_pred = model.predict(X_test_s)
    acc = accuracy_score(y_test, y_pred)
    print(f"C={C:>6}: 正解率 {acc:.4f}")
C=  0.01: 正解率 0.7761
C=   0.1: 正解率 0.9851
C=     1: 正解率 1.0000
C=    10: 正解率 1.0000
C=   100: 正解率 0.9851

考察:C はマージン違反へのペナルティの強さです。C=0.01 では誤分類を許しすぎて決定境界が単純になり、正解率は 77.6% にとどまります(未学習)。C=1 と C=10 では 100% に達しますが、C=100 では訓練データに合わせすぎて 98.5% にわずかに下がります(過学習の兆候)。適切な C はデータによって変わるので、第 5 章の GridSearchCV クラスなどで探します。