# Python实战:用Scikit-Learn的Lasso回归搞定特征选择(附完整代码)
最近在做一个预测项目,特征池里塞了上百个变量,跑出来的模型不仅臃肿,解释起来也让人头疼。同事建议试试Lasso回归,说它能在建模的同时自动“瘦身”,把不重要的特征系数直接压到零。我抱着试试看的心态跑了一遍,效果出乎意料——模型不仅预测精度没降,可解释性还大大提升了。如果你也在为高维数据下的特征冗余和模型复杂度发愁,今天这篇实战指南或许能给你一条清晰的路径。我们不会深究复杂的数学推导,而是聚焦于如何用Scikit-Learn库,一步步构建一个可复用的Lasso回归工作流,从数据预处理、参数调优到结果解读,把特征选择这件事落到实处。
这篇文章面向的是已经熟悉Python基础语法和机器学习基本概念的开发者。你可能知道线性回归,也听说过过拟合,但面对实际项目中“该选哪些特征”这个具体问题时,仍然需要一套拿来即用的工具和方法。我们将通过一个完整的案例,手把手演示如何将Lasso回归应用于特征选择,并附上可直接运行的代码块,让你能快速在自己的数据集上验证效果。
## 1. 环境准备与数据理解
在开始敲代码之前,确保你的工作环境已经就绪。我们需要一个标准的Python数据科学环境。我个人的习惯是使用Anaconda来管理环境,它能避免很多包依赖的麻烦。
```bash
# 创建一个新的conda环境(可选)
conda create -n lasso_demo python=3.9
conda activate lasso_demo
# 安装核心库
pip install numpy pandas scikit-learn matplotlib seaborn
```
> 提示:如果你使用Jupyter Notebook或VS Code进行开发,确保内核(Kernel)切换到了正确的环境。使用`scikit-learn`时,版本最好在1.0以上,以确保API的稳定性。
接下来是数据。为了演示的通用性,我们使用一个经典的、公开的数据集:加利福尼亚住房数据集。它比波士顿房价数据集更现代,也避免了后者的一些伦理争议。这个数据集包含了加州各街区的人口、地理位置和房屋中位数价格等信息。
```python
# 导入基础库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import fetch_california_housing
# 设置绘图风格
sns.set_theme(style="whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用于显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 加载数据
california = fetch_california_housing()
X_raw = california.data # 特征矩阵
y = california.target # 目标变量:房屋中位价(单位:十万美元)
feature_names = california.feature_names
# 转换为DataFrame便于查看
df_features = pd.DataFrame(X_raw, columns=feature_names)
df_features['MedHouseVal'] = y
print(f"数据集形状: {df_features.shape}")
print(f"特征名称: {feature_names}")
print("\n前5行数据预览:")
print(df_features.head())
```
运行上面的代码,你会看到数据有8个特征,分别是`MedInc`(收入中位数)、`HouseAge`(房龄中位数)、`AveRooms`(平均房间数)等,以及一个目标变量`MedHouseVal`。我们的任务就是利用这8个特征来预测房价,并在此过程中找出哪些特征才是真正重要的。
在应用Lasso之前,理解数据的尺度至关重要。Lasso回归对特征的尺度非常敏感,因为L1正则化项是对系数绝对值求和。如果某个特征的数值范围(例如“人口数”从0到上万)远大于其他特征(例如“房龄”从0到100),那么即使它对目标变量的影响很小,其系数也可能因为数值大而避免被压缩到零,这会导致特征选择出现偏差。因此,**标准化**是必不可少的一步。
## 2. 数据预处理与基线模型建立
数据预处理不仅仅是标准化。一个稳健的流程还包括处理缺失值、探索性数据分析以及划分训练集和测试集。我们的数据是完整的,所以可以跳过缺失值处理。
首先,让我们直观地感受一下特征与目标的关系,以及特征之间的相关性。
```python
# 1. 特征与目标变量的关系(以MedInc为例)
plt.figure(figsize=(6,4))
plt.scatter(df_features['MedInc'], df_features['MedHouseVal'], alpha=0.5, s=10)
plt.xlabel('收入中位数 (MedInc)')
plt.ylabel('房屋中位价 (MedHouseVal)')
plt.title('收入与房价关系散点图')
plt.tight_layout()
plt.show()
# 2. 特征间相关性热力图
corr_matrix = df_features.corr()
plt.figure(figsize=(10,8))
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', center=0,
square=True, linewidths=.5, cbar_kws={"shrink": .8})
plt.title('特征与目标变量相关性矩阵')
plt.tight_layout()
plt.show()
```
从相关性热力图中,你能快速发现哪些特征与房价高度相关(如`MedInc`),以及哪些特征之间可能存在较强的共线性(例如`AveRooms`和`AveBedrms`)。Lasso回归在处理共线性方面有优势,但理解数据的这一背景对后续解释模型结果很有帮助。
接下来是核心的预处理和基线模型建立。我们将数据标准化并划分,同时建立一个普通的线性回归模型作为性能对比的基准。
```python
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# 划分训练集和测试集 (80%/20%)
X_train_raw, X_test_raw, y_train, y_test = train_test_split(
X_raw, y, test_size=0.2, random_state=42
)
# 标准化特征:注意,只使用训练集的数据来拟合scaler,避免数据泄露
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train_raw)
X_test_scaled = scaler.transform(X_test_raw)
# 建立并训练普通线性回归模型(基线模型)
lr_baseline = LinearRegression()
lr_baseline.fit(X_train_scaled, y_train)
# 在测试集上评估基线模型
y_pred_baseline = lr_baseline.predict(X_test_scaled)
mse_baseline = mean_squared_error(y_test, y_pred_baseline)
mae_baseline = mean_absolute_error(y_test, y_pred_baseline)
r2_baseline = r2_score(y_test, y_pred_baseline)
print("=== 普通线性回归(基线模型)性能 ===")
print(f"均方误差 (MSE): {mse_baseline:.4f}")
print(f"平均绝对误差 (MAE): {mae_baseline:.4f}")
print(f"决定系数 (R²): {r2_baseline:.4f}")
print("\n基线模型系数:")
for name, coef in zip(feature_names, lr_baseline.coef_):
print(f" {name:12}: {coef:+.6f}")
```
这个基线模型给了我们一个性能参照点。注意看,所有8个特征的系数都不为零。现在,我们引入Lasso回归,看看它如何通过调整一个关键参数,在保持预测能力的同时,实现特征的自动筛选。
## 3. Lasso回归的核心:参数调优与特征选择
Lasso回归在`sklearn.linear_model.Lasso`中实现,其核心超参数是`alpha`(在有些文献中也叫`λ`)。`alpha`控制着正则化的强度:`alpha`越大,惩罚越重,更多的系数会被压缩至零。找到一个合适的`alpha`值是成功应用Lasso的关键。
盲目设置一个`alpha`值(比如0.1)是武断的。最佳实践是使用交叉验证来寻找使模型在未知数据上泛化性能最好的`alpha`值。`sklearn`提供了`LassoCV`类,它内置了交叉验证功能,非常方便。
```python
from sklearn.linear_model import LassoCV
import warnings
warnings.filterwarnings('ignore') # 忽略一些收敛警告
# 使用LassoCV进行交叉验证,寻找最佳alpha
# alphas参数是一系列待测试的alpha值,这里我们设置一个对数空间的范围
alphas = np.logspace(-4, 1, 50) # 从10^-4到10^1,生成50个点
lasso_cv = LassoCV(alphas=alphas, cv=5, random_state=42, max_iter=10000)
lasso_cv.fit(X_train_scaled, y_train)
# 输出最佳alpha值
best_alpha = lasso_cv.alpha_
print(f"通过5折交叉验证得到的最佳 alpha 值为: {best_alpha:.6f}")
# 使用最佳alpha训练最终的Lasso模型
lasso_best = Lasso(alpha=best_alpha, max_iter=10000)
lasso_best.fit(X_train_scaled, y_train)
# 评估最终Lasso模型
y_pred_lasso = lasso_best.predict(X_test_scaled)
mse_lasso = mean_squared_error(y_test, y_pred_lasso)
mae_lasso = mean_absolute_error(y_test, y_pred_lasso)
r2_lasso = r2_score(y_test, y_pred_lasso)
print("\n=== 最优Lasso回归模型性能 ===")
print(f"均方误差 (MSE): {mse_lasso:.4f}")
print(f"平均绝对误差 (MAE): {mae_lasso:.4f}")
print(f"决定系数 (R²): {r2_lasso:.4f}")
# 对比基线模型
print("\n=== 性能对比 (Lasso vs 基线) ===")
print(f"MSE降低: {(mse_baseline - mse_lasso):.4f} (越小越好)")
print(f"R²提升: {(r2_lasso - r2_baseline):.4f} (越大越好)")
```
现在,到了最激动人心的部分:查看Lasso模型筛选后的特征。我们将系数不为零的特征视为被模型“选中”的重要特征。
```python
# 获取Lasso模型系数
lasso_coef = lasso_best.coef_
# 创建系数表格,便于分析
coef_df = pd.DataFrame({
'特征': feature_names,
'系数值': lasso_coef,
'是否被选中 (系数非零)': lasso_coef != 0
}).sort_values(by='系数值', key=abs, ascending=False) # 按系数绝对值排序
print("=== Lasso回归特征选择结果 ===")
print(coef_df.to_string(index=False))
# 可视化系数
plt.figure(figsize=(10, 6))
colors = ['red' if c == 0 else 'steelblue' for c in lasso_coef]
bars = plt.barh(range(len(feature_names)), lasso_coef, color=colors)
plt.yticks(range(len(feature_names)), feature_names)
plt.xlabel('系数大小')
plt.title(f'Lasso回归特征系数 (alpha={best_alpha:.4f})')
plt.axvline(x=0, color='black', linestyle='-', linewidth=0.5)
# 添加数值标签
for i, (bar, coef) in enumerate(zip(bars, lasso_coef)):
if abs(coef) > 0.01: # 只给显著的系数加标签
width = bar.get_width()
plt.text(width/2 if width>0 else width-0.05, bar.get_y() + bar.get_height()/2,
f'{coef:.3f}', ha='center' if width>0 else 'right', va='center', color='white', fontweight='bold')
plt.tight_layout()
plt.show()
# 统计被选中的特征数量
selected_count = np.sum(lasso_coef != 0)
print(f"\n原始特征总数: {len(feature_names)}")
print(f"Lasso筛选后保留的特征数: {selected_count}")
print(f"特征压缩率: {(1 - selected_count/len(feature_names))*100:.1f}%")
```
通过这个分析,你能清晰地看到哪些特征的系数被保留(非零),哪些被彻底压缩为零。通常,被保留的特征可以解释为对目标变量有显著线性影响的因子。这个结果不仅简化了模型,也为我们提供了业务洞察。例如,如果`AveOccup`(平均入住人数)的系数被压缩为零,可能意味着在控制了收入、房龄等因素后,这个特征对房价的独立解释力很弱。
## 4. 深入分析:正则化路径与超参数影响
为了更深入地理解`alpha`参数如何影响特征选择,我们可以绘制**正则化路径图**。它展示了每个特征的系数随着`alpha`值增大而变化的轨迹。
```python
from sklearn.linear_model import lasso_path
# 计算一系列alpha下的系数路径
alphas_path, coefs_path, _ = lasso_path(X_train_scaled, y_train, alphas=alphas)
plt.figure(figsize=(12, 8))
# 取alpha的对数用于绘图,使变化更清晰
log_alphas_path = np.log10(alphas_path)
for i, coef in enumerate(coefs_path):
plt.plot(log_alphas_path, coef, label=feature_names[i], linewidth=2)
plt.axvline(x=np.log10(best_alpha), color='k', linestyle='--', label=f'最优 alpha ({best_alpha:.4f})')
plt.xlabel('log10(alpha)')
plt.ylabel('系数值')
plt.title('Lasso正则化路径:特征系数随alpha变化')
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
```
观察这张图,你会发现随着`alpha`增大(从左到右),正则化惩罚变强,越来越多的特征系数“收缩”并最终归零。这条路径清晰地揭示了模型的“抉择”过程:哪些特征最顽强(最后才归零),哪些特征最容易被舍弃。
除了`alpha`,Lasso回归还有其他一些实用参数,了解它们有助于应对更复杂的情况:
| 参数 | 类型 | 默认值 | 说明与调优建议 |
| :--- | :--- | :--- | :--- |
| `alpha` | float | 1.0 | **正则化强度**。最重要的参数,必须通过交叉验证等网格搜索方法确定。值越大,惩罚越强,模型越稀疏。 |
| `fit_intercept` | bool | True | 是否计算截距项。通常保持为True,除非你确信数据已经中心化。 |
| `normalize` | deprecated | - | **已弃用**。在`sklearn` 1.2版本后已移除。务必在拟合前使用`StandardScaler`手动标准化数据。 |
| `max_iter` | int | 1000 | 求解器最大迭代次数。对于某些数据集或较小的`alpha`,可能需要增加此值以避免“未收敛”警告。 |
| `tol` | float | 1e-4 | 优化的容忍度。如果成本函数的下降小于此值,则停止迭代。除非有特殊需求,一般无需调整。 |
| `selection` | str | ‘cyclic’ | 系数更新循环方式。‘cyclic’是默认的逐个特征更新;‘random’是随机更新,有时能加快收敛,但结果可能有微小随机性。 |
> 注意:在实际项目中,如果特征数量极大(例如上万维),`LassoCV`的计算可能会比较慢。此时可以考虑使用`sklearn.linear_model.MultiTaskLassoCV`(针对多输出问题)或者使用随机抽样减少`alphas`参数列表的长度来加速。另外,确保数据已经标准化,这是Lasso回归正确工作的前提。
最后,我们用一个完整的、可复用的函数来封装整个Lasso特征选择流程。这个函数接收原始数据和特征名称,输出训练好的模型、筛选出的特征列表以及性能报告。
```python
def lasso_feature_selection_pipeline(X, y, feature_names, test_size=0.2, random_state=42, alpha_range=(-4, 1, 50)):
"""
一个完整的Lasso特征选择流水线。
参数:
X: 原始特征矩阵 (n_samples, n_features)
y: 目标变量 (n_samples,)
feature_names: 特征名称列表
test_size: 测试集比例
random_state: 随机种子
alpha_range: 用于搜索的alpha对数范围 (start, stop, num)
返回:
model: 训练好的最优Lasso模型
selected_features: 被选中的特征名称列表
report_dict: 包含各种评估指标和详细结果的字典
"""
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LassoCV
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
import pandas as pd
# 1. 划分数据集
X_train_raw, X_test_raw, y_train, y_test = train_test_split(
X, y, test_size=test_size, random_state=random_state
)
# 2. 标准化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train_raw)
X_test = scaler.transform(X_test_raw)
# 3. 使用交叉验证寻找最佳alpha
alphas = np.logspace(*alpha_range)
lasso_cv = LassoCV(alphas=alphas, cv=5, random_state=random_state, max_iter=10000)
lasso_cv.fit(X_train, y_train)
best_alpha = lasso_cv.alpha_
# 4. 用最佳alpha训练最终模型
final_model = Lasso(alpha=best_alpha, max_iter=10000)
final_model.fit(X_train, y_train)
# 5. 评估与特征选择
y_pred = final_model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
# 获取非零系数特征
coef_nonzero_mask = final_model.coef_ != 0
selected_feature_names = np.array(feature_names)[coef_nonzero_mask].tolist()
selected_feature_indices = np.where(coef_nonzero_mask)[0].tolist()
# 6. 组装结果报告
report = {
'best_alpha': best_alpha,
'test_mse': mse,
'test_r2': r2,
'n_features_total': len(feature_names),
'n_features_selected': len(selected_feature_names),
'selected_feature_names': selected_feature_names,
'selected_feature_indices': selected_feature_indices,
'all_coefficients': pd.Series(final_model.coef_, index=feature_names).to_dict(),
'model': final_model,
'scaler': scaler
}
print("=== Lasso特征选择流水线报告 ===")
print(f"最佳正则化参数 alpha: {best_alpha:.6f}")
print(f"测试集 MSE: {mse:.4f}")
print(f"测试集 R²: {r2:.4f}")
print(f"特征选择结果: {len(selected_feature_names)}/{len(feature_names)} 个特征被保留")
print(f"保留的特征: {selected_feature_names}")
return final_model, selected_feature_names, report
# 使用封装好的函数运行整个流程
final_lasso_model, selected_feats, pipeline_report = lasso_feature_selection_pipeline(
X_raw, y, feature_names, test_size=0.2, random_state=42
)
```
这个函数提供了一个端到端的解决方案。你可以把它复制到自己的项目中,替换掉数据集和特征名称,就能快速得到Lasso特征选择的结果。它返回的`report`字典里包含了模型、标准化器、系数等所有必要信息,方便你进行后续的模型部署或更深入的分析。
特征选择从来不是终点。当你拿到Lasso筛选出的特征子集后,可以基于这些特征重新训练一个更简单的模型(比如普通的线性回归),或者将它们作为输入,尝试更复杂的集成模型。在实践中我发现,将Lasso作为特征预筛选器,再结合业务知识进行人工复核,往往能构建出既稳健又易于理解的预测模型。