Rで何かをしたり、読書をするブログ

政府統計の総合窓口のデータや、OECDやUCIやのデータを使って、Rの練習をしています。ときどき、読書記録も載せています。

Kaggle の Titanic に、base R と glmnet で挑戦した記録

KaggleのTitanicにbase Rと glmnetで挑戦してみました。
quartoでhtmlを作って、下に貼り付けました。
何かのご参考になれば幸いです。

 

 

 

Kaggle Titanic with base R and glmnet

Author

cross_hyou

Published

June 30, 2026

はじめに

この Quarto ドキュメントは、Kaggle の Titanic の competition に
base R と glmnet で挑む記録を記したものです。

ライブラリーの読み込み

はじめに glmnet パッケージの読み込みをします。

library(glmnet)
Loading required package: Matrix
Loaded glmnet 4.1-10

CSVファイルの読み込み

トレーニング用データとテスト用のデータのCSVファイルを読み込みます。
read.csv()関数を使います。

df_train <- read.csv("C:/R_folder/kaggle_titanic_train.csv")
df_test <- read.csv("C:/R_folder/kaggle_titanic_test.csv")

正しく読み込めたか、str()関数で確認してみます。

str(df_train)
'data.frame':   891 obs. of  12 variables:
 $ PassengerId: int  1 2 3 4 5 6 7 8 9 10 ...
 $ Survived   : int  0 1 1 1 0 0 0 0 1 1 ...
 $ Pclass     : int  3 1 3 1 3 3 1 3 3 2 ...
 $ Name       : chr  "Braund, Mr. Owen Harris" "Cumings, Mrs. John Bradley (Florence Briggs Thayer)" "Heikkinen, Miss. Laina" "Futrelle, Mrs. Jacques Heath (Lily May Peel)" ...
 $ Sex        : chr  "male" "female" "female" "female" ...
 $ Age        : num  22 38 26 35 35 NA 54 2 27 14 ...
 $ SibSp      : int  1 1 0 1 0 0 0 3 0 1 ...
 $ Parch      : int  0 0 0 0 0 0 0 1 2 0 ...
 $ Ticket     : chr  "A/5 21171" "PC 17599" "STON/O2. 3101282" "113803" ...
 $ Fare       : num  7.25 71.28 7.92 53.1 8.05 ...
 $ Cabin      : chr  "" "C85" "" "C123" ...
 $ Embarked   : chr  "S" "C" "S" "S" ...
str(df_test)
'data.frame':   418 obs. of  11 variables:
 $ PassengerId: int  892 893 894 895 896 897 898 899 900 901 ...
 $ Pclass     : int  3 3 2 3 3 3 3 2 3 3 ...
 $ Name       : chr  "Kelly, Mr. James" "Wilkes, Mrs. James (Ellen Needs)" "Myles, Mr. Thomas Francis" "Wirz, Mr. Albert" ...
 $ Sex        : chr  "male" "female" "male" "male" ...
 $ Age        : num  34.5 47 62 27 22 14 30 26 18 21 ...
 $ SibSp      : int  0 1 0 0 1 0 0 1 0 2 ...
 $ Parch      : int  0 0 0 0 1 0 0 1 0 0 ...
 $ Ticket     : chr  "330911" "363272" "240276" "315154" ...
 $ Fare       : num  7.83 7 9.69 8.66 12.29 ...
 $ Cabin      : chr  "" "" "" "" ...
 $ Embarked   : chr  "Q" "S" "Q" "S" ...

トレーニング用データとテスト用のデータの結合

データの前処理をするためには、
トレーニング用データとテスト用のデータを結合すると便利です。

テスト用データには、Survivedが無いので、Survivedを加えます

df_test$Survived <- NA

2つのデータフレームを縦方向につなげるときは、rbind()関数を使いますが
列の順番が同じでないとだめなので、工夫が必要です。

まず、df_trainの列の順番を保存します。

col_order <- names(df_train)
col_order
 [1] "PassengerId" "Survived"    "Pclass"      "Name"        "Sex"        
 [6] "Age"         "SibSp"       "Parch"       "Ticket"      "Fare"       
[11] "Cabin"       "Embarked"   

そうしたら、保存したcol_orderを使って、df_testの列の順番を変更します。
変更前の列の順番はこうなります。

str(df_test)
'data.frame':   418 obs. of  12 variables:
 $ PassengerId: int  892 893 894 895 896 897 898 899 900 901 ...
 $ Pclass     : int  3 3 2 3 3 3 3 2 3 3 ...
 $ Name       : chr  "Kelly, Mr. James" "Wilkes, Mrs. James (Ellen Needs)" "Myles, Mr. Thomas Francis" "Wirz, Mr. Albert" ...
 $ Sex        : chr  "male" "female" "male" "male" ...
 $ Age        : num  34.5 47 62 27 22 14 30 26 18 21 ...
 $ SibSp      : int  0 1 0 0 1 0 0 1 0 2 ...
 $ Parch      : int  0 0 0 0 1 0 0 1 0 0 ...
 $ Ticket     : chr  "330911" "363272" "240276" "315154" ...
 $ Fare       : num  7.83 7 9.69 8.66 12.29 ...
 $ Cabin      : chr  "" "" "" "" ...
 $ Embarked   : chr  "Q" "S" "Q" "S" ...
 $ Survived   : logi  NA NA NA NA NA NA ...

Survivedが一番最後にあることがわかります。
これをdf_trainと同じ順番にします。

df_test <- df_test[ , col_order]
str(df_test)
'data.frame':   418 obs. of  12 variables:
 $ PassengerId: int  892 893 894 895 896 897 898 899 900 901 ...
 $ Survived   : logi  NA NA NA NA NA NA ...
 $ Pclass     : int  3 3 2 3 3 3 3 2 3 3 ...
 $ Name       : chr  "Kelly, Mr. James" "Wilkes, Mrs. James (Ellen Needs)" "Myles, Mr. Thomas Francis" "Wirz, Mr. Albert" ...
 $ Sex        : chr  "male" "female" "male" "male" ...
 $ Age        : num  34.5 47 62 27 22 14 30 26 18 21 ...
 $ SibSp      : int  0 1 0 0 1 0 0 1 0 2 ...
 $ Parch      : int  0 0 0 0 1 0 0 1 0 0 ...
 $ Ticket     : chr  "330911" "363272" "240276" "315154" ...
 $ Fare       : num  7.83 7 9.69 8.66 12.29 ...
 $ Cabin      : chr  "" "" "" "" ...
 $ Embarked   : chr  "Q" "S" "Q" "S" ...

SurvivedがPassengerIdとPclassの間にあることがわかります。 そうしたら、rbind()関数でdf_trainとdf_testを結合します。

df_all <- rbind(df_train, df_test)
head(df_all)
  PassengerId Survived Pclass
1           1        0      3
2           2        1      1
3           3        1      3
4           4        1      1
5           5        0      3
6           6        0      3
                                                 Name    Sex Age SibSp Parch
1                             Braund, Mr. Owen Harris   male  22     1     0
2 Cumings, Mrs. John Bradley (Florence Briggs Thayer) female  38     1     0
3                              Heikkinen, Miss. Laina female  26     0     0
4        Futrelle, Mrs. Jacques Heath (Lily May Peel) female  35     1     0
5                            Allen, Mr. William Henry   male  35     0     0
6                                    Moran, Mr. James   male  NA     0     0
            Ticket    Fare Cabin Embarked
1        A/5 21171  7.2500              S
2         PC 17599 71.2833   C85        C
3 STON/O2. 3101282  7.9250              S
4           113803 53.1000  C123        S
5           373450  8.0500              S
6           330877  8.4583              Q
tail(df_all)
     PassengerId Survived Pclass                           Name    Sex  Age
1304        1304       NA      3 Henriksson, Miss. Jenny Lovisa female 28.0
1305        1305       NA      3             Spector, Mr. Woolf   male   NA
1306        1306       NA      1   Oliva y Ocana, Dona. Fermina female 39.0
1307        1307       NA      3   Saether, Mr. Simon Sivertsen   male 38.5
1308        1308       NA      3            Ware, Mr. Frederick   male   NA
1309        1309       NA      3       Peter, Master. Michael J   male   NA
     SibSp Parch             Ticket     Fare Cabin Embarked
1304     0     0             347086   7.7750              S
1305     0     0          A.5. 3236   8.0500              S
1306     0     0           PC 17758 108.9000  C105        C
1307     0     0 SOTON/O.Q. 3101262   7.2500              S
1308     0     0             359309   8.0500              S
1309     1     1               2668  22.3583              C

結合できました。

EDAと前処理

それでは、EDA (Exploratory Data Analysis) と前処理をやっていきます。

Survived

Survivedは0なら死亡、1なら生存です。
生存率を計算しましょう。

mean(df_all$Survived, na.rm = TRUE)
[1] 0.3838384

生存率は、38.4%でした。

Pclass

次は、Pclassです。これは、1、2、3に分かれています。
まず、NAが無いかみてみます。

sum(is.na(df_all$Pclass))
[1] 0

PclassにはNAは無いようです。
Pclassは、1, 2, 3と数値データですが、これは本当は、
1等客室、2等客室、3等客室のことなのでファクター型に変更します。

df_all$Pclass <- as.factor(df_all$Pclass)
summary(df_all$Pclass)
  1   2   3 
323 277 709 

3等客室の乗客が709人で一番多いことがわかります。

客室の等級によって生存率に差があるでしょうか?
バーチャートで視覚化してみましょう。

colors <- c("red", "green", "yellow")
barplot(table(df_all$Pclass, df_all$Survived),
        col = colors, legend = FALSE,
        main = "1 = Survived / 0 = Not-Survived")
legend("topright", legend = c("Pclass 1", "Pclass 2", "Pclass 3"),
       fill = colors, border = "black")



0, 死亡者のほうが、1, 生存者より多いこと、
Pclass 1の人は生存者のほうが多いことなどがわかります。

PclassがSurvivedに有意に関連しているかどうか、chisq.test()関数で検定します。

chisq.test( table(df_all$Pclass, df_all$Survived) )

    Pearson's Chi-squared test

data:  table(df_all$Pclass, df_all$Survived)
X-squared = 102.89, df = 2, p-value < 2.2e-16

p-value < 2.2e-16 とほとんど0ですので、
PclassはSurvivedに関連していることが確認できました。

Name

Nameは文字通り乗客の名前です。
いろいろ調べると、このNameの中からMr.やMiss.やMaster.などの肩書を
取得するのがTitancのキーポイントらしいです。

とりあえず、数行表示してみます。

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

観察すると、はじめのカンマの後にスペースがあって、
次にMr.やMrs.などの肩書が来るようです。

この肩書を取り出すにはどうしたらいいでしょうか?
はじめのカンマの位置と2番目の空白(スペース)の位置がわかれば
なんとかなりそうです。

例えば、
1234, Mr. ab,c
という文字列があるとします。

そうすると、はじめのカンマの位置は、5です。
2番目の空白の位置は、10です。

すると、Mr. は、(5+2)から(10-1)までになります。

はじめのカンマの位置を取得する関数は、regexpr()関数を使います 例:

full_name <- "1234, Mr. ab,c"
comma <- regexpr(",", full_name)[1]
comma
[1] 5

2番目の空白の位置を取得するのは、gregexpr()関数を使います。
regexpr()関数は、はじめに出てくる位置しか返しませんが、
gregexpr()関数は、すべての位置を返します。
例:

full_name <- "1234, Mr. ab,c"
spaces <- gregexpr(" ", full_name)[[1]]
space2 <- spaces[2]
space2
[1] 10

こうして、前後の位置がわかりましたので、Mr.が取り出せます。
これは、substr()関数で取り出せます。
今、commaには、5が入っていて、space2には10が入っています。
例:

full_name <- "1234, Mr. ab,c"
comma <- regexpr(",", full_name)[1]
spaces <- gregexpr(" ", full_name)[[1]]
space2 <- spaces[2]
substr(full_name, comma + 2, space2 - 1)
[1] "Mr."

上手く取り出せました。 いままでのことを利用して、肩書を取り出す関数式をつくりましょう。

get_katagaki <- function(string) {
  comma <- regexpr(",", string)[1]      # カンマの位置
  spaces <- gregexpr(" ", string)[[1]]  # 空白の位置(複数ある)
  space2 <- spaces[2]                   # 2番目の空白の位置
  substr(string, comma + 2, space2 - 1) #肩書を取り出す
}

このget_katagaki関数がうまく動くか試してみます。

get_katagaki(df_all$Name[1])
[1] "Mr."
get_katagaki(df_all$Name[2])
[1] "Mrs."
get_katagaki(df_all$Name[3])
[1] "Miss."
get_katagaki(df_all$Name[4])
[1] "Mrs."
get_katagaki(df_all$Name[5])
[1] "Mr."

うまくいきましたね!

このget_katagaki()関数をdf_all$Nameに使うためには、sapply()関数を使います。
このようにします。

df_all$title <- sapply(df_all$Name, get_katagaki)
head(df_all[ , "title"])
[1] "Mr."   "Mrs."  "Miss." "Mrs."  "Mr."   "Mr."  

titleの頻度をみてみます。

rev(sort(table(df_all$title)))

      Mr.     Miss.      Mrs.   Master.                Rev.       Dr.      Col. 
      736       256       191        59        36         8         8         4 
      Ms.     Mlle.    Major.       the      Mme. Jonkheer.      Don.     Capt. 
        2         2         2         1         1         1         1         1 

Mr.が736人、Miss.が256人、Mrs.が191人、Master.が59人で、空白が36人です。
その他は8人以下ですね。

titleは、Mr. Miss. Mrs. Master. その他は othersとして5つにまとめましょう。

# 4つのうちのどれかなら、TRUE
good_katagaki <- df_all$title %in% c("Mr.", "Miss.", "Mrs.", "Master.")

# good_katagakiでないものをothersにする
df_all$title[!good_katagaki] <- "others"

# うまくできたか確認
table(df_all$title)

Master.   Miss.     Mr.    Mrs.  others 
     59     256     736     191      67 

うまくできました。

では、titleとSurvivedの関係を見てみます。

table(df_all$title, df_all$Survived)
         
            0   1
  Master.  17  23
  Miss.    53 126
  Mr.     423  79
  Mrs.     24  97
  others   32  17

Mr.は死亡者が多くて、Miss.やMrs.は生存者が多いですね。
比率のテーブルにしてみます。prop.table()関数を使います。

prop.table(table(df_all$title, df_all$Survived), margin = 1)
         
                  0         1
  Master. 0.4250000 0.5750000
  Miss.   0.2960894 0.7039106
  Mr.     0.8426295 0.1573705
  Mrs.    0.1983471 0.8016529
  others  0.6530612 0.3469388

margin = 1 を使うと、横方法の合計が1.0になる比率のテーブルになります。
Master.は58%, Miss.は70%, Mr.は16%, Mrs.は80%, othersは35%の生存率です。

グラフも描いてみます。

colors <- c("yellow", "red", "green", "pink", "gray")
barplot(prop.table(table(df_all$title, df_all$Survived)),
        legend = FALSE, col = colors,
        main = "Title and Survived (1 = Survuved)")
legend("topright", legend = c("Master.", "Miss.", "Mr.", "Mrs.", "others"),
       fill = colors, border = "black")

 

Mr.は多くが死んでしまったことがわかります。

Nameの情報をtitleとして5つの肩書に集約しました。
titleはファクター型にして、
Nameは、もう必要が無いので、df_allから削除します。

df_all$title <- as.factor(df_all$title)
df_all$Name <- NULL

Sex

次はSexを調べます。まず、NAがあるかどうかを確認します。

sum(is.na(df_all$Sex))
[1] 0

結果は0でしたので、ファクター型にします。

df_all$Sex <- as.factor(df_all$Sex)

SexとSurvivedの関係をテーブルにしてみます。

table(df_all$Sex, df_all$Survived)
        
           0   1
  female  81 233
  male   468 109

女性は生存が多く、男性は生存は少ないですよね。
これは、titleでもMiss., Mrs.が生存が多くて、Mr.が生存が少なかったので
予想された結果です。

SurvivedとSexの関係もグラフにします。

barplot(table(df_all$Sex, df_all$Survived),
        legend = TRUE,
        main = "Survuved(= 1) and Sex")

 

femaleは多くが生存しています。

次に気になるのは、titleとSexです。
テーブルにしてみます。

table(df_all$title, df_all$Sex)
         
          female male
  Master.      0   59
  Miss.      256    0
  Mr.          0  736
  Mrs.       191    0
  others      19   48

others だけ男女ともにあります。Master.とMr.は男性だけ、
Miss.とMrs.は女性だけです。

Age

Ageを調べます。まず、NAがあるかどうかを確認します。

sum(is.na(df_all$Age))
[1] 263

Ageは、263もNAがあります。このNAを何かの値で置換しないといけないです。 その前に、AgeがNAだったら1で、そうでなければ0のダミー変数を作っておきます。

df_all$agena <- ifelse(is.na(df_all$Age), 1, 0)
table(df_all$agena)

   0    1 
1046  263 

1の値を持つ観測データが263個です。

AgeはPclassとtitleで中央値や平均値が違うと思います。
確認してみましょう。

# 中央値
tapply(df_all$Age, list(df_all$Pclass, df_all$title),
       function(x) median(x, na.rm = TRUE))
  Master. Miss. Mr. Mrs. others
1       6    30  42   45     45
2       2    20  30   31     30
3       6    18  26   31     30
# 平均値
tapply(df_all$Age, list(df_all$Pclass, df_all$title),
       function(x) round(mean(x, na.rm = TRUE),2))
  Master. Miss.   Mr.  Mrs. others
1    6.98 30.34 41.58 43.61  42.18
2    2.76 20.38 32.40 33.70  35.65
3    5.94 17.43 28.22 32.04  28.98
# 観測数
tapply(df_all$Age, list(df_all$Pclass, df_all$title),
       function(x) sum(!is.na(x)))
  Master. Miss. Mr. Mrs. others
1       5    59 129   66     25
2      11    46 134   53     17
3      36   101 299   45     20

Master. は子どもなんですね。知らなかったです。

AgeのNAをどうするか、今回はPclass(3通り) X title(5通り) = 15通りの
組み合わせで、NAは中央値に置き換えてみたいと思います。

まず、NAに置き換える自作関数を作ります。

replace_na <- function(x) {      # xはベクトル
  med <- median(x, na.rm = TRUE) # xの中央値を計算
  x[is.na(x)] <- med             # NAのところをmed(中央値)に置換
  return(x)                      # x を返す
}

replace_na()が上手く動くか試します。

test <- c(1, 2, NA, 4, 5)
test
[1]  1  2 NA  4  5
replace_na(test)
[1] 1 2 3 4 5

NAが中央値の3に置き換わっています。

次は、Pclass X title = 15とおりにこのreplace_na()関数を適合することを
考えます。

これは、ave()関数を使います。
ave()関数は、ave(x, g1, g2, FUN)のように使い、
x を g1 X g2 のグループにして、それぞれにFUNという関数を適用する。
というものです。

試しにこんなデータフレームを作ってみます。

df_tameshi <- data.frame(
  x = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
  g1 = c("a", "a", "a", "b", "b", "b", "c", "c", "c", "c"),
  g2 = c("d", "e", "d", "e", "d", "e", "d", "e", "d", "e")
)
df_tameshi
    x g1 g2
1   1  a  d
2   2  a  e
3   3  a  d
4   4  b  e
5   5  b  d
6   6  b  e
7   7  c  d
8   8  c  e
9   9  c  d
10 10  c  e

このデータフレームで、g1(3とおり) X g2(2とおり) = 6つのグループでの
平均値をみてみます。

ave(df_tameshi$x, df_tameshi$g1, df_tameshi$g2, FUN = mean)
 [1] 2 2 2 5 5 5 8 9 8 9

この結果が正しいか検証しやすいように、tapply()関数で
それぞれのグループの平均値を算出してみます。

tapply(df_tameshi$x, list(df_tameshi$g1, df_tameshi$g2), mean)
  d e
a 2 2
b 5 5
c 8 9

平均値は、ad = 2, ae = 2, bd = 5, be = 5, cd = 8, ce = 9 です。

xは、ad, ae, ad, be, bd, be, cd, ce, cd, ce の順番になっています。
平均値に置き換えると、
2, 2, 2, 5, 5, 5, 8, 9, 8, 9ですので、ave()関数の結果と同じです。

ave()関数の働きを確認しましたので、実際にAgeのNAを
それぞれの中央値に置き換えます。

df_all$Age <- ave(df_all$Age, 
                  df_all$Pclass, df_all$title,
                  FUN = replace_na)
summary(df_all$Age)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   0.17   21.00   26.00   29.30   36.50   80.00 

replace_na()関数は、NAを中央値に置き換える関数なので、
summary()関数の結果をみると、NAがなくなっていることがわかります。

これで、Ageの前処理は終わりました。

SurvivalとAgeの関係を見てみます。
hist()関数でヒストグラムで、Survived == 1 と Survived == 0 で
わけて描いてみます。

par(mfrow = c(2, 1))
kugiri <- seq(0, 80, by = 10)
hist(df_all$Age[df_all$Survived == 1], main = "生存", breaks = kugiri,
     xlab = "年齢")
hist(df_all$Age[df_all$Survived == 0], main = "死亡", breaks = kugiri,
     xlab = "年齢")

 

par(mfrow = c(1, 1))

生存者のほうが年齢の幅が広いように見えます。

SibSpとParch

次は、SibSpとParchです。
これは、合計して、familyとします。

df_all$family = df_all$SibSp + df_all$Parch
summary(df_all$family)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 0.0000  0.0000  0.0000  0.8839  1.0000 10.0000 

familyは一緒に乗船している家族の人数です。0は一人でタイタニックに
乗った人です。
最大が10ですから、11人の家族でのった人もいますね。

SibSpとParchはもう必要ないので、削除します。

df_all$SibSp <- NULL
df_all$Parch <- NULL

familyの分布をみてみます。 table()関数を使います。

table(df_all$family)

  0   1   2   3   4   5   6   7  10 
790 235 159  43  22  25  16   8  11 

0が790人、1が235人、2が159人でこの3つで大半を占めていますね。

familyとSurvivedの関係をみてみます。
familyごとにSurvivedの平均値を計算して、生存率をみます。

tapply(df_all$Survived, df_all$family,
       FUN = function(x) round(mean(x, na.rm = TRUE), 2))
   0    1    2    3    4    5    6    7   10 
0.30 0.55 0.58 0.72 0.20 0.14 0.33 0.00 0.00 

0は30%, 1は55%, 2は58%, 3は72%です。

グラフも描いてみます。

# グラフのための表を生成
object <- tapply(df_all$Survived, df_all$family,
       FUN = function(x) round(mean(x, na.rm = TRUE), 2))

# 高さに比例した色(生存率が高いほど明るい)
cols <- gray(object)

# グラフ描画
barplot(object, col = cols, main = "Survival Rate by Family")

 

こうしてグラフを見ると、familyは、0, 1~3, 4~10 の3つのグループに
カテゴリー化したほうがよさそうです。

どうやりましょうか?
cut()関数を使います。 cut()関数は数値型のベクトルをファクター型に関数です。 cut(ベクトル, breaks = 区切り, labels = ファクターの水準) という構造です。

breaks = c(-1, 0, 3, Inf) とすると、
(-1, 0]のグループ(-1より大きくて0以下)
(0, 3]のグループ(0より大きくて3以下)
(3, Inf]のグループ(3より大きくてInf以下)
に分かれます。

df_all$family <- cut(df_all$family,
                           breaks = c(-1, 0, 3, Inf),
                           labels = c("alone", "small", "large"))
summary(df_all$family)
alone small large 
  790   437    82 

できました。

Ticket

Ticketをみてみましょう。これは文字列ですね。とりあえず、20個くらいを
みてみます。

head(df_all[ , c("PassengerId", "Ticket")], 20)
   PassengerId           Ticket
1            1        A/5 21171
2            2         PC 17599
3            3 STON/O2. 3101282
4            4           113803
5            5           373450
6            6           330877
7            7            17463
8            8           349909
9            9           347742
10          10           237736
11          11          PP 9549
12          12           113783
13          13        A/5. 2151
14          14           347082
15          15           350406
16          16           248706
17          17           382652
18          18           244373
19          19           345763
20          20             2649

数字だけのものもあれば、アルファベットも混じったものもありますね。

NAはあるでしょうか?

sum(is.na(df_all$Ticlet))
[1] 0

NAは無いです。

どうしましょうか? はじめの1文字だけにしてみましょう。
substr()関数を使います。

df_all$Ticket <- substr(df_all$Ticket, 1, 1)
head(df_all$Ticket, 20)
 [1] "A" "P" "S" "1" "3" "3" "1" "3" "3" "2" "P" "1" "A" "3" "3" "2" "3" "2" "3"
[20] "2"

できました。

頻度と、Survivedとのクロス表を作成しましょう。

# 頻度
table(df_all$Ticket)

  1   2   3   4   5   6   7   8   9   A   C   F   L   P   S   W 
210 278 429  11   3   9  13   2   2  42  77  13   5  98  98  19 
# 数のクロス表
table(df_all$Survived, df_all$Ticket)
   
      1   2   3   4   5   6   7   8   9   A   C   F   L   P   S   W
  0  54  98 229   8   3   5   8   2   0  27  31   3   3  23  44  11
  1  92  85  72   2   0   1   1   0   1   2  16   4   1  42  21   2
# 比率のクロス表
round(prop.table(table(df_all$Survived, df_all$Ticket), margin = 2), 2)
   
       1    2    3    4    5    6    7    8    9    A    C    F    L    P    S
  0 0.37 0.54 0.76 0.80 1.00 0.83 0.89 1.00 0.00 0.93 0.66 0.43 0.75 0.35 0.68
  1 0.63 0.46 0.24 0.20 0.00 0.17 0.11 0.00 1.00 0.07 0.34 0.57 0.25 0.65 0.32
   
       W
  0 0.85
  1 0.15

ある程度数が多くて、生存率が高いのは、1(63%), 2(46%), P(65%)ですね。

1, 2, P をgt(good ticketの意味)、それ以外をbt(bad ticket)にしてみます。

df_all$Ticket[df_all$Ticket %in% c("1", "2", "P")] <- "gt"
df_all$Ticket[df_all$Ticket != "gt"] <- "bt"
table(df_all$Ticket)

 bt  gt 
723 586 

btが723人、gtが586人です。1(210人) + 2(278人) + P(98人) = 586人なので正しく
分類されました。

ファクター型にしておきます。

df_all$Ticket <- as.factor(df_all$Ticket)

Ticket別のSurvivedをグラフにしてみます。

colors <- c("blue", "red")
barplot(prop.table(table(df_all$Survived, df_all$Ticket)),
        legend = FALSE, col = colors, 
        main = "Survived by Ticket")
legend("topright", legend = c("Not Survived", "Survived"),
       fill = colors)



gtのチケットを持った人は生存者が多いことがわかります。

Fare

Fareをみてみましょう。これは乗船料ですね。
NAがあるかどうかを確認します。
is.na()関数で、NAならばTRUEを返して、sum()関数でTRUEの数を合計します。
Rでは、TRUEは1、FALSEは0としてsum()関数は処理します。

sum(is.na(df_all$Fare))
[1] 1

1つNAがありました。これは、中央値に置き換えます。

df_all$Fare[is.na(df_all$Fare)] <- median(df_all$Fare, na.rm = TRUE)

Fareの分布をヒストグラムにしてみます。

hist(df_all$Fare)

 

あらら、これは完全に右側の裾野が広い対数正規分布のような分布ですね。
summary()関数で最小値、平均値、中央値、最大値などと確認します。

summary(df_all$Fare)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.000   7.896  14.454  33.281  31.275 512.329 

最小値が0なので、そのまま対数変換はできないですね。
1を足してから対数変換してみようかと思います。
ただ、その前にヒストグラムで、対数変換したらどのような分布になるかを
確認します。

hist(log(df_all$Fare + 1, base = 10))

 

う~ん、ちょっと気に入らないです。
平方根ではどうでしょうか

hist(sqrt(df_all$Fare))

 

3乗根はどうでしょうか?

hist(df_all$Fare^(1 / 3))

 

悩みますね。。。
そもそも、なんでこんなに乗船料がいろいろなんでしょうね。
新幹線なら自由席、指定席、グリーン席の3種類だし、
飛行機ならエコノミー、プレミアムエコノミー、ビジネス、ファーストクラスの
4種類くらいですよね。。。

ここは思い切って、4分位にわけてカテゴリー化してしまいましょう。

まず、4分位の値を求めます。quantile()関数を使います。

yonbuni <- quantile(df_all$Fare,
                    probs = c(0, 0.25, 0.5, 0.75, 1))
yonbuni
      0%      25%      50%      75%     100% 
  0.0000   7.8958  14.4542  31.2750 512.3292 

このyonbuniに基づいてFareをカテゴリー化します。cut()関数を使います。

df_all$Fare <- cut(df_all$Fare,
                   breaks = yonbuni,
                   include.lowest = TRUE,
                   labels = c("economy", "pre_eco", "biz", "first"))
table(df_all$Fare)

economy pre_eco     biz   first 
    337     321     328     323 

飛行機のように、エコノミー、プレミアムエコノミー、ビジネス、ファーストクラスに
してみました。

Survivedとの関係を見てみます。

barplot(table(df_all$Survived, df_all$Fare),
        legend = TRUE)


やっぱり、エコノミークラスの乗客は生存者は少なく、

ファーストクラスの乗客は生存者は多いですね。

Cabin

次はCabinです。NAの有無を確認します。

sum(is.na(df_all$Cabin))
[1] 0

NAはありませんでした。

Cabinも適当に20個くらいをみてみます。sample()関数で適当に20個表示します。

set.seed(3)
sample(df_all$Cabin, 20, replace = FALSE)
 [1] "E77" ""    ""    ""    ""    ""    ""    ""    ""    "B18" ""    "D37"
[13] ""    ""    ""    ""    ""    ""    ""    ""   

“” というのが多いですね。
“” が何個あるか数えてみます。

sum(df_all$Cabin == "")
[1] 1014

1014もあります。df_allは全部で1309の観測数ですので、大半が “” です。

Cabinは、“” とそれ以外 のダミー変数にしましょう。

df_all$Cabin <- ifelse(df_all$Cabin == "", 0, 1)
table(df_all$Cabin)

   0    1 
1014  295 

“” は0、その他は1のダミー変数に変換しました。

Survivedとの関係をみてみます。

tab <- table(df_all$Cabin, df_all$Survived)
names(dimnames(tab)) <- c("Cabin", "Survived")
tab
     Survived
Cabin   0   1
    0 481 206
    1  68 136

Cabinが0の人はSurvivedが1の人が多く、Cabinが1の人はSurvivedが1の人が多いです。

カイ二乗検定で、この関係が偶然でなく、CabinとSurvivedが統計的に有意に
関係しているかを確認します。

chisq.test(tab)

    Pearson's Chi-squared test with Yates' continuity correction

data:  tab
X-squared = 87.941, df = 1, p-value < 2.2e-16

p-value < 2.23-16 ですので、CabinとSurvivedは関連あるのは間違いないようです。

グラフで視覚化もしてみます。

barplot(tab,
        main = "Survived by Cabin",
        legend = TRUE)


左棒グラフがSurvived = 0です。黒い色、Cabin = 0が

圧倒的に多いことがわかります。

Embarked

いよいよ最後の変数、Embarkedまでたどり着きました。
いつものように、NAの有無を確認しましょう。
NAの有無はEDA(Exploratory Data Analysis)の「いろはの”い”」ですよね。

sum(is.na(df_all$Embarked))
[1] 0

NAは無いようです。
tabled()関数で度数を確認します。

table(df_all$Embarked)

      C   Q   S 
  2 270 123 914 

あれ、なんか、空白のものが2個ありますね。
これは、一番度数の多いSにしてしまいます。

idx <- df_all$Embarked %in% c("C", "Q", "S") # 空白の位置を特定
df_all$Embarked[!idx] # idxが上手く動いているか確認
[1] "" ""
df_all$Embarked[!idx] <- "S" # ""をSに置換
table(df_all$Embarked) # 結果確認

  C   Q   S 
270 123 916 

うまく空白のものをSに置換できました。
EmbarkedはC, Q, Sの3種類なので、ファクター型に変換します。

df_all$Embarked <- as.factor(df_all$Embarked)
summary(df_all$Embarked)
  C   Q   S 
270 123 916 

ファクター型にすると、summery()関数でもtable()関数のように度数がわかります。

Survivedとの関係をみてみます。

tab <- table(df_all$Embarked, df_all$Survived)
names(dimnames(tab)) <- c("Embarked", "Survived")
tab
        Survived
Embarked   0   1
       C  75  93
       Q  47  30
       S 427 219

Cの乗客はSurvivedが1の人が多いことがわかります。

prop.table()関数で比率のテーブルにしたほうがわかりやすいです。

round(prop.table(tab, margin = 1), 2)
        Survived
Embarked    0    1
       C 0.45 0.55
       Q 0.61 0.39
       S 0.66 0.34

margin = 1 で横方向の比率の合計値が1になるようにして、
round()関数で小数点以下2桁まえの表示にしました。

Cの乗客は生存率は55%です。

カイ二乗検定してみます。

chisq.test(tab)

    Pearson's Chi-squared test

data:  tab
X-squared = 25.964, df = 2, p-value = 2.301e-06

p-valueは2.301e-06 ですので、EmbarkedとSurvivedは関連ありそうです。

ちなみに、format()関数で、scientific = FALSE にすると、
2.301e-06をe-06を使わないで表示できます。

format(2.301e-06, scientific = FALSE)
[1] "0.000002301"

これで、全ての変数のEDAが終了しました。

その他のEDA

いままでで、全ての変数のEDAとデータの修正が終了しました。
summary()関数で全体を把握しておきましょう。

summary(df_all)
  PassengerId      Survived      Pclass      Sex           Age        Ticket  
 Min.   :   1   Min.   :0.0000   1:323   female:466   Min.   : 0.17   bt:723  
 1st Qu.: 328   1st Qu.:0.0000   2:277   male  :843   1st Qu.:21.00   gt:586  
 Median : 655   Median :0.0000   3:709                Median :26.00           
 Mean   : 655   Mean   :0.3838                        Mean   :29.30           
 3rd Qu.: 982   3rd Qu.:1.0000                        3rd Qu.:36.50           
 Max.   :1309   Max.   :1.0000                        Max.   :80.00           
                NA's   :418                                                   
      Fare         Cabin        Embarked     title         agena       
 economy:337   Min.   :0.0000   C:270    Master.: 59   Min.   :0.0000  
 pre_eco:321   1st Qu.:0.0000   Q:123    Miss.  :256   1st Qu.:0.0000  
 biz    :328   Median :0.0000   S:916    Mr.    :736   Median :0.0000  
 first  :323   Mean   :0.2254            Mrs.   :191   Mean   :0.2009  
               3rd Qu.:0.0000            others : 67   3rd Qu.:0.0000  
               Max.   :1.0000                          Max.   :1.0000  
                                                                       
   family   
 alone:790  
 small:437  
 large: 82  
            
            
            
            

SexとTicketはファクター型ですが、どちらも水準は2つしかありません。
なので、Sexはmaleなら1、femaleなら0のダミー変数に、
Ticketはgtなら1、btなら0のダミー変数にしましょう。

df_all$male <- ifelse(df_all$Sex == "male", 1, 0)
df_all$gt <- ifelse(df_all$Ticket == "gt", 1, 0)
df_all$Sex <- NULL
df_all$Ticket <- NULL
summary(df_all)
  PassengerId      Survived      Pclass       Age             Fare    
 Min.   :   1   Min.   :0.0000   1:323   Min.   : 0.17   economy:337  
 1st Qu.: 328   1st Qu.:0.0000   2:277   1st Qu.:21.00   pre_eco:321  
 Median : 655   Median :0.0000   3:709   Median :26.00   biz    :328  
 Mean   : 655   Mean   :0.3838           Mean   :29.30   first  :323  
 3rd Qu.: 982   3rd Qu.:1.0000           3rd Qu.:36.50                
 Max.   :1309   Max.   :1.0000           Max.   :80.00                
                NA's   :418                                           
     Cabin        Embarked     title         agena          family   
 Min.   :0.0000   C:270    Master.: 59   Min.   :0.0000   alone:790  
 1st Qu.:0.0000   Q:123    Miss.  :256   1st Qu.:0.0000   small:437  
 Median :0.0000   S:916    Mr.    :736   Median :0.0000   large: 82  
 Mean   :0.2254            Mrs.   :191   Mean   :0.2009              
 3rd Qu.:0.0000            others : 67   3rd Qu.:0.0000              
 Max.   :1.0000                          Max.   :1.0000              
                                                                     
      male             gt        
 Min.   :0.000   Min.   :0.0000  
 1st Qu.:0.000   1st Qu.:0.0000  
 Median :1.000   Median :0.0000  
 Mean   :0.644   Mean   :0.4477  
 3rd Qu.:1.000   3rd Qu.:1.0000  
 Max.   :1.000   Max.   :1.0000  
                                 

SexとTicketは必要ないので、削除しています。

Survived, Age, Cabin, agena, male, gtの5つは数値データです。 この数値データ同士の相関係数をみてみます。

ok_cols <- c("Survived", "Age", "Cabin", "agena", "male", "gt")
ok_rows <- !is.na(df_all$Survived)
round(cor(df_all[ok_rows , ok_cols]), 2)
         Survived   Age Cabin agena  male    gt
Survived     1.00 -0.06  0.32 -0.09 -0.54  0.31
Age         -0.06  1.00  0.28 -0.07  0.10  0.22
Cabin        0.32  0.28  1.00 -0.14 -0.14  0.43
agena       -0.09 -0.07 -0.14  1.00  0.06 -0.06
male        -0.54  0.10 -0.14  0.06  1.00 -0.12
gt           0.31  0.22  0.43 -0.06 -0.12  1.00

被説明変数のSurvivedと相関が強いのは、
正の相関ではCabin, gt
負の相関ではmaleです。

Ageだけが連続数値データですが
AgeとCabinの相関は0.28, Ageとgtの相関は0.22, Ageとmaleの相関は0.10と
どれもそんなに相関は強くありません。

ですので、Age x Cabin, Age x gt, Age x maleの交差項も作成しておきます。

df_all$AgeCabin <- df_all$Age * df_all$Cabin
df_all$Agegt <- df_all$Age * df_all$gt
df_all$Agemale <- df_all$Age * df_all$male
summary(df_all)
  PassengerId      Survived      Pclass       Age             Fare    
 Min.   :   1   Min.   :0.0000   1:323   Min.   : 0.17   economy:337  
 1st Qu.: 328   1st Qu.:0.0000   2:277   1st Qu.:21.00   pre_eco:321  
 Median : 655   Median :0.0000   3:709   Median :26.00   biz    :328  
 Mean   : 655   Mean   :0.3838           Mean   :29.30   first  :323  
 3rd Qu.: 982   3rd Qu.:1.0000           3rd Qu.:36.50                
 Max.   :1309   Max.   :1.0000           Max.   :80.00                
                NA's   :418                                           
     Cabin        Embarked     title         agena          family   
 Min.   :0.0000   C:270    Master.: 59   Min.   :0.0000   alone:790  
 1st Qu.:0.0000   Q:123    Miss.  :256   1st Qu.:0.0000   small:437  
 Median :0.0000   S:916    Mr.    :736   Median :0.0000   large: 82  
 Mean   :0.2254            Mrs.   :191   Mean   :0.2009              
 3rd Qu.:0.0000            others : 67   3rd Qu.:0.0000              
 Max.   :1.0000                          Max.   :1.0000              
                                                                     
      male             gt            AgeCabin          Agegt      
 Min.   :0.000   Min.   :0.0000   Min.   : 0.000   Min.   : 0.00  
 1st Qu.:0.000   1st Qu.:0.0000   1st Qu.: 0.000   1st Qu.: 0.00  
 Median :1.000   Median :0.0000   Median : 0.000   Median : 0.00  
 Mean   :0.644   Mean   :0.4477   Mean   : 8.353   Mean   :14.84  
 3rd Qu.:1.000   3rd Qu.:1.0000   3rd Qu.: 0.000   3rd Qu.:30.00  
 Max.   :1.000   Max.   :1.0000   Max.   :80.000   Max.   :80.00  
                                                                  
    Agemale     
 Min.   : 0.00  
 1st Qu.: 0.00  
 Median :22.00  
 Mean   :19.33  
 3rd Qu.:30.00  
 Max.   :80.00  
                

これでEDAは終わりです。
次はモデル構築になります。

モデル構築

train_X, train_Y, test_Xの作成

glmnetでモデルを構築する関数は、glmnet()関数、cv.glmnet()関数です。
どちらもデータフレームではなくて、マトリックス型のオブジェクトしか
処理できないので、はじめに、df_allというデータフレームから、
train_X, train_Y, test_Xというマトリックス型のオブジェクト作成する
必要があります。

トレーニング用データとテスト用のデータを分ける行のインデックスは
ok_rowsというインデックスを作成してありますので、
Xの列を指定するベクトルを作成します。

model_cols <- c("Pclass", "Age", "Fare", "Cabin", "Embarked", "title",
                "agena", "family", "male", "gt", "AgeCabin", "Agegt",
                "Agemale")

glmnetパッケージにある、make()関数でデータフレームからマトリックス型を
生成できます。

train_X <- makeX(df_all[ok_rows, model_cols])
test_X <- makeX(df_all[!ok_rows, model_cols])
train_Y <- df_all[ok_rows, "Survived"]

head()関数ではじめの数行、数個のデータを表示して、確認します。

head(train_X)
  Pclass1 Pclass2 Pclass3 Age Fareeconomy Farepre_eco Farebiz Farefirst Cabin
1       0       0       1  22           1           0       0         0     0
2       1       0       0  38           0           0       0         1     1
3       0       0       1  26           0           1       0         0     0
4       1       0       0  35           0           0       0         1     1
5       0       0       1  35           0           1       0         0     0
6       0       0       1  26           0           1       0         0     0
  EmbarkedC EmbarkedQ EmbarkedS titleMaster. titleMiss. titleMr. titleMrs.
1         0         0         1            0          0        1         0
2         1         0         0            0          0        0         1
3         0         0         1            0          1        0         0
4         0         0         1            0          0        0         1
5         0         0         1            0          0        1         0
6         0         1         0            0          0        1         0
  titleothers agena familyalone familysmall familylarge male gt AgeCabin Agegt
1           0     0           0           1           0    1  0        0     0
2           0     0           0           1           0    0  1       38    38
3           0     0           1           0           0    0  0        0     0
4           0     0           0           1           0    0  1       35    35
5           0     0           1           0           0    1  0        0     0
6           0     1           1           0           0    1  0        0     0
  Agemale
1      22
2       0
3       0
4       0
5      35
6      26
head(test_X)
    Pclass1 Pclass2 Pclass3  Age Fareeconomy Farepre_eco Farebiz Farefirst
892       0       0       1 34.5           1           0       0         0
893       0       0       1 47.0           1           0       0         0
894       0       1       0 62.0           0           1       0         0
895       0       0       1 27.0           0           1       0         0
896       0       0       1 22.0           0           1       0         0
897       0       0       1 14.0           0           1       0         0
    Cabin EmbarkedC EmbarkedQ EmbarkedS titleMaster. titleMiss. titleMr.
892     0         0         1         0            0          0        1
893     0         0         0         1            0          0        0
894     0         0         1         0            0          0        1
895     0         0         0         1            0          0        1
896     0         0         0         1            0          0        0
897     0         0         0         1            0          0        1
    titleMrs. titleothers agena familyalone familysmall familylarge male gt
892         0           0     0           1           0           0    1  0
893         1           0     0           0           1           0    0  0
894         0           0     0           1           0           0    1  1
895         0           0     0           1           0           0    1  0
896         1           0     0           0           1           0    0  0
897         0           0     0           1           0           0    1  0
    AgeCabin Agegt Agemale
892        0     0    34.5
893        0     0     0.0
894        0    62    62.0
895        0     0    27.0
896        0     0     0.0
897        0     0    14.0
head(train_Y)
[1] 0 1 1 1 0 0

うまくできましたね。

Ridge, LASSO, Elastic-Net

glmnet()関数、cv.glmnet()関数でモデルの学習をするとき、
alphaというパラメータがあります。

alpha = 0 だとRidge,
alpha = 1 だとLASSO,
alpha が0より大きく1より小さいとElastic-Netになります。

数式を使わずに雰囲気で説明すると、
Ridgeは全ての変数を残し、係数を縮小する。
LASSOは不要な変数の係数を0にする。
Elastic-NetはRidgeとLASSOのいいとこどり。
ということです。

百聞は一見に如かず、といいますから、ちょっとやってみます。

Ridge(alpha = 0)

rid_mod <- glmnet(train_X, train_Y, family = "binomial",
                  alpha = 0)
plot(rid_mod, xvar = "lambda", label = TRUE)

 

右から左へと-Log(λ)が小さくなるにつれて、それぞれの変数の係数が0に
近づいているのがわかります。

13がプラスの係数では一番大きい値からスタートして、
21がマイナスの係数では一番(絶対値が)大きい値からスタートしています。

どんな変数か確認します。

colnames(train_X)[c(13, 21)]
[1] "titleMaster." "familylarge" 

titleMaster.とfamilylargeです。titleがMaster.の人は生存しやすく、
familyがlargeの人は生存しにくい、ということが読み取れます。

LASSO(alpha = 1)

las_mod <- glmnet(train_X, train_Y, family = "binomial",
                  alpha = 1)
plot(las_mod, xvar = "lambda", label = TRUE)



-Log(λ)が小さくなるにつれて、変数の係数が0になり、モデルから脱落している
ことがわかります。22が一番最後まで生き残っています。

colnames(train_X)[22]
[1] "male"

22番目の変数はmaleでした。やはり、男性か女性かが一番重要なようです。

Elasitic-Net(alpha = 0.5) とりあえず、alpha = 0.5でやってみます。

ela_mod <- glmnet(train_X, train_Y, family = "binomial",
                  alpha = 0.5)
plot(ela_mod, xvar = "lambda", label = TRUE)

 

Elastic-Netでも-Log(λ)が小さくなるにつれて、生き残る係数が減っていくことが
わかります。

予測の実験(lambda.minとlambda.1seの比較)

glmnetパッケージでモデルを学習して、予測をする、という一連の流れを
確認しましょう。

まず、train_X、train_Yをモデル学習用、テスト用に分けます。

set.seed(1000)
train_idx <- sample(1:nrow(train_X), nrow(train_X) * 0.7,
                    replace = FALSE)

これで、train_idxの行の観測値はトレーニング用のデータ、それ以外は
テスト用のデータにわかれます。

そうしたら、cv.glmnet()関数でモデルを学習します。
とりあえず、alpha = 0.5 にします。

set.seed(1)
fit <- cv.glmnet(train_X[train_idx, ], train_Y[train_idx],
                 alpha = 0.5,
                 family = "binomial")

fitというモデルが学習できました。
plot()関数でfitをグラフにしてみます。

plot(fit)

 

モデルの係数を確認します。

cbind(
  coef(fit, s = "lambda.min"),
  coef(fit, s = "lambda.1se")
)
27 x 2 sparse Matrix of class "dgCMatrix"
              lambda.min  lambda.1se
(Intercept)   0.70719960  0.52656761
Pclass1       0.47091782  0.25199065
Pclass2       .           .         
Pclass3      -0.37590315 -0.28650832
Age           .           .         
Fareeconomy  -0.10083510  .         
Farepre_eco   .           .         
Farebiz       .           .         
Farefirst     0.06982751  .         
Cabin         0.35230895  0.50553269
EmbarkedC     .           .         
EmbarkedQ     0.55073495  .         
EmbarkedS     .           .         
titleMaster.  1.82097365  0.81591013
titleMiss.    .           .         
titleMr.     -0.76434703 -0.75918553
titleMrs.     0.23646668  0.09520891
titleothers  -0.44546642  .         
agena        -0.12004937  .         
familyalone   .           .         
familysmall   .           0.04734862
familylarge  -1.87519618 -0.94110635
male         -1.14992767 -0.89855394
gt            0.57980031  0.37290835
AgeCabin      0.01700343  0.00680552
Agegt         .           .         
Agemale      -0.03368960 -0.02389263

s = “lambda.min”のほうは、クロスバリデーションで最も良い性能(最小のCV誤差)
を示したモデルに対応する λ です。
s = “lambda.1se”のほうは、lambda.min と比較してCV誤差が
1標準誤差以内に収まり、かつより大きなλ(=より単純なモデル)
を選択するための値です。
一般に、lambda.1se で得られるモデルは正則化が強く、過学習しにくいため、
未知データに対する予測性能が安定しやすい傾向があります。

このfitを使って、予測します。

# s = lambda.minのモデルの予測
pred_min <- predict(fit, train_X[-train_idx, ], type = "class",
                    s = "lambda.min") 

# s = lambda.1seのモデルの予測
pred_1se <- predict(fit, train_X[-train_idx, ], type = "class",
                    s = "lambda.1se")

table()関数で答え合わせをします。

table(pred_min, train_Y[-train_idx])
        
pred_min   0   1
       0 135  36
       1  15  82
table(pred_1se, train_Y[-train_idx])
        
pred_1se   0   1
       0 133  36
       1  17  82

このケースでは、s = lambda.minのモデルのほうが135+82=217個正解で
s = lambda.1seのモデルが133+82=215個正解ですので、lambda.minのほうが
未知のデータに対しても予測結果はよかったですね。

でも、この中のset.seed()関数の中の数字を変えてみると、違う結果になります。
set.seed()関数は乱数のタネを決める関数です。

set.seed(6) # <- ここを6に変更しました。
train_idx <- sample(1:nrow(train_X), nrow(train_X) * 0.7,
                    replace = FALSE)

set.seed(4) # <- ここを4に変更しました。
fit <- cv.glmnet(train_X[train_idx, ], train_Y[train_idx],
                 alpha = 0.5,
                 family = "binomial")

# s = lambda.minのモデルの予測
pred_min <- predict(fit, train_X[-train_idx, ], type = "class",
                    s = "lambda.min") 

# s = lambda.1seのモデルの予測
pred_1se <- predict(fit, train_X[-train_idx, ], type = "class",
                    s = "lambda.1se")

# s = lambda.minの予測結果
table(pred_min, train_Y[-train_idx])
        
pred_min   0   1
       0 152  29
       1  17  70
# s = lambda.1seの予測結果
table(pred_1se, train_Y[-train_idx])
        
pred_1se   0   1
       0 154  27
       1  15  72

train_idxを作るときのset.seed()を6に、
cv.glmnet()を走らせるときのset.seed()を4に変えてみたところ、
s = lambda.minの正解は、152+70=222個。
s = lambda.1seの正解は、154+72=226個となり、lambda.1seのほうが良いです。

モデルは、同じ性能ならば単純であればあるほど良いモデルですので、
s = lambda.1seのほうが未知のデータに対しては、良いモデルであると言えます。

alphaの選択

cv.glmnet()関数は、lambdaの水準はCV(クロスバリデーション)で調べてくれますが、
alphaの水準は、自分で決めないといけません。

alphaの水準を入力 >>> cv.glmnet()関数でモデル作成 >>> 予測結果
こんな感じで関数を作ってみます。

alpha_check <- function(x, seed) { # xはalpha, seedはset.seed

# 学習用のインデックスを作成  
set.seed(seed)
train_idx <- sample(1:nrow(train_X), nrow(train_X) * 0.7,
                    replace = FALSE)

# モデル学習
set.seed(seed) 
fit <- cv.glmnet(train_X[train_idx, ], train_Y[train_idx],
                 alpha = x,
                 family = "binomial")

# s = lambda.1seのモデルの予測
pred_1se <- predict(fit, train_X[-train_idx, ], type = "class",
                    s = "lambda.1se")

# s = lambda.minの予測結果(正解率)
mean(pred_1se == train_Y[-train_idx])
  
}

alpha_check()関数というものを作りました。
alphaの水準とset.seed()の値を入力すると、cv.glmnet()関数でモデルを作り
s = “lambda.1se” での予測の正解率を出力する関数です。

エラーなく動くか試してみます。

alpha_check(0.5, 1)
[1] 0.8246269
alpha_check(0.5, 2)
[1] 0.8134328
alpha_check(0.5, 3)
[1] 0.8656716

うまく動いていますね。

次は、alpha = 0.1 で、set.seed()を1~100まで試して、
その平均値を計算してみましょう

results_0.1 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.1[i] <- alpha_check(0.1, i)
}

# 正解率の平均値
mean(results_0.1)
[1] 0.831306

83.1%がalpha=0.1で100試したときの平均正解率です。

alpha = 0.2 ではどうでしょうか?

results_0.2 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.2[i] <- alpha_check(0.2, i)
}

# 正解率の平均値
mean(results_0.2)
[1] 0.8295149

83.0%がalpha=0.2で100回試したときの平均正解率です。

alpha = 0.3 ではどうでしょうか?

results_0.3 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.3[i] <- alpha_check(0.3, i)
}

# 正解率の平均値
mean(results_0.3)
[1] 0.8286194

82.9%でした。

alpha = 0.4 ではどうでしょうか?

results_0.4 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.4[i] <- alpha_check(0.4, i)
}

# 正解率の平均値
mean(results_0.4)
[1] 0.8280597

正解率は82.8%でした。

alpha = 0.5 ではどうでしょうか?

results_0.5 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.5[i] <- alpha_check(0.5, i)
}

# 正解率の平均値
mean(results_0.5)
[1] 0.828097

正解率は82.8%でした。

alpha = 0.6 ではどうでしょうか?

results_0.6 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.6[i] <- alpha_check(0.6, i)
}

# 正解率の平均値
mean(results_0.6)
[1] 0.8280224

正解率は82.8%でした。

alpha = 0.7ではどうでしょうか?

results_0.7 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.7[i] <- alpha_check(0.7, i)
}

# 正解率の平均値
mean(results_0.7)
[1] 0.8272015

正解率は82.7%でした。

alpha = 0.8ではどうでしょうか?

results_0.8 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.8[i] <- alpha_check(0.8, i)
}

# 正解率の平均値
mean(results_0.8)
[1] 0.8272015

正解率は82.7%でした。

alpha = 0.9ではどうでしょうか?

results_0.9 <- numeric(0) # 結果を入れるベクトル

# for loopで100回計算
for (i in 1:100) {
  results_0.9[i] <- alpha_check(0.9, i)
}

# 正解率の平均値
mean(results_0.9)
[1] 0.8262687

正解率は82.6%でした。

alpha = 0.1 ~ 0.9まで試したところ、alpha = 0.1が一番正解率が高かったです。

lambdaの選択

cv.glmnet()関数では、通常、約100個のlambdaを試します。
今回は、300個のlambdaを試すようにしてみます。

# lambdaの300個のシークエンスを作成
lambda_seq <- exp(seq(-4, 4, length.out = 300))

set.seed(1)
fit <- cv.glmnet(train_X, train_Y, 
                 family = "binomial",
                 alpha = 0.1,
                 lambda = lambda_seq,
                 nfolds = 10
                 )

plot()関数でグラフにしてみます。

plot(fit)

 

-Log(λ)が -1よりも小さいところは、選択された変数が0になっていますね。
少し範囲を変えて、0から5の範囲でもう一度やってみます。

# lambdaの300個のシークエンスを作成
lambda_seq <- exp(seq(-5, 0, length.out = 300))

set.seed(1)
fit <- cv.glmnet(train_X, train_Y, 
                 family = "binomial",
                 alpha = 0.1,
                 lambda = lambda_seq,
                 nfolds = 10
                 )

plot(fit)

 

モデルの係数を確認します。

cbind(
  coef(fit, s = "lambda.1se"),
  coef(fit, s = "lambda.min")
)
27 x 2 sparse Matrix of class "dgCMatrix"
               lambda.1se   lambda.min
(Intercept)   0.526590835  0.918469472
Pclass1       0.342299340  0.482861436
Pclass2       .            .          
Pclass3      -0.398277481 -0.487163822
Age          -0.004183394 -0.011106036
Fareeconomy  -0.101161027 -0.255962115
Farepre_eco   .           -0.124329754
Farebiz       .            0.096258985
Farefirst     0.112332513  0.288922389
Cabin         0.423721490  0.552693847
EmbarkedC     0.028737394  .          
EmbarkedQ     0.003075255  0.191153051
EmbarkedS    -0.148921485 -0.195027781
titleMaster.  0.822192007  1.634072238
titleMiss.    0.378168715  0.339783040
titleMr.     -0.672372460 -0.811591741
titleMrs.     0.491161847  0.680165743
titleothers  -0.258136326 -0.578104210
agena        -0.042845860 -0.224342346
familyalone   .            0.293335274
familysmall   0.095551331  .          
familylarge  -1.217302805 -2.231859167
male         -0.706278918 -1.000646939
gt            0.407945591  0.551208462
AgeCabin      0.003963810  0.004090083
Agegt         0.001937733  0.001581456
Agemale      -0.019313711 -0.023996678

alpha = 0.1 とRidge回帰に近いモデルですので、多くの変数が残っています。

s = “lambda.1se” の係数をグラフにしてみます。

# グラフのデータ
variables <- coef(fit, s = "lambda.1se") # 係数のマトリックス
var_values <- as.vector(variables)[-1] # 係数の数値(Interceptは除外)
var_names <- rownames(variables)[-1] # 係数の名前
top_values <- var_values[abs(var_values) > 0.2] # abs(0.2)より大きい係数のみ
top_names <- var_names[abs(var_values) > 0.2] # abs(0.2)より大きい変数名

# グラフ作成
par(mar = c(4, 12, 4, 2))  # 左側の余白を広げる(横向き用)

barplot(
  top_values,
  names.arg = top_names,
  las = 1,        # 横向きラベル
  horiz = TRUE,    # ★ これで 90 度回転(横棒グラフ)
  main = "モデルの係数"
)

 

par(mar = c(5.1, 4.1, 4.1, 2.1)) # 初期設定に戻す

family-largeやmale, title-Mr.などは生存にはマイナスの影響、
title-Mrs.やtitle-Miss., title-Master.などは生存にプラスの影響だとわかります。

テストデータで予測

cv.glmnet()関数でモデルができました。
いよいよ、テストデータで予測です。

predict()関数を使います。

pred <- predict(fit, newx = test_X, type = "class",
                s = "lambda.1se")
head(pred)
    lambda.1se
892 "0"       
893 "1"       
894 "0"       
895 "0"       
896 "1"       
897 "0"       

これをCSVファイルに出力します。
data.frame()関数でデータフレームを作成して、
write.csv()関数でCSVファイルとして出力します。

results <- data.frame(
  PassengerId = rownames(pred),
  Survived = pred[, 1]
)
head(results)
    PassengerId Survived
892         892        0
893         893        1
894         894        0
895         895        0
896         896        1
897         897        0
write.csv(results, "submit.csv", row.names = FALSE)

Kaggleに提出してみたら、スコアは。。。

Kaggleでの結果は、0.77990 でした。
base R と glmnetで挑んだスコアとしては、まあまあの結果かなと思います。

これでおしまいです。
読んでいただき、ありがとうございました。