概要
- Kaggleのタイタニックのデータを使用して、統計解析と可視化の演習を行ないます。機械学習による予測分析は行ないません。
- 実行環境は Jupyter(Python 3.6)です。
- あらかじめ、Kaggleにログインして学習データ/教師データ (train.csv) をダウンロードしておきます。
データの読込みと先頭・末尾の確認
from IPython.display import display
import pandas as pd
train = pd.read_csv('train.csv')
display(train.head(4))
display(train.tail(4))
実行すると次のような結果が得られます。
pd.read_csv
では「utf8」を想定して読み込むため、他の文字コードを使っている場合は pd.read_csv('xxx.csv',encoding="SHIFT-JIS")
のようにします。
head(X)
で先頭からX行のデータを表示して内容を確認します。データの上部には、データ作成日や作成者などのメタ情報が存在している場合があります。それらは、必要に応じて削除しますが、今回は特に削除すべきものはないようです。引数を指定しない場合 head()
は先頭から5行を表示します。
tail(X)
は末尾のX行を表示します。末尾には合計値や平均値、メタ情報が存在していることもあるので、チェックしておきます。
なお、各列の意味は、https://www.kaggle.com/c/titanic/data から確認できます(要ログイン)。PassengerIdは「乗客番号(0からの連番)」、Survivedは「生存できたか(0
:死亡、1
:生存)」、Pclassは「チケットクラス/客室等級(1
:一等船室、2
:二等船室、3
:三等船室)」、Nameは「氏名」、Sexは「性別(male
:男性、female
:女性)」、Ageは「年齢」、Ticketは「チケットID」、Fareは「乗船料金」、Cabinは「船室番号」になります。
Embarkedは「乗船した港(C
:シェルブール(フランス北西部)、Q
:クイーンズタウン(アイルランド) 、S
:サウサンプトン(イギリス))」です。タイタニック号は、サウサンプトン港 → シェルブール港 → アイルランドの順で寄港して乗客を乗せました。
SibSpは「一緒に乗船している兄弟と配偶者の人数」、Parchは「一緒に乗船している親と子どもの人数」のようです。
データの概要を確認
df = train
print(df.shape) # 行数と列数の確認 (行数,列数)のタプル
print(df.columns.tolist()) # 列名の確認 リストに変換
print(df.isnull().sum()) # 各列のnull値(欠損値)の数
s = df.isnull().sum()
print(s[s>0]) # nullが存在する各列のnull値の合計数
print(df.dtypes) # 列名と型の確認
display(df.describe()) # 各種統計値 int float型の列を対象
-
df.shape
により(891, 12)
が得られます。このCSVには819行、12列のデータが存在することが確認できます。つまり、819人の乗船者がいたことが分かります。- テストデータ(test.scv)に含まれる417名と合わせると1236名、Wikipediaによると実際の乗船者(乗客乗員)は2200人以上とのことです。
-
df.columns.tolist()
により、各列のヘッダ(見出し)が文字列リスト['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked']
として得られます。 -
df.isnull().sum()
で、各列における「null値」データの数をチェックします。ちなみに、CSVデータの空欄が null として認識されます。Ageについて177件、Cabin(船室番号)で687件、Embarked(乗船港)で2件の null値 がありました。 -
df.dtypes
により、各列の列名とデータの型が確認できます。 -
df.describe()
により、データ型が int または float の列について次のように統計量が確認できます。なお、PassengerId は分析用につけられたIDであり、これについての統計量はほぼ意味を持ちません。
count は各列のデータ数です(Ageが714になっているのは177件の欠損値があるためです)。mean は平均、std は標準偏差、min は最小値、max は最大値、25% は小さいほうからデータを並べたとき(昇順にソートしたとき)先頭から25%番目のデータの値(=第一四分位数)となります。50%は中央値に相当します。
ざっくりとした統計解析ですが、次のようなことが分かります。
- Survived の mean が 0.383838 であることから、生存者は約38%、死亡者は約62% である。
- Pclass の四分位数から、半数以上の乗客は三等船室である。
- Age から、最年少は 0.42歳、最高齢は 80歳である。
- 0.42という値はちょっと気になるので(データのミスなど可能性もあるので)次のセクションで確認作業をします。
- SibSp の統計量から、半数以上の乗客は兄弟・配偶者と一緒に乗船していない。
- Parch の統計量から、75%以上の乗客は、親や子と一緒に乗船していない。個人でで乗船している人が多かった?(船員?)
- Fare の統計量から、乗船料金が 0 である人(乗組員?特別ゲスト?)の存在が確認できます。また、中央値(=14.5)と平均値(=32.3)が大きく離れていることがから、少数だと思われますが、非常に高額の乗船料を払っている人がいることが確認できます。
年齢(小数値)に関する確認
df.describe()
の結果、Age(年齢)の最小値として 0.42 という値がでてきましたが、これが「生後5ヵ月」という意味での適切な値なのか、なんらかのミスの結果で出てきた値なのかを検討していきます。年齢をキーにデータを昇順にソートして、そのデータから考えていきます。
display(df.sort_values('Age').head(10))
年齢について小数値をとるデータは 7件 存在し、いずれも12を掛けると整数に近い値( 0.42x12=5.04、0.67x12=8.04、0.75x12=9、0.83x12=9.96、0.92x12=11.04)となります。また、Parch(一緒に乗船した親(または子ども)の人数)が、1または2になっています。このことから、小数値は「生後Xヵ月」という値を表しているものと考えて問題がないと判断します。
なお、これらのデータからは以下のようなこともわかります。
- 1歳未満の乗船者の生存割合は 100% である。
- 1歳未満でも、乗船料金はしっかり徴収されている。
- Cabinの項目が欠損値になっている割合が高い(全体と比較して)。
乗船料金(無料)に関する確認
df.describe()
の結果、Fare(乗船料金)最小値が 0.00 という値がでてきましたが、これはどういうことか考えていきます。乗員(乗務員・乗組員)なのか、招待された特別ゲストなのか、なんらかのミスのなのか?
次のコードにより、Fare で昇順にソートしてデータを観察してみます。
display(df.sort_values('Fare').head(17))
Fare が 0.00 の乗船者が15名確認できました。該当者について次のようなことが分かります。
- 乗船港が全員、
S
:サウサンプトン(出発港)である。乗員なら当然? - 年齢が欠損している割合が 8/15 と高い(全体における欠損割合は 177/819)
- Ticket が「LINE」となっている人が4名存在する。
- 生存割合は 1/15 = 6.7% と著しく低い。乗員なので最後まで救助活動のため船内に残った?
- 全員が男性である。
- 全員が個人で乗船している(親や子、兄弟などが一緒に乗り合わせていない)。乗員であることを裏付ける?
- 料金が0にも関わらず1等船室が5名いる。1等船室のお客様対応係?
乗員のような気がしますが、、、このデータに含まれる乗船者819人のなかで、乗組員が15人というのは少なすぎる気がするので、、、「Fare が 0.00 の人は乗組員である」とは言いずらい気がします。一方で、乗員であるのに料金が発生しているのも変ですが・・・(このデータには乗客だけではなく、乗員も含まれているはずです)。
文字列型の列についての要素・出現数を確認
Sex や Embarked などの文字列型(object型)の列について、その要素と出現数(度数)について調べていきます。
display(train['Sex'].value_counts(dropna=False))
display(train['Embarked'].value_counts(dropna=False))
value_counts()
で調べることができます。引数に dropna=Fals
を与えると null値の個数についても数えてくれます。
male 577
female 314
Name: Sex, dtype: int64
S 644
C 168
Q 77
NaN 2
Name: Embarked, dtype: int64
今度は、これらを百分率の情報を付加して出力してみます。データの概要がつかみやすくなります。書式指定文字列 >5.1%
により「右寄せ」「固定幅5」「小数点以下1桁」「パーセント表示」で整形出力ができます。
s = train['Sex'].value_counts(dropna=False)
display( s.apply( lambda p: '{0:4} ({1:>5.1%})'.format(p, p/s.sum())) )
s = train['Embarked'].value_counts(dropna=False)
display( s.apply( lambda p: '{0:4} ({1:>5.1%})'.format(p, p/s.sum())) )
male 577 (64.8%)
female 314 (35.2%)
Name: Sex, dtype: object
S 644 (72.3%)
C 168 (18.9%)
Q 77 ( 8.6%)
NaN 2 ( 0.2%)
Name: Embarked, dtype: object
客室等級と料金の関係を可視化
Pclass(チケットクラス、客室等級)と Fare(乗船料金)の関係を把握するために可視化してみます。等級が良いほど基本的には料金が高くなる関係にあるはずです。
まずは、散布図で確認してみます。横軸に Pclass、縦軸に Fare でプロットしてみます。
%matplotlib inline
import matplotlib.pyplot as plt
df.plot.scatter(x='Pclass', y='Fare', xticks=[1,2,3])
plt.show()
散布図ではプロットした点が重なってしまって、どの料金範囲にデータが集中しているのか把握しずらいです。そこで、今度は、箱ひげ図を使って可視化していきます。コードはだいぶ複雑になります。
%matplotlib inline
import matplotlib.pyplot as plt
dfg = df.groupby('Pclass')
plt.boxplot( [dfg.get_group(i)['Fare'] for i in range(1,4)] )
plt.xlabel('Pclass')
plt.ylabel('Fare')
plt.show()
matplotlib の箱ひげ図では、箱の長さの1.5倍以上のデータは外れ値とみなして丸でプロットされます。500を超える外れ値があって、見づらくなっているので plt.ylim
で表示範囲を限定します。
dfg = df.groupby('Pclass')
plt.boxplot( [dfg.get_group(i)['Fare'] for i in range(1,4)] )
plt.xlabel('Pclass')
plt.ylabel('Fare')
plt.ylim(-5,200)
plt.show()
箱ひげ図にすることで、およそどの区間にデータが存在するかを把握することができます。オレンジ色の線は中央値を表します。
各種データのクロス集計
生存有無について、性別、客室等級などとクロス集計します。クロス集計により、男女のそちらのほうが生存率が高いのか?客室等級と生存率に関係はあるのか?といったことを見ていきます。クロス集計には pd.crosstab()
を使用します。margins=True
を省略すると、行・列ともに All の項目は作成されません。
# 性別
x = pd.crosstab(index=df['Sex'], columns=df['Survived'], margins=True)
t = x.applymap( lambda p: '{0:4} ({1:>6.1%})'.format(p, p/len(df)))
t = t.rename(columns={ 0:'死亡', 1:'生存'})
display(t)
# 客室等級
x = pd.crosstab(index=df['Pclass'], columns=df['Survived'], margins=True)
t = x.applymap( lambda p: '{0:4} ({1:>6.1%})'.format(p, p/len(df)))
t = t.rename(columns={ 0:'死亡', 1:'生存'})
display(t)
これらの表からは「全体のうち、半数以上が死亡した男性である一方で、死亡した女性は10%未満である」「三等客室の死亡者が全体の40%を占めている」といったことが読み取れます。
しかし、「男性である」「女性である」といった属性と、生存の有無の関係を読み取りずらいです。つまり、「男性(女性)についての生存/死亡の割合」や「生存者(死亡者)についての男女割合」などの情報です。これは、次のようにして求めることができます。
x = pd.crosstab(index=df['Sex'], columns=df['Survived'], margins=True)
x = x.rename(columns={ 0:'死亡', 1:'生存'})
t = x.apply( lambda p: p/p[-1] , axis=1 )
t = t.applymap( lambda p: '{0:>5.1%}'.format(p) )
display(t)
t = x.T.apply( lambda p: p/p[-1] , axis=1 )
t = t.applymap( lambda p: '{0:>5.1%}'.format(p) )
display(t)
これらの表は、各行ごとに(横方向のみ)読み取ります。例えば、ひとつめの表では、1行目は女性における死亡割合(=25.8%)、生存割合(=74.2%)、その合計(当然100%)を表しています。3行名は男女をあわせた死亡割合、生存割合、その計を意味しています。男性のほうが圧倒的に死亡割が高いことが分かりますね。
一方で、ふたつめの表の1行目は、死亡者のなかでの女性割合(=14.8%)、男性割合(=85.2%)、その計を意味しています。このような形式にしたほうが、属性と生存の関係をつかみやすいですね。
客室等級×性別と生存の関係
客室等級と性別で区分したときの生存率の関係はどのようになるかを考えます。例えば「三等客室の男性は死亡率が高いが、一等客室の男性の死亡率は低い」といったことが分かるかもしれません。
x = df.assign( **{'死亡' : lambda p: p['Survived']==0,
'生存' : lambda p: p['Survived']!=0} )
display(x.head())
x = x.groupby(['Pclass','Sex'])[['死亡','生存']].sum()
display(x.head())
t = x.applymap( lambda p: '{0:4.0f} ({1:>5.1%})'.format(p, p/len(df)))
display(t)
コードは、やや複雑になっているので細かく解説していきます。まず、最初の display(x.head())
で次のような出力が得られます。赤枠で囲ったような列が追加されます。
つづいての display(x.head())
で次のような出力が得られます。死亡/生存の列の値は、上記の表のTrueを1、Falseを0として合計を求めた値になります。1行目の 3.0 は、一等船室の女性の死亡数です。
このままでは分かりずらいので、乗員数(891)で割って割合に直します。この結果、display(t)
で次のような結果が得られます。
これにより、全体がどのように構成されているかが分かります。ただ、これも属性に対する生存率を読み取るためにはいまいちな表なので、次のようにします。
x = df.assign( **{'死亡' : lambda p: p['Survived']==0,
'生存' : lambda p: p['Survived']!=0} )
x = x.groupby(['Pclass','Sex'])[['死亡','生存']].sum()
x = x.assign(all=lambda p: p.sum(axis=1) )
t = x.apply( lambda p: p/p[-1], axis=1 )
t = t.applymap( lambda p: '{0:>5.1%}'.format(p) )
display(t)
ここの表からは次のようなことが読み取れます。
- 一等客室における女性の死亡率は極めて低い。一方で、三等客室での女性の死亡率は50.0%である。
- 男女ともに客室等級が下がるとともに、死亡率が高くなっている。
- 死亡率を高い順に並べると、三等室男性、二等室男性、一等室男性、三等室女性、二等室女性、一等室女性。
「その2」に続きます...