タイタニック号沈没事故

統計・データ解析のタイタニック号沈没事故でも扱ったデータをPythonでいじってみよう。

ここでは seaborn のデータレポジトリにあるデータを使う:

import seaborn as sns

titanic = sns.load_dataset('titanic')
titanic
     survived  pclass     sex   age  sibsp  ...  adult_male  deck  embark_town alive  alone
0           0       3    male  22.0      1  ...        True   NaN  Southampton    no  False
1           1       1  female  38.0      1  ...       False     C    Cherbourg   yes  False
2           1       3  female  26.0      0  ...       False   NaN  Southampton   yes   True
3           1       1  female  35.0      1  ...       False     C  Southampton   yes  False
4           0       3    male  35.0      0  ...        True   NaN  Southampton    no   True
..        ...     ...     ...   ...    ...  ...         ...   ...          ...   ...    ...
886         0       2    male  27.0      0  ...        True   NaN  Southampton    no   True
887         1       1  female  19.0      0  ...       False     B  Southampton   yes   True
888         0       3  female   NaN      1  ...       False   NaN  Southampton    no  False
889         1       1    male  26.0      0  ...        True     C    Cherbourg   yes   True
890         0       3    male  32.0      0  ...        True   NaN   Queenstown    no   True

[891 rows x 15 columns]

乗客891人しか含んでいない。Rのデータセットは全体2201人,乗客1316人であった。これは Kaggle の titanic データセット の train.csv から生成したデータであるためである(test.csv の418人分のデータは入っていない)。

titanic.columns
Index(['survived', 'pclass', 'sex', 'age', 'sibsp', 'parch', 'fare',
       'embarked', 'class', 'who', 'adult_male', 'deck', 'embark_town',
       'alive', 'alone'],
      dtype='object')

それぞれ,生存(0/1),乗客クラス(1〜3),性別(male/female),年齢,同乗の兄弟姉妹配偶者の数,同乗の親子供の数,料金,乗船港(C = Cherbourg, Q = Queenstown, S = Southampton),乗客クラス(First/Second/Third),男女子供(man/woman/child),成人男性(True/False),デッキ(A〜G),乗船港(Cherbourg/Queenstown/Southampton),生存(yes/no),一人旅(True/False)である。内訳を見るには次のようにする:

titanic['survived'].value_counts()
0    549
1    342
Name: survived, dtype: int64
titanic['deck'].value_counts()
C    59
B    47
D    33
E    32
A    15
F    13
G     4
Name: deck, dtype: int64

足しても891人になりそうにない。欠測値(NA = Not Available)があるのではないか。欠測値を落とさないオプション dropna=False を付けてみよう:

titanic['deck'].value_counts(dropna=False)
NaN    688
C       59
B       47
D       33
E       32
A       15
F       13
G        4
Name: deck, dtype: int64

NaN(Not a Number)は欠測値である。

年齢が 1 以上で xx.5 のような小数になっているものは推定年齢が xx であることを意味する。

いくつかの項目は冗長である。後述のように,alone は sibsp + parch が 0 に等しいことを意味する。child は16歳未満(15歳以下)を意味する。成人男性と who == 'man' は同義である。

Kaggle のデータでは他に Name(氏名),Ticket(チケット番号),Cabin(客室番号)が含まれている。seaborn の deck は Kaggle の Cabin の頭1文字に相当する。例えば2番目の乗客は客室番号 C85 であるのでデッキは C である。

項目ごとの欠測値の数は次の通りである:

titanic.isna().sum()
survived         0
pclass           0
sex              0
age            177
sibsp            0
parch            0
fare             0
embarked         2
class            0
who              0
adult_male       0
deck           688
embark_town      2
alive            0
alone            0
dtype: int64

age には欠測値が多いが who にはない。Kaggle のデータに who に相当する項目がないので最初不思議に思ったが,どうやら単純に age < 16 を child としているだけで,欠測は自動的に大人にされてしまっているようだ:

((titanic['who'] == 'child') == (titanic['age'] < 16)).all()
True

adult_male も機械的に導いているようだ:

((titanic['who'] == 'man') == titanic['adult_male']).all()
True

alive が yes であることと survived が True であることは等価である:

((titanic['alive'] == 'yes') == titanic['survived']).all()
True

sibsp + parch が 0 であることと alone が True であることも等価である:

((titanic['sibsp'] + titanic['parch'] == 0) == titanic['alone']).all()
True

(特にする必要はないが)独立な変数だけに切り詰めるには次のようにすればよい:

titanic = titanic[['survived', 'pclass', 'sex', 'age', 'sibsp',
                   'parch', 'fare', 'embarked', 'deck']]

次のようにすれば数値データの項目について要約統計量が得られる:

titanic.describe()
         survived      pclass         age       sibsp       parch        fare
count  891.000000  891.000000  714.000000  891.000000  891.000000  891.000000
mean     0.383838    2.308642   29.699118    0.523008    0.381594   32.204208
std      0.486592    0.836071   14.526497    1.102743    0.806057   49.693429
min      0.000000    1.000000    0.420000    0.000000    0.000000    0.000000
25%      0.000000    2.000000   20.125000    0.000000    0.000000    7.910400
50%      0.000000    3.000000   28.000000    0.000000    0.000000   14.454200
75%      1.000000    3.000000   38.000000    1.000000    0.000000   31.000000
max      1.000000    3.000000   80.000000    8.000000    6.000000  512.329200

いろいろ図を描いてみよう:

sns.boxplot(x='sex', y='age', data=titanic)
sns.boxplot(x='sex', y='age', hue='survived', data=titanic)

箱ひげ図ではあまりよくわからない。swarmplot を描いてみよう。swarm(スウォーム)は(ミツバチなどの)群れを意味する英語である。

sns.swarmplot(x='sex', y='age', hue='survived', data=titanic)

女性と子供が多く生存していることがわかる。

男性だけについて,乗客クラスと料金と生存の関係を調べよう:

sns.swarmplot(x='pclass', y='fare', hue='survived',
              data=titanic[titanic['sex'] == 'male'])

どちらかといえば first class が多く生存したようだ。

UserWarning: XX.X% of the points cannot be placed; you may want to decrease the size of the markers or use stripplot. のような警告が出るので,デフォルトのマーカーの大きさ size=5 を減らすか,sns.stripplot() を使うか,その両方を行う。

titanic[titanic['sex'] == 'male']titanic.query("sex == 'male'") でもよい(How to select rows from a DataFrame based on column values)。

このあとは,なんちゃって機械学習(本当はちゃんと勉強してからやろう)。

まずは年齢の欠測値を適当に埋める。ここでは中央値とした。

titanic['age'] = titanic['age'].fillna(titanic['age'].median())

女/男は 0/1 でエンコードする:

titanic['sex'] = titanic['sex'].map({'female': 0, 'male': 1})

あとは,以下では使わないが,他の欠測値も例えば次のようにして埋められる:

import pandas as pd

titanic['embarked'] = titanic['embarked'].fillna('N')
titanic['deck'] = pd.Categorical(titanic['deck'],
                                 categories=['A','B','C','D','E','F','G','N']
                                 ).fillna('N')

とりあえず旅客クラス・性・年齢で機械学習してみる:

from sklearn.model_selection import train_test_split

X = titanic[['pclass', 'sex', 'age']].values
y = titanic['survived'].values
X_train, X_test, y_train, y_test = train_test_split(X, y)

それぞれの度数分布は import numpy as np して例えば np.unique(y_test, return_counts=True) のようにすれば求められる。

決定木の場合:

from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier(max_depth=3)

ロジスティック回帰の場合:

from sklearn.linear_model import LogisticRegression

model = LogisticRegression()

ニューラルネットの場合:

from sklearn.neural_network import MLPClassifier

model = MLPClassifier()

フィットして性能を調べる:

model.fit(X_train, y_train)
y_pred = model.predict(X_test)

正解率:

from sklearn.metrics import accuracy_score, confusion_matrix

accuracy_score(y_test, y_pred)

混同行列:

confusion_matrix(y_test, y_pred)

結果は(数値を文字で置き換えて)次のような感じになる:

array([[a, b],
       [c, d]])

y_test が 0(実際に死亡)が a + by_test が 1(実際に生存)が c + d になる。また,y_pred が 0(予測は死亡)が a + cy_pred が 1(予測が生存)が b + d になる。正解率 accuracy score は (a + d) / (a + b + c + d) である。

病気の診断の場合にも混同行列がよく使われる。例えば

陰性(病気なしと判断)陽性(病気ありと判断)
実際に病気なし真陰性偽陽性
実際に病気あり偽陰性真陽性

真陽性 / (偽陰性 + 真陽性) を感度,真陰性 / (真陰性 + 偽陽性) を特異度という。例えば

陰性(病気なしと判断)陽性(病気ありと判断)
実際に病気なし991
実際に病気あり37

の場合に感度・特異度を計算してみよう。


Last modified: