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?

2026年3月26日の学習記録 Kaggleコンペ『Predicting Heart Disease』への参加 振り返り

0
Posted at

【1】.やったこと

 Kaggleのコンペ『Predicting Heart Disease』に参加したので、そこでやったことを振
り返っていく。
 使用した環境はKaggleNotebook

 ※今回は初参加ということで、実装面でのキャッチアップに非常に時間が取られてしま
  ったため、やったことのみの報告とする。

(1).pd.readで躓いたのでそれの解消
  inputのところには『Predicting Heart Disease』と書いてあったから、ディレクトリ
 名がそれだと思ったら、違った。
  !ls /kaggle/inputで調べることができ、調べてみると『playground-series-s6e2』が
 ディレクトリ名だった。(全然違う)

  また、月の後半に再開したとき、ディレクトリ名が変わっていた。
  Kaggleでは途中でディレクトリ名が変わることはよくあるようだ。

  うまくいかないときは調べれば問題ないので、今後はそうすること。

(2).EDA(Exploratory Data Analyses)
  まずは、データの特性やパターン、異常値、変数間の関係を確認する。
  また、Pandas_Profilingが使えなくなっているのと、今回は初コンペのため、EDAを
 丁寧に行うこととした。
  (Pandas_Profilingの後継として使えるものは、ydata_profilingがあるのは既知)
  
 ①.コンペの概要を確認し、目的変数や評価基準を確認する。
   一番最初に、何を予想するのかと、評価基準(どのようにして点数が付くのか)をチ
  ェックする。

  (a).目的変数
   ・確認するためにやったこと
      sampleに.columnsを実施。

   ・わかったこと
      目的変数は『HeartDisease』
      これが1か0かを予測する学習アルゴリズムを作り、予測用のデータの
     HeartDiseaseの値の予測をさせる。

  (b).評価基準
   ・確認するためにやったこと
      Kaggleのコンペ『Predicting Heart Disease(Playground Series
      - Season 6 Episode 2』のページでOverviewの下のEvaluationを確認

   ・わかったこと
      評価基準は『AUC』(ROC曲線下面積)
      要するに、予測した確率が正例と負例をどれだけうまく順位付けできている
     かかどうか。
      (0を0.8と予測してしまったり、1を0.2と予測することが少ないものが良い)
      また、ここからHeartDiseaseの値は0~1.0を取ることがわかった。

 ②.全体像の把握
  (a).やったこと
    ・.shape
      行数と列数を見るメソッド
    ・.head()
      先頭n行を見るメソッド(nは()内の数値)
    ・.tail()
      末尾n行を見るメソッド(nha()内の数値)

      これらを各データフレームに行う。

  (b).やった結果わかったこと
    ・train
    630,000行、15列
    ・test
      270,000行、14列
    ・sample_submission
      270,000行、2列(IDとHeart Disease)
    ・存在する列
      全15列
       id 個人識別の番号
       Age 年齢
       Sex 性別
       Chest pain type
       BP
       Cholesterol
       FBS over
       EKG results
       Max HR
       Exercise angina
       ST depression
       Slope of ST
       Number of vessels fluro
       Thallium
       Heart Disease

      調べれば、どれが何かはわかるだろうが、先入観を排除したいため、初期
     EDAの段階では調べないで進める。
      (また、このコンペは過去に元となるコンペがあるため、応えが見つかって
     しまう危険性もあるので、それも避ける意図もある。)

 ③.データ型と欠損の確認
  (a).やったこと
    .info()で各データフレームを確認

  (b).やった結果わかったこと
   ・train
     dtype :ST depressionがfloat、Heart Diseaseがobject、他はint64
     Non-null :全列が630000
     欠損 :なし

   ・test
     dtype :ST depressionがfloat64、他はint64
     Non-null :全列が270000
     欠損 :なし

   ・sample_submission
     dtype :全列int64
     Non-null :全列270000
     欠損 :なし 

     非常にきれいなデータである。

 ④.数値データの概要の確認
   (a).やったこと
    .describe()で各データフレームを確認

   (b).やった結果わかったこと
    ・trainについて
      ChastPaintypeの値の範囲は1~4 尺度は不明
      FBS over 120は2値
      EKG resultsはの値の範囲0~2 尺度は不明
      Exercise anginaは2値
      Slope of STは値の範囲1~3 尺度は不明
      Number of vessels fluroは値の範囲0~3 尺度は不明
      Thalliumは値の範囲が3~7 中央値が3なので大半は3であることがわかる。

    ・testについて
      trainとデータの構造がかなり似ている。(あくまでも要約統計量においては)
      ただ、要約統計量が似ているということはデータの傾向は似ている気がする。

 ⑤.カテゴリ変数の確認
   (a).やったこと
     .value_counts()をtrainの全ての列に対して行い確認。

   (b).やった結果わかったこと
    ・Age
      50台がデータの1/3を占める。
      いない年齢もある。

    ・Sex
      大半が1。
      1と0どっちが男なのかわからないが、性別が偏ってそうなことはわかる。

    ・Chest pain type
      ほとんどが3と4。      

    ・BP
      たぶんきりの悪い数字もあるんだろうけど、省略されてる。
      120と130が一番多い。もしかすると血圧?

    ・Cholesterol
      たぶんこれはコレステロール

    ・FBS over 120
      特になし

    ・EKG results
      1が異様に少ない。0と2が同じくらい。
      つまり1は特異な値の可能性がある。

    ・Max HR
      93つカテゴリがある
      量的変数ではあるっぽい。

    ・Exercise angina
      かなりの割合が0

    ・ST depression
      恐らく比例尺度

    ・.Slope of ST
      3が少ない。特異?

    ・Number of vessels fluro
      0が大半。2と3は少ない。

    ・Thallium
      3か6か7の3つのカテゴリのみ
      6が妙に少ないけど。

    ・Heart Disease
      だいたい半々くらい。
      きれいすぎるので、恣意的にこのようになってる気がする。

 ⑥.目的変数の分布の確認
   (a).やったこと
    train[Heart Disease].value_counts()で確認

   (b).やった結果わかったこと
    完全に半分とは言えないが、偏ってたとしても6:4程度

 ⑦.目的変数 × 各変数の関係性の確認
   (a).やったこと
    1つの列につき、
     train.groupby("Heart Disease")[""].mean()
     train.groupby("")["Heart Disease"].mean()
    の2つを実施。

   (b).やった結果わかったこと
    ・sexをグループにしたとき、1の方が心臓病の人が多そう。
      sexの違いは何か意味がありそう。

    ・Chest pain typeが4の場合だけ突出して心臓病の人が多い。
      胸の痛みは何か4であることに意味がありそう。

    ・BPはHeart Diseaseの1と0で差があんまりない。
      BPをグループにしてみたら値毎で結構違いがありそうだが、現時点では意味
     を見出しづらいので、詳しく見るとしてもベースラインの後で良さそうな数字
     な気がする。

    ・CholesterolもBPと同様

    ・FBS over 120は微妙に高いほど心臓病である確率が上がってそう。
      関係ありそう。(Sexよりは影響少なそうだけど)

    ・EKG resultsは心臓病の人ほど高い。そして2の心臓病である確率が高い。
      関係ありそう

    ・Max HRは低い方が心臓病でありやすい?

    ・Exercise anginaは0と1の落差が大きい。
      心臓病の人はExercise anginaが1の人が多い
      逆にしてみたら、差が歴然。これは結構関係ありそうだ。
      Exercise anginaが何かはわからないが。
 
    ・ST depressionは関係ありそう?
      心臓病の人は高い傾向ある。

    ・Slope of STは2か3であることに意味がありそう。
      心臓病の人ほど高い。

    ・Number of vessels fluroは0以外は高い確率で心臓病
      心臓病の人の平均も高め

    ・Thalliumは意味がありそう。
      6と7の人は半分以上が心臓病

    ・整理すると、0と1を分ける差の有無において
      差が小さそうな列:BP、Cholesterol、FBS over 120、Max HR
      偏りはありそうな列:Chest pain type、EKG results、Slope of ST、
                Thallium
      差が大きそうな列:Age、Sex、Exercise angina、ST depression
               Number of vessels fluro、

 ⑧.一部列の可視化
   (a).やったこと
    差が大きそうな列をsns.histplot()で確認

   (b).やった結果わかったこと
    ・Age
      55歳あたりを境に、年齢が高い側は心臓病の人の方が多い。
      年齢が低い側は心臓病でない人が多い。

    ・Sex
      1の方は心臓病の人が多い。
      0の方は心臓病でない人が多い。

    ・Exercise angina
      1の方は心臓病の人が多い。
      0の方は心臓病でない人が多い。

    ・ST depression
      0.8を境に右側は心臓病の人が多い。
      ただ、0の人は突出して心臓病じゃない人が多い。

    ・Number of vessels fluro
      1より右側は心臓病の人がとても多い。
      0だけは心臓病じゃない人が多い。
      ただ、偏りな気はするし偏りでない気もする。

 ⑨.仮説建て
    ⑧の可視化から、これらは0と1をかなりはっきり分けている傾向があるという仮
   説が立つので、これらを特徴量としてベースラインの作成に取り掛かることとした。

(3).ベースラインの作成
 ①.バリデーション設計
  (a).バリデーションの方針
    今回のコンペは1日5回しかsubmitできないため、提出前に自前で検証できる必要
   がある。
    また、学習用データへの過学習を疑うために、ベースライン自体の検証も行う
   必要がある。

    ベースライン自体の検証はホールドアウト検証(検証はデータの1割
    また、学習については交差検証を使って行う。

  (b).データ分割の設計
    データが大量にあることから、ある程度データの傾向は似ると判断。
    ここから、testはtrainと同じような傾向を持つと仮定して、データ分割を行う。
    そのため、trainにおけるHeart Diseaseのデータ傾向を保ったデータ分割を行う。

  (c).trainのHeart Diseaseにおける割合を確認。
    Heart Diseaseのデータ型がobjectのため、"Absence": 0,"Presence": 1に変換し
   たのちに、割合を確認。
    (後に機械学習に流し込むために、現時点でint64に変換した)
    結果、0:0.55166、0.44834

  (d).ベースライン検証用のデータの切り出し
    train_test_splitを使って、90%を学習データ、10%を検証用データとして切り出し。
    (なお、90%は後に5分割して交差検証を行う)

 ②.モデル学習
   ここの実装は「Kaggleで磨く 機械学習の実践力』を参考にしている。

  (a).モデルの選定
    今回は表データに強く、学習が早いとされるLightGBMを使用する。

  (b).バリデーション設定
     今回は5分割とする。
     (最適は経験則的に10分割とされているが、5分割でも制度はさほど変わらず、
     かつ計算資源が半分で済むため)

  (c).使う特徴量
     (2)の⑨に従い、Age、Sex、Exercise angina、ST depression、
    Number of vessels fluroの5つを特徴量として学習を行う。

  (d).コード
     まずは機械学習のセル。
     次に、学習した際に検証も行っているため、その評価結果を
――――――――
#アルゴリズム学習

#予測確率を入れる箱(OOFとholdout予測)
oof_pred = np.zeros(len(X_train_full))

#StratifiedKFold(あなたと同じ)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

#LightGBMモデル(まず堅いベースライン)
params = dict(
objective="binary",
learning_rate=0.05,
n_estimators=10000, # early stopping前提で大きく
num_leaves=31,
subsample=0.8,
colsample_bytree=0.8,
random_state=42,
n_jobs=-1
)

models = []

for fold, (tr_idx, va_idx) in enumerate(skf.split(X_train_full, y_train_full), 1):
X_train = X_train_full.iloc[tr_idx]
X_valid = X_train_full.iloc[va_idx]
y_train = y_train_full.iloc[tr_idx]
y_valid = y_train_full.iloc[va_idx]

model = lgb.LGBMClassifier(**params)

model.fit(
    X_train, y_train,
    eval_set=[(X_valid, y_valid)],
    eval_metric="auc",
    callbacks=[
        lgb.early_stopping(stopping_rounds=200, verbose=True),
        lgb.log_evaluation(period=100)
    ]
)

# OOF(valid部分の予測)
oof_pred[va_idx] = model.predict_proba(X_valid, num_iteration=model.best_iteration_)[:, 1]

# testも平均していく(提出用)
test_pred += model.predict_proba(X_test, num_iteration=model.best_iteration_)[:, 1] / skf.n_splits

fold_auc = roc_auc_score(y_valid, oof_pred[va_idx])
print(f"[Fold {fold}] AUC = {fold_auc:.5f} | best_iter = {model.best_iteration_}")

models.append(model)

――――――――
#CVの全体AUC
cv_auc = roc_auc_score(y_train_full, oof_pred)
print(f"\n[CV OOF] AUC = {cv_auc:.5f}")
――――――――
#holdout評価
holdout_pred = np.zeros(len(X_holdout))
for model in models:
holdout_pred += model.predict_proba(X_holdout, num_iteration=model.best_iteration_)[:, 1] / len(models)

holdout_auc = roc_auc_score(y_holdout, holdout_pred)
print(f"[Holdout] AUC = {holdout_auc:.5f}")
――――――――

(4).スコア改善のための試行①(特徴量を増やす)
 特徴量を足してみる。
 まず、心臓病とそうでない場合に偏りがありそうな列であったChest pain type、EKG results、
Slope of ST、Thalliumを特徴量として追加してみた。
 すると、スコアが改善した。(AUC:0.88371→0.94376)
 次に、差が小さそうな列であったBP、Cholesterol、FBS over 120、Max HRも一応入れてみた。
 すると、これでもスコアが改善してしまった。(AUC:0.94376→0.95484)

(5).スコア改善のための試行②(特徴量を減らす)
 今度は、どれかノイズになっている特徴量がないかを試してみる。
 一つずつ、特徴量として外してみたところ、どれを減らしてもスコアが下がる。
 つまり、全てがそれなりに効いてはいる。
 (どれがどれくらい効いているのかの差はあったが)

(6).スコア改善のための試行③(新たな特徴量を追加する)(未実装)
 ①.仮説
   予想を間違えているデータの中に、何かヒントがあるかもしれない。

 ②.やったこと。
  (a).誤分類の抽出、表示
     やりたいことは、交差検証の再に、1なのに0.5未満の確率を付与してしまった
    データと、0なのに0.5以上の確率を付与してしまったデータのから、何か共通の
    構造がないかを調べること。
     そのため、ご分類しているデータを抽出した。
     コードは機械学習を終えた後のセルに下記3つのセルを追加。
――――――――
oof_df = X_train_full.copy()
oof_df["Heart Disease"] = y_train_full.values
oof_df["pred_proba"] = oof_pred
――――――――
oof_df["pred_label"] = (oof_df["pred_proba"] >= 0.5).astype(int)

false_negative = oof_df[(oof_df["Heart Disease"] == 1) & (oof_df["pred_label"] == 0)]
false_positive = oof_df[(oof_df["Heart Disease"] == 0) & (oof_df["pred_label"] == 1)]
――――――――
print("FN:", len(false_negative), "FP:", len(false_positive))
display(false_negative.head(10))
display(false_positive.head(10))
――――――――
    
  (b).確認の結果得られた洞察
     ※以下、FN=false_negative、FP=false_positive
    ・FPとFNを見た結果
      FNは先頭10行に関して言えば、全てThalliumが3でFBS over 120が0
      FNはCholesterolがほとんど200以上(でもそれはFPも同じ)
      FNはExercise anginaが0の傾向あり(あくまで傾向)
      FPはExercise anginaがFNより1が多い(とはいえ0の方が多数ではあるが)

     これだけではまだ何かやれる洞察にならなかった。
     そのため、次にもっと決定的に間違えてるデータに抽出を絞り込むことにした。

  (c).決定的に予測を間違えてるデータの抽出
     fp_high[cols]:HeartDeseaseが0なのに0.9以上の確率を付与してしまった行
     fn_low[cols] :HeartDeseaseが1なのに0.1以下の確率を付与してしまった行
     と、定義して再度抽出を行う。
     コードは下記。
――――――――
fp_high = oof_df[(oof_df["Heart Disease"]==0) & (oof_df["pred_proba"]>=0.9)]
fn_low = oof_df[(oof_df["Heart Disease"]==1) & (oof_df["pred_proba"]<=0.1)]

print("FP_high:", len(fp_high))
print("FN_low :", len(fn_low))

cols = ["Age","Sex","Chest pain type","Max HR","Exercise angina","ST depression",
"Number of vessels fluro","Thallium","BP","Cholesterol","FBS over 120","EKG results","Slope of ST"]

display(fp_high[cols].head(20))
display(fn_low[cols].head(20))
――――――――

     抽出後、今度は.describeで要約統計量も確認した。

  (d).確認の結果得られた考察

   ・fp_high[cols].describeを見て気が付いたこと
     fp_high[cols]はほとんど男
     Chest pain typeの標準偏差が結構低め(0.5)ということはほとんどが3~4
     Max HRは全体的に低い傾向
     Thalilumは全体的に高い(標準偏差1.5で平均が6)

   ・fn_low[cols].describeを見て気が付いたこと
     Exercise anginaがだいたい0
     Number of vessels fluroがだいたい0
     Slope of STがだいたい1
     Max HRは全体的に高い傾向
     Thalilumは全体的に低い(標準偏差0.8で平均が3)

  この考察をしたところで今回は締め切りが来てしまった。
  本当であれば、ここからMaxHR×Thalilumの交互作用を特徴量として加えたかった。

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?