1. 项目概述:从泰坦尼克号到数据洞察
“你能在泰坦尼克号上活下来吗?”——这不仅仅是一个引人入胜的历史假设,更是数据科学领域最经典的入门项目。它像一块试金石,让无数初学者通过Python和探索性数据分析(EDA)的透镜,亲手触摸数据背后的故事与规律。这个项目之所以经久不衰,是因为它将一个宏大的历史悲剧,浓缩成了一个结构清晰、特征丰富的数据集,为我们提供了一个近乎完美的沙盘,来演练从数据清洗、可视化到初步建模的完整分析流程。
简单来说,这个项目就是利用Python及其强大的数据分析库(如Pandas, NumPy, Matplotlib, Seaborn),对泰坦尼克号乘客的生存数据进行分析,旨在回答一个核心问题:哪些因素显著影响了乘客的生存几率?是头等舱的奢华,还是“妇女儿童优先”的规则?是年龄带来的脆弱,还是同行家人的庇护?通过EDA,我们不是要预测某个具体乘客的命运,而是要理解数据中隐藏的模式、关联和异常,为后续可能的机器学习建模打下坚实的基础。无论你是刚接触Python的数据分析新手,还是想巩固EDA技能的中级从业者,这个项目都能让你在解决一个有趣问题的过程中,掌握一套可复用的数据分析方法论。
2. 核心分析思路与框架设计
面对泰坦尼克号数据集,一个清晰的思路是成功分析的关键。我们不能一头扎进代码里,而应该先建立分析框架。我的整体思路遵循“总-分-总”的逻辑:先对数据有一个全局的、概括性的认识,然后深入到各个特征进行细致的单变量和多变量分析,最后综合所有发现,形成对生存影响因素的初步结论。
2.1 分析目标拆解
首先,我们需要将宏大的问题具体化。我们的终极目标是理解“生存率的影响因素”。为此,可以将其拆解为以下几个子目标:
- 数据理解与质量评估:数据里有什么?有多少乘客?特征是否完整?有多少缺失值?这是所有分析的基础。
- 单变量分析:抛开其他因素,单独看每个特征(如性别、船舱等级、年龄)的分布情况。例如,乘客的年龄分布是均匀的吗?头等舱、二等舱、三等舱的乘客比例如何?
- 生存率的核心单因素分析:针对每个特征,计算其不同类别下的生存率。这是最直观的洞察来源。比如,男性和女性的生存率各是多少?
- 多变量交叉分析:现实情况是多种因素共同作用的。我们需要探究特征之间的交互影响。例如,同样是女性,头等舱和三等舱的生存率有差异吗?儿童在不同舱位的生存情况如何?
- 特征工程与关系挖掘:从原始特征中衍生出新的、可能更有预测力的特征。例如,将“姓名”字段中的称谓(Mr., Mrs., Miss, Master)提取出来作为社会地位或年龄段的代理变量;根据“兄弟姐妹/配偶”和“父母/子女”的数量构建“家庭规模”特征。
这个分析框架确保了我们的探索是系统性的,而不是随机的。每一步都为下一步提供上下文和线索。
2.2 工具选型与准备
工欲善其事,必先利其器。对于这个项目,一个经典的Python数据分析技术栈就足够了,我通常使用Jupyter Notebook或VS Code作为交互环境,因为它们能即时展示代码结果和图表,非常适合探索性工作。
核心库及其分工:
- Pandas:数据分析的基石。用于加载数据(
read_csv)、查看数据概览(info(),describe(),head())、处理缺失值(isnull(),fillna())、筛选排序、以及进行各种分组聚合计算(groupby)。可以说,80%的数据操作都靠它。 - NumPy:提供高效的数值计算支持。虽然Pandas构建于NumPy之上,但在一些底层数组运算或数学函数调用时,我们会直接用到它。
- Matplotlib:绘图库的“老祖宗”,高度可定制化。我们用它来绘制一些基础但需要精细控制的图表。
- Seaborn:基于Matplotlib的统计图形库。它的API更友好,默认样式更美观,并且内置了许多用于可视化数据关系的复杂图表类型(如箱线图、小提琴图、热力图、配对图等)。在EDA中,Seaborn往往是主力。
- SciPy(可选):如果需要进行更严格的统计检验(如卡方检验、t检验),会用到它。
提示:对于初学者,我建议先专注于掌握Pandas和Seaborn的常用功能。Matplotlib在需要调整Seaborn图表细节(如标题、坐标轴标签)时会用到。安装这些库通常只需一行命令:
pip install pandas numpy matplotlib seaborn jupyter。
3. 数据初探与质量清洗
拿到数据后的第一步,不是急着画图,而是像侦探勘察现场一样,先了解“物证”的全貌和可信度。泰坦尼克号数据集通常包含两个文件:train.csv(训练集,包含生存标签)和test.csv(测试集,不包含生存标签)。我们主要对train.csv进行EDA。
3.1 加载与首次观察
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns sns.set_style("whitegrid") # 设置Seaborn绘图风格 # 加载数据 df = pd.read_csv('titanic/train.csv') # 首次窥探:查看前几行 print("数据前5行:") print(df.head()) print("\n" + "="*50 + "\n") # 了解数据整体信息:行数、列数、数据类型、非空值数量 print("数据集信息:") print(df.info()) print("\n" + "="*50 + "\n") # 查看数值型特征的统计摘要 print("数值特征统计摘要:") print(df.describe())运行df.info()会立刻给我们一个下马威:缺失值。通常我们会发现:
Age(年龄):约有20%的缺失。这是一个关键特征,缺失值不能简单丢弃。Cabin(船舱号):缺失率极高,超过70%。这个特征信息量大(舱位位置),但缺失太多,处理起来需要技巧。Embarked(登船港口):仅有2个缺失值,相对容易处理。
df.describe()则会告诉我们一些数字故事:平均年龄约30岁,最小不到1岁(婴儿),最大80岁;平均票价约32美元,但标准差巨大(约49),说明票价差异悬殊,存在极端值(可能是豪华套间)。
3.2 缺失值处理策略
处理缺失值是数据清洗的核心环节,方法的选择直接影响分析结果的可靠性。
Embarked(登船港口):只有2个缺失,我们可以查看这两条记录的其他信息,或者直接使用众数(出现频率最高的港口)填充。这是最安全的方法。
# 查看Embarked的分布 print(df['Embarked'].value_counts()) # 假设众数是'S'(南安普顿),则填充 df['Embarked'].fillna('S', inplace=True)Age(年龄):20%的缺失不能忽视。简单用均值或中位数填充会扭曲分布。一个更聪明的策略是利用其他相关特征来预测年龄。例如,我们可以根据
Title(从姓名中提取的称谓,如Mr., Miss)、Pclass(船舱等级)和SibSp(兄弟姐妹/配偶数)来分组,用各组的中位数年龄进行填充。这比全局均值更合理。# 首先从姓名中提取称谓 df['Title'] = df['Name'].str.extract(' ([A-Za-z]+)\.', expand=False) # 将一些稀有称谓归类 df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col', 'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare') df['Title'] = df['Title'].replace('Mlle', 'Miss') df['Title'] = df['Title'].replace('Ms', 'Miss') df['Title'] = df['Title'].replace('Mme', 'Mrs') # 按Title和Pclass分组,用中位数填充Age df['Age'] = df.groupby(['Title', 'Pclass'])['Age'].apply(lambda x: x.fillna(x.median()))Cabin(船舱号):缺失太多,直接作为分类特征使用价值低。一个常见的做法是提取船舱号的首字母(代表甲板区域)作为一个新特征
Deck,对于缺失Cabin的,赋值为'U'(Unknown)。这至少能区分“有舱位记录”和“无舱位记录”的乘客,后者可能多为船员安排仓促的低等级乘客。df['Deck'] = df['Cabin'].apply(lambda x: x[0] if pd.notnull(x) else 'U')Fare(票价):在测试集中可能有一个缺失值,可以用中位数填充。
实操心得:处理
Age缺失值时,千万不要直接用df['Age'].fillna(df['Age'].median(), inplace=True)。我早期这样做过,结果发现填充后,Age与Survived的相关性图谱出现了不自然的“断层”。分组填充虽然代码稍多,但能更好地保持数据内在的群体结构,结论也更可信。对于Cabin,如果后续分析发现Deck特征与生存率关联不强,可以考虑在建模时直接舍弃,但在EDA阶段,保留它作为探索维度是值得的。
3.3 特征工程初尝试
在EDA阶段进行简单的特征工程,常常能发现新的视角。
- 家庭规模:将
SibSp(兄弟姐妹/配偶数)和Parch(父母/子女数)相加,得到FamilySize。你可能会发现,独自出行(FamilySize=1)的乘客生存率较低,而小型家庭(FamilySize=2,3,4)生存率较高,但特大家庭(FamilySize>4)生存率又下降了。 - 是否独自出行:从
FamilySize衍生出一个布尔特征IsAlone(FamilySize == 1)。这是一个非常强的特征。 - 称谓(Title):如上文所述,从姓名提取
Title,它隐含了年龄、性别和社会地位信息。Master是对未成年男孩的尊称,他们的生存率通常很高。
df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 # 加上自己 df['IsAlone'] = 1 # 默认是独自 df.loc[df['FamilySize'] > 1, 'IsAlone'] = 0 # 家庭规模大于1,则不是独自4. 单变量与核心生存率分析
清洗完数据,我们就可以开始真正的“探索”了。让我们先看看各个特征本身的样子,以及它们与生存率最直接的关系。
4.1 关键分类特征与生存率
我们使用条形图来可视化分类特征的分布及其对应的生存率。Seaborn的countplot和barplot是绝佳工具。
fig, axes = plt.subplots(2, 3, figsize=(16, 10)) # 性别 vs 生存 sns.barplot(x='Sex', y='Survived', data=df, ax=axes[0,0], palette='Set2') axes[0,0].set_title('Survival Rate by Sex') # 船舱等级 vs 生存 sns.barplot(x='Pclass', y='Survived', data=df, ax=axes[0,1], palette='Set2') axes[0,1].set_title('Survival Rate by Pclass') # 登船港口 vs 生存 sns.barplot(x='Embarked', y='Survived', data=df, ax=axes[0,2], palette='Set2') axes[0,2].set_title('Survival Rate by Embarked') # 是否独自 vs 生存 sns.barplot(x='IsAlone', y='Survived', data=df, ax=axes[1,0], palette='Set2') axes[1,0].set_title('Survival Rate by IsAlone') # 称谓 vs 生存 sns.barplot(x='Title', y='Survived', data=df, ax=axes[1,1], order=sorted(df['Title'].unique()), palette='Set2') axes[1,1].set_title('Survival Rate by Title') axes[1,1].tick_params(axis='x', rotation=45) # 旋转x轴标签 # 甲板区域 vs 生存 (由于U类太多,可能拉低可读性,可考虑过滤掉U或单独分析) deck_survival = df[df['Deck'] != 'U'].groupby('Deck')['Survived'].mean().sort_values(ascending=False) deck_survival.plot(kind='bar', ax=axes[1,2], color=sns.color_palette('Set2')[0]) axes[1,2].set_title('Survival Rate by Deck (Excluding Unknown)') axes[1,2].set_ylabel('Survival Rate') plt.tight_layout() plt.show()通过这几张图,一些震撼的事实会直观呈现:
- 性别:女性的生存率(约74%)远高于男性(约19%)。这是数据集中最强烈的信号,印证了“妇女儿童优先”的撤离原则被一定程度执行。
- 船舱等级:生存率随舱位等级下降而急剧下降。头等舱(Pclass=1)生存率超过60%,三等舱(Pclass=3)则低于25%。这揭示了社会阶级在灾难中的残酷影响——更好的舱位通常意味着更靠近救生艇的上层甲板。
- 是否独自:独自出行的乘客生存率显著低于有家人同行者。这可能是因为家庭单位在混乱中互相协助,或者船员在组织撤离时倾向于让家庭团聚。
- 称谓:
Mrs和Miss(女性)生存率高,Master(男孩)生存率也非常高,而Mr(成年男性)生存率极低。这比单纯的性别特征提供了更细腻的划分。
4.2 数值型特征分布与生存
对于年龄和票价这样的连续数值特征,我们关心它们的分布形态,以及不同生存状态下的分布差异。直方图、核密度估计图(KDE)和箱线图是首选。
fig, axes = plt.subplots(1, 3, figsize=(18, 5)) # 年龄分布直方图 sns.histplot(data=df, x='Age', hue='Survived', bins=30, kde=True, ax=axes[0], palette={0:'red', 1:'green'}, alpha=0.6) axes[0].set_title('Age Distribution by Survival') axes[0].axvline(df['Age'].median(), color='black', linestyle='--', label='Median Age') axes[0].legend(['Not Survived', 'Survived', 'Median']) # 年龄与生存关系的箱线图 sns.boxplot(x='Survived', y='Age', data=df, ax=axes[1], palette='Set2') axes[1].set_title('Age Distribution by Survival (Boxplot)') axes[1].set_xlabel('Survived (0=No, 1=Yes)') # 票价与生存关系的箱线图(由于票价偏态严重,常取对数查看) df['Fare_log'] = np.log1p(df['Fare']) # log(1+Fare) 避免对数为负无穷 sns.boxplot(x='Survived', y='Fare_log', data=df, ax=axes[2], palette='Set2') axes[2].set_title('Log(Fare) Distribution by Survival (Boxplot)') axes[2].set_ylabel('Log(Fare + 1)') plt.tight_layout() plt.show()从年龄分布图我们可以看出:
- 乘客年龄呈右偏分布,中青年居多。
- 幸存者(绿色)的分布中,儿童(特别是10岁以下)的峰值非常明显,再次印证了“儿童优先”。
- 遇难者(红色)中,20-40岁的青壮年男性是主要群体。
- 箱线图则清晰显示,幸存者的年龄中位数略低于遇难者,且幸存者中有更多的低龄异常值(儿童)。
票价的分析则更具冲击力:幸存者的票价(取对数后)中位数和整体分布都远高于遇难者。支付更高票价的人,往往位于更高级的舱位,也拥有更高的生存机会。这张图将社会经济地位与生存概率直接关联了起来。
5. 多变量交互分析与深入洞察
单变量分析看到了森林,多变量分析则要看清树木之间的关系。我们需要探究当多个条件叠加时,生存率会发生怎样的变化。
5.1 性别与舱位的交叉影响
“妇女儿童优先”的规则,在不同社会阶级中执行力度是否一致?我们可以使用一个分组柱状图或热力图来揭示。
# 使用Seaborn的pointplot或barplot展示交互效应 plt.figure(figsize=(10,6)) sns.barplot(x='Pclass', y='Survived', hue='Sex', data=df, palette='Set2') plt.title('Survival Rate by Pclass and Sex') plt.ylabel('Survival Rate') plt.show() # 或者使用pandas的透视表生成数值矩阵 pivot_table = pd.pivot_table(df, values='Survived', index='Sex', columns='Pclass', aggfunc='mean') print("生存率透视表(性别 vs 舱位):") print(pivot_table) print("\n")输出结果会非常清晰地显示:
- 在任何舱位,女性的生存率都远高于男性。
- 但是,阶级差异在女性中同样存在:头等舱女性的生存率(~97%)几乎接近完美,而三等舱女性的生存率(~50%)则大打折扣。
- 对于男性,阶级差异同样触目惊心:头等舱男性尚有约37%的生存率,而三等舱男性则骤降至约14%。
这个交叉分析告诉我们,性别是首要因素,但舱位等级在性别内部制造了巨大的生存鸿沟。特权阶级的女性得到了最大程度的保护。
5.2 年龄、舱位与生存的复杂关系
年龄的影响并非线性,且与舱位交织。我们可以使用小提琴图或分面网格来可视化这种复杂关系。
# 创建一个新的年龄分段特征,便于分析 df['AgeGroup'] = pd.cut(df['Age'], bins=[0, 12, 18, 35, 60, 100], labels=['Child','Teen','Adult','Middle','Senior']) plt.figure(figsize=(12,6)) sns.barplot(x='AgeGroup', y='Survived', hue='Pclass', data=df, palette='Set2') plt.title('Survival Rate by Age Group and Pclass') plt.ylabel('Survival Rate') plt.show()这张图可能揭示:
- 儿童(Child):在所有舱位都有较高的生存率,尤其是在头等舱和二等舱,几乎全部获救,三等舱稍低,但依然显著高于成人。这是“儿童优先”原则的有力体现。
- 青少年(Teen)和成人(Adult):生存率呈现强烈的舱位依赖。头等舱的年轻成人仍有不错的机会,而三等舱的同龄人则机会渺茫。
- 中年(Middle)和老年(Senior):生存率普遍较低,且舱位间的差异相对缩小,说明在年龄较大时,阶级特权带来的优势减弱,生理因素可能占主导。
5.3 家庭因素的多维度审视
家庭规模的影响可能不是单调的。我们可以同时观察FamilySize和IsAlone。
fig, axes = plt.subplots(1, 2, figsize=(14,5)) # 家庭规模与生存率 sns.pointplot(x='FamilySize', y='Survived', data=df, ax=axes[0], color='b') axes[0].set_title('Survival Rate by Family Size') axes[0].set_ylabel('Survival Rate') axes[0].set_xlabel('Family Size') # 是否独自与舱位的交叉 sns.barplot(x='Pclass', y='Survived', hue='IsAlone', data=df, ax=axes[1], palette='Set2') axes[1].set_title('Survival Rate by Pclass and IsAlone') axes[1].set_ylabel('Survival Rate') plt.tight_layout() plt.show()分析结果可能显示:
- 生存率在
FamilySize=2-4时达到峰值,独身者(FamilySize=1)生存率较低,而大家庭(FamilySize>4)生存率也较低。这可能是因为独身者缺乏援助,而大家庭在混乱中难以协调全部成员撤离。 - 在“是否独自”与“舱位”的交叉中,我们会发现,在头等舱,是否独自对生存率影响相对较小;但在三等舱,独自出行者的生存率尤其惨淡。这暗示着,底层乘客的社会连接(家庭)在危机中可能扮演了更关键的支持角色。
6. 高级可视化与相关性探索
为了获得更全局、更综合的视角,我们需要一些更强大的可视化工具。
6.1 特征间相关性热力图
对于数值型特征,我们可以计算皮尔逊相关系数矩阵,并用热力图可视化。这能快速发现哪些特征与生存(Survived)线性相关性强。
# 选择数值型特征 numerical_features = ['Survived', 'Pclass', 'Age', 'SibSp', 'Parch', 'Fare', 'FamilySize'] plt.figure(figsize=(10,8)) corr_matrix = df[numerical_features].corr() sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', square=True, cbar_kws={"shrink": .8}) plt.title('Correlation Heatmap of Numerical Features') plt.show()从热力图中,我们可以直观看到:
Fare(票价)与Pclass(舱位等级)呈强负相关(约-0.55),这符合预期:舱位越高,票价越贵。Fare与Survived呈正相关(约0.26),Pclass与Survived呈负相关(约-0.34)。这量化了我们之前的观察:票价越高、舱位等级越高,生存几率越大。FamilySize与SibSp、Parch强相关是自然的。Age与Survived的线性相关系数很弱(约-0.08),但这并不意味着年龄不重要!这恰恰说明了关系的非线性(儿童和老人可能更受照顾),线性相关系数无法捕捉这种复杂模式。
6.2 配对图洞察多特征关系
Seaborn的pairplot可以一次性展示多个数值特征之间的散点图和分布图,并按生存状态着色,是发现模式的利器。
# 为了图的可读性,我们选取部分关键特征 pairplot_features = ['Survived', 'Pclass', 'Age', 'Fare', 'FamilySize'] sns.pairplot(df[pairplot_features], hue='Survived', palette={0:'red', 1:'green'}, diag_kind='kde', plot_kws={'alpha':0.6}) plt.suptitle('Pairplot of Key Features Colored by Survival', y=1.02) plt.show()在配对图中,我们可以仔细审视每个散点图。例如,在FarevsPclass图中,可以看到幸存者(绿点)更多聚集在高票价、低Pclass(即头等舱)区域。在AgevsFare图中,或许能看到一些高票价的儿童幸存者(右上角的绿点)。
7. 分析总结与核心发现提炼
经过一轮完整的探索性数据分析,我们可以将散落的洞察编织成一个连贯的故事,回答最初的问题:在泰坦尼克号上,什么样的人更可能活下来?
首要决定性因素是社会规范与生理属性:
- 性别是最大的生存分水岭。“妇女儿童优先”的海事礼仪得到了切实执行,女性生存率是男性的近四倍。这是一个压倒性的信号。
- 儿童,尤其是低龄儿童,获得了显著的保护。年龄与生存率的非线性关系在分析中暴露无遗,12岁以下的儿童群体生存率远高于其他年龄段。
社会经济地位是第二道残酷的筛选器:3.舱位等级(Pclass)直接划定了生存概率的基线。头等舱乘客的生存优势是全方位的,这不仅体现在他们更高的整体生存率上,更体现在交叉分析中:即便是头等舱的男性,其生存机会也远高于三等舱的女性。票价作为舱位的代理变量,其与生存率的强正相关也印证了这一点。 4.特权在危机时刻被放大。头等舱和二等舱的妇女儿童几乎被完全保全,而三等舱的同类人群则面临近一半的死亡率。舱位带来的物理位置(更靠近救生艇的上层甲板)、信息优先权以及可能的社会资源,在生死关头产生了巨大差异。
社会连接与家庭结构提供了缓冲,但有其限度:5.有家庭成员同行显著提高了生存几率,独自出行风险最高。家庭可能在混乱中提供了情感支持、信息共享和行动协助。 6.然而,家庭规模并非越大越好。中型家庭(2-4人)生存率最高,而超过4人的大家庭生存率反而下降。这可能是因为在紧急疏散中,协调和照顾所有家庭成员变得异常困难。
数据质量与特征工程的启示:7.原始特征需要被“翻译”和“增强”。直接从姓名中提取的“称谓”(Title)是一个比“性别”更细腻的特征,成功区分了“成年男性”(Mr.)和“男孩”(Master.)。从“船舱号”衍生出的“甲板区域”(Deck)和从家庭信息构建的“是否独自”(IsAlone),都成为了强有力的分析维度。 8.缺失值处理需要智慧。对于年龄这样重要的连续变量,采用基于分组(如Title和Pclass)的中位数填充,远比简单的全局均值/中位数填充更能保留数据背后的群体差异和结构信息。
回到最初那个略带游戏色彩的问题——“你能在泰坦尼克号上活下来吗?”——基于数据的答案是:如果你是一位头等舱或二等舱的妇女或儿童,那么你的生存希望非常大;如果你是一位三等舱的单身男性,那么局势将极其严峻。这个项目之所以经典,正是因为它用冰冷的数据,无比清晰地映射了历史事件中人性、阶级与命运的交织。而通过Python完成的这次EDA之旅,不仅教会了我们如何使用pandas和seaborn这些工具,更训练了我们一种数据思维:如何提出假设,如何利用可视化进行验证,如何从混杂的信号中剥离出清晰的模式。这些技能,远比一个生存预测模型本身更为宝贵。