作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。
会一些的技术:数据分析、算法、SQL、大数据相关、python
欢迎加入社区:码上找工作
作者专栏每日更新:
LeetCode解锁1000题: 打怪升级之旅
python数据分析可视化:企业实战案例
备注说明:方便大家阅读 公众号 数据分析螺丝钉 回复关键词 python可视化 领取完整notebook 一起打怪升级
泰坦尼克号的沉没是历史上最臭名昭著的沉船事故之一。
1912 年 4 月 15 日,在她的处女航中,被广泛认为“不沉”的泰坦尼克号与冰山相撞后沉没。不幸的是,船上没有足够的救生艇,导致 2224 名乘客和机组人员中有 1502 人死亡。
虽然生存有一定的运气成分,但似乎某些群体比其他群体更有可能生存。
在本次挑战中,我们要求您建立一个预测模型来回答以下问题:“什么样的人更有可能生存?”使用乘客数据(即姓名、年龄、性别、社会经济阶层等)。
有两个文件,test.csv、train.csv通过这两个文件的数据进行预测输出什么人更有可能生存。包括以下几个字段
用notebook把下载的文件都放在一起方便查看
train.csv
: 包含用于训练机器学习模型的数据。test.csv
: 包含用于模型预测的测试数据。gender_submission.csv
: 提供一个提交示例,展示了预测结果的正确格式。import pandas as pd # 加载数据 train_df = pd.read_csv('./train.csv') test_df = pd.read_csv('./test.csv') # 数据概览 print("数据前五行:") print(train_df.head()) print("\n数据集基本信息:") print(train_df.info()) print("\n数值型特征的描述性统计:") print(train_df.describe()) # 缺失值检查 print("\n各列缺失值计数:") print(train_df.isnull().sum())
检查数据有没缺
# 检查训练集中的缺失值 print(train_df.isnull().sum()) # 检查测试集中的缺失值 print(test_df.isnull().sum())
可以看到train年龄(Age)有177个缺失值,客舱号码(Cabin)有687个缺失值,还有登船港口(Embarked)有2个缺失值
Age
,可以使用中位数或平均值来填充。Embarked
(仅在训练集中有少量缺失),可以使用众数(最常见的值)来填充。Cabin
,考虑到缺失较多,可以填充为一个常数,如'Unknown'
,或者从中提取有用信息,如甲板号。Fare
缺失值(在测试集中仅有1个)可以用中位数或平均值填充。由于数据的轻微右偏和存在离群点,使用中位数填充缺失值比使用平均值更为合适。中位数对离群点不敏感,并且可以更好地代表“典型”的乘客年龄,而不会被极端值所扭曲。分析如下
# 绘制年龄的直方图 train_df['Age'].hist(bins=50) plt.title('Histogram of Age') plt.xlabel('Age') plt.ylabel('Frequency') plt.show() # 绘制年龄的箱线图 plt.boxplot(train_df['Age'].dropna()) plt.title('Box Plot of Age') plt.xlabel('Age') plt.ylabel('Value') plt.show()
# 填充年龄 train_df['Age'].fillna(train_df['Age'].median(), inplace=True) train_df['Age'].fillna(train_df['Age'].median(), inplace=True)
# 填充登船港口 most_common_embarked = train_df['Embarked'].mode()[0] train_df['Embarked'].fillna(most_common_embarked, inplace=True)
考虑到 Cabin
字段中有大量的缺失值,通常不会尝试去预测或估计具体的船舱号,也不能删除这个数据,因为其他的字段是有用的,这里我们把它表示为unkowne
# 填充Cabin的缺失值为 'Unknown' train_df['Cabin'].fillna('Unknown', inplace=True) # 提取Cabin的甲板号,将NaN视为 'U'(代表Unknown) train_df['Deck'] = train_df['Cabin'].apply(lambda x: x[0] if pd.notna(x) else 'U') # 检查提取后的甲板号 print(train_df['Deck'].value_counts())
可以看到标识为U的有687个
# 填充票价 train_df['Fare'].fillna(test_df['Fare'].median(), inplace=True)
Pclass
、Sex
、Age
、SibSp
、Parch
、Fare
和Embarked
可能是有用的特征。这与社会经验相关,理论上经济条件好的票价贵获得的服务更多,生存概率可能更大,其他的也类似,比如性别年龄等等SibSp
+Parch
)。可以看到女的生存率较大,女士优先在各种场合都适用
乘客社会等级越高,幸存率越高,努力挣钱很重要
在紧急情况下,拥有一到两名家庭成员可能有利于生存,而大家庭可能会因为尝试集体行动而处于不利地位
无父母/子女: 没有携带父母或子女的乘客生存率较低。这可能是因为他们没有亲人的帮助或动力去寻求生存的机会。
一位父母/子女: 与一个父母或子女同行的乘客生存率有所提高。这可能是因为他们可以相互支持,共同寻找逃生的机会。
两位父母/子女: 生存率略有下降,但仍然高于独自旅行的乘客。这可能表明在一些情况下,担负照顾多个家庭成员的责任可能会影响生存机会。
三位父母/子女: 生存率显著增加,这可能意味着拥有一个较大家庭的乘客可能会在疏散时得到优先考虑,或者家庭成员之间的合作对生存至关重要。
Cherbourg (C):从Cherbourg登船的乘客有最高的生存率。这可能表明从这个港口登船的乘客在社会经济地位上可能较高,因此可能在船上有更好的住宿条件,或者在疏散过程中获得了更好的机会。
一些称呼如 Miss
、Mrs
、和 Master
(通常用于未婚女士、已婚女士和年轻男性)显示出较高的生存率,这可能反映了妇女和儿童优先的救生原则。
称呼 Mr
和 Rev
(牧师)的生存率相对较低,这可能是因为成年男性在撤离时被赋予了较低的优先级。
有几个称呼,如 Sir
、Lady
、和 Countess
表现出极高的生存率,这些称呼可能指的是社会地位较高的乘客,他们可能因此获得了更好的救援机会。
称呼 Don
、Col
(上校)和 Capt
(船长)的生存率较低或为零,这可能表明这些乘客有职责在灾难时帮助他人或保持纪律,从而牺牲了自己的生存机会。
注意样本量差异:某些称呼如 Jonkheer
(荷兰的贵族)和 Mlle
(法语中的小姐)样本量可能非常小,因此生存率可能不具有统计意义。
这个核密度估计图(KDE图)展示了泰坦尼克号上生还者和未生还者的年龄分布。从图中可以看出:
儿童生还率较高:在较低年龄段,特别是10岁以下的儿童,生还曲线(蓝色)高于未生还曲线(橙色),表明儿童的生还率相对较高。
年轻成人生还率:年龄在20到30岁之间的成人生还率似乎低于未生还率,这可能是因为这个年龄段的成年男性较多,而在泰坦尼克号沉船事件中,成年男性的生还率相对较低。
老年生还率:年龄在60岁以上的乘客生还率也较低,这与整体的生还趋势一致,即年龄较大的乘客生还机会减少。
密度的差异:在所有年龄段,未生还者的密度普遍高于生还者,这可能反映了未生还者在整体乘客中占比较大。
峰值分析:生还者的密度峰值似乎在稍微低于30岁的地方,而未生还者的密度峰值略高于30岁,这可能反映了生还者中有更多的较年轻乘客。
机器学习模型通常需要输入是数值型数据,因此需要将类别变量转换为数值型。这通常通过编码技术如独热编码或标签编码来实现。
类别变量编码:
Sex
: 将性别转换为数值(0和1)。Embarked
: 使用独热编码,因为它是非序列的类别型数据。# 将性别转换为数值 train_df['Sex'] = train_df['Sex'].map({'male': 0, 'female': 1}) test_df['Sex'] = test_df['Sex'].map({'male': 0, 'female': 1}) # 使用独热编码处理登船港口 train_df = pd.get_dummies(train_df, columns=['Embarked']) test_df = pd.get_dummies(test_df, columns=['Embarked'])
通过创建新的特征以揭示数据中的更深层次信息,这些信息可能跟生存概率有关系,这个需要有一些社会经验。
Title
提取自乘客的全名,表示他们的社会称谓,这可能与生存概率相关。FamilySize
是从 SibSp
和 Parch
计算得出的,表示乘客的家庭成员总数。IsAlone
是基于 FamilySize
创建的,表明乘客是否独自一人在船上。Deck
是从 Cabin
的第一个字母提取的,代表乘客的船舱甲板位置,这可能会影响他们在紧急情况下的逃生机会。TicketPrefix
提取自 Ticket
,可能包含了与票价或船舱位置相关的信息。# 从Name中提取称谓作为新特征Title train_df['Title'] = train_df['Name'].apply(lambda name: name.split(',')[1].split('.')[0].strip()) test_df['Title'] = test_df['Name'].apply(lambda name: name.split(',')[1].split('.')[0].strip()) # 创建FamilySize特征 train_df['FamilySize'] = train_df['SibSp'] + train_df['Parch'] + 1 test_df['FamilySize'] = test_df['SibSp'] + test_df['Parch'] + 1 # 创建IsAlone特征 train_df['IsAlone'] = 1 # initialize to yes/1 is alone train_df['IsAlone'].loc[train_df['FamilySize'] > 1] = 0 # now update to no/0 if family size is greater than 1 test_df['IsAlone'] = 1 # initialize to yes/1 is alone test_df['IsAlone'].loc[test_df['FamilySize'] > 1] = 0 # now update to no/0 if family size is greater than 1 # 从Cabin特征提取甲板信息作为新特征Deck # 如果Cabin值缺失,则使用 'U' 表示Unknown train_df['Deck'] = train_df['Cabin'].apply(lambda x: x[0] if pd.notna(x) else 'U') test_df['Deck'] = test_df['Cabin'].apply(lambda x: x[0] if pd.notna(x) else 'U') # 从Ticket特征提取票据前缀 train_df['TicketPrefix'] = train_df['Ticket'].apply(lambda x: x.split()[0] if not x.isdigit() else 'None') test_df['TicketPrefix'] = test_df['Ticket'].apply(lambda x: x.split()[0] if not x.isdigit() else 'None') # 查看数据集中新构造的特征 print(train_df[['Title', 'FamilySize', 'IsAlone', 'Deck', 'TicketPrefix']].head())
特征选择的目的是从原始数据中选择最相关的特征子集以用于模型训练。这样做有几个好处:
# 特征选择 y_train = train_df['Survived'] X_train = train_df.drop('Survived', axis=1) forest = RandomForestClassifier(n_estimators=100, random_state=42) forest.fit(X_train, y_train) selector = SelectFromModel(forest, threshold='mean', prefit=True) X_important_train = selector.transform(X_train) important_feature_names = X_train.columns[selector.get_support()] print("Selected features after feature selection:", important_feature_names)
RandomForestClassifier
被训练在完整的训练数据上,然后 SelectFromModel
根据随机森林估计出的特征重要性来选择特征。选择标准 threshold='mean'
意味着将选择重要性大于平均重要性的特征。selector.transform(X_train)
创建了一个新的特征矩阵,只包含这些选定的重要特征。selector.get_support()
提供了一个布尔数组,用于标识哪些特征被选中。important_feature_names
就是那些被选择的特征的名字。输出结果如下
详细对每个特征做一些分析,发现passengerId与存活率应该关系不大需要剔除
# 删除不再需要的原始列 drop_columns = ['PassengerId','Name', 'Ticket', 'Cabin', 'Sex', 'Embarked', 'Title'] train_df.drop(columns=drop_columns, inplace=True) test_df.drop(columns=drop_columns, inplace=True)
使用分割后的训练数据(X_train
和 y_train
)来训练选定的机器学习模型。这可能包括:
# 分割数据进行本地验证 X_train, X_valid, y_train, y_valid = train_test_split(X_important_train, y_train, test_size=0.2, random_state=42)
使用分割出的验证数据集(X_valid
和 y_valid
)来评估模型的性能。常用的评估指标包括:
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, classification_report # 模型初始化 model = RandomForestClassifier(n_estimators=100, random_state=42) # 模型训练 model.fit(X_train, y_train) # 模型预测 y_pred = model.predict(X_valid) # 模型评估 print("Accuracy:", accuracy_score(y_valid, y_pred)) print("Classification Report:\n", classification_report(y_valid, y_pred))
准确率 (Accuracy): 81.56%
这个模型具有相对较高的准确率,但还有提升的空间。特别是对于分类为幸存(1
)的实例,我们可以看到召回率和精确率略低于未幸存(0
)的实例。这可能意味着模型在区分幸存者时不如在区分未幸存者那么准确。根据这个报告,我们可能会考虑进一步优化模型,比如通过调整阈值、尝试不同的特征组合、使用不同的算法或调整模型超参数来改进模型性能。
寻找随机森林模型最佳超参数组合的标准流程,并使用找到的最佳模型在验证数据上评估性能
# 超参数调优和模型训练 rfc = RandomForestClassifier(random_state=42) param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [5, 10, 15], 'min_samples_leaf': [1, 2, 4] } grid_search = GridSearchCV(estimator=rfc, param_grid=param_grid, cv=5, n_jobs=-1, verbose=2) grid_search.fit(X_train, y_train) # 最佳参数模型 best_clf = grid_search.best_estimator_ # 模型评估 y_pred = best_clf.predict(X_valid) print("Accuracy:", accuracy_score(y_valid, y_pred)) print(classification_report(y_valid, y_pred))
RandomForestClassifier(random_state=42)
初始化了随机森林分类器,并设定了随机种子以确保结果的可重复性。param_grid
定义了三个参数:n_estimators
、max_depth
和 min_samples_leaf
的搜索范围,GridSearchCV
将会尝试这些参数的所有组合。GridSearchCV(...)
对象配置了用于搜索的估计器(estimator
),参数网格(param_grid
),交叉验证折数(cv=5
),并行作业数(n_jobs=-1
使用所有可用核心),和详细程度(verbose=2
显示详细输出)。grid_search.fit(X_train, y_train)
在训练数据上运行网格搜索来找出最佳参数。输出
报告摘要:
最佳模型参数:
max_depth
): 5。min_samples_leaf
): 1。n_estimators
): 300。用相同的方式处理训练集,生成预测数据csv
# 训练模型 X_train = train_df.drop(['Survived','PassengerId'], axis=1) y_train = train_df['Survived'] model = RandomForestClassifier(n_estimators=300, max_depth=5, random_state=42) model.fit(X_train, y_train) # 预测测试集 X_test = test_df.drop('PassengerId', axis=1) predictions = model.predict(X_test) # 生成提交文件 submission = pd.DataFrame({'PassengerId': test_df['PassengerId'], 'Survived': predictions}) submission.to_csv('submission.csv', index=False)
第一次提交后发现只有 77%的准确率,在kaggle排400名,看来这个赛道还是卷的不行
# Define the models to train classifiers = { 'LogisticRegression': LogisticRegression(max_iter=1000, random_state=42), 'SVC': SVC(random_state=42), 'GradientBoosting': GradientBoostingClassifier(random_state=42) }
生成不同的上传文件
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.ensemble import GradientBoostingClassifier from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer # Load datasets train_df = pd.read_csv('train.csv') test_df = pd.read_csv('test.csv') # Separate features and target y = train_df['Survived'] X = train_df.drop(['Survived', 'PassengerId', 'Name', 'Ticket', 'Cabin'], axis=1) # Define categorical and numerical features categorical_features = ['Sex', 'Embarked'] numerical_features = X.select_dtypes(include=['int64', 'float64']).columns.tolist() # Create preprocessors categorical_transformer = Pipeline(steps=[ ('imputer', SimpleImputer(strategy='constant', fill_value='missing')), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]) numerical_transformer = Pipeline(steps=[ ('imputer', SimpleImputer(strategy='median')), ('scaler', StandardScaler()) ]) # Combine preprocessors preprocessor = ColumnTransformer( transformers=[ ('num', numerical_transformer, numerical_features), ('cat', categorical_transformer, categorical_features) ] ) # Define the models to train classifiers = { 'LogisticRegression': LogisticRegression(max_iter=1000, random_state=42), 'SVC': SVC(random_state=42), 'GradientBoosting': GradientBoostingClassifier(random_state=42) } # Process of training models and creating submission files for classifier_name, classifier in classifiers.items(): # Create pipeline pipeline = Pipeline(steps=[('preprocessor', preprocessor), ('classifier', classifier)]) # Train the model pipeline.fit(X, y) # Using all available data for training # Make predictions on the test data X_test = test_df.drop(['PassengerId', 'Name', 'Ticket', 'Cabin'], axis=1) predictions = pipeline.predict(X_test) # Create submission file submission = pd.DataFrame({'PassengerId': test_df['PassengerId'], 'Survived': predictions}) submission_file = f'submission_{classifier_name}.csv' submission.to_csv(submission_file, index=False) print(f"Created submission file: {submission_file}")