第 5 章 演習解答¶
第 5 章「前処理と特徴量選択」の演習問題の解答例です。問題は 第 5 章 演習問題 を参照してください。
演習 5-1: 欠損値処理の実践¶
import seaborn as sns
# 1. データの読み込みと欠損値の確認
titanic = sns.load_dataset("titanic")
missing = titanic.isnull().sum()
ratio = (missing / len(titanic) * 100).round(1)
print("欠損値の数と割合(欠損のある列のみ):")
print(missing[missing > 0])
print(ratio[missing > 0])
# 2. age 列を中央値で補完
titanic["age"] = titanic["age"].fillna(titanic["age"].median())
# 3. embarked 列と embark_town 列を、それぞれの最頻値で補完
titanic["embarked"] = titanic["embarked"].fillna(
titanic["embarked"].mode()[0]
)
titanic["embark_town"] = titanic["embark_town"].fillna(
titanic["embark_town"].mode()[0]
)
# 4. deck 列を削除
titanic = titanic.drop(columns=["deck"])
# 5. 欠損値がないことを確認
print(f"\n処理後の欠損値の合計: {titanic.isnull().sum().sum()}")
欠損値の数と割合(欠損のある列のみ):
age 177
embarked 2
deck 688
embark_town 2
dtype: int64
age 19.9
embarked 0.2
deck 77.2
embark_town 0.2
dtype: float64
処理後の欠損値の合計: 0
考察:age は数値で分布に偏りがあるので、外れ値の影響を受けにくい中央値で補完しました。embarked はカテゴリ変数なので最頻値です。deck は77.2%が欠損していて、補完しても大半が推測の値になるので削除しました。embark_town は embarked と同じ内容で、欠損も同じ2行です。
演習 5-2: スケーリングの効果を比較¶
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
# 1. データの準備(数値特徴量4列)
penguins = sns.load_dataset("penguins").dropna()
feature_cols = ["bill_length_mm", "bill_depth_mm",
"flipper_length_mm", "body_mass_g"]
X = penguins[feature_cols].values
y = penguins["species"].values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 2. 3パターンで k-NN を学習し、3. 精度を比較
patterns = {
"スケーリングなし": None,
"StandardScaler": StandardScaler(),
"MinMaxScaler": MinMaxScaler(),
}
for name, scaler in patterns.items():
if scaler is None:
X_train_s, X_test_s = X_train, X_test
else:
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
model = KNeighborsClassifier(n_neighbors=5)
model.fit(X_train_s, y_train)
acc = accuracy_score(y_test, model.predict(X_test_s))
print(f"{name}: {acc:.4f}")
考察:精度はスケーリングなしで0.8209、StandardScaler クラスと MinMaxScaler クラスではどちらも1.0000でした。body_mass_g は数千グラム、bill_depth_mm は十数ミリと値の範囲が大きく違います。スケーリングしないと、距離がほぼ体重だけで決まってしまいます。k-NN のように距離を使うモデルでは、スケーリングが必要です。
演習 5-3: パイプラインの構築¶
import seaborn as sns
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score
# データの準備
titanic = sns.load_dataset("titanic")
num_cols = ["age", "fare"]
cat_cols = ["sex", "embarked", "pclass"]
X = titanic[num_cols + cat_cols]
y = titanic["survived"]
# 数値列: 中央値補完 → 標準化
num_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
# カテゴリ列: 最頻値補完 → ワンホットエンコーディング
cat_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore"))
])
# 列ごとに前処理を振り分けるColumnTransformer
preprocessor = ColumnTransformer([
("num", num_pipeline, num_cols),
("cat", cat_pipeline, cat_cols)
])
# 前処理 + k-NNのパイプライン
pipeline = Pipeline([
("preprocessor", preprocessor),
("classifier", KNeighborsClassifier())
])
# 5分割交差検証で評価
scores = cross_val_score(pipeline, X, y, cv=5)
print(f"各分割のスコア: {scores.round(4)}")
print(f"平均スコア: {scores.mean():.4f}")
考察:前処理をパイプラインに入れると、交差検証の各分割で、訓練側だけで fit して検証側には transform だけを使う手順が自動で守られます。そのためデータ漏洩が起きません。平均スコアは0.8047(約80%)でした。どの特徴量が効いているかは、同じ分割のまま特徴量の組み合わせを変えて比べると調べられます。
演習 5-4: GridSearchCV によるモデル最適化¶
import seaborn as sns
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split, GridSearchCV
# データの準備(演習5-3と同じ)
titanic = sns.load_dataset("titanic")
num_cols = ["age", "fare"]
cat_cols = ["sex", "embarked", "pclass"]
X = titanic[num_cols + cat_cols]
y = titanic["survived"]
# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 演習5-3と同じパイプライン
num_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
cat_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore"))
])
preprocessor = ColumnTransformer([
("num", num_pipeline, num_cols),
("cat", cat_pipeline, cat_cols)
])
pipeline = Pipeline([
("preprocessor", preprocessor),
("classifier", KNeighborsClassifier())
])
# 2-3. k-NN の近傍数と重み付け方法を GridSearchCV で探索
param_grid = {
"classifier__n_neighbors": [3, 5, 7, 9, 11],
"classifier__weights": ["uniform", "distance"],
}
grid_search = GridSearchCV(pipeline, param_grid, cv=5)
grid_search.fit(X_train, y_train)
# 4. 最適なパラメータと交差検証スコア
print(f"最適なパラメータ: {grid_search.best_params_}")
print(f"交差検証スコア: {grid_search.best_score_:.4f}")
# 5. テストデータでの最終評価
test_acc = grid_search.score(X_test, y_test)
print(f"テストデータでの精度: {test_acc:.4f}")
最適なパラメータ: {'classifier__n_neighbors': 3, 'classifier__weights': 'uniform'}
交差検証スコア: 0.8230
テストデータでの精度: 0.7877
考察:パイプライン内のパラメータは ステップ名__パラメータ名(アンダースコア2つ)で指定します。選ばれたのは n_neighbors=3、weights="uniform" で、交差検証スコアは0.8230、テストデータでの精度は0.7877でした。1回の分割だけでは、過学習の有無は判断できません。パラメータの探索は訓練データ内の交差検証で行い、テストデータは最後に一度だけ使います。