【文章系列】
第一章 初探Kaggle竞赛————Kaggle竞赛系列_Titanic比赛
第二章 知识补充_随机森林————数学建模系列_随机森林
第三章 知识补充_LightGBM————集成学习之Boosting方法系列_LightGBM
第四章 再战Kaggle竞赛————Kaggle竞赛系列_SpaceshipTitanic比赛
第五章 重温回顾_学习金牌方法_数据分析————Kaggle竞赛系列_SpaceshipTitanic金牌方案分析_数据分析
第六章 重温回顾_学习金牌方法_数据处理————Kaggle竞赛系列_SpaceshipTitanic金牌方案分析_数据处理
第七章 重温回顾_学习金牌方法_建模分析————Kaggle竞赛系列_SpaceshipTitanic金牌方案分析_建模分析
【前言】
Spaceship Titanic比赛,类似Titanic比赛,只是增加了更多的属性以及更大的数据量,仍是一个二分类问题。
今天要分析的是一篇大神的解决方案,看完后觉得干货满满,由衷地敬佩他们对数据分析的细致程度,对比之下只觉得之前自己的分析仅仅是表面功夫,单纯靠着模型的强大能力去完成任务。看来以后还是得不断地向各位前辈大佬学习,完善自己的解决方案!!!
【比赛简介】
Spaceship Titanic比赛是一个在Kaggle上举办的机器学习挑战,参赛者的任务是预测Spaceship Titanic在与时空异常碰撞时,哪些乘客被传送到了另一个维度。这个比赛提供了从飞船损坏的计算机系统中恢复的一组个人记录,参赛者需要使用这些数据来进行预测。
【回顾】
对数据的分析已经在上一篇博客中提到:Kaggle竞赛系列_SpaceshipTitanic金牌方案分析_数据分析
本篇博客主要对数据预处理的实际操作进行分析
【正文】
(一)缺失值处理
处理缺失值的最简单方法是对连续特征使用中位数,对分类特征使用众数。这将“足够好”地工作,但如果我们想最大限度地提高模型的准确性,那么我们需要在缺失的数据中寻找模式。要做到这一点,方法是通过观察特征的联合分布,例如,来自同一组的乘客是否往往来自同一家庭?
[!TIP]
通常将要填充的属性与先前划分的分组(eg:Group、CabinDeck、Surname……)进行联合分布查看规律
1. HomePlanet与Group
(1)联合分布
查看是否能找出HomePlanet的分布规律。
# Group与HomePlanet的联合分布
GHP_gb=data.groupby(['Group','HomePlanet'])['HomePlanet'].size().unstack().fillna(0)
GHP_gb.head()
补充:
.size()输出不同HomePlanet出现的频次
.unstack()将多级索引转换为二维表格,行是Group的唯一值,列是HomePlanet的唯一值
计算每个group中拥有不同唯一‘HomePlanet’数量的情况。
使用 seaborn 的
countplot
函数来绘制计数图。GHP_gb
是之前计算的联合分布表,GHP_gb > 0
会创建一个布尔值矩阵,其中为 True 表示对应的 ‘Group’ 拥有至少一个 ‘HomePlanet’,为 False 表示没有。然后,sum(axis=1)
对每个 ‘Group’ 计算了拥有至少一个 ‘HomePlanet’ 的数量。
# 唯一值的计数图
sns.countplot((GHP_gb>0).sum(axis=1))
plt.title('Number of unique home planets per group')
此图说明了同一个group的乘客都来自同一个Homeplanet,因此我们可以根据此信息来填充缺失的Homeplanet信息。
(2)缺失值填充
# 之前的缺失值
HP_bef=data['HomePlanet'].isna().sum()
# HomePlanet缺失的乘客和已知HomePlanet的乘客
GHP_index=data[data['HomePlanet'].isna()][(data[data['HomePlanet'].isna()]['Group']).isin(GHP_gb.index)].index
# 填充缺失值
data.loc[GHP_index,'HomePlanet']=data.iloc[GHP_index,:]['Group'].map(lambda x: GHP_gb.idxmax(axis=1)[x])
# 打印剩余缺失值的数目
print('#HomePlanet missing values before:',HP_bef)
print('#HomePlanet missing values after:',data['HomePlanet'].isna().sum())
.isin(GHP_gb.index)
是一个条件表达式,它检查Group
列中的每个元素是否在GHP_gb
数据框的索引中。GHP_index=data[data[‘HomePlanet’].isna()][(data[data[‘HomePlanet’].isna()][‘Group’]).isin(GHP_gb.index)].index 找到数据框
data
中,那些具有缺失值的HomePlanet
列并且它们的Group
列的值存在于GHP_gb
数据框的索引中的行的索引值。
idxmax()
方法返回包含最大值的元素的索引,如果有多个元素具有相同的最大值,则返回第一个最大值的索引。data.loc[GHP_index,‘HomePlanet’]=data.iloc[GHP_index,:][‘Group’].map(lambda x: GHP_gb.idxmax(axis=1)[x]):使用
.loc
方法将缺失的 ‘HomePlanet’ 值填充为相应 ‘Group’ 的最大 ‘HomePlanet’ 值,因为’HomePlanet’只有0与非零值,因此就是填充为非零的‘HomePlanet’值。
输出为:
初步处理了131个缺失值。
2. HomePlanet与其他属性
(1)CabinDeck
具体处理过程同上
# CabinDeck与HomePlanet的联合分布
CDHP_gb=data.groupby(['Cabin_deck','HomePlanet'])['HomePlanet'].size().unstack().fillna(0)
# 缺失值的热图分析
plt.figure(figsize=(10,4))
sns.heatmap(CDHP_gb.T, annot=True, fmt='g', cmap='coolwarm')
发现:A、B、C或T甲板上的乘客来自木卫二。G甲板上的乘客来自地球。D、E或F甲板上的乘客来自多个星球。
# 之前的缺失值
HP_bef=data['HomePlanet'].isna().sum()
# 甲板A、B、C或 T来自木卫二
data.loc[(data['HomePlanet'].isna()) & (data['Cabin_deck'].isin(['A', 'B', 'C', 'T'])), 'HomePlanet']='Europa'
# 甲板G 来自地球
data.loc[(data['HomePlanet'].isna()) & (data['Cabin_deck']=='G'), 'HomePlanet']='Earth'
# 打印剩余缺失值的数目
print('#HomePlanet missing values before:',HP_bef)
print('#HomePlanet missing values after:',data['HomePlanet'].isna().sum())
(2)Surname
# Surname和HomePlanet联合分布
SHP_gb=data.groupby(['Surname','HomePlanet'])['HomePlanet'].size().unstack().fillna(0)
# 唯一值的计数图
plt.figure(figsize=(10,4))
sns.countplot((SHP_gb>0).sum(axis=1))
plt.title('Number of unique planets per surname')
# 之前的缺失值
HP_bef=data['HomePlanet'].isna().sum()
# 缺失Homeplanet的乘客和已知Homeplanet的家庭成员
SHP_index=data[data['HomePlanet'].isna()][(data[data['HomePlanet'].isna()]['Surname']).isin(SHP_gb.index)].index
# 填充缺失值
data.loc[SHP_index,'HomePlanet']=data.iloc[SHP_index,:]['Surname'].map(lambda x: SHP_gb.idxmax(axis=1)[x])
# 打印剩余缺失值的数目
print('#HomePlanet missing values before:',HP_bef)
print('#HomePlanet missing values after:',data['HomePlanet'].isna().sum())
(3)Destination
# 只剩下10个HomePlanet缺失值
data[data['HomePlanet'].isna()][['PassengerId','HomePlanet','Destination']]
发现剩余缺失值的目的地都是TRAPPIST-1e。
# HomePlanet和Destination的联合分布
HPD_gb=data.groupby(['HomePlanet','Destination'])['Destination'].size().unstack().fillna(0)
# 缺失值的热图
plt.figure(figsize=(10,4))
sns.heatmap(HPD_gb.T, annot=True, fmt='g', cmap='coolwarm')
去往TRAPPIST-1e的大多来自地球,因此填充为地球。
# 之前的缺失值
HP_bef=data['HomePlanet'].isna().sum()
# 用地球(如果不在D甲板上)或火星(如果在D甲板上)填充剩余的HomePlanet缺失值,用地球(如果不在D甲板上)或火星(如果在D甲板上)填充剩余的HomePlanet缺失值
data.loc[(data['HomePlanet'].isna()) & ~(data['Cabin_deck']=='D'), 'HomePlanet']='Earth'
data.loc[(data['HomePlanet'].isna()) & (data['Cabin_deck']=='D'), 'HomePlanet']='Mars'
print('#HomePlanet missing values before:',HP_bef)
print('#HomePlanet missing values after:',data['HomePlanet'].isna().sum())
3. Surname与Group
(1)联合分布
# Group和Surname的联合分布
GSN_gb=data[data['Group_size']>1].groupby(['Group','Surname'])['Surname'].size().unstack().fillna(0)
# 唯一值的计数图
plt.figure(figsize=(10,4))
sns.countplot((GSN_gb>0).sum(axis=1))
plt.title('Number of unique surnames by group')
同一个Group中的乘客绝大多数都来自同一个Surname(家族)
(2)缺失值填充
# 之前的缺失值
SN_bef=data['Surname'].isna().sum()
# Surname缺失的乘客,在已知大多数Surname的group中
GSN_index=data[data['Surname'].isna()][(data[data['Surname'].isna()]['Group']).isin(GSN_gb.index)].index
# 填充缺失值
data.loc[GSN_index,'Surname']=data.iloc[GSN_index,:]['Group'].map(lambda x: GSN_gb.idxmax(axis=1)[x])
print('#Surname missing values before:',SN_bef)
print('#Surname missing values after:',data['Surname'].isna().sum())
根据作者所说,这已经是该属性缺失值所能处理的极限了,对于剩余缺失值,我们会将其丢弃。
# 用离群值替换NaN(这样我们就可以使用map)
data['Surname'].fillna('Unknown', inplace=True)
# 更新family size
data['Family_size']=data['Surname'].map(lambda x: data['Surname'].value_counts()[x])
# 把NaN放在异常值处
data.loc[data['Surname']=='Unknown','Surname']=np.nan
# 缺失的surname意味着没有family
data.loc[data['Family_size']>100,'Family_size']=0
4. CabinSide与Group
(1)联合分布
# Group和Cabin features的联合分布
GCD_gb=data[data['Group_size']>1].groupby(['Group','Cabin_deck'])['Cabin_deck'].size().unstack().fillna(0)
GCN_gb=data[data['Group_size']>1].groupby(['Group','Cabin_number'])['Cabin_number'].size().unstack().fillna(0)
GCS_gb=data[data['Group_size']>1].groupby(['Group','Cabin_side'])['Cabin_side'].size().unstack().fillna(0)
# 计数图
fig=plt.figure(figsize=(16,4))
plt.subplot(1,3,1)
sns.countplot((GCD_gb>0).sum(axis=1))
plt.title('#Unique cabin decks per group')
plt.subplot(1,3,2)
sns.countplot((GCN_gb>0).sum(axis=1))
plt.title('#Unique cabin numbers per group')
plt.subplot(1,3,3)
sns.countplot((GCS_gb>0).sum(axis=1))
plt.title('#Unique cabin sides per group')
fig.tight_layout()
发现同一个Group中的乘客都来自同一个CabinSIde。
(2)缺失值填充
# 之前的缺失值
CS_bef=data['Cabin_side'].isna().sum()
# Cabin_side缺失的乘客和Cabin_side已知的乘客
GCS_index=data[data['Cabin_side'].isna()][(data[data['Cabin_side'].isna()]['Group']).isin(GCS_gb.index)].index
# 填充缺失值
data.loc[GCS_index,'Cabin_side']=data.iloc[GCS_index,:]['Group'].map(lambda x: GCS_gb.idxmax(axis=1)[x])
print('#Cabin_side missing values before:',CS_bef)
print('#Cabin_side missing values after:',data['Cabin_side'].isna().sum())
5. CabinSide与其他属性
(1)Surname
# Surname和Cabin side的联合分布
SCS_gb=data[data['Group_size']>1].groupby(['Surname','Cabin_side'])['Cabin_side'].size().unstack().fillna(0)
SCS_gb['Ratio']=SCS_gb['P']/(SCS_gb['P']+SCS_gb['S'])
# 比率直方图
plt.figure(figsize=(10,4))
sns.histplot(SCS_gb['Ratio'], kde=True, binwidth=0.05)
plt.title('Ratio of cabin side by surname')
print('Percentage of families all on the same cabin side:', 100*np.round((SCS_gb['Ratio'].isin([0,1])).sum()/len(SCS_gb),3),'%')
SCS_gb.head()
说明同一家族的人大多数都分布在同一个CabinSIde。
# 之前的缺失值
CS_bef=data['Cabin_side'].isna().sum()
# 丢弃比率这个属性
SCS_gb.drop('Ratio', axis=1, inplace=True)
# Cabin_side缺失的乘客和已知Cabin_side的family
SCS_index=data[data['Cabin_side'].isna()][(data[data['Cabin_side'].isna()]['Surname']).isin(SCS_gb.index)].index
# 填充缺失值
data.loc[SCS_index,'Cabin_side']=data.iloc[SCS_index,:]['Surname'].map(lambda x: SCS_gb.idxmax(axis=1)[x])
# 丢弃surname这个属性(我们不再需要了)
data.drop('Surname', axis=1, inplace=True)
print('#Cabin_side missing values before:',CS_bef)
print('#Cabin_side missing values after:',data['Cabin_side'].isna().sum())
剩下的缺失值我们用离群值替代。
data['Cabin_side'].value_counts()
# 之前的缺失值
CS_bef=data['Cabin_side'].isna().sum()
# 用离群值填充剩余的缺失值
data.loc[data['Cabin_side'].isna(),'Cabin_side']='Z'
print('#Cabin_side missing values before:',CS_bef)
print('#Cabin_side missing values after:',data['Cabin_side'].isna().sum())
6. CabinDeck与其他属性
(1)Group
# 之前的缺失值
CD_bef=data['Cabin_deck'].isna().sum()
# Cabin_deck缺失的乘客和Cabin_deck已知的乘客
GCD_index=data[data['Cabin_deck'].isna()][(data[data['Cabin_deck'].isna()]['Group']).isin(GCD_gb.index)].index
# 填充缺失值
data.loc[GCD_index,'Cabin_deck']=data.iloc[GCD_index,:]['Group'].map(lambda x: GCD_gb.idxmax(axis=1)[x])
print('#Cabin_deck missing values before:',CD_bef)
print('#Cabin_deck missing values after:',data['Cabin_deck'].isna().sum())
(2)HomePlanet
# 联合分布
data.groupby(['HomePlanet','Destination','Solo','Cabin_deck'])['Cabin_deck'].size().unstack().fillna(0)
发现:来自火星的乘客很可能在F甲板。来自木卫二的乘客(或多或少)如果独自旅行,最有可能在C甲板,否则在B甲板。来自地球的乘客(或多或少)最有可能在G甲板。
# 之前的缺失值
CD_bef=data['Cabin_deck'].isna().sum()
# 用众数填充
na_rows_CD=data.loc[data['Cabin_deck'].isna(),'Cabin_deck'].index
data.loc[data['Cabin_deck'].isna(),'Cabin_deck']=data.groupby(['HomePlanet','Destination','Solo'])['Cabin_deck'].transform(lambda x: x.fillna(pd.Series.mode(x)[0]))[na_rows_CD]
print('#Cabin_deck missing values before:',CD_bef)
print('#Cabin_deck missing values after:',data['Cabin_deck'].isna().sum())
- na_rows_CD=data.loc[data[‘Cabin_deck’].isna(),‘Cabin_deck’].index 这一行代码找出了数据框
data
中“Cabin_deck”列所有缺失值(NaN)的行索引,并将这些索引存储在变量na_rows_CD
中。- data.loc[data[‘Cabin_deck’].isna(),‘Cabin_deck’]这行代码定位到数据框
data
中“Cabin_deck”列的缺失值。- .transform(lambda x: x.fillna(pd.Series.mode(x)[0]))
transform
方法对每个分组应用一个函数。这里使用的是一个lambda函数,该函数用每个组内“Cabin_deck”列的众数来填充NaN值。如果存在多个众数(即最频繁出现的值),pd.Series.mode(x)[0]
确保只使用第一个众数。
7. 其他属性分析
后续作者还对CabinNumber与Cabin_Deck、VIP、Age、CryoSleep、Expenditure等属性逐一进行了缺失值处理,由于篇幅原因我这里只对VIP、Age两个属性进行分析。
(1)VIP属性
因为该属性的分布高度不平衡,因此我们直接使用众数进行填充
data['VIP'].value_counts()
# 之前的缺失值
V_bef=data['VIP'].isna().sum()
# 填充
data.loc[data['VIP'].isna(),'VIP']=False
print('#VIP missing values before:',V_bef)
print('#VIP missing values after:',data['VIP'].isna().sum())
(2)Age属性
年龄因许多特征而异,如HomePlanet、group_size、No_spending和Cabin_deck,因此我们将根据这些子群体的中位数来推算缺失值。
# 联合分布
data.groupby(['HomePlanet','No_spending','Solo','Cabin_deck'])['Age'].median().unstack().fillna(0)
# 之前的缺失值
A_bef=data[exp_feats].isna().sum().sum()
# 中位数填充
na_rows_A=data.loc[data['Age'].isna(),'Age'].index
data.loc[data['Age'].isna(),'Age']=data.groupby(['HomePlanet','No_spending','Solo','Cabin_deck'])['Age'].transform(lambda x: x.fillna(x.median()))[na_rows_A]
print('#Age missing values before:',A_bef)
print('#Age missing values after:',data['Age'].isna().sum())
更新年龄分组属性
data.loc[data['Age']<=12,'Age_group']='Age_0-12'
data.loc[(data['Age']>12) & (data['Age']<18),'Age_group']='Age_13-17'
data.loc[(data['Age']>=18) & (data['Age']<=25),'Age_group']='Age_18-25'
data.loc[(data['Age']>25) & (data['Age']<=30),'Age_group']='Age_26-30'
data.loc[(data['Age']>30) & (data['Age']<=50),'Age_group']='Age_31-50'
data.loc[data['Age']>50,'Age_group']='Age_51+'
(二)数据预处理
1. 划分训练、测试数据集
X=data[data['PassengerId'].isin(train['PassengerId'].values)].copy()
X_test=data[data['PassengerId'].isin(test['PassengerId'].values)].copy()
2. 丢弃无用属性
X.drop(['PassengerId', 'Group', 'Group_size', 'Age_group', 'Cabin_number'], axis=1, inplace=True)
X_test.drop(['PassengerId', 'Group', 'Group_size', 'Age_group', 'Cabin_number'], axis=1, inplace=True)
3. 对数变化
对数变换用于减少分布中的偏态,特别是有较大异常值的分布。它可以使算法更容易“学习”正确的关系。我们将把它应用于Expenditure特征,因为这些特征被异常值严重扭曲(大多数人都没有花费)
fig=plt.figure(figsize=(12,20))
for i, col in enumerate(['RoomService','FoodCourt','ShoppingMall','Spa','VRDeck','Expenditure']):
plt.subplot(6,2,2*i+1)
sns.histplot(X[col], binwidth=100)
plt.ylim([0,200])
plt.title(f'{col} (original)')
plt.subplot(6,2,2*i+2)
sns.histplot(np.log(1+X[col]), color='C1')
plt.ylim([0,200])
plt.title(f'{col} (log-transform)')
fig.tight_layout()
plt.show()
补充:对数变化
优点:
- 减少数据偏斜:很多机器学习模型和统计技术都假设数据是正态分布的。对数变换可以帮助减少数据的偏斜度,使其更接近正态分布。
- 缩小极端值影响:在数据集中,一些极端值(或称异常值)可能会对分析结果产生不成比例的影响。对数变换有助于减少这些极端值的影响,因为它压缩了数值的范围。
- 变量稳定化:对数变换可以稳定变量的方差,当数据的方差随着其均值增加而增加时(即方差不恒定),对数变换特别有用。
- 线性化关系:在某些情况下,变量之间的关系可能是指数型的。对数变换可以帮助线性化这些关系,使得线性模型(如线性回归)成为可能。
- 处理比率数据:当数据是比率或者有比例性质时(例如,收入分配),对数变换可以使数据分布变得更加对称。
缺点:
- 无法直接对包含零值/负数的数据进行操作
for col in ['RoomService','FoodCourt','ShoppingMall','Spa','VRDeck','Expenditure']:
X[col]=np.log(1+X[col])
X_test[col]=np.log(1+X_test[col])
可以看到,进行对数变化后,数据的分布比之前要“好看”很多。
4. 字典编码
# 划分连续与离散字段
numerical_cols = [cname for cname in X.columns if X[cname].dtype in ['int64', 'float64']]
categorical_cols = [cname for cname in X.columns if X[cname].dtype == "object"]
# 数据标准化
numerical_transformer = Pipeline(steps=[('scaler', StandardScaler())])
# 独热编码
categorical_transformer = Pipeline(steps=[('onehot', OneHotEncoder(drop='if_binary', handle_unknown='ignore',sparse=False))])
# 对连续字段进行数据标准化,对离散字段进行独热编码
ct = ColumnTransformer(
transformers=[
('num', numerical_transformer, numerical_cols),
('cat', categorical_transformer, categorical_cols)],
remainder='passthrough')
X = ct.fit_transform(X)
X_test = ct.transform(X_test)
print('Training set shape:', X.shape)
5. PCA降维
对于特征的处理,一般有两种方法
- 特征选择,在原数据集中选择最有用的特征子集。
- 特征提取,从原始数据中构造新的特征。
对于特征选择,常见的有Warpper、FIlter以及Embedded方法
更详细的信息可以参考我的另一篇博客kaggle竞赛系列_特征筛选
而对于特征提取,有PCA、LDA、t-SNE等等方法。
该方案通过PCA降维进行特征提取
# 提取3个主成分
pca = PCA(n_components=3)
components = pca.fit_transform(X)
# 计算这3个主成分保留了总方差的多少比例
total_var = pca.explained_variance_ratio_.sum() * 100
# 数据的降维可视化(3维)
fig = px.scatter_3d(
components, x=0, y=1, z=2, color=y, size=0.1*np.ones(len(X)), opacity = 1,
title=f'Total Explained Variance: {total_var:.2f}%',
labels={'0': 'PC 1', '1': 'PC 2', '2': 'PC 3'},
width=800, height=500
)
fig.show()
# 查看降到多少维时保留方差比例
pca = PCA().fit(X)
fig, ax = plt.subplots(figsize=(10,4))
xi = np.arange(1, 1+X.shape[1], step=1)
yi = np.cumsum(pca.explained_variance_ratio_)
plt.plot(xi, yi, marker='o', linestyle='--', color='b')
# 绘图设置
plt.ylim(0.0,1.1)
plt.xlabel('Number of Components')
plt.xticks(np.arange(1, 1+X.shape[1], step=2))
plt.ylabel('Cumulative variance (%)')
plt.title('Explained variance by each component')
plt.axhline(y=1, color='r', linestyle='-')
plt.text(0.5, 0.85, '100% cut-off threshold', color = 'red')
ax.grid(axis='x')
可以看到,当降到25维的时候,已经可以代表数据的原始特征。
6. 划分训练、验证数据集
X_train, X_valid, y_train, y_valid = train_test_split(X,y,stratify=y,train_size=0.8,test_size=0.2,random_state=0)
(三)归纳总结
数据分析流程
数据处理流程
(四)写在最后
至此,我们已经完成了对数据集的分析与预处理,下一篇我们会建立模型并解决问题。