理学療法士がKaggleで脳卒中予測に挑戦した話|EDA〜ロジスティック回帰まで
はじめに
理学療法士として急性期病院で2年、地域医療の現場で6年働いてきました。公衆衛生学修士(MPH)も取得していますが、普段使わないのでほぼ忘れています。現在はIT企業に勤務しています。(データ分析歴3ヶ月)
データ分析を始めた理由はシンプルで、「なんかかっこいいと思った」からです。せっかくやるなら医療データで、将来はヘルスケア分野に関わりたいと思っています。そのはじめの一歩としてこのテーマを選びました。
Pythonは初心者です。つまずいたところも含めて、正直に記録します。
データ概要
使用したのはKaggleのStroke Prediction Dataset(fedesoriano)です。
- 行数:5110件
- 列数:12列
- 目的変数:stroke(0: 脳卒中なし / 1: 脳卒中あり)
- 脳卒中あり:249件(約5%)→ 不均衡データ
EDA(探索的データ分析)
まずデータの基本的な確認から始めました。
df.shape
df.dtypes
df.describe()
df.isnull().sum()
気づいたこと:数値型でもカテゴリとして扱うべき列がある
hypertensionやheart_diseaseは0/1の数値ですが、意味はカテゴリです。df.nunique()でユニーク数を確認すると見抜きやすいです。
血糖値271を外れ値と切り捨てなかった理由
avg_glucose_levelはmean=106、75%=114に対してmax=271と乖離が大きく、外れ値を疑いました。ただし血糖値271は糖尿病として臨床的にあり得る値です。医療データの外れ値はドメイン知識で判断する必要があると学びました。
前処理
欠損値の補完
欠損があったのはbmi列のみ。中央値で補完しました。
df['bmi'] = df['bmi'].fillna(df['bmi'].median())
平均値ではなく中央値を選んだのは、外れ値の影響を受けにくいからです。迷ったら中央値が安全、と覚えました。
カテゴリ変数のエンコーディング
df = pd.get_dummies(df, drop_first=True)
drop_first=Trueにする理由は、全列残すと情報が重複して(多重共線性)モデルが不安定になるからです。落とした列が基準(ベースライン)になります。最初はここの意味がわかりませんでした(何だこれ。。?)。
モデル構築と評価
層化5分割交差検証でロジスティック回帰を評価しました。
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
model = LogisticRegression(random_state=42, max_iter=1000, class_weight='balanced')
class_weight='balanced'なし→ありで結果が激変した
| Recall | Precision | F1 | |
|---|---|---|---|
| balancedなし | 0.004 | - | - |
| balancedあり | 0.795 | 0.134 | 0.229 |
balancedなしはほぼ全員をstroke=0と予測していました。不均衡データでは何もしないと多数派に引っ張られます。
Recall=感度、Precision=PPVとして読む
医療スクリーニングの文脈では、Recall(見逃さない力)を重視します。疫学でいう感度と同じ考え方です。Recall0.795は「脳卒中の約80%を拾えている」と解釈できます。
つまずいたところ
初心者丸出しのミスを残します。
-
isnullとisnull()の違い → ()をつけないと関数が実行されない。属性は()不要、メソッドは()必要。 -
df('bmi')ではなくdf['bmi']→ 列の取り出しは角括弧。 - One-Hot Encodingで順序を振ると、存在しない大小関係をモデルが学習してしまう。
- 不均衡データでAccuracyを使うと、全員0と予測しても95%になる。最初はこれに気づかなかった。
次のステップ
ベースラインとしてロジスティック回帰まで完成しました。次はこの2点に取り組む予定です。
- 特徴量エンジニアリング:新しい特徴量の作成、予測に効かない変数の除外
- モデルの変更:決定木、ランダムフォレスト、LightGBMなどを試す
続きは別の記事で書きます。