忍者ブログ
統計、機械学習、AIを学んでいきたいと思います。 お役に立てば幸いです。

【DS検定対策】名前のワナを攻略!分類の王道「ロジスティック回帰」

統計学や機械学習を学び始めると最初にぶつかる「名前の矛盾」。それが「ロジスティック回帰」です。なぜ回帰なのに分類なのか、その仕組みをスッキリ整理しましょう。

1. 【 問題 】

「ロジスティック回帰」に関する説明として、最も適切なものはどれでしょうか?

① 数値を予測する「回帰」の手法であり、住宅価格の予想などに用いられる。
② データを2つのクラスに分ける「分類」の手法であり、ある事象が起こる確率を予測する。
③ データのグループ化を行う「非教師あり学習」の手法である。
④ 決定木をたくさん組み合わせた「アンサンブル学習」の手法である。


2. 【 解答 】

正解: ② データを2つのクラスに分ける「分類」の手法

3. 整理:確率を計算して「境界線」を引く世界

ロジスティック回帰は、入力データから「あるクラスに属する確率」を計算し、その値が0.5(50%)を超えたら「合格(1)」、そうでなければ「不合格(0)」のように判定します。

【 ロジスティック回帰の仕組みイメージ 】

[ 1. 入力データの計算 ]
年齢や購入履歴などのデータを、線形式で計算する。

[ 2. シグモイド関数を通す ]
★ ここが核心!
どんな大きな値(または小さな値)も、0 から 1 の範囲にギュッと押し込める。

[ 3. 確率の出力 ]
「この客が購入する確率は 0.82 (82%) です」と出力。

[ 4. 分類 ]
しきい値(0.5など)で区切り、「購入するクラス」に分類する。

--------------------------

ポイント: 計算過程で「数値を予測(回帰)」しているため名前に「回帰」と付きますが、目的は「分類」です。

4. 覚えておくべき重要キーワード

1. シグモイド関数: 出力値を0〜1の間に収めるための関数。S字型のカーブを描きます。
2. オッズ比: ある事象が起こる確率と起こらない確率の比率。ロジスティック回帰の解釈に不可欠です。
3. 二値分類: 「Yes/No」「合格/不合格」など、2つのクラスに分けるのが基本です。


5. DS検定形式:実戦4択クイズ

問:ロジスティック回帰において、出力(確率 $p$)を求める際に用いられる、以下の数式で表される関数を何と呼ぶか。
$$f(x) = \frac{1}{1 + e^{-x}}$$

① ソフトマックス関数   ② シグモイド関数   ③ 恒等関数   ④ 階段関数

【 正解: ② 】

解説: この数式は「標準シグモイド関数」と呼ばれ、ロジスティック回帰の心臓部です。$x$ がどんな値でも $f(x)$ は必ず 0 より大きく 1 より小さい値になるため、「確率」として扱うのに非常に適しています。


6. まとめ

DS検定において「回帰という名前だが分類に使われる」「シグモイド関数で確率を出力する」という特徴が出たら、迷わずロジスティック回帰を選びましょう。シンプルながら解釈性が高く、今でもビジネス現場の第一線で使われている強力な手法です!




PR

【Kaggle挑戦記】Titanic 攻略 #11:血縁データの統合。「家族サイズ」が0.78の壁を打ち破る

前回(攻略 #10)のデバッグでは、自作の「子供フラグ」がモデルにとって不純なノイズとなっていた現実を直視しました。今回はその反省を活かし、バラバラに存在していた「血縁データ」をロジカルに統合。ついに 0.78 の壁を突破する強力なヒント FamilySize(家族人数) を導入します。

1. 今回の着眼点:孤立か、連帯か

タイタニックのデータには、家族に関する項目が2つ用意されています。しかし、これらを別々に見ていては「その人がどれほどの集団で行動していたか」という実態が見えてきません。

  • SibSp: 兄弟、配偶者の数
  • Parch: 両親、子供の数

避難の際、1人で行動していたのか、あるいは守るべき家族がいたのか。この「群れ」の大きさを一つの指標として統合し、モデルに新たな視点を与えます。自分自身(+1)を足すことで、「1 = 単独客」「2以上 = 家族連れ」という、生存率を分ける明確な境界線が生まれます。

2. 【実装】FamilySize導入とモデル構築の全コード

属性別Age補完を継承しつつ、FamilySizeを計算してランダムフォレストに投入するまでの全工程です。

import pandas as pd
from sklearn.ensemble import RandomForestClassifier

# 1. データの読み込み
train_data = pd.read_csv('train.csv')
test_data = pd.read_csv('test.csv')

# 2. Age(年齢)を Pclass×Sex の中央値で補完
group_cols = ['Pclass', 'Sex']
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))
test_data['Age'] = test_data['Age'].fillna(test_data.groupby(group_cols)['Age'].transform('median'))

# 3. 家族サイズ(FamilySize)の算出
train_data['FamilySize'] = train_data['SibSp'] + train_data['Parch'] + 1
test_data['FamilySize'] = test_data['SibSp'] + test_data['Parch'] + 1

# テストデータの Fare(運賃)の欠損値を中央値で補完
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 4. 学習に使用する特徴量の定義
features = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize"]

# カテゴリ変数のダミー変数化
X = pd.get_dummies(train_data[features])
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features])

# 5. モデルの構築と学習(木500本、深さ5)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

# 6. 予測と提出ファイルの出力
predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('submission_family_size.csv', index=False)

print("Score 0.78229 達成用の提出ファイルを保存しました。")

3. 実験結果:0.77990 → 0.78229 へ上昇!

家族サイズを導入した結果、ついに停滞していたスコアが大きく動きました。

  • 前回(Age補完のみ): Score 0.77990
  • 今回(FamilySize追加): Score 0.78229

エンジニア的な考察:
ついに 0.78 の大台に乗りました。単なる数値だった「SibSp」と「Parch」を合計して『家族というひとつの単位』として捉え直したことで、モデルは「独身男性(死亡率高)」や「小家族(生存率高)」という構造を、より正確に切り分けられるようになったと言えます。アルゴリズムをいじるのではなく、データの「見方」を変えることが勝利の鍵であることを証明しました。

4. 次なる一手:名前の「敬称」から社会的地位を暴く

0.78229 というスコアは一つの通過点に過ぎません。まだ伸び代は十分にあります。次は、これまで手をつけていなかった巨大な情報源 Name(名前) の解析に挑みます。ここから「Mr.」「Miss.」「Master.」といった敬称を抜き出せば、年齢だけでは判別できない「既婚・未婚」や「家柄(社会的地位)」という決定的なヒントが手に入るはずです。


「質の高い燃料(特徴量)」を投入することで、ランダムフォレストは見違えるような精度を見せ始めました。この勢いのまま、次はテキストデータの解析に踏み込み、さらなる高みを目指します。


【Kaggle挑戦記】Titanic 攻略 #10:モデルの検証。自作フラグは「ノイズ」だったのか?

前回(攻略 #9)では、生存予測の精度を上げるため、以下のデータ加工とモデル構築を行いました。

1. 前回の復習:投入データの構成と加工

  • Age(年齢)の精密補完: 単純な平均値ではなく、Pclass(客室階級)と Sex(性別)を掛け合わせたグループごとの中央値で欠損値を補完。
  • IsChild_Final(自作フラグ): 補完した「Age(年齢)が12歳以下」という条件に、さらに「Name(名前)に Master を含む」という確実性の高い敬称データを組み合わせて作成。
  • 投入した特徴量: Pclass, Sex, Age, IsChild_Final, SibSp(兄弟・配偶者数), Parch(親子数), Fare(運賃)。
  • モデル: ランダムフォレスト(n_estimators=500)。

しかし、結果は 0.77751 → 0.77511 へと下落。「子供と女性は優先される」という強力なドメイン知識を反映したはずが、なぜ逆効果になるのか。学習済みモデルの内部数値を直接取り出してデバッグします。

2. 思考のステップ:なぜ「子供フラグ」は失敗したのか

  1. 「Age」と「Name」から導いた子供フラグはダメだった。
    実際に精度が下がった以上、このフラグの設定には致命的な「何か」が足りない。
  2. 「子供と女性は優先されていた」というドメイン知識は揺るがない。
    歴史的事実として、避難優先順位の筆頭は「Women and children」である。
  3. モデルには「子供フラグ」と「Sex(女性)」という2つの軸を与えてある。
    理論上、これで優先グループを網羅できているはずだ。
  4. 【核心】ならば、モデル内部で、この2つの軸が「生存を切り分けるための重要度」としてどう扱われているかを確認すべきではないか?

3. 検証:ランダムフォレストが算出する「重要度」の仕組み

今回使用しているランダムフォレストは、学習(fit)のプロセスで500本の独立した決定木を作成します。その際、各項目の「重要度(Feature Importance)」は以下のステップで計算されています。

  • 不純度の減少量を累積: 各決定木がデータを分割する際、「性別」や「子供フラグ」などの軸を使います。その軸で分けた結果、グループ内の「生存・死亡」がきれいに分かれるほど(=不純度が下がるほど)、その軸に高いスコアが与えられ、500本分累積されます。
  • 比率の算出: 全項目が稼いだ累積スコアの総和を 1.0 (100%) としたとき、各項目が占める割合を算出します。

4. 【実装】モデルが各項目をどれだけ「信頼」したかを確認する

import pandas as pd
from sklearn.ensemble import RandomForestClassifier

# 1. 前回のデータ準備(攻略 #9の状態を再現)
train_data = pd.read_csv('train.csv')
group_cols = ['Pclass', 'Sex']

# Ageの補完:PclassとSexごとの中央値
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))

# Age(年齢)とName(敬称Master)から導いた子供フラグ
train_data['IsChild_Final'] = ((train_data['Age'] <= 12) | (train_data['Name'].str.contains('Master'))).astype(int)

# 学習に使用する特徴量の定義
features = ["Pclass", "Sex", "Age", "IsChild_Final", "SibSp", "Parch", "Fare"]
X = pd.get_dummies(train_data[features])
y = train_data["Survived"]

# 2. モデルの学習(500本の決定木を生成し、それぞれの分割性能を記録)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

# 3. 各項目の重要度(Feature Importance)を抽出
importances = pd.Series(model.feature_importances_, index=X.columns).sort_values(ascending=False)

print("--- [Feature Importance: 500本の木による信頼度の集計結果] ---")
print(importances)

# 4. 補足検証:子供フラグ(IsChild_Final)の実際の生存率
print("\n--- [Actual Survival Rate: 子供フラグ別の生存率(学習データ)] ---")
print(train_data.groupby('IsChild_Final')['Survived'].mean())

5. 実行結果:残酷な現実

検証コードを走らせた結果、以下の数値が返ってきました。

■ 500本の木による信頼度の集計結果(Feature Importance)

Sex_female       0.273813  (27.4%)
Sex_male         0.251328  (25.1%)
Fare             0.146034  (14.6%)
Pclass           0.125932  (12.6%)
Age              0.090933  (9.1%)
SibSp            0.053964  (5.4%)
IsChild_Final    0.029075  (2.9%)  ← ★ここ
Parch            0.028921  (2.9%)

■ 子供フラグ別の実際の生存率(学習データ)

IsChild_Final
0 (大人など)  : 0.366 (36.6%)
1 (子供)      : 0.575 (57.5%)

6. 考察:数字が示す「子供フラグ」の敗北原因

  • 【データ:累積貢献度 2.9%】子供フラグは「無視」された:
    500本の決定木が生存予測の軸として信頼した割合(Feature Importance)は、わずか 0.029 (2.9%) でした。これは、運賃(14.6%)や客室階級(12.6%)、さらには生の年齢(9.1%)よりも遥かに低く、モデルが「このフラグは生存を占う上で、ほとんど信用に値しない」と公式に判定したことを示しています。
  • 【データ:生存率 57.5%】「6割・4割」では使えない:
    モデルがこのフラグを信頼しなかった直接の根拠は、実行結果の IsChild_Final = 1(子供グループ)の生存率が 57.5% に留まったことです。これは「約6割が生き、42.5%(4割以上)が死亡している」という極めて不純な状態です。モデルが求めているのは生存か死亡かを明確に切り分けられる軸ですが、このフラグで分けても4割以上も死亡者が混ざってしまうため、生存予測の条件(if文)としては不適格と見なされました。
  • 【結論】不純な境界線がスコアを落とした:
    Age(年齢)とName(敬称Master)から導き出した「12歳以下」という人為的な境界線では、モデルが生存を確信できるほどの「純度」を生み出せませんでした。この「6割生存・4割死亡」という分離性能の低さこそが、累積貢献度を押し下げ、結果として全体の予測精度(スコア)を下落させた正体です。

【Kaggle挑戦記】Titanic 攻略 #9:名前(Name)による救出作戦と、裏目に出た結果

前回(攻略 #8)では、「12歳以下の子供フラグ(IsChild)」を導入した結果、スコアが 0.77990 → 0.77751 へと下落しました。この原因を「中央値補完によって大人扱いされた子供がノイズになった」と仮説を立て、今回は名前(Name)に含まれる確定情報を用いてその矛盾の解消を試みました。

1. 今回のロジック:名前と年齢のハイブリッド判定

Ageが欠損していた乗客に対し、一律の中央値で補完したことで生じた「誤判定」を上書きするのが今回の狙いです。以下の条件のいずれかを満たす場合に「真の子供(IsChild_Final)」と定義しました。

  • 条件A: 年齢(Age)が12歳以下である。
  • 条件B: 名前(Name)の中に、男の子を示す敬称 "Master." が含まれている。

名前に "Master" とあれば、たとえAgeが空欄で中央値(例:25歳)が割り当てられていても、確実に子供として救出できる計算です。論理的には、前回よりも情報の精度は高まっているはずでした。

2. 【実装】コード全文

前回(#8)からの変更点(OR条件によるフラグ作成)を反映した全文です。

import pandas as pd
from sklearn.ensemble import RandomForestClassifier

# 1. データの読み込み
train_data = pd.read_csv('train.csv')
test_data = pd.read_csv('test.csv')

# 2. Age の中央値補完(攻略 #7 継承)
group_cols = ['Pclass', 'Sex']
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))
test_data['Age'] = test_data['Age'].fillna(test_data.groupby(group_cols)['Age'].transform('median'))
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 3. 【攻略 #9 での変更点】真の子供フラグ(IsChild_Final)の作成
# ---------------------------------------------------------
# 年齢(Age)が12歳以下、または名前(Name)に "Master" を含む場合にフラグを立てる
# ---------------------------------------------------------
train_data['IsChild_Final'] = ((train_data['Age'] <= 12) | (train_data['Name'].str.contains('Master'))).astype(int)
test_data['IsChild_Final'] = ((test_data['Age'] <= 12) | (test_data['Name'].str.contains('Master'))).astype(int)

# 4. 特徴量の選択
features = ["Pclass", "Sex", "Age", "IsChild_Final", "SibSp", "Parch", "Fare"]

X = pd.get_dummies(train_data[features])
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features])

# 5. モデルの学習(500本維持)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('submission_master_rescue.csv', index=False)

3. 実験結果:さらなるスコア下落(0.77511)

期待に反し、スコアは前回の 0.77751 からさらに下がり、0.77511 となりました。精度を高めるための「Master救出作戦」でしたが、結果としては裏目に出た形です。

4. 考察:なぜ「正しいはずの情報」でスコアが落ちるのか

論理的に正しいはずのフラグを追加してスコアが落ちる場合、以下の可能性が考えられます。

  • 過学習(Overfitting): 「Master = 生存」という強いルールを学習しすぎた結果、テストデータに含まれる「不幸にも助からなかった Master」を正しく予測できなくなった。
  • 情報の競合: すでに Age や Parch(親子数)から「子供」であることをモデルが把握していた場合、新しいフラグの追加が判断のノイズになり、木構造が不安定になった。

「フラグを立てれば立てるほど良くなる」というわけではないのが、機械学習の難しさです。これまでは「子供」という一点にのみ執着してきましたが、その偏った視点自体が現在の停滞を招いているのかもしれません。


次は、子供(Master)だけでなく、未婚女性(Miss)や既婚女性(Mrs)、成人男性(Mr)といった「すべての敬称」を網羅的に数値化し、よりフラットな視点で特徴量エンジニアリングを進めていきます。特定の属性に固執せず、全体像を捉え直すことが 0.78 突破への道かもしれません。


【Kaggle挑戦記】Titanic 攻略 #8:フラグ追加の試みと、見えてきた「欠損値補完」の罠

前回(攻略 #7)では、性別と客室クラスを掛け合わせた精緻な Age(年齢)補完を行い、スコアは 0.77511 → 0.77990 と微増しました。今回は、さらに「子供」という属性を強調し、0.78 の大台を突破するための特徴量エンジニアリングに挑戦しました。

1. これまでの振り返りと今回の仮説

現在の私たちの戦績と構成は以下の通りです。

  • 攻略 #6: パラメータ調整(木の数)ではスコアに変化なし
  • 攻略 #7: Age(年齢)を補完して投入。スコアが 0.00479 上昇

現在、Age は連続値としてモデルに渡っています。しかし、生存の鍵は「子供か大人か」という二値的な境界線にあるはず。そこで、「12歳以下(Child)」か「それ以外(Adult)」かというフラグ(IsChild)を明示的に作ることで、モデルの判断を助けるという仮説を立てました。

2. 【実装】子供フラグ(IsChild)の導入とコード全文

Age が 12 以下の場合は 1、それ以外を 0 とする新しいカラム IsChild を作成し、特徴量に加えました。前回からの変更・追加箇所がわかるように実装したコードの全文です。

import pandas as pd
from sklearn.ensemble import RandomForestClassifier

# 1. データの読み込み
train_data = pd.read_csv('train.csv')
test_data = pd.read_csv('test.csv')

# 2. 【攻略 #7からの継承】Age の欠損値を「性別×客室クラス」ごとの中央値で補完
# ※ここでの中央値補完が、後のフラグ判定に影響を与えた可能性あり
group_cols = ['Pclass', 'Sex']
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))
test_data['Age'] = test_data['Age'].fillna(test_data.groupby(group_cols)['Age'].transform('median'))
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 3. 【攻略 #8での追加】子供フラグ(IsChild)の作成
# ---------------------------------------------------------
# 12歳以下を子供(1)、それ以外を大人(0)として新規カラム作成
# ---------------------------------------------------------
train_data['IsChild'] = (train_data['Age'] <= 12).astype(int)
test_data['IsChild'] = (test_data['Age'] <= 12).astype(int)

# 4. 【攻略 #8での変更】特徴量の選択
# ---------------------------------------------------------
# 新たに作成した "IsChild" をリストに追加
# ---------------------------------------------------------
features = ["Pclass", "Sex", "Age", "IsChild", "SibSp", "Parch", "Fare"]

X = pd.get_dummies(train_data[features])
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features])

# 5. モデルの構築(攻略 #6 で決めた 500本 を維持)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

# 6. 予測の実行
predictions = model.predict(X_test)

# 7. 提出用ファイルの作成
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('submission_ischild_added.csv', index=False)

3. 実験結果:無情にもスコアは 0.77511 へ下落

期待に反し、スコアは前回の Age 投入前と同じ 0.77511 まで落ち込んでしまいました。良かれと思って追加した「子供フラグ」が、なぜ足を引っ張ったのでしょうか。

4. 考察:欠損値補完が「真の子供」を消した可能性

ここで非常に重要な仮説が浮かび上がりました。「Ageが空欄で、適当な中央値で埋められた人たち」の中に、実は助かったはずの子供(Master)が混ざっていたのではないか? という点です。

攻略 #7 で行った「クラス別の中央値補完」では、たとえば3等客のAge欠損値に対し、一律で「20代半ば」といった大人の数値を割り当ててしまいました。その結果、本来は子供であったはずの乗客が、この IsChild フラグによって「確定的な大人(0)」としてモデルに誤学習されてしまった恐れがあります。

つまり、現在の「Ageベースのフラグ」は、補完データの不正確さを増幅させるノイズになってしまったと言えます。


次の一手は、この矛盾を解消するために「名前(Name)」に含まれる情報を使います。年齢が不明でも、名前に "Master." とあればその乗客は確実に男の子です。次回、「名前 + 年齢」による真の子供特定に挑み、0.78 の壁を叩き割ります。