引言

房价预测是机器学习领域中一个经典且实用的回归问题。通过分析房屋的各种特征(如面积、位置、房龄等),我们可以构建模型来预测房屋的售价。scikit-learn作为Python中最受欢迎的机器学习库之一,提供了丰富的工具来完成从数据预处理到模型评估的全流程。

本文将通过一个完整的案例,详细解析如何使用scikit-learn构建房价预测模型。我们将从数据加载和清洗开始,逐步进行特征工程、模型选择、训练和优化,最终得到一个性能良好的预测模型。整个过程将使用Python代码进行演示,确保读者能够跟随操作并理解每个步骤的原理。

1. 数据准备与加载

1.1 数据集介绍

我们将使用一个经典的房价数据集——波士顿房价数据集(Boston Housing Dataset)。这个数据集包含506个样本,每个样本有13个特征和1个目标变量(房屋的中位数价格)。特征包括:

  • CRIM:人均犯罪率
  • ZN:住宅用地比例
  • INDUS:城镇中非零售业务占地比例
  • CHAS:是否临近查尔斯河(1是,0否)
  • NOX:氮氧化物浓度
  • RM:平均房间数
  • AGE:1940年前建成的房屋比例
  • DIS:到波士顿就业中心的加权距离
  • RAD:放射状公路可达性指数
  • TAX:每万美元财产税率
  • PTRATIO:师生比
  • B:黑人比例
  • LSTAT:低收入人群比例
  • MEDV:房屋中位数价格(目标变量)

1.2 加载数据

首先,我们需要导入必要的库并加载数据。scikit-learn内置了这个数据集,可以直接加载。

import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV from sklearn.preprocessing import StandardScaler, PolynomialFeatures from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error import warnings warnings.filterwarnings('ignore') # 加载数据集 boston = load_boston() X = boston.data y = boston.target feature_names = boston.feature_names # 转换为DataFrame以便分析 df = pd.DataFrame(X, columns=feature_names) df['MEDV'] = y print("数据集形状:", df.shape) print("n前5行数据:") print(df.head()) print("n数据集基本信息:") print(df.info()) 

1.3 数据探索性分析(EDA)

在开始数据清洗之前,我们需要对数据进行探索性分析,了解数据的分布、缺失值、异常值等情况。

# 检查缺失值 print("n缺失值统计:") print(df.isnull().sum()) # 描述性统计 print("n描述性统计:") print(df.describe()) # 目标变量分布 plt.figure(figsize=(10, 6)) sns.histplot(df['MEDV'], kde=True) plt.title('房价分布') plt.xlabel('房价(千美元)') plt.ylabel('频数') plt.show() # 特征与目标变量的相关性 corr_matrix = df.corr() plt.figure(figsize=(12, 10)) sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt='.2f') plt.title('特征相关性热力图') plt.show() 

通过探索性分析,我们可以发现:

  1. 数据集没有缺失值
  2. 目标变量MEDV大致呈正态分布,但有右偏趋势
  3. RM(平均房间数)与MEDV呈强正相关(0.7)
  4. LSTAT(低收入人群比例)与MEDV呈强负相关(-0.74)

2. 数据清洗与预处理

2.1 处理异常值

在房价数据中,异常值可能对模型产生较大影响。我们可以通过箱线图或统计方法识别异常值。

# 检查目标变量的异常值 plt.figure(figsize=(8, 6)) sns.boxplot(y=df['MEDV']) plt.title('房价箱线图') plt.ylabel('房价(千美元)') plt.show() # 使用IQR方法检测异常值 Q1 = df['MEDV'].quantile(0.25) Q3 = df['MEDV'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers = df[(df['MEDV'] < lower_bound) | (df['MEDV'] > upper_bound)] print(f"异常值数量: {len(outliers)}") print("异常值统计:") print(outliers.describe()) # 处理异常值 - 这里我们选择保留,因为房价可能有极端值,但可以考虑对数变换 # 对于特征中的异常值,我们也可以检查 for col in df.columns[:-1]: # 排除目标变量 Q1 = df[col].quantile(0.25) Q3 = df[col].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers_count = len(df[(df[col] < lower_bound) | (df[col] > upper_bound)]) if outliers_count > 0: print(f"{col}: {outliers_count} 个异常值") 

2.2 特征缩放

由于不同特征的量纲不同(如犯罪率和房间数),我们需要进行特征缩放,使所有特征具有相同的尺度。这有助于提高模型的收敛速度和性能。

# 分离特征和目标变量 X = df.drop('MEDV', axis=1) y = df['MEDV'] # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 特征缩放 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) print("训练集形状:", X_train_scaled.shape) print("测试集形状:", X_test_scaled.shape) 

3. 特征工程

特征工程是提高模型性能的关键步骤。我们可以通过创建新特征、多项式特征等方式来增强模型的表达能力。

3.1 创建新特征

基于领域知识,我们可以创建一些新的特征。例如,我们可以创建”房间密度”特征(RM与LSTAT的交互)。

# 将缩放后的数据转换回DataFrame以便操作 X_train_df = pd.DataFrame(X_train_scaled, columns=feature_names) X_test_df = pd.DataFrame(X_test_scaled, columns=feature_names) # 创建新特征:房间密度(RM与LSTAT的交互) X_train_df['ROOM_DENSITY'] = X_train_df['RM'] * X_train_df['LSTAT'] X_test_df['ROOM_DENSITY'] = X_test_df['RM'] * X_test_df['LSTAT'] # 创建新特征:犯罪率与低收入人群比例的交互 X_train_df['CRIM_LSTAT'] = X_train_df['CRIM'] * X_train_df['LSTAT'] X_test_df['CRIM_LSTAT'] = X_test_df['CRIM'] * X_test_df['LSTAT'] # 更新特征列表 new_features = ['ROOM_DENSITY', 'CRIM_LSTAT'] feature_names_extended = list(feature_names) + new_features print("扩展后的特征数量:", len(feature_names_extended)) print("扩展后的特征:", feature_names_extended) 

3.2 多项式特征

对于线性模型,我们可以生成多项式特征来捕捉特征之间的非线性关系。

# 生成多项式特征(仅对原始特征,不包括新创建的特征) poly = PolynomialFeatures(degree=2, include_bias=False) X_train_poly = poly.fit_transform(X_train_df[feature_names]) X_test_poly = poly.transform(X_test_df[feature_names]) # 获取多项式特征的名称 poly_feature_names = poly.get_feature_names_out(feature_names) print("多项式特征数量:", len(poly_feature_names)) print("前10个多项式特征:", poly_feature_names[:10]) # 将多项式特征与新创建的特征合并 X_train_final = np.hstack([X_train_poly, X_train_df[new_features].values]) X_test_final = np.hstack([X_test_poly, X_test_df[new_features].values]) # 更新最终特征列表 final_feature_names = list(poly_feature_names) + new_features print("最终特征数量:", len(final_feature_names)) 

4. 模型选择与训练

4.1 基础模型训练

我们首先训练几个基础模型,包括线性回归、岭回归、Lasso回归和随机森林回归,比较它们的性能。

# 定义模型 models = { 'Linear Regression': LinearRegression(), 'Ridge Regression': Ridge(alpha=1.0), 'Lasso Regression': Lasso(alpha=1.0), 'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42) } # 训练和评估模型 results = {} for name, model in models.items(): # 训练模型 model.fit(X_train_final, y_train) # 预测 y_pred = model.predict(X_test_final) # 计算指标 mse = mean_squared_error(y_test, y_pred) rmse = np.sqrt(mse) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) # 存储结果 results[name] = { 'MSE': mse, 'RMSE': rmse, 'MAE': mae, 'R2': r2 } print(f"n{name}模型性能:") print(f" MSE: {mse:.4f}") print(f" RMSE: {rmse:.4f}") print(f" MAE: {mae:.4f}") print(f" R²: {r2:.4f}") # 可视化结果 results_df = pd.DataFrame(results).T results_df.plot(kind='bar', figsize=(12, 6)) plt.title('不同模型性能比较') plt.ylabel('误差值') plt.xticks(rotation=45) plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left') plt.tight_layout() plt.show() 

4.2 模型性能分析

通过比较不同模型的性能,我们可以发现:

  • 线性模型(线性回归、岭回归、Lasso)的性能相近,R²在0.75-0.80之间
  • 随机森林回归的性能最好,R²达到0.85以上
  • 随机森林的RMSE和MAE都明显低于线性模型

这表明非线性模型更适合这个数据集,因为房价与特征之间的关系可能不是线性的。

5. 模型优化

5.1 超参数调优

为了进一步提高模型性能,我们可以使用网格搜索(Grid Search)来优化随机森林的超参数。

# 定义随机森林的参数网格 param_grid = { 'n_estimators': [50, 100, 200], 'max_depth': [None, 10, 20, 30], 'min_samples_split': [2, 5, 10], 'min_samples_leaf': [1, 2, 4] } # 创建网格搜索对象 rf = RandomForestRegressor(random_state=42) grid_search = GridSearchCV( estimator=rf, param_grid=param_grid, cv=5, scoring='neg_mean_squared_error', n_jobs=-1, verbose=1 ) # 执行网格搜索 print("开始网格搜索...") grid_search.fit(X_train_final, y_train) # 输出最佳参数 print("n最佳参数:", grid_search.best_params_) print("最佳交叉验证分数:", -grid_search.best_score_) # 使用最佳模型 best_rf = grid_search.best_estimator_ y_pred_best = best_rf.predict(X_test_final) # 评估最佳模型 mse_best = mean_squared_error(y_test, y_pred_best) rmse_best = np.sqrt(mse_best) mae_best = mean_absolute_error(y_test, y_pred_best) r2_best = r2_score(y_test, y_pred_best) print("n最佳模型性能:") print(f" MSE: {mse_best:.4f}") print(f" RMSE: {rmse_best:.4f}") print(f" MAE: {mae_best:.4f}") print(f" R²: {r2_best:.4f}") 

5.2 集成学习方法

除了随机森林,我们还可以尝试其他集成学习方法,如梯度提升树(Gradient Boosting)。

# 梯度提升回归 gb_model = GradientBoostingRegressor( n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42 ) gb_model.fit(X_train_final, y_train) y_pred_gb = gb_model.predict(X_test_final) # 评估梯度提升模型 mse_gb = mean_squared_error(y_test, y_pred_gb) rmse_gb = np.sqrt(mse_gb) mae_gb = mean_absolute_error(y_test, y_pred_gb) r2_gb = r2_score(y_test, y_pred_gb) print("n梯度提升模型性能:") print(f" MSE: {mse_gb:.4f}") print(f" RMSE: {rmse_gb:.4f}") print(f" MAE: {mae_gb:.4f}") print(f" R²: {r2_gb:.4f}") # 比较优化前后的随机森林 print("n优化前后随机森林性能对比:") print(f"优化前 - RMSE: {results['Random Forest']['RMSE']:.4f}, R²: {results['Random Forest']['R2']:.4f}") print(f"优化后 - RMSE: {rmse_best:.4f}, R²: {r2_best:.4f}") 

5.3 特征重要性分析

了解哪些特征对预测最重要可以帮助我们进一步优化模型和解释结果。

# 获取特征重要性 feature_importance = best_rf.feature_importances_ # 创建特征重要性DataFrame importance_df = pd.DataFrame({ 'Feature': final_feature_names, 'Importance': feature_importance }).sort_values('Importance', ascending=False) # 可视化前20个重要特征 plt.figure(figsize=(12, 8)) sns.barplot(x='Importance', y='Feature', data=importance_df.head(20)) plt.title('特征重要性排名(前20)') plt.xlabel('重要性') plt.ylabel('特征') plt.tight_layout() plt.show() print("最重要的10个特征:") print(importance_df.head(10)) 

6. 模型评估与验证

6.1 交叉验证

为了更可靠地评估模型性能,我们使用交叉验证。

# 对优化后的随机森林进行交叉验证 cv_scores = cross_val_score(best_rf, X_train_final, y_train, cv=5, scoring='neg_mean_squared_error') cv_rmse = np.sqrt(-cv_scores) print("交叉验证RMSE分数:", cv_rmse) print(f"平均RMSE: {cv_rmse.mean():.4f} (+/- {cv_rmse.std():.4f})") # 对梯度提升模型进行交叉验证 cv_scores_gb = cross_val_score(gb_model, X_train_final, y_train, cv=5, scoring='neg_mean_squared_error') cv_rmse_gb = np.sqrt(-cv_scores_gb) print("n梯度提升交叉验证RMSE分数:", cv_rmse_gb) print(f"平均RMSE: {cv_rmse_gb.mean():.4f} (+/- {cv_rmse_gb.std():.4f})") 

6.2 残差分析

残差分析可以帮助我们检查模型是否满足回归假设,以及是否存在系统性偏差。

# 计算残差 residuals = y_test - y_pred_best # 残差图 fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # 残差 vs 预测值 axes[0].scatter(y_pred_best, residuals, alpha=0.6) axes[0].axhline(y=0, color='r', linestyle='--') axes[0].set_xlabel('预测值') axes[0].set_ylabel('残差') axes[0].set_title('残差 vs 预测值') # 残差分布 sns.histplot(residuals, kde=True, ax=axes[1]) axes[1].set_xlabel('残差') axes[1].set_ylabel('频数') axes[1].set_title('残差分布') plt.tight_layout() plt.show() # 残差的统计描述 print("残差统计描述:") print(f"均值: {residuals.mean():.4f}") print(f"标准差: {residuals.std():.4f}") print(f"最小值: {residuals.min():.4f}") print(f"最大值: {residuals.max():.4f}") 

6.3 预测结果可视化

将真实值与预测值进行对比,直观展示模型性能。

# 创建预测结果DataFrame results_df = pd.DataFrame({ '真实值': y_test, '预测值': y_pred_best }).sort_values('真实值') # 绘制真实值与预测值对比图 plt.figure(figsize=(12, 6)) plt.plot(results_df['真实值'].values, label='真实值', linewidth=2) plt.plot(results_df['预测值'].values, label='预测值', linewidth=2, alpha=0.7) plt.xlabel('样本索引') plt.ylabel('房价(千美元)') plt.title('真实值与预测值对比') plt.legend() plt.grid(True, alpha=0.3) plt.show() # 散点图 plt.figure(figsize=(8, 8)) plt.scatter(y_test, y_pred_best, alpha=0.6) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2) plt.xlabel('真实房价') plt.ylabel('预测房价') plt.title('真实房价 vs 预测房价') plt.grid(True, alpha=0.3) plt.show() 

7. 模型部署与预测

7.1 保存模型

将训练好的模型保存,以便后续使用。

import joblib # 保存模型和缩放器 joblib.dump(best_rf, 'optimized_random_forest_model.pkl') joblib.dump(scaler, 'feature_scaler.pkl') joblib.dump(poly, 'polynomial_features.pkl') print("模型已保存为 'optimized_random_forest_model.pkl'") print("特征缩放器已保存为 'feature_scaler.pkl'") print("多项式特征生成器已保存为 'polynomial_features.pkl'") 

7.2 加载模型进行预测

# 加载模型 loaded_model = joblib.load('optimized_random_forest_model.pkl') loaded_scaler = joblib.load('feature_scaler.pkl') loaded_poly = joblib.load('polynomial_features.pkl') # 创建一个新样本进行预测 new_sample = np.array([0.02731, 0.0, 7.07, 0, 0.469, 6.421, 78.9, 4.9671, 2.0, 242.0, 17.8, 396.90, 9.14]) # 预处理新样本 new_sample_scaled = loaded_scaler.transform(new_sample.reshape(1, -1)) new_sample_poly = loaded_poly.transform(new_sample_scaled) # 创建新特征(与训练时相同) new_sample_df = pd.DataFrame(new_sample_scaled, columns=feature_names) new_sample_df['ROOM_DENSITY'] = new_sample_df['RM'] * new_sample_df['LSTAT'] new_sample_df['CRIM_LSTAT'] = new_sample_df['CRIM'] * new_sample_df['LSTAT'] # 合并特征 new_sample_final = np.hstack([new_sample_poly, new_sample_df[['ROOM_DENSITY', 'CRIM_LSTAT']].values]) # 预测 prediction = loaded_model.predict(new_sample_final) print(f"n新样本预测房价: {prediction[0]:.2f} 千美元") 

8. 总结与展望

8.1 项目总结

通过本项目,我们完成了一个完整的房价预测模型构建流程:

  1. 数据准备:加载波士顿房价数据集,进行探索性分析
  2. 数据清洗:处理异常值,进行特征缩放
  3. 特征工程:创建新特征,生成多项式特征
  4. 模型训练:训练多个基础模型,比较性能
  5. 模型优化:通过网格搜索优化随机森林参数,尝试梯度提升
  6. 模型评估:使用交叉验证、残差分析等方法全面评估模型
  7. 模型部署:保存模型,演示预测流程

最终,我们得到了一个性能良好的房价预测模型,R²达到0.85以上,RMSE约为3.5。

8.2 改进方向

虽然当前模型性能良好,但仍有改进空间:

  1. 更多特征工程:可以尝试更多特征组合和变换
  2. 更多模型尝试:可以尝试XGBoost、LightGBM等更先进的模型
  3. 集成学习:可以尝试模型堆叠(Stacking)等集成方法
  4. 深度学习:对于更复杂的数据集,可以尝试神经网络
  5. 实时数据:结合实时市场数据,定期更新模型

8.3 实际应用建议

在实际应用中,房价预测模型需要注意:

  • 数据时效性:房价受市场影响大,需要定期更新数据
  • 区域差异:不同地区的房价影响因素不同,可能需要分区域建模
  • 特征可解释性:在某些场景下,模型的可解释性比准确性更重要
  • 业务理解:结合领域知识进行特征工程和模型选择

通过本项目的完整流程,读者可以掌握使用scikit-learn进行回归模型构建的完整技能,并将其应用到其他回归问题中。