题外话:想着更早的接触到数据竞赛,就可以更早的学到理论,练到技术,拿到名次,赚得奖金~功利心太重了,屡屡开始入门比赛,屡屡不了了之。加入俱乐部,发现大家本科就知道这些,已经参加了各种数据科学竞赛,并且有了不小的学习成就和心得体会,当然还有白花花的银子,然而,我没有在第一时间找到小白同学共同学习,没有抱紧大佬大腿努力学习,而是打算自己慢慢摸爬滚打入门,再找大家交流。结果自己没有摸出门道,提前放弃,一起的小伙伴也在通往大佬的路上越走越远,我还停在原地。再后来,了解到DataWhale这个神奇的开源组织,从街景字符识别开始,研究水哥、鱼佬给出的baseline,还是我不够努力,对于我而言,能弄懂baseline就挺费劲的了,在这个基础上,增加特征,我无从下手,也没有请教别人。归根结底,都是我懒惰,加急功近利的性子导致的,太害怕失败,害怕困难,而选择就此止步,可我又不甘如此,经常做着自我斗争。年轻人终归是要奋斗的,要拿出年轻人的朝气,在此替过去的自己对现在的自己说声抱歉。
在有了一定基础之后,再次看泰坦尼克号这个题目,觉得好简单。然而,还是有很多工作需要做,从数据分析,到如何建立一个有效的模型,再到调参分析等,都是新手需要钻研的。以下是处理数据分析比赛的一个基本思路:
1 分析赛题
直译:泰坦尼克号的沉没是历史上最臭名昭著的沉船事件之一。1912年4月15日,被广泛认为是永不沉没的皇家邮轮泰坦尼克号在处女航中与冰山相撞后沉没。不幸的是,船上没有足够的救生艇运载所有乘客,导致2224名乘客和船员中有1502人死亡。虽然生存有一些运气的因素,但似乎某些群体的人比其他人更有可能存活下来。在这次挑战中,我们要求你建立一个预测模型来回答以下问题:什么样的人更有可能存活?使用乘客资料(如姓名、年龄、性别、社会经济等级等)。
首先分析一下赛题,赛题定位是一个二分类问题,任务是通过train.csv
(已经标注label的乘客信息)来预测test.csv
(未标注label的乘客信息)中的label,其中label就是乘客是否存活,存活标记为1,未存活标记为0。然后我们需要将预测出来的结果存入submit.csv
中,包括乘客id
和label
即可。
Goal
It is your job to predict if a passenger survived the sinking of the Titanic or not.
For each in the test set, you must predict a 0 or 1 value for the variable.
Metric
Your score is the percentage of passengers you correctly predict. This is known as accuracy.
A
c
c
u
r
a
c
y
=
(
T
P
+
T
N
)
/
(
T
P
+
T
N
+
F
P
+
F
N
)
w
h
e
r
e
:
T
P
=
T
r
u
e
p
o
s
i
t
i
v
e
;
F
P
=
F
a
l
s
e
p
o
s
i
t
i
v
e
;
T
N
=
T
r
u
e
n
e
g
a
t
i
v
e
;
F
N
=
F
a
l
s
e
n
e
g
a
t
i
v
e
Accuracy = (TP + TN)/(TP + TN + FP + FN)\\ where: TP = True positive; FP = False positive; TN = True negative; FN = False negative
Accuracy=(TP+TN)/(TP+TN+FP+FN)where:TP=Truepositive;FP=Falsepositive;TN=Truenegative;FN=Falsenegative
Submission File Format
You should submit a csv
file with exactly 418 entries plus a header row. Your submission will show an error if you have extra columns (beyondPassengerId
and Survived
) or rows.
The file should have exactly 2 columns:
PassengerId
(sorted in any order)- Survived (contains your binary predictions: 1 for survived, 0 for deceased)
2 分析数据:
这一部分需要做的有以下几点:
- 读取数据,明确数据规模
shape
,统计数据的各项指标。- 然后进行特征理解分析,单特征分析,明确各个特征值的类型(数值类型/类别类型)以及取值;多变量统计分析,综合多个特征联合考虑对label的影响;数据绘图
- 针对以上的分析,对数据的缺失值、异常值查看并处理,连续数的标准化/归一化,特征重要性选择,特征间相关性
数据集字段解释:
pclass: A proxy for socio-economic status (SES)社会经济地位的代表
1st = Upper
2nd = Middle
3rd = Lower
age: Age is fractional if less than 1. If the age is estimated, is it in the form of xx.5
sibsp: The dataset defines family relations in this way...
Sibling = brother, sister, stepbrother, stepsister
Spouse = husband, wife (mistresses and fiancés were ignored)
parch: The dataset defines family relations in this way...
Parent = mother, father
Child = daughter, son, stepdaughter, stepson
Some children travelled only with a nanny(保姆), therefore parch=0 for them.
大体查看以下这个数据集,可以直接本地打开scv文件进行查看。是否获救,可能和社会地位有关,社会地位分为3个等级;性别包括男女,还有年龄(几个月到80多的都有),看电影中先救小孩和妇女,因此,这两个字段也可能会有关系;接下来兄弟姐妹数、父母孩子数,这两个数加起来就是整个家庭上船的人数;船票表示的票号,应该是没有关系的,而且票号数据很乱,一会暂不考虑;fare船票价格,对比pclass可以发现,1等人的船票都在几十甚至一百多,三等人的船票有几块的,这两个特征的相关性很强;船舱号cabin,这个特征缺失值太多,虽然有船舱号的大概都是有钱人吧,这个特征也考虑删除;embarked上船的港口,可以取3个值,这个值不知道是否和survived相关,一会需要处理一下。姓名这个特征中好像包含一些Mr,Miss等字段,可能会有用,同时性别特征应该能代表该特征,因此也暂时不考虑。
2.1 数据集统计分析:
在kaggle
中,赛题的数据都在../input
目录下存放着,我们可以通过os.walk(dir_path)
的方式遍历到目录dir_path
中的所有文件名,然后将根目录和文件名拼接起来就是数据文件的绝对路径了。
import os
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
# /kaggle/input/titanic/train.csv
# /kaggle/input/titanic/test.csv
# /kaggle/input/titanic/gender_submission.csv
知道数据集地址之后,使用pandas
读取数据集,pandas
进行数据分析的一些常用方法可以自己简单了解一下。pandas
读取的文件结果是DataFrame
类型,即二维表格类型;head()
返回的是前几条数据,tail()
返回的后几行数据,括号中可以填写你想查看的行数,默认是5。对于较大的数据集,我一般还会打印train_data.shape
,train_data.columns
查看数据大小和列名,方便分析每一列的属性。还可以通过train_data.describe()
表示对数据集的统计性分析,详细介绍看这里。.value_counts()
表示对每一个属性不同取值的计数;.count()
表示每一个属性的非零取值的计数,即value_counts()
的和
train_data = pd.read_csv("/kaggle/input/titanic/train.csv")
print(train_data.shape,train_data.columns)
"""
((891, 12), Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'], dtype='object'))
"""
train_data.head() # 图1
train_data.describe() # 图2,count表示非空值个数,后面的意思分别是均值、方差、最大最小值、百分位数
查看每一列的取值范围,train_data['Pclass'].unique()
可以得到array([3, 1, 2])
,即乘客的阶级为三个等级。用.value_counts()
可以得到每一个取值的样本数,通过查看len(train_data[i].unique())
得到不同属性的取值个数。同理,依次对每一个属性进行分析:
for i in train_data.columns:
print(i,len(train_data[i].unique()))
PassengerId 891 唯一性
Survived 2 {1,0}表示survive/die
Pclass 3 {1 = 1st, 2 = 2nd, 3 = 3rd}表示社会阶层
Name 891 唯一性
Sex 2 {male,female}表示男/女
Age 89 {0.0,...,80}表示年龄
SibSp 7 兄弟姐妹/配偶
Parch 7 父母/孩子
Ticket 681 船票号
Fare 248 票价
Cabin 148 客舱号(房间号)
Embarked 4 {C = Cherbourg, Q = Queenstown, S = Southampton}表示出发港(上船地点)
可视化更加明了:
for i in train_data.columns:
print(train_data[i].value_counts(),'\n')
0 549
1 342
Name: Survived, dtype: int64
3 491
1 216
2 184
Name: Pclass, dtype: int64
Name: Name, Length: 891, dtype: int64
male 577
female 314
Name: Sex, dtype: int64
24.00 30
22.00 27
18.00 26
19.00 25
30.00 25
..
55.50 1
70.50 1
66.00 1
23.50 1
0.42 1
Name: Age, Length: 88, dtype: int64
0 608
1 209
2 28
4 18
3 16
8 7
5 5
Name: SibSp, dtype: int64
0 678
1 118
2 80
5 5
3 5
4 4
6 1
Name: Parch, dtype: int64
CA. 2343 7
347082 7
1601 7
3101295 6
CA 2144 6
..
3101264 1
226875 1
330980 1
315096 1
28664 1
Name: Ticket, Length: 681, dtype: int64
8.0500 43
13.0000 42
7.8958 38
7.7500 34
26.0000 31
..
8.4583 1
9.8375 1
8.3625 1
14.1083 1
17.4000 1
Name: Fare, Length: 248, dtype: int64
G6 4
B96 B98 4
C23 C25 C27 4
E101 3
C22 C26 3
..
D28 1
D19 1
C111 1
C118 1
D6 1
Name: Cabin, Length: 147, dtype: int64
S 644
C 168
Q 77
Name: Embarked, dtype: int64
可以看到,以上字段中,PassengerId
和Name
都具有唯一性,Survived
取值{0,1}
,其余的字段意思请看上面代码输出中标注。
类别类型的特征:性别,等级
2.2 对于缺失值的处理:
查看是否有缺失值,对于缺失值的处理,其一,是用某种值去填充;其二,若该列不太重要,可以直接删除该列。在describe()
的描述中,可以看到年龄是714个非空值,有部分缺失;其余字符类型的字段没有显示,此处再次查看一下他们的缺失值:
for i in train_data.columns:
print(i,'\n',train_data[i].count())
# Age 714; Cabin 204; Embarked 889
可以看到船舱号Cabin
缺失占比较大,年龄Age
和出发港口Embarked
都有部分缺失;其中我认为船舱号和社会阶级Pclass
是分不开的,且由于cabin
取值类别太多,对缺失值不容易填充,因此直接删除该属性,暂时不做考虑。
其次,乘客ID和票号我觉得不具有参考价值,因此选择删除列;name中包含的信息较多,包括Mr/Mrs/master
等会蕴含一些信息,但是性别和票价并无缺失,应该可以代替这个属性,因此也删除该列。
目前,剩余有缺失的属性只有年龄和港口,若缺失值在总数据集占比不大的话,认为是坏样本,可以直接将有缺失值的行删除。age有将近20%的缺失值,embarked有两个缺失值。暂时先不处理,下面参考的博文直接删掉着20%,我觉得着数据量挺大的,怎么能瞎删呢?
2.3 测试集类比分析
同样步骤对测试集进行分析,数据集为(418,11)
,年龄的非缺失值有332条,同样缺失率为20%左右;fare有1条缺失值;缺失率最大的仍然是cabin
仅有91条,缺失率在78%左右。测试集是不可以删除行的,因此对fare和age进行填充,将cabin这一列去掉。如何填充需要对数据进行统计分析,如下是我将不同属性的取值较多的值粘贴的结果。同样是参考博客说fare的众数是7.75,因此直接用该值填充,我觉得众数并不明显,以下四个取值数差不多
#处理一下测试集里的缺失值,测试集的缺失数据不能删
#处理Fare,先看一下分布,发现明显有个众数非常突出,且训练集和测试集众数接近:
test_data['Fare']=test_data['Fare'].fillna(test_data['Fare'].dropna().mode()[0])
3 218
1 107
2 93
Name: Pclass, dtype: int64
male 266
female 152
Name: Sex, dtype: int64
24.0 17
21.0 17
22.0 16
30.0 15
18.0 13
Name: Age, Length: 79, dtype: int64
0 283
1 110
2 14
4 4
3 4
Name: SibSp, dtype: int64
0 324
1 52
2 33
Name: Parch, dtype: int64
7.7500 21
26.0000 19
8.0500 17
13.0000 17
Name: Fare, Length: 169, dtype: int64
S 270
C 102
Q 46
Name: Embarked, dtype: int64
2.3 数据模式探索/猜测
根据上面数据的统计结果,训练集中存活率为38%
,男女比例为:577:314
,
根据情境,当时看电影,女人和小孩先上船,由于年龄Age
信息缺失较大,我们先假设所有女性female
都存活了,为了验证我们的假设是否合理,因此,下面计算女性存活的比例:
women = train_data.loc[train_data.Sex == 'female']["Survived"] # 获得性别为“female”幸存者数据
rate_women = sum(women)/len(women) # 计算女性幸存者比率
print("ratio of women who survived:", rate_women) # 0.7420382165605095,同理,男性存活率0.1889081456
这就为我们提供了一个思路,在分析数据的时候,我们可以按照类别分析不同属性值所占的比例,查看样本分布是否均匀,分析出来对类别影响较大的特征。
train_data = train_data.drop(columns=['PassengerId','Cabin','Ticket','Name'])
for column in train_data.columns:
print(train_data.groupby('Survived')[column].value_counts()) # Series类型,[0]为died,[1]为survived
Survived Survived
0 0 549
1 1 342
Name: Survived, dtype: int64
Survived Pclass
0 3 372
2 97
1 80
1 1 136
3 119
2 87
Name: Pclass, dtype: int64
Survived Sex
0 male 468
female 81
1 female 233
male 109
Name: Sex, dtype: int64
Survived Age
0 21.0 19
28.0 18
18.0 17
25.0 17
19.0 16
..
1 43.0 1
47.0 1
53.0 1
55.0 1
80.0 1
Name: Age, Length: 142, dtype: int64
Survived SibSp
0 0 398
1 97
2 15
4 15
3 12
8 7
5 5
1 0 210
1 112
2 13
3 4
4 3
Name: SibSp, dtype: int64
Survived Parch
0 0 445
1 53
2 40
4 4
5 4
3 2
6 1
1 0 233
1 65
2 40
3 3
5 1
Name: Parch, dtype: int64
Survived Fare
0 8.0500 38
7.8958 37
13.0000 26
7.7500 22
26.0000 16
..
1 82.1708 1
83.4750 1
106.4250 1
108.9000 1
247.5208 1
Name: Fare, Length: 330, dtype: int64
Survived Embarked
0 S 427
C 75
Q 47
1 S 217
C 93
Q 30
Name: Embarked, dtype: int64
分析每一个字段:
- 阶级: 1 s t : 2 n d : 3 r d = 216 : 184 : 491 ≈ 2 : 2 : 5 1st:2nd:3rd=216:184:491\approx 2:2:5 1st:2nd:3rd=216:184:491≈2:2:5,死亡阶级人数比例: 1 s t : 2 n d : 3 r d = 80 : 97 : 372 ≈ 1 : 1 : 4 1st:2nd:3rd=80:97:372\approx 1:1:4 1st:2nd:3rd=80:97:372≈1:1:4,生存阶级人数比例: 1 s t : 2 n d : 3 r d = 136 : 87 : 119 ≈ 1.5 : 1 : 1.3 1st:2nd:3rd=136:87:119\approx 1.5:1:1.3 1st:2nd:3rd=136:87:119≈1.5:1:1.3,每一个阶级的生还比例: 3 r d = 24.24 % ; 2 n d = 47.28 % ; 1 s t = 62.96 % 3rd=24.24\%;2nd=47.28\%;1st=62.96\% 3rd=24.24%;2nd=47.28%;1st=62.96%,看来穷人生还比例最低。那是否穷人里面女性和儿童人数少呢?
- 年龄:生还的人年龄分布
import matplotlib.pyplot as plt
age_survived = train_data.groupby('Survived')['Age'].value_counts()[1]
plt.bar(age_survived.index,age_survived)
- 兄弟姐妹数:可以看出来,少的生还率高些,人数太多的生还率低
sib_sur_cnt = train_data.groupby('Survived')['SibSp'].value_counts()[1]
sib_all_cnt = train_data['SibSp'].value_counts()
for i in sib_all_cnt.index:
if i == 5 or i == 8:
sib_sur_cnt[i]=0
print('sibling number:',i,' survived rate:',sib_sur_cnt[i]/sib_all_cnt[i])
sibling number: 0 survived rate: 0.34539473684210525
sibling number: 1 survived rate: 0.5358851674641149
sibling number: 2 survived rate: 0.4642857142857143
sibling number: 4 survived rate: 0.16666666666666666
sibling number: 3 survived rate: 0.25
sibling number: 8 survived rate: 0.0
sibling number: 5 survived rate: 0.0
- 父母孩子:人数太多的生还率低
pc_sur_cnt = train_data.groupby('Survived')['Parch'].value_counts()[1]
pc_all_cnt = train_data['Parch'].value_counts()
# print(pc_all_cnt.index,pc_sur_cnt.index)
for i in pc_all_cnt.index:
if i == 4 or i == 6:
pc_sur_cnt[i]=0
print('parent-child number:',i,' survived rate:',pc_sur_cnt[i]/pc_all_cnt[i])
parent-child number: 0 survived rate: 0.34365781710914456
parent-child number: 1 survived rate: 0.5508474576271186
parent-child number: 2 survived rate: 0.5
parent-child number: 5 survived rate: 0.2
parent-child number: 3 survived rate: 0.6
parent-child number: 4 survived rate: 0.0
parent-child number: 6 survived rate: 0.0
- Fare
train_data.groupby('Pclass')['Fare'].sum()/train_data.groupby('Pclass')['Survived'].count()
# Pclass 各阶级人均车票钱: 1 84.154687; 2 20.662183; 3 13.675550
- 上车站:觉得这个影响因素本身就不大
parent-child number: S survived rate: 0.33695652173913043
parent-child number: C survived rate: 0.5535714285714286
parent-child number: Q survived rate: 0.38961038961038963
因此,在以上分析过程中,生还可能性与阶级、性别、年龄、家人数都有关系,只是关系强弱还未分析,目测与上车口关系不大,车票价钱其实在阶级上应该有所反应,应该联合起来分析一下。
然而,手动提取特征非常受限;因此,我们采用机器学习的方式进行自动选取特征,首先要学习的是“随机森林”,森林由多棵“决策树”构成,每一棵决策树会得到一个结果,最后采用“投票法”选择票数最高的特征作为结果。(一般的数据科学比赛最初都用的是树模型,比如较火的lightGBM,XgBoost,CatBoost等)
最近学到的新方式,不用上面自己去计数,然后算比例:
data.groupby(['sex','survived'])['survived'].count()
就可以得到不同性别中是否获救的人数data[['sex','survived']].groupby('sex').mean()
或者写为data.groupby('sex')['survived'].mean()
:可以得到按照性别分组的组内获救比例,可以用直方图或者柱状图可视化显示
3 随机森林
建模阶段:特征和标签准备,数据集切分,建立模型(初始化模型参数)
下面是’咖金’那里拷来的代码,直接用sklearn封装好的库好容易,但还是要深入学习原理,才能更好地利用它:
from sklearn.ensemble import RandomForestClassifier # 导入随机森林模块
# 获取用于预测的X,y
y = train_data["Survived"]
# 和上面分析的结果一致,年龄缺失较多,从图上分析,可能也没那么重要
features = ["Pclass", "Sex", "SibSp", "Parch"]
X = pd.get_dummies(train_data[features])
X_test = pd.get_dummies(test_data[features])
# 训练模型
model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=1)
model.fit(X, y)
predictions = model.predict(X_test)
# 输出预测并保存为本地文件
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('my_submission.csv', index=False)
print("Your submission was successfully saved!")
pd.get_dummies():在使用随机森林时,需要将非数值列转换成数值型或者one-hot编码,这个方法就是将feature中的Sex进行了onehot编码(或者可以用sklearn
的sklearn.preprocessing.OneHotEncoder
)
4 XGboost
5 参考博文
后记:
想把思路写的清楚一些,写完之后发现有些繁琐了,不够有条理,凑活能看,也是学习的过程,加油!