随机森林实战:用Python从零构建一个分类模型(附泰坦尼克数据集完整代码)
随机森林实战:用Python从零构建一个分类模型(附泰坦尼克数据集完整代码)
如果你刚开始接触机器学习,可能会被各种算法搞得眼花缭乱。线性回归、逻辑回归、支持向量机……每个听起来都挺复杂。但有一种算法,它既强大又好用,甚至被许多数据科学家称为“开箱即用”的利器,那就是随机森林。我第一次在Kaggle竞赛中用它时,惊讶地发现,没做太多调优,它的表现就轻松超过了之前精心调整的其他模型。这让我意识到,对于很多实际问题,尤其是当你手头数据有些“杂乱”、特征关系不那么清晰时,随机森林往往能提供一个稳健且高效的起点。
这篇文章,我们就来亲手搭建一个随机森林分类模型。我不会过多纠缠于背后的数学公式——那些当然重要,但实践出真知。我们将以经典的泰坦尼克号生存预测数据集为战场,用Python和scikit-learn库,从数据加载、清洗、探索,一步步走到模型训练、评估和调优。你会发现,构建一个可用的模型并不神秘,关键在于理解每个步骤背后的“为什么”,以及如何避开那些新手常踩的坑。无论你是想快速完成一个课程项目,还是为更复杂的预测任务打基础,这篇手把手的指南都希望能给你带来实实在在的帮助。
1. 环境准备与数据初探
在开始写任何代码之前,确保你的工作环境已经就绪。我强烈推荐使用Jupyter Notebook或JupyterLab进行这类探索性数据分析(EDA)和建模工作,它的交互式特性非常适合一步步查看数据变化和模型结果。
首先,我们需要安装核心的库。打开你的终端或Anaconda Prompt,执行以下命令:
pip install numpy pandas scikit-learn matplotlib seaborn
numpy和pandas是数据处理的双子星,一个负责高效数值计算,一个提供灵活的数据结构(DataFrame)。scikit-learn是我们今天的主角,它提供了随机森林以及其他几乎所有主流机器学习算法的实现。matplotlib和seaborn用于数据可视化,帮助我们直观理解数据分布和特征关系。
安装完成后,在Notebook的第一个单元格中,导入这些库:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 设置绘图风格,让图表更好看
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 从sklearn导入我们即将用到的模块
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.impute import SimpleImputer
# 设置随机种子,确保结果可复现
np.random.seed(42)
接下来,加载泰坦尼克数据集。这个数据集在Kaggle和许多机器学习教程中都很常见。假设你已经将train.csv和test.csv下载到了当前工作目录下的data文件夹中。
# 加载训练集和测试集
train_df = pd.read_csv('./data/train.csv')
test_df = pd.read_csv('./data/test.csv')
# 先看一眼训练数据长什么样
print("训练集形状:", train_df.shape)
print("\n训练集前5行:")
display(train_df.head())
print("\n训练集信息:")
train_df.info()
运行这段代码,你会看到数据的基本情况:891条乘客记录,12个特征列(包括目标列Survived)。info()方法能快速告诉我们每列的数据类型和非空值数量,这是数据质量检查的第一步。
注意:
test.csv中没有Survived列,那是我们需要预测的目标。在真正的Kaggle比赛中,你需要将预测结果提交到test.csv对应的乘客ID上。
一个非常关键但常被忽视的步骤是:将测试集放在一边,暂时不要看它。我们所有的数据探索、特征工程和模型训练,都应该只在训练集(或从训练集划分出的验证集)上进行。过早地让测试集信息影响决策,会导致模型在未知数据上的表现被高估,这被称为数据泄露,是机器学习项目中的大忌。
2. 深入数据清洗与特征工程
原始数据很少是完美无缺的。泰坦尼克数据集虽然经典,但也包含了缺失值、分类变量需要编码等问题。这一步做得好不好,直接决定了模型性能的上限。
2.1 处理缺失值
我们先系统性地检查一下缺失值:
# 计算并可视化缺失值比例
missing_train = train_df.isnull().sum().sort_values(ascending=False)
missing_percent_train = (missing_train / len(train_df)) * 100
missing_table_train = pd.concat([missing_train, missing_percent_train], axis=1, keys=['缺失数量', '缺失比例'])
print("训练集缺失值统计:")
print(missing_table_train[missing_table_train['缺失数量'] > 0])
# 对测试集也做同样检查
missing_test = test_df.isnull().sum().sort_values(ascending=False)
missing_percent_test = (missing_test / len(test_df)) * 100
missing_table_test = pd.concat([missing_test, missing_percent_test], axis=1, keys=['缺失数量', '缺失比例'])
print("\n测试集缺失值统计:")
print(missing_table_test[missing_table_test['缺失数量'] > 0])
你会发现,Cabin(船舱号)缺失严重(约77%),Age(年龄)缺失约20%,Embarked(登船港口)缺失极少。对于Fare(船票价格)在测试集中也有个别缺失。
-
Age(年龄):这是一个重要的连续特征,直接删除缺失行会损失大量信息。常见的填充策略是用中位数(对异常值不敏感)或均值。更精细的做法是利用其他特征(如Pclass客舱等级、Title称呼)分组进行填充。这里我们采用一个简单有效的方法:用乘客所属Pclass和Sex分组下的年龄中位数来填充。# 定义一个函数来填充年龄 def fill_age(df): # 按Pclass和Sex分组,计算年龄中位数 age_median = df.groupby(['Pclass', 'Sex'])['Age'].median() def fill_age_row(row): if pd.isnull(row['Age']): return age_median[row['Pclass'], row['Sex']] return row['Age'] df['Age'] = df.apply(fill_age_row, axis=1) return df train_df = fill_age(train_df) test_df = fill_age(test_df) -
Cabin(船舱号):缺失太多,直接作为特征使用价值有限,且处理起来复杂(包含字母和数字)。一个巧妙的思路是,我们不一定需要具体的舱号,可以从中提取一个更有用的信息:该乘客是否有记录的船舱信息。这可能暗示了乘客的社会地位、购票渠道等信息。train_df['HasCabin'] = train_df['Cabin'].apply(lambda x: 0 if pd.isnull(x) else 1) test_df['HasCabin'] = test_df['Cabin'].apply(lambda x: 0 if pd.isnull(x) else 1) # 之后可以删除原始的Cabin列 train_df.drop('Cabin', axis=1, inplace=True) test_df.drop('Cabin', axis=1, inplace=True) -
Embarked(登船港口):只有两个缺失值,直接用众数(出现最频繁的港口)填充。embarked_mode = train_df['Embarked'].mode()[0] train_df['Embarked'].fillna(embarked_mode, inplace=True) # 测试集Embarked没有缺失,但为了代码健壮性也可以检查填充 if test_df['Embarked'].isnull().any(): test_df['Embarked'].fillna(embarked_mode, inplace=True) -
Fare(船票价格):测试集中有一个缺失,用中位数填充。fare_median = test_df['Fare'].median() test_df['Fare'].fillna(fare_median, inplace=True)
2.2 创造新特征与编码分类变量
机器学习模型,包括随机森林,虽然能处理分类变量,但scikit-learn的实现通常要求输入是数值。我们需要将文本类别的特征(如Sex, Embarked)转换为数字。同时,从现有特征中挖掘新信息,往往能显著提升模型表现。
-
从
Name中提取Title(称呼):姓名中包含了Mr., Mrs., Miss, Master等称呼,这与社会地位、年龄、婚姻状况高度相关,是一个强特征。# 提取称呼的函数 def extract_title(name): title_search = re.search(' ([A-Za-z]+)\.', name) if title_search: return title_search.group(1) return "" import re for df in [train_df, test_df]: df['Title'] = df['Name'].apply(extract_title) # 将一些不常见的称呼归为'Rare' 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') # 查看称呼分布 print(train_df['Title'].value_counts()) -
创建
FamilySize(家庭规模):将SibSp(兄弟姐妹/配偶数)和Parch(父母/子女数)相加,并加1(乘客自己)。家庭规模可能影响互助和逃生优先级。for df in [train_df, test_df]: df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 # 进一步,可以根据家庭规模创建类别特征,例如是否独自一人 df['IsAlone'] = 0 df.loc[df['FamilySize'] == 1, 'IsAlone'] = 1 -
对分类特征进行标签编码(Label Encoding):对于有序类别不多的特征,如
Sex(男/女)、Embarked(S/C/Q)、Title,我们可以用LabelEncoder将其转换为0,1,2...这样的数字。label_cols = ['Sex', 'Embarked', 'Title'] le_dict = {} for col in label_cols: le = LabelEncoder() # 在训练集上拟合编码器,并同时转换训练集和测试集 le.fit(pd.concat([train_df[col], test_df[col]], axis=0)) train_df[col] = le.transform(train_df[col]) test_df[col] = le.transform(test_df[col]) le_dict[col] = le # 保存编码器以备后用(如果需要反向转换) -
删除不再需要的列:
PassengerId是索引,Name和Ticket(船票号)信息已被提取或过于杂乱,可以删除。SibSp和Parch已被融合到新特征中。drop_columns = ['PassengerId', 'Name', 'Ticket', 'SibSp', 'Parch'] train_df.drop(drop_columns, axis=1, inplace=True) # 测试集需要保留PassengerId用于最终提交,所以先存起来 test_passenger_ids = test_df['PassengerId'] test_df.drop(['PassengerId', 'Name', 'Ticket', 'SibSp', 'Parch'], axis=1, inplace=True)
现在,再次检查处理后的数据:
print("处理后的训练集形状:", train_df.shape)
print("\n处理后的训练集列名:", train_df.columns.tolist())
print("\n训练集前5行:")
display(train_df.head())
你应该看到一个干净、全是数值型的DataFrame,准备就绪,可以喂给模型了。
3. 构建与评估基础随机森林模型
数据准备好了,我们终于可以开始建模了。首先,将数据划分为特征(X)和目标(y),然后进一步划分出训练集和验证集。
# 分离特征和目标
X = train_df.drop('Survived', axis=1)
y = train_df['Survived']
# 划分训练集和验证集(80%训练,20%验证)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
print(f"训练集大小: {X_train.shape}")
print(f"验证集大小: {X_val.shape}")
这里使用了stratify=y参数,确保训练集和验证集中生存与未生存的比例与原始数据集一致,这在类别不平衡时很重要。
现在,创建我们的第一个随机森林分类器。我们先使用默认参数,看看它的基线表现如何。
# 初始化随机森林分类器(使用默认参数)
rf_base = RandomForestClassifier(random_state=42, n_jobs=-1) # n_jobs=-1 使用所有CPU核心加速
# 在训练集上训练模型
rf_base.fit(X_train, y_train)
# 在训练集和验证集上进行预测
y_train_pred = rf_base.predict(X_train)
y_val_pred = rf_base.predict(X_val)
# 计算准确率
train_accuracy = accuracy_score(y_train, y_train_pred)
val_accuracy = accuracy_score(y_val, y_val_pred)
print(f"基础模型 - 训练集准确率: {train_accuracy:.4f}")
print(f"基础模型 - 验证集准确率: {val_accuracy:.4f}")
你可能会看到训练集准确率接近100%,而验证集准确率在80%出头。训练集准确率远高于验证集,这是典型的过拟合(Overfitting)迹象。模型把训练数据中的噪声和特定细节都学得太好了,导致在新数据上泛化能力下降。别担心,这正是我们需要调优的原因。
除了准确率,我们还需要更细致的评估指标,尤其是当类别不平衡时(泰坦尼克数据集中生存率约为38%)。classification_report和混淆矩阵能提供更多信息。
print("\n验证集分类报告:")
print(classification_report(y_val, y_val_pred))
# 绘制混淆矩阵
from sklearn.metrics import ConfusionMatrixDisplay
disp = ConfusionMatrixDisplay.from_estimator(rf_base, X_val, y_val, cmap=plt.cm.Blues)
plt.title("验证集混淆矩阵")
plt.show()
classification_report会给出精确率(Precision)、召回率(Recall)和F1分数。例如,对于“生存”这个类别,高召回率意味着模型能找出更多实际生存的人,高精确率意味着模型预测生存的人中,确实生存的比例高。根据你的业务目标(比如是宁可错救也不愿遗漏,还是确保救援资源精准投放),可以侧重不同的指标。
3.1 特征重要性分析
随机森林一个非常强大的功能是能够评估特征重要性。它衡量了每个特征在森林中所有树上,对于减少不纯度(如基尼不纯度)的平均贡献。这不仅能帮助我们理解模型决策的依据,还能用于特征筛选。
# 获取特征重要性
feature_importances = pd.DataFrame({
'feature': X_train.columns,
'importance': rf_base.feature_importances_
}).sort_values('importance', ascending=False)
print("特征重要性排序:")
print(feature_importances)
# 可视化特征重要性
plt.figure(figsize=(10, 6))
sns.barplot(x='importance', y='feature', data=feature_importances)
plt.title('随机森林特征重要性')
plt.xlabel('重要性得分')
plt.tight_layout()
plt.show()
你可能会发现Fare(票价)、Title(称呼)、Sex(性别)和Age(年龄)是最重要的特征。这非常符合直觉:票价和称呼反映了社会经济地位,而“妇女儿童优先”的逃生原则使得性别和年龄至关重要。如果某些特征的重要性极低(例如我们创建的HasCabin或IsAlone),在后续的迭代中可以考虑移除它们,以简化模型。
4. 模型调优与性能提升
默认模型已经不错,但我们知道它过拟合了。现在,我们通过超参数调优来尝试找到泛化能力更强的模型。随机森林有许多超参数可以调整,主要分为两类:控制森林整体结构的和控制单棵决策树生长的。
| 参数类别 | 参数名 | 说明 | 一般调整方向 |
|---|---|---|---|
| 森林结构 | n_estimators |
森林中树的数量。 | 增加通常能提升性能,但计算成本增加,收益递减。常见值100-500。 |
max_features |
寻找最佳分割时考虑的最大特征数。 | 减少此值可以增加树的多样性,降低过拟合。常用‘sqrt’或‘log2’。 | |
bootstrap |
是否使用bootstrap抽样构建每棵树。 | 默认为True。设为False则使用整个数据集,可能降低多样性。 | |
| 单树生长 | max_depth |
树的最大深度。 | 限制深度是防止过拟合最有效的手段之一。可以从None(不限制)开始调小。 |
min_samples_split |
分裂内部节点所需的最小样本数。 | 增大此值会使树更保守,防止学习过于局部的模式。 | |
min_samples_leaf |
叶节点所需的最小样本数。 | 增大此值可以平滑模型,对异常值更鲁棒。 | |
min_impurity_decrease |
分裂需要达到的最小不纯度减少量。 | 增大此值会提前停止分裂,简化树。 |
调优策略通常有两种:网格搜索(GridSearchCV) 和随机搜索(RandomizedSearchCV)。网格搜索会遍历所有参数组合,计算量大但彻底;随机搜索则在指定的参数分布中随机抽样,用更少的尝试找到不错的组合,效率更高。对于随机森林这种参数空间较大的模型,通常先进行随机搜索缩小范围,再进行精细的网格搜索。
# 首先定义参数网格(范围可以设得宽一些)
param_dist = {
'n_estimators': [100, 200, 300],
'max_depth': [None, 10, 20, 30],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4],
'max_features': ['auto', 'sqrt', 'log2'],
'bootstrap': [True, False]
}
# 初始化随机森林
rf_for_tuning = RandomForestClassifier(random_state=42, n_jobs=-1)
# 使用RandomizedSearchCV,设置迭代次数和交叉验证折数
from sklearn.model_selection import RandomizedSearchCV
random_search = RandomizedSearchCV(estimator=rf_for_tuning,
param_distributions=param_dist,
n_iter=50, # 随机尝试50组参数
cv=5, # 5折交叉验证
scoring='accuracy',
verbose=1,
n_jobs=-1,
random_state=42)
# 在训练集上执行随机搜索(注意:这里用完整的X_train, y_train,交叉验证会自己划分)
random_search.fit(X_train, y_train)
# 输出最佳参数和最佳得分
print("随机搜索最佳参数:", random_search.best_params_)
print("随机搜索最佳交叉验证得分: {:.4f}".format(random_search.best_score_))
随机搜索完成后,我们得到了一个表现更好的参数组合。接下来,可以围绕这个最佳组合,定义一个更小的、精细的网格,进行网格搜索。
# 基于随机搜索的最佳结果,定义精细网格
best_params_rs = random_search.best_params_
param_grid = {
'n_estimators': [best_params_rs['n_estimators'] - 50, best_params_rs['n_estimators'], best_params_rs['n_estimators'] + 50],
'max_depth': [best_params_rs['max_depth']] if best_params_rs['max_depth'] is None else [best_params_rs['max_depth'] - 5, best_params_rs['max_depth'], best_params_rs['max_depth'] + 5],
'min_samples_split': [best_params_rs['min_samples_split'] - 1, best_params_rs['min_samples_split'], best_params_rs['min_samples_split'] + 1],
'min_samples_leaf': [best_params_rs['min_samples_leaf'] - 1, best_params_rs['min_samples_leaf'], best_params_rs['min_samples_leaf'] + 1],
'max_features': [best_params_rs['max_features']], # 通常固定这个
'bootstrap': [best_params_rs['bootstrap']] # 通常固定这个
}
# 移除可能为None或无效的值
param_grid = {k: v for k, v in param_grid.items() if v and all(x is not None and x > 0 for x in v if isinstance(x, (int, float)))}
grid_search = GridSearchCV(estimator=RandomForestClassifier(random_state=42, n_jobs=-1),
param_grid=param_grid,
cv=5,
scoring='accuracy',
verbose=1,
n_jobs=-1)
grid_search.fit(X_train, y_train)
print("网格搜索最佳参数:", grid_search.best_params_)
print("网格搜索最佳交叉验证得分: {:.4f}".format(grid_search.best_score_))
现在,我们用得到的最佳模型在验证集上进行最终评估:
# 获取最佳模型
best_rf = grid_search.best_estimator_
# 在验证集上评估
y_val_pred_best = best_rf.predict(X_val)
val_accuracy_best = accuracy_score(y_val, y_val_pred_best)
print(f"调优后模型 - 验证集准确率: {val_accuracy_best:.4f}")
print("\n调优后模型分类报告:")
print(classification_report(y_val, y_val_pred_best))
比较一下调优前后的验证集准确率。理想情况下,调优后的模型在验证集上应该有提升,并且训练集和验证集准确率的差距应该缩小,说明过拟合得到了缓解。
最后,我们可以用这个调优好的模型对真正的测试集(test_df)进行预测,生成Kaggle提交所需的格式。
# 确保测试集的特征列与训练集完全一致(顺序和内容)
# 我们的特征工程步骤已经保证了一致性,但最好再检查一下
assert set(X_train.columns) == set(test_df.columns), "训练集和测试集特征列不一致!"
test_df = test_df[X_train.columns] # 按训练集的列顺序重排
# 进行预测
test_predictions = best_rf.predict(test_df)
# 创建提交文件
submission = pd.DataFrame({
'PassengerId': test_passenger_ids,
'Survived': test_predictions
})
submission.to_csv('titanic_random_forest_submission.csv', index=False)
print("提交文件已保存为 'titanic_random_forest_submission.csv'")
至此,一个完整的、从数据到预测的随机森林分类项目就完成了。回顾整个过程,数据清洗和特征工程花费了最多精力,但这恰恰是机器学习项目中最有价值的部分。模型调优带来提升,但前提是数据本身提供了足够的信息。随机森林的稳健性让我们在特征工程时可以有更多尝试,即使有些特征不那么完美,它也能通过集成的方式抵消一部分噪声。在实际项目中,你还可以尝试更多,比如使用交叉验证更稳健地评估模型、尝试其他集成方法如梯度提升树(如XGBoost, LightGBM)进行对比,或者将多个模型的预测结果进行融合(Stacking)。机器学习是一个迭代的过程,每一次代码运行和结果分析,都会让你对数据和模型有更深的理解。
更多推荐



所有评论(0)