hello_world.py

"Doing good is part of our code"

機械学習によるタイタニック号の生存者予測 with Python(Part 2)

f:id:tanajun99:20150624020544p:plain

tanajun99.hatenablog.com

つづきです。

階級別年齢テーブルの作成

元のコードは可読性が悪いのでやや修正する。

median_ages = np.zeros((2,3))  # 2行3列のマトリックスを作成
print median_ages

import itertools

for gi, pi in itertools.product(range(0, 2), range(3)): # productで2x3の直積を計算
    gender, pclass = gi, pi + 1
    gender_mask = (df["Gender"] == gender)
    pclass_mask = (df["Pclass"] == pclass)
    mask = gender_mask & pclass_mask
    median_ages[gi, pi] = df[mask]["Age"].dropna().median()
    
print median_ages

実行結果

[[ 0. 0. 0.]
[ 0. 0. 0.]]
[[ 35. 28. 21.5]
[ 40. 30. 25. ]]

Ageの欠損値を直接書き換える事も可能であるが慎重に処理するために元データは保持し、AgeFillという新しい列を作成

df["AgeFill"] = df["Age"]
df.head()

実行結果

f:id:tanajun99:20150624012132p:plain

df[ df["Age"].isnull() ][ ["Gender", "Pclass", "Age", "AgeFill"] ].head(10)

実行結果

f:id:tanajun99:20150624012215p:plain

# 欠損値であるかを0/1で示すカラムも作っておく
df["AgeIsNull"] = pd.isnull(df.Age).astype(int)
df.describe()

実行結果

f:id:tanajun99:20150624012328p:plain

SibSp(兄弟と夫/妻の数)とParch(親と子供の数)を足してFamilySizeカラムを作成する。
階級が高くて若い方が生存しやすいためAgeFillとPclassを掛けあわせたAge*Classカラムを作る。

df["FamilySize"] = df["SibSp"] + df["Parch"]
df["Age*Class"]  = df["AgeFill"] * df["Pclass"]
df.describe()

最後の準備

pandasのDataFrameはそのままではscikit-learnに渡せない。
数値データではないカラムへの対処、数値データのnumpy.ndarrayへの変換が必要

# 各カラムの型を見る
df.dtypes

実行結果

f:id:tanajun99:20150624012639p:plain

# 型名がobjectである項目のみをフィルタ
# objectは文字列であることが分かる
df.dtypes[df.dtypes.map(lambda x: x == "object")]

# 不必要なカラムを捨てる
df2 = df.drop(["Name", "Sex", "Ticket", "Cabin", "Embarked"], axis=1)

# Ageもコピーしたカラムがあるので必要ない
df2 = df2.drop(["Age"], axis=1)

# dropnaでNaNを含むレコードを削除できる
# しかしいずれかの項目一つがNaNである場合でも
# そのレコードの他のカラムの値が全て捨てられることに注意
df3 = df2.dropna()

df3.dtypes

実行結果

f:id:tanajun99:20150624012859p:plain

# ここまででクリーンできっちりしたデータを準備することが出来た
# 最後にvaluesメソッドを使うことでnumpy.ndarrayに変換可能
train_data = df3.values

scikit-learnに食わせるデータが完成!

scikit-learnを用いたRandomForestによる生存予測

scikit-learnとは、pythonにおける機械学習ライブラリ。
回帰、識別、クラスタリング、次元削減などにおける諸々のアルゴリズムが実装されている。


RandomForestとは、決定木を用いたアンサンブル学習の手法である。識別や回帰に使える。
ここでは生存したか否かの識別に用いる。

学習させてみる

sklearn.ensembleモジュールのRandomForestClassifierを使う。
train_dataのdtypesを見て何を学習データとして何を正解ラベルデータとするかを見る。
1列目はIDなので要らない
2列目が生存ラベルなのでこれを正解ラベルとする
3列目以降を学習データにする
評価データを準備し、学習データを作成するのと同じ要領で作成する。
scikit-learnのモデル学習、予測のAPIはさまざまなアルゴリズムで共通
モデルを作成したいアルゴリズムクラスのコンストラクタを読んで作成
モデルインスタンスのfitメソッドで学習
学習データのshapeは常に(nsample, nfeature)の形をとる。
モデルインスタンスのpredictメソッドで予測する。

xs = train_data[:, 2:] # 3列目以降
y  = train_data[:, 1]  # 正解ラベル

print xs.shape
print y.shape

実行結果

(891, 9)
(891,)

from sklearn.ensemble import RandomForestClassifier

# 決定木の数を100に設定してモデルインスタンスを作成
forest = RandomForestClassifier(n_estimators = 100)

# 学習
forest = forest.fit(xs, y)

# テストデータの読み込みおよび加工
# 学習データと同じ方法でテストデータを処理する必要有
def preprocessing_read(file_path):
    # csvファイルの読み込み
    df = pd.read_csv(file_path, header=0) # 0行目がヘッダー(defaultの動作、指定しない場合はNoneを指定)
    df["Gender"] = df["Sex"].map( {'female': 0, "male": 1} ).astype(int)  # 辞書を渡すだけで変換してくれる
    df["AgeFill"] = df["Age"]

    # 階級、性別ごとに中央値テーブルを使って欠損値を置換
    for gi, pi in itertools.product(range(2), range(3)): # productで2x3の直積を計算
        gender = gi
        pclass = pi + 1
        gender_mask = (df["Gender"] == gender)
        pclass_mask = (df["Pclass"] == pclass)
        age_mask    = df["Age"].isnull()
        mask = gender_mask & pclass_mask & age_mask
        df.ix[mask, "AgeFill"] = median_ages[gi,pi]

    # 欠損値であるかを0/1で示すカラムも作っておく
    df["AgeIsNull"] = pd.isnull(df.Age).astype(int)
    
    df["FamilySize"] = df["SibSp"] + df["Parch"]
    df["Age*Class"]  = df["AgeFill"] * df["Pclass"]
    df = df.drop(["Age", "Name", "Sex", "Ticket", "Cabin", "Embarked"], axis=1)
    df = df.dropna()
    return df

test_df = preprocessing_read("./data/test.csv")
test_df.dtypes

実行結果

PassengerId int64
Pclass int64
SibSp int64
Parch int64
Fare float64
Gender int64
AgeFill float64
AgeIsNull int64
FamilySize int64
Age*Class float64
dtype: object

# 予測
test_data = test_df.values
xs_test = test_data[:, 1:]
output = forest.predict(xs_test)

print len(test_data[:,0]), len(output)
print zip(test_data[:,0].astype(int), output.astype(int))

実行結果

f:id:tanajun99:20150624020606p:plain

# ファイル書き込み
with open("tutorial/random_forest_model.csv", "wb") as f:
    writer = csv.writer(f)
    writer.writerow(["PassengerId", "Survived"])
    for pid, survived in zip(test_data[:,0].astype(int), output.astype(int)):
        writer.writerow([pid, survived])

これでsubmitできると見せかけて、実はPassengerId 1044のテストデータに対する予測が欠けているためにこれでは通らない。
dropna()でレコードごと落ちたため?
Fareは階級と発着場から決まると仮定して調べる
PassengerId 1044の階級は3、発着場はS

mask = (df.Pclass == 3) & (df.Embarked == "S")
df.ix[mask, ["Pclass", "Embarked", "Fare"]]

df.ix[mask, "Fare"].describe()

実行結果

f:id:tanajun99:20150624020948p:plain

def preprocessing_read2(file_path):
    df = pd.read_csv(file_path, header=0)
    df["Gender"] = df["Sex"].map( {'female': 0, "male": 1} ).astype(int)
    df["AgeFill"] = df["Age"]

    for gi, pi in itertools.product(range(2), range(3)):
        gender = gi
        pclass = pi + 1
        gender_mask = (df["Gender"] == gender)
        pclass_mask = (df["Pclass"] == pclass)
        age_mask    = df["Age"].isnull()
        mask = gender_mask & pclass_mask & age_mask
        df.ix[mask, "AgeFill"] = median_ages[gi,pi]

    df["AgeIsNull"] = pd.isnull(df.Age).astype(int)    
    df["FamilySize"] = df["SibSp"] + df["Parch"]
    df["Age*Class"]  = df["AgeFill"] * df["Pclass"]
    df = df.drop(["Age", "Name", "Sex", "Ticket", "Cabin", "Embarked"], axis=1)
    
    # Fareが欠損している場合の処理
    df.ix[df["Fare"].isnull(), "Fare"] = 8.05
    df = df.dropna()
    return df

test_df = preprocessing_read2("./data/test.csv")
print len(test_df)
test_df.dtypes

実行結果

f:id:tanajun99:20150624021104p:plain

# 無事に418件得られたので改めて予測
test_data = test_df.values
xs_test = test_data[:, 1:]
output = forest.predict(xs_test)

# ファイル書き込み
with open("tutorial/random_forest_model.csv", "wb") as f:
    writer = csv.writer(f)
    writer.writerow(["PassengerId", "Survived"])
    for pid, survived in zip(test_data[:,0].astype(int), output.astype(int)):
        writer.writerow([pid, survived])

Submitする

これでやっとSubmitできるようになる。

from IPython.display import display, Image
display(Image("./img/second_submit.png"))


機械学習ガチ勢目指して頑張りたいです。!!