👋

初めてのタイタニックコンペ備忘録

2024/04/14に公開

Aidemy premiumで勉強を始めて約一か月。
kaggleに挑戦できるまでコースを進めたので、初めてのタイタニックコンペ挑戦を備忘録として残したいと思います。
同じように初めて挑戦する人への参考になったらうれしいです。

コンペ内容の確認を確認しよう

コンペの目的

今回参加するタイタニックコンペはチュートリアルとして通年開催されているコンペです。
コンペの目的は、タイタニック号の沈没事故における生存者を予測することです。
乗客の属性(性別や年齢など)が与えられるので、それをもとに、各乗客が生き延びることができたのかを予測します。

評価方法

今回の評価方法は正解率です。
機械学習における「正解率(accuracy)」は、分類問題におけるモデルの性能を評価するための指標の一つで、モデルが予測したラベルと実際のラベルがどれだけ一致しているかを表す物です。
要は、予測がどれくらい当たったかを単純に示しているので直感的に理解しやすいです。

参考:正解率とは

※以下は教科書的な説明を自分用に残しておきます。

数式で表すと、全データポイントの数に対して正しく分類されたデータポイントの割合で計算されます。

\text{正解率 (Accuracy)} = \frac{\text{正しく分類されたデータポイントの数}}{\text{全データポイントの数}}

ここで、

  • 「正しく分類されたデータポイントの数」は、真陽性(True Positive, TP)と真陰性(True Negative, TN)の合計です。これは、モデルが正のクラスを正と予測し、負のクラスを負と予測したケースの数です。
  • 「全データポイントの数」は、データセットに含まれるサンプルの総数で、これは真陽性(TP)、偽陽性(False Positive, FP)、真陰性(TN)、偽陰性(False Negative, FN)の合計に相当します。

したがって、正解率の計算式は以下のように具体化できます:

\text{Accuracy} = \frac{TP + TN}{TP + FP + TN + FN}

正解率は0から1の間の値を取り、1に近いほどモデルの性能が良いことを示します。
ただし、クラス間のサンプル数に偏りがある場合(不均衡データセット)、正解率だけでモデルの性能を評価することは適切ではないとされています。このような場合、他の性能指標(例えば、適合率、再現率、F1スコアなど)と併用することが推奨されています。

正解率(Accuracy)だけでは適切に評価できない例として、不均衡データセット(Imbalanced Dataset)の場合が挙げられます。不均衡データセットとは、クラス(ラベル)の分布が大きく偏っているデータセットのことです。たとえば、ある病気の診断を行う場合、実際にその病気に罹患している人は全体のごく一部であることが多いです。このような場合、ほとんどのデータが「病気ではない」というクラスに属しており、極端な例で「病気ではない」と常に予測するモデルであっても、高い正解率を達成することができます。

例えば、1000個のデータポイントがあり、そのうち950個が「病気ではない」クラスに属し、残りの50個が「病気である」クラスに属しているとします。モデルが全てのケースを「病気ではない」と予測した場合、正解率は以下のように計算されます:

\text{Accuracy} = \frac{\text{正しく分類されたデータポイントの数}}{\text{全データポイントの数}} = \frac{950}{1000} = 0.95

この場合、正解率は95%と非常に高い値になります。しかし、このモデルは「病気である」クラスのデータポイントを一つも正しく識別できていません。つまり、実際には重要な「病気である」ケースを見逃しているにもかかわらず、高い正解率が得られてしまいます。

このような不均衡データセットの場合、正解率はモデルの性能を過大評価する可能性があり、特に少数クラスの識別性能を正確に反映していません。実際には、少数クラスの正確な識別が重要な場合が多く、このような状況では他の指標(例えば、適合率(Precision)、再現率(Recall)、F1スコア)を用いてモデルの性能を総合的に評価することが重要です。これらの指標は、クラス間の偏りを考慮に入れ、特に少数クラスの識別能力に焦点を当てることで、よりバランスの取れた性能評価を可能にします。

背景知識をインプットしよう

データ分析に取り掛かる前に、タイタニック号とは何なのかを改めて確認したいと思います。
実務におけるデータ分析でもドメイン知識が大切になるように、コンペであってもその背景知識を知ることは分析の精度に大きく影響してくるのかなと思っています。
以下はwikipediaからの引用を含むので、気になる人はタイタニック号のwikipediaページをご確認ください。
沈没事故についてはこちらです。

タイタニック号とは

タイタニックは、イギリスのホワイト・スター・ライン社が北大西洋航路用に計画し、造船家のアレクサンダー・カーライル英語版)とトーマス・アンドリューズによって設計され、北アイルランドベルファストにあるハーランド・アンド・ウルフ造船所で建造された豪華客船である。タイタニックの正式名称「RMS Titanic」のRMS(Royal Mail ShipまたはSteamer)は遠洋郵便船(英国郵便汽船)を意味する艦船接頭辞であり、船上でステーショナリーの購入、手紙の投函も可能だった<sup id="cite_ref-1"><a href="https://ja.wikipedia.org/wiki/タイタニック_(客船)#cite_note-1">[1]</a></sup>。タイタニックはホワイト・スター・ラインが保有する3隻のオリンピック級客船の2番船であり、姉妹船にオリンピックブリタニックがある。
処女航海中の1912年4月14日深夜、北大西洋上で氷山に接触、翌日未明にかけて沈没した。犠牲者数は乗員乗客合わせて1,513人(ほかに1,490人、1,517人、1,522または1,523人、1,609人などさまざまな説がある)であり、戦時中に沈没した船舶を除くと20世紀最大の海難事故であった<sup id="cite_ref-2"><a href="https://ja.wikipedia.org/wiki/タイタニック_(客船)#cite_note-2">[注 1]</a></sup>。生還者数は710人だった。タイタニックとその事故は、しばしば映画化されるなどして、世界的にその名を知られている。

ここまでは何となく知っている知識ですね。
諸説あるようですが、犠牲者数が1,513人、生還者数が710人とのことなので、総乗組員数は2,223人、生存率は31.9%(死亡率68.1%)ということですね。
ということは、この生存率に近くなる予測モデルを作ることがコンペの目標になりそうですね。

概要はわかりましたが、予測するためにはもう少し情報が必要なので、wikipediaをさらに深掘ってみます。

事故概要

タイタニック号沈没事故(タイタニックごうちんぼつじこ)とは、1912年4月14日の夜から4月15日の朝にかけて、イギリスサウサンプトンアメリカ合衆国ニューヨーク行きの航海中の4日目に、北大西洋で起きた海難事故である。

ニューヨーク港に向けて航行中に「海氷が存在する」という警告を4月14日中に7件受けていたにもかかわらず、タイタニック号の見張りが氷山に気付いたとき船は最高速に近いスピードで進んでいた。衝突を避けようとしたが、船は右舷側に斜方向からの打撃を受け、全16区画のうち5つの区画に穴が開いてしまった。
タイタニックの船首部は4つの区画が浸水しても沈まないように設計されていたが、それでも十分ではなく、敏感なクルーはこの船が沈没することを察知した。クルーは遭難信号灯と無線で助けを求め、乗客を救命ボートに乗せた。しかし、それは近くの救助船までの移乗用として簡易的に設計されたもので、搭載数もすべての乗船者を載せるにはあまりに少ないものだった。

船体沈没の進行は予想よりも早かった。やむなくボートには女性と幼い子供が優先的に乗せられ、多くの男性は強制的に排除されたが、クルーも救助活動に不慣れな者が多く、定員に満たないまま出発するボートもあった。結果的に多数の乗客乗員が船に取り残された。
タイタニックは1,000人以上を乗せたまま沈んだ。海に浸かった人のほとんどが数分後に低体温症により死亡した。救助にあたった客船「カルパチア」が4月15日の9時15分に最後の1人を救い上げた時は、既に船の沈没から7時間、衝突から実に約9時間半が経っていた。

非常召集の成果は乗客の等級に左右された。一等船室の客室係は数室のみを担当していたが、二等と三等船室の客室係は大勢の人々をさばかなければならなかった。一等の客室係は直接的な支援を行い、客が服を着るのを手伝い、デッキまで誘導した。しかし、二等と三等の客室係のほとんどはドアを開け放ち、救命胴衣をつけて上に来るよう乗客に伝えるのが精一杯だった。三等船室に至っては、おおむね乗客自身の判断で行動するほかなかった<sup id="cite_ref-FOOTNOTEBartlett2011121_51-0"><a href="https://ja.wikipedia.org/wiki/タイタニック号沈没事故#cite_note-FOOTNOTEBartlett2011121-51">[50]</a></sup>。

この災害は、救命ボートの数、緩い規則、旅客の等級によって異なる避難時の対応など、ずさんな危機管理体制が多くの人の義憤を引き起こした。この事故をきっかけとして救助のあり方が見直され、1914年に海上における人命の安全のための国際条約(SOLAS)が作られた。これは今も海の安全を守っている。

あらためて事故概要を見てみると、今では考えられない杜撰さですね。。。
さて、ここにはいくつか分析に使えそうな情報がありそうです。

  • 全16区画のうち5つの区画に穴があいた
  • ボートには女性と幼い子供が優先的に乗せられ、多くの男性は強制的に排除された
  • 旅客の等級によって異なる避難時の対応があった

これらの情報から以下のような予測が立ちます。

  • 穴の開いた区画から遠い区画に滞在していた場合は生存率が高いのではないか
  • 女性、幼い子供が男性と比べて生存率が高いのではないか(独身の子供がいない男性の生存率が最も低いのではないか)
  • 旅客の等級が高いほうが生存率が高いのではないか

これらの予測も頭に入れながら分析を行っていきたいと思います。

分析前の下準備

データセットのダウンロード

それではいよいよ本格的にデータ分析に取り組んでいきたいと思います。
まずは使用するデータセットの準備です。
kaggleのサイト上でコードを実行することもできますが、今回は自分の環境で分析したいのでデータをダウンロードしておきます。

kaggleのタイタニックコンペのページに行き、最下部にダウンロードボタンがあるのでそこからダウンロードします。

ダウンロードできるデータは3種類です。

  1. train.csv:訓練用データ
  2. test.csv:テスト用データ
  3. gender_submission.csv:サブミット用のサンプルデータ

提出方法の確認

次に提出物・提出方法の確認です。
まず、提出物データ、ヘッダー行を含む418行のcsvファイルになります。
PassengerIdとSurvivedを超える追加の列(項目)や行がある場合、提出はエラーとなります。

ファイルには正確に2列が必要です:

  • PassengerId(任意の順序でソート)
  • Survived(二元予測を含む:生存は1、死亡は0)

提出方法は、とても簡単でSubmit Predictionsボタンをクリックしてファイルをアップロードするだけです。

  1. 「Submit Predictions」ボタンをクリックします
  2. 提出ファイル形式でCSVファイルをアップロードします。1日に10回まで提出できます。

さあ、これで準備は完了です。
いよいよ分析を開始したいと思います。

データ分析開始

データの外観

それではデータ内容の確認から開始したいと思います。
Pandasライブラリを使ってデータの中身を見ていきます。

import os
# データのあるディレクトリに移動(私の環境での下準備です)
```python
os.chdir("..")
os.chdir('titanic_data')

# データの読み込み
```python
train_df_raw = pd.read_csv("train.csv")
test_df_raw = pd.read_csv("test.csv")
sample_submission = pd.read_csv("gender_submission.csv")

train_df = train_df_raw.copy()
test_df = test_df_raw.copy()

# データの外観を確認
```python
print(f"訓練データの行列数 : {train_df.shape}")
display(train_df.head())
print(f"\nテストデータの行列数 : {test_df.shape}")
display(test_df.head())
print(f"\nサンプル提出データの行列数 : {sample_submission.shape}")
display(sample_submission.head())
訓練データの行列数 : (891, 12)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th...) female 38.0 1 0 PC 17599 71.2833 C85 C
3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
テストデータの行列数 : (418, 11)
PassengerId Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
892 3 Kelly, Mr. James male 34.5 0 0 330911 7.8292 NaN Q
893 3 Wilkes, Mrs. James (Ellen Needs) female 47.0 1 0 363272 7.0000 NaN S
894 2 Myles, Mr. Thomas Francis male 62.0 0 0 240276 9.6875 NaN Q
895 3 Wirz, Mr. Albert male 27.0 0 0 315154 8.6625 NaN S
896 3 Hirvonen, Mrs. Alexander (Helga E Lindqvist) female 22.0 1 1 3101298 12.2875 NaN S
サンプル提出データの行列数 : (418, 2)
PassengerId Survived
892 0
893 1
894 0
895 0
896 1

各カラムの意味は以下の通りです:

  • PassengerId:乗客のID番号。
  • Survived:生存状況(0 = 死亡、1 = 生存)。これは目的変数です。
  • Pclass:チケットのクラス(1 = 1等、2 = 2等、3 = 3等)。社会経済的地位の指標と見なされます。
  • Name:乗客の名前。
  • Sex:性別(male = 男性、female = 女性)。
  • Age:年齢。一部の乗客の年齢は推測されています。
  • SibSp:タイタニック号に乗船している兄弟姉妹や配偶者の数。
  • Parch:タイタニック号に乗船している親や子供の数。
  • Ticket:チケット番号。
  • Fare:乗船料金。
  • Cabin:客室番号。一部の乗客にしか記載されていません。
  • Embarked:乗船した港(C = シェルブール、Q = クイーンズタウン、S = サウサンプトン)。

この中で、Survivedが生存状況を表す列になるので、目的変数(予測するべき変数にあたります)
なので、テストデータにはSurvived列は含まれていませんね。

sample_submissionデータにはルール通りPassengerIDSurvived列しか含まれていませんね。
これから作成する提出データもこの形になっているかきちんと確認してからやっていこうと思います。

分析方針①

簡単にカラムを確認したので、最初の分析方針を考えていきたいと思います。

EDA

まず第一ステップとして基本統計量、欠損値、train/test間での重複、表記の揺れなどを確認していきたいと思います。(修正は前処理ステップで行います)
その次に変数間の関係を確認していきます。
今回のカラム数であれば最初からすべて詳細に確認していってもいいですが、まず最初は事前にタイタニック沈没事故の概要を確認した時点で気になっていた、性別年齢客室の等級を中心に確認していこうと思います。

前処理

前処理は以下の順番で処理を行っていきます。

  1. 欠損値の置換
  2. 特徴量ごとの作業
  3. 数値データのカテゴリカル変数化
  4. カテゴリカル変数のOne-Hot Encoding
  5. 検証データの作成

学習・評価

モデルを学習させたうえで検証データでの正解率を評価します。
初回はざっくり使えるLightGBMを使おうと思いまうs。

提出

学習・評価ステップで作成したモデルを使ってテストデータで予測を行います。
最後に結果を提出ファイルにまとめて提出します。

いったんこのような分析方針で予測をおこなっていこうと思いますが、臨機応変に対応していきたいと思います。

EDAステップ

統計量の確認

最初に各種統計量を確認して欠損値やデータのばらつきなどを確認したいと思います。

# 統計量の確認
display(train_df.describe())
display(test_df.describe())
PassengerId Survived Pclass Age SibSp Parch Fare
count 891.000000 891.000000 891.000000 714.000000 891.000000 891.000000
mean 446.000000 0.383838 2.308642 29.699118 0.523008 0.381594
std 257.353842 0.486592 0.836071 14.526497 1.102743 0.806057
min 1.000000 0.000000 1.000000 0.420000 0.000000 0.000000
25% 223.500000 0.000000 2.000000 20.125000 0.000000 0.000000
50% 446.000000 0.000000 3.000000 28.000000 0.000000 0.000000
75% 668.500000 1.000000 3.000000 38.000000 1.000000 0.000000
max 891.000000 1.000000 3.000000 80.000000 8.000000 6.000000
PassengerId Pclass Age SibSp Parch Fare
count 418.000000 418.000000 332.000000 418.000000 418.000000
mean 1100.500000 2.265550 30.272590 0.447368 0.392344
std 120.810458 0.841838 14.181209 0.896760 0.981429
min 892.000000 1.000000 0.170000 0.000000 0.000000
25% 996.250000 1.000000 21.000000 0.000000 0.000000
50% 1100.500000 3.000000 27.000000 0.000000 0.000000
75% 1204.750000 3.000000 39.000000 1.000000 0.000000
max 1309.000000 3.000000 76.000000 8.000000 9.000000

PassengerIdPclassは文字列として扱った方が良さそうなので、変換して再度出力します。

# カテゴリカル変数を数値から文字列へ変換
train_df = train_df.astype({'PassengerId': str, "Pclass":str})
test_df = test_df.astype({'PassengerId':str, "Pclass":str})

print('訓練データの統計量')
display(train_df.describe())

print("\nテストデータの統計量")
display(test_df.describe())

訓練データの統計量

Survived Age SibSp Parch Fare
count 891.000000 714.000000 891.000000 891.000000 891.000000
mean 0.383838 29.699118 0.523008 0.381594 32.204208
std 0.486592 14.526497 1.102743 0.806057 49.693429
min 0.000000 0.420000 0.000000 0.000000 0.000000
25% 0.000000 20.125000 0.000000 0.000000 7.910400
50% 0.000000 28.000000 0.000000 0.000000 14.454200
75% 1.000000 38.000000 1.000000 0.000000 31.000000
max 1.000000 80.000000 8.000000 6.000000 512.329200

テストデータの統計量

Age SibSp Parch Fare
count 332.000000 418.000000 418.000000 417.000000
mean 30.272590 0.447368 0.392344 35.627188
std 14.181209 0.896760 0.981429 55.907576
min 0.170000 0.000000 0.000000 0.000000
25% 21.000000 0.000000 0.000000 7.895800
50% 27.000000 0.000000 0.000000 14.454200
75% 39.000000 1.000000 0.000000 31.500000
max 76.000000 8.000000 9.000000 512.329200

この統計量から以下のような内容を読み取りました。

  • Ageのカウントが足りないので欠損値がある
  • Ageは平均が30歳前後で最高齢が80歳なので、乗船していた人の年齢にはばらつきがありそう(最低が0より小さいので生後1年未満の子供も乗船していた)
  • SibspParchは中央値が0なので半分以上は一人で乗船していたのだろう
  • SibspParchは平均、中央値の割に最大値が大きいので、乗船していた家族の割合にはばらつきがありそう
  • Fareは最大値が中央値の30倍以上あるので外れ値が存在していそう
  • Fareにも一つだけ欠損値がある。

次にカテゴリカル変数についても統計量を確認したいと思います。

# カテゴリカルデータの統計量を表示

print('訓練データの統計量(カテゴリカル変数)')
display(train_df.describe(exclude='number'))

print("\nテストデータの統計量(カテゴリカル変数)")
display(test_df.describe(exclude='number'))

訓練データの統計量(カテゴリカル変数)

PassengerId Pclass Name Sex Ticket Cabin Embarked
count 891 891 891 891 891 204 889
unique 891 3 891 2 681 147 3
top 1 3 Braund, Mr. Owen Harris male 347082 B96 B98 S
freq 1 491 1 577 7 4 644

テストデータの統計量(カテゴリカル変数)

PassengerId Pclass Name Sex Ticket Cabin Embarked
count 418 418 418 418 418 91 418
unique 418 3 418 2 363 76 3
top 892 3 Kelly, Mr. James male PC 17608 B57 B59 B63 B66 S
freq 1 218 1 266 5 3 270

カテゴリカル変数の統計量からは以下のような内容を読み取りました。

  • CabinEmbarkedには欠損値がある。
  • Ticketのuniqueがcountと同じではないので、同じチケット番号で乗船している人がいる
  • PassengerIdNameはすべてユニークな値になっている

次に欠損値を確認していきたいと思います。

欠損値の確認

trainデータとtestデータそれぞれでどれくらい欠損値があるか確認します。

# 欠損値の確認
train_missing_value = train_df.isnull().sum()
test_missing_value = test_df.isnull().sum()

# 欠損値の出力
print(f"訓練データの欠損値 \n{train_missing_value}")
print(f"\nテストデータの欠損値\n{test_missing_value}")
訓練データの欠損値 
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64

テストデータの欠損値
PassengerId      0
Pclass           0
Name             0
Sex              0
Age             86
SibSp            0
Parch            0
Ticket           0
Fare             1
Cabin          327
Embarked         0
dtype: int64

訓練データでは、AgeCabinEmbarkedに欠損値があり、
テストデータではAgeFareCabinに欠損値があることがわかります。

次に、欠損値がどれくらいの割合で含まれているのかを確認します。
欠損値の割合が小さい場合は推測して他の値を代入すればいいですが、あまりに欠損値が多い場合にはその列を予測に使用しないことも視野に入れるべきかと思います。

# 欠損値の割合を確認
train_missing_percentage = (train_df[['Age', 'Cabin', 'Embarked']].isnull().sum() / len(train_df)) * 100
test_missing_percentage = (test_df[['Age', "Fare","Cabin"]].isnull().sum() / len(test_df)) * 100

# 出力
print(f"訓練データの欠損値割合\n{train_missing_percentage}")
print(f"\nテストデータの欠損値割合\n{test_missing_percentage}")
訓練データの欠損値割合
Age         19.865320
Cabin       77.104377
Embarked     0.224467
dtype: float64

テストデータの欠損値割合
Age      20.574163
Fare      0.239234
Cabin    78.229665
dtype: float64

この結果から以下の方針で対応しようと思います。

  • Age:それなりに欠損は多いもののカバーできる範囲内と判断し中央値で置換する。
  • Cabin:欠損値が半分以上なので特徴量として使用しない。
  • Fare:欠損値が1レコードのみなので最頻値で置換する。
  • Embarked:欠損値が2レコードのみなので最頻値で置換する。

Train/Test間での重複の確認

次に、カテゴリカル変数の訓練データとテストデータの重複を確認します。
重複があまりにも少ないと、予測モデルは学習していない状況の予測を行うことになるので、訓練データでの予測精度とテストデータでの予測精度に乖離が生じる原因となります。
このようなカラムは、訓練に用いない、もしくは特徴量エンジニアリングを工夫するなどして対処する必要があります。

#!pip install matplotlib_venn
import matplotlib.pyplot as plt
from matplotlib_venn import venn2

fig ,axes = plt.subplots(figsize=(8,8),ncols=4,nrows=1)

for col_name,ax in zip(
    ['Name','Pclass','Cabin', 'Ticket']
    ,axes.ravel()
    ):
    venn2(
        # train_dfとtest_dfのユニークな要素を抽出し、セットにする
        subsets=(set(train_df[col_name].unique()), set(test_df[col_name].unique())),
        set_labels=('Train', 'Test'),
        ax=ax
    )
    ax.set_title(col_name)
    

この結果から以下のようなことを考えました。

  • Nameはtrainとtestで二人しか重複していないので、そのまま訓練に使うと精度が悪くなりそう。
  • Pclassはどちらかにしかないデータがないのでそのまま使って問題なさそう。
  • Cabinも重複しているのでおそらく家族で乗船している人がtrainとtestで分かれている。これも重複が少ないのでそのまま使うのは避けた方がよさそう。
  • Ticketの重複も家族での乗船を表していると考えられる。

表記ゆれの確認

次に表記ゆれについて確認してみます。
表記ゆれの修正をやりだすときりがないので、いったんここでは明らかな間違いがないかのみ確認したいと思います。
各カラムでユニークなデータの数が決まっているのは以下になるかと思います。

  • Survived:0 = 死亡、1 = 生存の二つの値しか出現しないはず
  • Pclass:1 = 1等、2 = 2等、3 = 3等の三つの値しか出現しないはず
  • Sex:male = 男性、female = 女性の二つの値しか出現しないはず
  • Embarked:C = シェルブール、Q = クイーンズタウン、S = サウサンプトンの三つの値しか出現しないはず

これらの列についてユニークな値と出現回数について調べてみます。

# 各列のユニークな値とその出現回数を表示
# 訓練データの確認
print("訓練データの表記ゆれ確認")
for column in ['Survived', 'Pclass', 'Sex', 'Embarked']:
    print(f"{column}のユニークな値とその出現回数")
    print(train_df[column].value_counts())
    print("\n")

print('テストデータの表記ゆれ確認')
#テストデータの確認(テストデータにはSurvived列がない)
for column in ['Pclass', 'Sex', 'Embarked']:
    print(f"{column}のユニークな値とその出現回数")
    print(test_df[column].value_counts())
    print("\n")
訓練データの表記ゆれ確認
Survivedのユニークな値とその出現回数
Survived
0    549
1    342
Name: count, dtype: int64


Pclassのユニークな値とその出現回数
Pclass
3    491
1    216
2    184
Name: count, dtype: int64


Sexのユニークな値とその出現回数
Sex
male      577
female    314
Name: count, dtype: int64


Embarkedのユニークな値とその出現回数
Embarked
S    644
C    168
Q     77
Name: count, dtype: int64


テストデータの表記ゆれ確認
Pclassのユニークな値とその出現回数
Pclass
3    218
1    107
2     93
Name: count, dtype: int64


Sexのユニークな値とその出現回数
Sex
male      266
female    152
Name: count, dtype: int64


Embarkedのユニークな値とその出現回数
Embarked
S    270
C    102
Q     46
Name: count, dtype: int64

出力結果を確認すると、予想通りのユニークな値しか出現していないので、表記ゆれについて問題ないことがわかりました。

ここまで分かったこと・気になったことをまとめると以下のようになります。

  • わかったこと・確かめること

    • Ageのカウントが足りないので欠損値がある
    • Ageは平均が30歳前後で最高齢が80歳なので、乗船していた人の年齢にはばらつきがありそう(最低が0より小さいので生後1年未満の子供も乗船していた)
    • SibspParchは中央値が0なので半分以上は一人で乗船していたのだろう
    • SibspParchは平均、中央値の割に最大値が大きいので、乗船していた家族の割合にはばらつきがありそう
    • Fareは最大値が中央値の30倍以上あるので外れ値が存在していそう
    • Fareにも一つだけ欠損値がある。
    • Cabinには欠損値がある。
    • Ticketのuniqueがcountと同じではないので、同じチケット番号で乗船している人がいる
    • PassengerIdNameはすべてユニークな値になっている
    • 表記ゆれは問題なさそう
  • 前処理で行うこと

    • Age:それなりに欠損は多いもののカバーできる範囲内と判断し中央値で置換する。
    • Cabin:欠損値が半分以上なので特徴量として使用しない。
    • Fare:欠損値が1レコードのみなので最頻値で置換する。
    • Embarked:欠損値が2レコードのみなので最頻値で置換する。

データの可視化

ここからデータを可視化しながら変数間の関係などを見ていきたいと思います。
まず、これまで発見した分析ポイントを確認してみます。

タイタニック号沈没事故の概要を調べたときに考えたのは以下の予想でした。

  • 穴の開いた区画から遠い区画に滞在していた場合は生存率が高いのではないか
  • 女性、幼い子供が男性と比べて生存率が高いのではないか(独身の子供がいない男性の生存率が最も低いのではないか)
  • 旅客の等級が高いほうが生存率が高いのではないか

穴の開いた区画というのは今回のデータでは分析が難しそうですが、その他の二つは分析して確認できそうですね。
それから、実際にデータの中身を確認しながら立てた予想は以下の通りでした。

  • Ageは平均が30歳前後で最高齢が80歳なので、乗船していた人の年齢にはばらつきがありそう(最低が0より小さいので生後1年未満の子供も乗船していた)
  • SibspParchは中央値が0なので半分以上は一人で乗船していたのだろう
  • SibspParchは平均、中央値の割に最大値が大きいので、乗船していた家族の割合にはばらつきがありそう
  • Fareは最大値が中央値の30倍以上あるので外れ値が存在していそう

訓練データとテストデータの結合

まず最初に訓練データとテストデータを、後から区別できるようにしたうえで結合します。
これから可視化するうえで毎回訓練データとテストデータを分けて出力するのも大変ですし、データの加工を行う際にも一度に加工を行った方が簡単です。

# テストデータにフラグを立てる
test_df['test_flag'] = 1

# 訓練データとテストデータの結合
all_df = pd.concat([train_df, test_df], axis=0).reset_index(drop=True)
print(f'訓練データの行列数:{train_df.shape}')
print(f'テストデータの行列数:{test_df.shape}')
print(f'結合データの行列数:{all_df.shape}')

# test_flagの欠損値を0で置き換え
all_df['test_flag'] =  all_df['test_flag'].fillna(0)
print('\ntest_flagの確認')
print(all_df['test_flag'].value_counts())
訓練データの行列数:(891, 12)
テストデータの行列数:(418, 12)
結合データの行列数:(1309, 13)

test_flagの確認
test_flag
0.0    891
1.0    418
Name: count, dtype: int64

テストデータをもともとSurvivedが無かったので11列でしたが、test_flagを追加したので12列になっています。
二つのデータを結合した結果、行数は単純に合計値になり、列数はtest_flagが追加されたので13列になっています。

また、test_flagも正しく設定できているので、test_flagが0か1かで訓練データとテストデータを再分割できるようになっています。

それでは可視化を進めていきましょう。
まずはライブラリのインポートです。

# 可視化用のライブラリをインポート
import matplotlib.pyplot as plt
import seaborn as sns

Age

# Ageについて可視化
fig = sns.FacetGrid(all_df, col='test_flag', hue='test_flag', height=4)
fig.map(sns.histplot, 'Age', bins=30, kde=False)

20歳から30際にかけて山があり、それ以降右肩下がりになる傾向が見て取れますね。
また、最高齢は80歳ですが、外れ値と呼ぶほどのばらつきではなさそうですね。
次に年齢と生存率の関係を見てみましょう。

# 年齢と生存率の関係を可視化
fig = sns.FacetGrid(all_df, col='Survived', hue='Survived', height=4)
fig.map(sns.histplot, 'Age', bins=40, kde=False)
plt.show()

左右の棒グラフの高さを比較してみます。
0~10歳程度の子供の生存率が高く、20~30歳の生存率が低いことがわかります。
年齢帯によって生存率が変化しているので、特徴量として、10歳程度の区分に分けることが予測精度を上げることにつながるかもしれません。

Sex

まずは訓練データとテストデータの分布をみてみます。

# Sexについて可視化
sns.countplot(x='Sex', hue='test_flag', data=all_df) 
plt.show()

どちらも男性の方が多く女性の方が少ないですね。どれくらいの割合なのか見てみましょう。

# 男女比の割合を計算
sex_percentage = all_df['Sex'].value_counts(normalize=True).reset_index()
display(sex_percentage)
Sex proportion
0 male 0.644003
1 female 0.355997

男性が64%、女性が36%だということがわかりました。
それでは次に、性別と生存率の関係を見てみます。
Ageは数値データだったのでヒストグラムで確認しましたが、Sexはカテゴリカルデータなのでcountplotで確認します。

# 性別と生存率の関係を可視化
sns.countplot(x='Sex', hue='Survived', data=train_df) 
plt.show()

男性と女性でかなり生存率に差があることがわかります。
どれくらいの差があるのか、割合を確認してみましょう。

# sexとsurvivedのクロス集計表を作成
cross_tab = pd.crosstab(all_df['Sex'], all_df['Survived'], margins=True)
# 合計で割ってパーセンテージを計算
percentage = cross_tab.div(cross_tab['All'], axis=0) * 100

# All列を削除
percentage = percentage.drop(columns="All")
display(percentage)
Survived 0.0 1.0
Sex
female 25.796178 74.203822
male 81.109185 18.890815
All 61.616162 38.383838

このことから以下のことがわかります。

  • 全体でみると生存率は38.4%、死亡率は61.6%
  • 女性で見ると、生存率は74.2%、死亡率は25.8%
  • 男性で見ると、生存率は18.9%、死亡率は81.1%

性別だけでここまで生存率に差があることが分かったのは大きな発見です。
他に何も情報が無くても、女性なら生存、男性なら死亡とあてずっぽうで予測しても、それなりに正解できることがわかります。

Pclass

まずは訓練データとテストデータを比較してみます

# Pclassをtest_flagで色分け
sns.countplot(x='Pclass', hue='test_flag', data=all_df) 
plt.show()

どのチケットクラスも訓練データとテストデータの割合は同程度のようですね。
一応比率も見てみましょう。

# Pclassとtest_flagのクロス集計表を作成
cross_tab = pd.crosstab(all_df['Pclass'], all_df['test_flag'], margins=True)
# 合計で割ってパーセンテージを計算
percentage = cross_tab.div(cross_tab['All'], axis=0) * 100

# All列を削除
percentage = percentage.drop(columns="All")
display(percentage)
test_flag 0.0 1.0
Pclass
1 66.873065 33.126935
2 66.425993 33.574007
3 69.252468 30.747532
All 68.067227 31.932773

やはりどのチケットクラスでも訓練データとテストデータでの比率はあまり変わりませんね。

次に、チケットクラスと生存率の関係を見てみます。

# Pclassについて可視化
sns.countplot(x='Pclass', hue='Survived', data=train_df) 
plt.show()

パッと見て、クラス3の生存率が低く、1の生存率が高いことがわかります。
2はその中間くらいなので、チケットクラスが上がるにつれて生存率が上がっていることがわかりますね。

# Pclassとsurvivedのクロス集計表を作成
cross_tab = pd.crosstab(all_df['Pclass'], all_df['Survived'], margins=True)
# 合計で割ってパーセンテージを計算
percentage = cross_tab.div(cross_tab['All'], axis=0) * 100

# All列を削除
percentage = percentage.drop(columns="All")
display(percentage)
Survived 0.0 1.0
Pclass
1 37.037037 62.962963
2 52.717391 47.282609
3 75.763747 24.236253
All 61.616162 38.383838

比率も見てみるとよりはっきりしますね。

  • 第1クラスの乗客の生存率が最も高い

    • 約63%の第1クラスの乗客が生存。
    • 第1クラスの乗客は、より良い設備と救命設備へのアクセスがあった可能性が高い。
  • 第3クラスの乗客の生存率が最も低い

    • 約24%の第3クラスの乗客のみが生存。
    • 船内での位置や救命ボートへのアクセスの制限が生存率に大きく影響した可能性がある。
  • 第2クラスの乗客の生存率はほぼ半分

    • 約47%の第2クラスの乗客が生存。
    • 第1クラスほどの優先度はなかったが、第3クラスよりは救助の機会が多少良かった可能性がある。

チケットクラスも生存率を予想するうえでは重要な要素になりそうです。

Embarked

まずは訓練データとテストデータを比較します。
比率も併せてみます。

# Embarkedをtest_flagで色分け
sns.countplot(x='Embarked', hue='test_flag', data=all_df) 
plt.show()

# Embarkedとtest_flagのクロス集計表を作成
cross_tab = pd.crosstab(all_df['Embarked'], all_df['test_flag'], margins=True)
# 合計で割ってパーセンテージを計算
percentage = cross_tab.div(cross_tab['All'], axis=0) * 100

# All列を削除
percentage = percentage.drop(columns="All")
display(percentage)
test_flag 0.0 1.0
Embarked
C 62.222222 37.777778
Q 62.601626 37.398374
S 70.459519 29.540481
All 68.018363 31.981637

サウサンプトンからの乗船者だけ少し差があるでしょうか。
次に生存率との関係を見てみます。

# Embarkedをtest_flagで色分け
sns.countplot(x='Embarked', hue='Survived', data=all_df) 
plt.show()

# Embarkedとtest_flagのクロス集計表を作成
cross_tab = pd.crosstab(all_df['Embarked'], all_df['Survived'], margins=True)
# 合計で割ってパーセンテージを計算
percentage = cross_tab.div(cross_tab['All'], axis=0) * 100

# All列を削除
percentage = percentage.drop(columns="All")
display(percentage)
Survived 0.0 1.0
Embarked
C 44.642857 55.357143
Q 61.038961 38.961039
S 66.304348 33.695652
All 61.754781 38.245219

乗船した港によって生存率に差があることがわかります。
クイーンズランドから乗船すると生存率が高く、サウサンプトンから乗船すると生存率が低くなることがわかります。
これも生存率の予測に影響を与える要素になりそうです。

Fare

次はFareです。

# Fareについて可視化
fig = sns.FacetGrid(all_df, col='test_flag', hue='test_flag', height=4)
fig.map(sns.histplot, 'Fare', bins=30, kde=False)
plt.show()

これまでのステップで見てきたように、テストデータ、訓練データともに500という外れ値が存在していますね。
このまま予測に使用すると精度を下げる原因になるので、前処理ステップで処理したいと思います。

# 運賃と生存率の関係を可視化
fig = sns.FacetGrid(all_df, col='Survived', hue='Survived', height=4)
fig.map(sns.histplot, 'Fare', bins=20, kde=False)
plt.show()

生存率との関係を見てみると、乗船運賃が低い(特に0~100前後)ほど生存率が低い傾向が見て取れます。
外れ値をうまく処理して予測に加えたいと思います。

Sibsp

次はSibSp、つまり兄弟姉妹や配偶者の数についてみていきます。

# Sibspについて可視化
sns.countplot(x='SibSp', hue='test_flag', data=all_df) 
plt.show()

8人というのかなりの大所帯なので外れ値といえるかもしれません。

# SibSPと生存率の関係
sns.countplot(x='SibSp', hue='Survived', data=all_df) 
plt.show()

SibSpが0、つまり一人で乗船している人の生存率がとても低いですね。
また、一人、二人の場合は生存率が高くなりますが、逆に3人を超えるとまた生存率が低くなっています。

Parch

次はParch、両親・子供の人数です。
これはSibSPと相関がありそうですね。

# 変数間の相関をヒートマップで表示
sns.heatmap(
    train_df[['Survived','Age','SibSp','Parch','Fare']].corr(),
    vmax=1,vmin=-1,annot=True
    )
plt.show()

相関をヒートマップで表現してみると、ParchSibSpの相関係数は0.41となり、正の弱い相関があることがわかります。
逆にSibSpAgeには負の弱い相関があることもわかります。

# Parchと生存率の関係
sns.countplot(x='Parch', hue='Survived', data=all_df) 
plt.show()

Parchでもやはり一人で乗船している人の生存率は低くなっていますね。
また、一人、二人、三人までは生存率が高くなり、それより多くなると生存率が低くなっています。

ParchSibSpどちらからも、一緒に同乗している人数が生存率に影響を与えていることがわかりました。
どうやら、「同乗者0人」「同乗者1~3人」「同乗者3人以上」というくくりで生存率が変わりそうな気がします。

PassengerId

PassengerIdですが、連番ですべての乗客に一意の値が付与されているので、予測には使えなさそうです。
特徴量から削除してしまおうと思います。

# PassengerIdのユニークな値を表示
all_df['PassengerId'].head(20)
0      1
1      2
2      3
3      4
4      5
5      6
6      7
7      8
8      9
9     10
10    11
11    12
12    13
13    14
14    15
15    16
16    17
17    18
18    19
19    20
Name: PassengerId, dtype: object

Ticket

次にTicketです。
どのような値になっているのか確認してみます。
EDAステップで確認した通り、訓練データとテストデータでTicketには重複がありましが、これは家族であることなどが原因と考えられます。
一緒に乗船した人数が生存率に影響があることはわかっていますので、もしかするとTicketも生存率に影響を与えるかもしれません。
ここでは、Ticketの出現回数と生存率の関係を見てみます。

# チケットの出現回数を計算
ticket_counts = all_df['Ticket'].value_counts()

# 各チケットにおける生存者数を集計
survived_counts = all_df.groupby('Ticket')['Survived'].sum()

# 両方のシリーズをデータフレームに結合
combined_df = pd.concat([ticket_counts, survived_counts], axis=1)

# 列名を指定
combined_df.columns = ['Ticket Count', 'Survived Count']

# 生存率を計算して新しい列に追加
combined_df['Survival Rate'] = combined_df['Survived Count'] / combined_df['Ticket Count']
display(combined_df.sort_values(by='Ticket Count', ascending=False))

Ticket Ticket Count Survived Count Survival Rate
CA. 2343 11 0.0 0.000000
1601 8 5.0 0.625000
CA 2144 8 0.0 0.000000
PC 17608 7 2.0 0.285714
S.O.C. 14879 7 0.0 0.000000
... ... ... ...
PC 17531 1 0.0 0.000000
347471 1 0.0 0.000000
A./5. 3338 1 0.0 0.000000
365235 1 0.0 0.000000
359309 1 0.0 0.000000

929 rows × 3 columns

# 散布図を描画
plt.figure(figsize=(10, 6))  # グラフのサイズ設定
sns.scatterplot(x='Ticket Count', y='Survival Rate', data=combined_df, alpha=0.6)

plt.title('Survival Rate vs Ticket Count')  # グラフのタイトル
plt.xlabel('Ticket Count')  # x軸のラベル
plt.ylabel('Survived Count')  # y軸のラベル
plt.grid(True)  # グリッドの表示
plt.show()  # グラフの表示

# 'Survival Rate' と 'Ticket Count' の相関係数を計算
correlation = combined_df[['Survival Rate', 'Ticket Count']].corr()
display(correlation)

Survival Rate Ticket Count
Survival Rate 1.000000 0.130279
Ticket Count 0.130279 1.000000

Ticketの出現回数と生存率の関係を見てみましたが、相関係数は0.13なのでほとんど関係はなさそうですね。
今回は特徴量から削除しようと思います。

Name

最後にNameです。
この変数も基本的に乗客毎に一意になる値ですが、まずは中身を見てみます。

# Name列のユニークな値を取得
unique_names = all_df['Name'].unique()

# ユニークな値を持つDataFrameを作成
unique_names_series = pd.Series(unique_names)

# 結果を表示
print(unique_names_series[:20])

0                               Braund, Mr. Owen Harris
1     Cumings, Mrs. John Bradley (Florence Briggs Th...
2                                Heikkinen, Miss. Laina
3          Futrelle, Mrs. Jacques Heath (Lily May Peel)
4                              Allen, Mr. William Henry
5                                      Moran, Mr. James
6                               McCarthy, Mr. Timothy J
7                        Palsson, Master. Gosta Leonard
8     Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)
9                   Nasser, Mrs. Nicholas (Adele Achem)
10                      Sandstrom, Miss. Marguerite Rut
11                             Bonnell, Miss. Elizabeth
12                       Saundercock, Mr. William Henry
13                          Andersson, Mr. Anders Johan
14                 Vestrom, Miss. Hulda Amanda Adolfina
15                     Hewlett, Mrs. (Mary D Kingcome) 
16                                 Rice, Master. Eugene
17                         Williams, Mr. Charles Eugene
18    Vander Planke, Mrs. Julius (Emelia Maria Vande...
19                              Masselmani, Mrs. Fatima
dtype: object

ざっと眺めてみると、MrMrsなどの敬称が複数回出現していることがわかります。
MrやMrsは性別や既婚・未婚などを表しているので、もしかすると生存率に影響を与えるかもしれません。
敬称をTitleとして取り出して、出現回数を確認してみます。

# 敬称を抽出したカラム「Title」を追加
all_df['Title'] = all_df.Name.str.extract(' ([A-Za-z]+)\.', expand=False)

# 「Title」の欠損数を表示
print('--- 欠損 ----')
print(all_df['Title'].isna().sum())

# 訓練データとテストデータ別に、各敬称の登場回数を集計
# Test_Flag=0 が訓練データ
# Test_Flag=1 がテストデータ
display(pd.crosstab(all_df['test_flag'],all_df['Title']))

--- 欠損 ----
0

Title Capt Col Countess Don Dona Dr Jonkheer Lady Major Master Miss Mlle Mme Mr Mrs Ms Rev Sir
test_flag
0.0 1 2 1 1 0 7 1 1 2 40 182 2 1 517 125 1 6 1
1.0 0 2 0 0 1 1 0 0 0 21 78 0 0 240 72 1 2 0

この結果から以下のことがわかります。

  • テストデータにしか登場しない敬称がある。
  • 各敬称の登場回数の集計結果から、「Master」、「Miss」、「Mr」、「Mrs」以外は登場頻度が非常に少ない
# TitleとAgeの関係
all_df.groupby('Title')['Age'].mean()
Title
Capt        70.000000
Col         54.000000
Countess    33.000000
Don         40.000000
Dona        39.000000
Dr          43.571429
Jonkheer    38.000000
Lady        48.000000
Major       48.500000
Master       5.482642
Miss        21.774238
Mlle        24.000000
Mme         24.000000
Mr          32.252151
Mrs         36.994118
Ms          28.000000
Rev         41.250000
Sir         49.000000
Name: Age, dtype: float64

Masterの平均年齢が5.5歳であることから、子供を表す敬称であることがわかります。
幼い子供生存率が高いことはわかっているので、敬称がMasterであるという特徴量を追加することも予測に影響を与えるかもしれません。

前処理ステップ

このステップでは欠損値の処理や特徴量エンジニアリングなどの前処理を加えていきたいと思います。

欠損値の処理

EDAステップで発見した欠損値を処理していきたいと思います。

  • Age:それなりに欠損は多いもののカバーできる範囲内と判断し中央値で置換する。
  • Cabin:欠損値が半分以上なので特徴量として使用しない。
  • Fare:欠損値が1レコードのみなので最頻値で置換する。
  • Embarked:欠損値が2レコードのみなので最頻値で置換する。
# Ageの欠損を中央値で置換
all_df['Age'] = all_df['Age'].fillna(all_df['Age'].median())

# Cabinをデータから削除
all_df = all_df.drop('Cabin', axis=1)

# Fareの欠損を最頻値で置換
all_df['Fare'] = all_df['Fare'].fillna(all_df['Fare'].mode()[0])

# Embarkedの欠損を最頻値で置換
all_df['Embarked'] = all_df['Embarked'].fillna(all_df['Embarked'].mode()[0])
# 欠損値の確認
all_missing_value = all_df.isnull().sum()

# 欠損値の出力
print(f"合算データの欠損値 \n{all_missing_value}")

合算データの欠損値 
PassengerId      0
Survived       418
Pclass           0
Name             0
Sex              0
Age              0
SibSp            0
Parch            0
Ticket           0
Fare             0
Embarked         0
test_flag        0
Title            0
dtype: int64

これで欠損値を処理することができました。

同乗者の人数を特徴量にする

SibSpParchの分析からわかったように、同乗人数は生存率に影響を与えています。
まずはSibSpParchをもとにFamily_sizeという特徴量を作ります。

# 同乗した家族の人数 = 兄弟・配偶者の人数 + 両親・子供の人数 + 本人
all_df['Family_size'] = all_df['SibSp'] + all_df['Parch'] + 1
# FamilySizeと生存率の関係を可視化
sns.countplot(
    x='Family_size',
    hue='Survived'
    , data=all_df
    )
plt.legend(title='Survived' ,loc='upper right')
plt.show()

思った通り、Family_sizeと生存率には関係がありますね。
Family_sizeが2~4までは生存率が高くなりますが、それ以外は生存率が低くなりますね。
もう少し詳しく見てみます。

# 家族人数毎のデータに含まれる割合
display(all_df['Family_size'].value_counts(ascending=False,normalize=True))
Family_size
1     0.603514
2     0.179526
3     0.121467
4     0.032850
6     0.019099
5     0.016807
7     0.012223
11    0.008403
8     0.006112
Name: proportion, dtype: float64

一人で乗船した人が全体の6割、2~4人で乗船した人が全体の3割ほどいることがわかります。

# 家族人数毎の生存率
display(pd.crosstab(all_df['Family_size'], all_df['Survived'], normalize='index'))
Survived 0.0 1.0
Family_size
1 0.696462 0.303538
2 0.447205 0.552795
3 0.421569 0.578431
4 0.275862 0.724138
5 0.800000 0.200000
6 0.863636 0.136364
7 0.666667 0.333333
8 1.000000 0.000000
11 1.000000 0.000000

2~4人で乗船している人の生存率が50%を超えている一方で、それ以外は30%を下回っています。

この結果を踏まえて、「同乗者0人」「同乗者1~3人」「同乗者3人以上」という特徴量を作ります。

# 同乗した家族の人数をもとに分割
all_df['alone'] = all_df['Family_size'].map(lambda s: 1 if s <= 1  else 0)
all_df['MedF']   = all_df['Family_size'].map(lambda s: 1 if 2 <= s <= 4 else 0)
all_df['LargeF'] = all_df['Family_size'].map(lambda s: 1 if s >= 5 else 0)
# 結果を確認
display(pd.crosstab(all_df['alone'], all_df['Survived'], normalize='index'))
display(pd.crosstab(all_df['MedF'], all_df['Survived'], normalize='index'))
display(pd.crosstab(all_df['LargeF'], all_df['Survived'], normalize='index'))

Alone:

Survived 0.0 1.0
alone
0 0.494350 0.505650
1 0.696462 0.303538

MedF:

Survived 0.0 1.0
MedF
0 0.711185 0.288815
1 0.421233 0.578767

LargeF:

Survived 0.0 1.0
LargeF
0 0.599517 0.400483
1 0.838710 0.161290

思った通りの特徴量を作成することができました。
これもモデルに学習させたいと思います。

敬称を特徴量にする

敬称については、各データの敬称を抽出し、「Master」、「Miss」、「Mr」、「Mrs」、「それ以外」の5つのカテゴリーを持つカテゴリカル変数を作成しようと思います。

# 敬称を抽出
all_df['Title'] = all_df.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
# 敬称を該当するカテゴリに変換する関数を定義
def prepro_name_title(Title): 
    if Title == 'Master':
        return 0
    elif Title == 'Miss':
        return 1
    elif Title == 'Mr':
        return 2
    elif Title == 'Mrs':
        return 3 
    else:
        return 4

# Titelをカテゴリカル変数化
all_df['Title_Encode'] = all_df['Title'].map(prepro_name_title)

カテゴリカル変数をOne-Hot Encodingする

# カテゴリカル変数をOne-Hot Encoding
all_df = pd.get_dummies(all_df, columns= ['Sex', 'Pclass','Age','Embarked'], dtype=float)
# PassengerIdをint型にする
all_df['PassengerId'] = all_df['PassengerId'].astype('int')

訓練データ、テストデータの作成

# 訓練データとテストデータに戻す
train = all_df[all_df['test_flag']==0]
test = all_df[all_df['test_flag']==1].reset_index(drop=True)
target = train['Survived']

# 学習に不要な特徴量を削除
drop_col = [
    'Ticket','Title', 'PassengerId',
    'test_flag','Name','Survived'
    ]

train = train.drop(drop_col, axis=1)
test = test.drop(drop_col, axis=1)
print(f"train:{train.shape}")
print(f"test:{test.shape}")
train:(891, 114)
test:(418, 114)

予測してみる

それではLightGBMを実装していきます。
※ハイパーパラメーターはGridSearchを使って見つけたベストパラメータを使用しています。

import pandas as pd
import numpy as np
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split
import lightgbm as lgb
from sklearn.metrics import accuracy_score


# 訓練データをさらに学習セットとテストセットに分割
X_train, X_test, y_train, y_test = train_test_split(train, target, test_size=0.2, random_state=42 )

# LightGBMモデルの設定(ハイパーパラメータを直接指定)
model = lgb.LGBMClassifier(learning_rate=0.1, n_estimators=30, num_leaves=31, verbose=-1)

# モデルの学習
model.fit(X_train, y_train)

# テストデータに対する予測
y_pred = model.predict(X_test)

# 正解率の計算
accuracy = accuracy_score(y_test, y_pred)
print(f"\nテストデータでの正解率: {accuracy}")

テストデータでの正解率: 0.8044692737430168

テストデータでの正解率が0.80ということなのでそこそこ高い数字が出ている気がします。

提出してみる

それでは今回の結果で提出データを作成してSubmittしてみたいと思います。

# テストデータを予測
test_pred = model.predict(test)

# 予測結果をサブミットするファイル形式に変更
sample_submission["Survived"] = np.where(test_pred>=0.5, 1, 0)
display(sample_submission.head(10))

# 提出ファイルを出力
sample_submission.to_csv("submission_1.csv", index=False)

結果は0.77751となりました。
女性=生存とした場合が76.5%なのでそれより少しいいくらいでしょうか。

ひとまず初めてのコンペ参加としてはこれくらいで満足です。
今後はいろいろな人のモデルを参考にして精度を上げていきたいと思います。

Discussion