机器学习实操指南:用UCI真实数据集跑通第一个模型
1. 这不是又一篇“机器学习入门指南”,而是一份我踩过坑、调过参、被数据骂醒后写下的实操手记
你点开这篇文章,大概率正坐在电脑前,刚装完Anaconda,对着Jupyter里一片空白的cell发呆;或者已经翻烂了三本《机器学习实战》,却连一个能跑通的房价预测模型都搭不起来;又或者,你反复运行model.fit(),结果训练损失曲线像心电图一样上下乱跳,测试集准确率比随机猜高不了几个百分点——然后默默关掉浏览器,心想:“是不是我不适合干这行?”
别急着关页面。我干这行八年,从用Excel做线性回归开始,到后来带团队部署千节点分布式训练集群,中间换过七次GPU服务器,重装过二十三次Python环境,被真实业务数据毒打过上百次。今天这篇,不讲Transformer的注意力机制有多精妙,不画损失函数的梯度下降动画,也不推荐你去啃《统计学习方法》第127页的定理证明。我就用你明天就能打开、运行、改参数、看结果的真实路径,带你用UCI仓库里最经典、最“不完美”的几个数据集,把机器学习从“听说很厉害”变成“我亲手跑出来了”。
核心关键词就三个:真实数据集、可复现步骤、零理论堆砌。我们全程用scikit-learn+pandas+matplotlib这套最稳、最没坑、新手装一次就能用三年的组合。所有代码你复制粘贴进Jupyter就能跑,所有数据集我给你标好下载地址和校验方式,所有报错信息我都提前试出来、写清楚怎么解。比如Boston Housing数据集,官方早已下架,但它的替代品——California Housing数据集,字段含义、数据分布、常见陷阱,我全给你拆明白。再比如,为什么用StandardScaler之前必须先train_test_split?为什么RandomForestRegressor的n_estimators=100在小数据上反而不如n_estimators=20?这些不是玄学,是数据在硬盘里躺了十年后,给每个认真调参的人留下的指纹。
适合谁看?如果你能写出import pandas as pd,知道.csv文件双击会用Excel打开,那你就完全够格。不需要数学博士背景,不需要背熟所有算法公式,甚至不需要搞懂什么是“偏置项”。你需要的只是一台能联网的电脑,和一点“我今天非要把这个模型跑通”的执念。接下来的内容,就是按这个执念设计的——每一步都有目的,每一行代码都有交代,每一个坑我都替你踩过了。
2. 为什么死磕“真实数据集”?因为教科书里的数据,根本不会骗你
2.1 真实世界的数据,从来不是“干净”的
教科书和Kaggle入门赛里,数据集往往像实验室培养皿里的细胞:特征列名规整(feature_1,feature_2),缺失值被填得严丝合缝(fillna(0)或dropna()一键解决),目标变量分布平滑如正态曲线,类别标签比例均衡得像天平。这种数据,是用来帮你理解算法原理的,不是用来训练能上线的模型的。
而UCI Machine Learning Repository里的数据,是活生生从现实世界里“扒”下来的。以我们马上要用的California Housing数据集为例(它正是Boston Housing的现代继承者):
- 它来自1990年美国人口普查,包含20640个街区的房屋中位数价格;
- 特征包括:人均收入(
median_income)、房屋年龄(housing_median_age)、平均房间数(total_rooms)、平均卧室数(total_bedrooms)、人口(population)、家庭数(households)、经纬度(latitude,longitude); - 关键来了:
total_bedrooms这一列,有207个缺失值——不是0,不是空字符串,是真正的NaN; - 更要命的是,
median_income的单位是“万美元”,但数值范围是0.4999到15.0001,这意味着最小收入是4999美元,最大是15万美元——这显然不符合常识,实际是做了缩放处理,但文档没说清楚; - 目标变量
median_house_value,上限被硬性截断在500001美元,导致分布右端出现一个尖锐的“峰”,这不是自然分布,是数据采集规则造成的。
提示:这些“不完美”,恰恰是机器学习工程师每天面对的真相。你花三天时间调参让模型在干净数据上提升0.5%的R²,不如花一小时处理好
total_bedrooms的缺失值,让模型在真实场景下稳定10%。真实数据的“脏”,不是障碍,而是你和算法之间最诚实的对话起点。
2.2 为什么选UCI,而不是Kaggle或自建数据?
Kaggle上的竞赛数据,往往经过主办方精心清洗、特征工程、甚至注入噪声来增加难度,它的目标是筛选出算法高手,不是教你建模流程。而自建数据集,对新手来说门槛太高——你得先搞定数据采集、存储、标注,光是爬虫反爬就可能耗掉一周。
UCI仓库则完全不同。它像一个老派的数据博物馆:
- 历史久、版本稳:California Housing数据集自1997年发布,至今未变,你今天跑的代码,五年后还能复现;
- 文档实、陷阱明:每个数据集都有
data.names文件,详细说明每个字段的物理意义、单位、取值范围,甚至会写明“该数据集曾被用于验证空间自相关性假设”; - 体积小、上手快:2万条记录,内存占用不到10MB,普通笔记本秒加载,不用等数据下载,不用配Docker,不用申请GPU配额。
我试过用Kaggle的“Titanic”数据集教新人,结果80%的人卡在pd.get_dummies()处理Cabin字段的缺失值上——因为Cabin有1309个不同值,其中1014个是NaN,get_dummies直接生成1300+列,内存爆掉。而UCI的California数据集,所有特征都是数值型,缺失值仅存在于单一列,处理逻辑清晰:用中位数填充,或用KNNImputer基于地理邻近性填充。这就是“可教学性”和“可复现性”的本质区别。
2.3 为什么坚决绕开“深度学习”和“大模型”?
看到标题里“10个建议”,你可能期待“如何用PyTorch搭建CNN”或“微调Llama3做文本分类”。抱歉,这次真没有。原因很实在:
- 硬件门槛:训练一个像样的CNN,至少需要一块RTX 3060(12GB显存),而你的MacBook Air或公司配的办公本,大概率只有集成显卡;
- 调试成本:当
val_loss不下降时,你是该调学习率?改Batch Size?换优化器?还是检查数据增强是否引入了标签泄露?这些问题的答案,需要你对反向传播、梯度计算、CUDA内存管理有扎实理解——而这,恰恰是初学者最缺的底层认知; - 收益倒挂:用
LinearRegression在California数据集上,R²能达到0.6左右;换成MLPRegressor(3层全连接),R²可能只涨到0.62,但训练时间从0.3秒变成12秒,代码量从15行变成80行,出错概率翻5倍。
我的经验是:先让模型“能跑”,再让它“跑好”,最后才让它“跑快”。就像学开车,你得先在空地练熟离合、油门、方向盘,再去高速上飙车。这10个建议,全部锚定在“让第一个模型在真实数据上稳稳跑通”这个唯一目标上。等你亲手用DecisionTreeRegressor画出第一棵决策树,用cross_val_score看到5折交叉验证的分数波动小于±0.02,那时你再去看Transformer论文,感受会完全不同——因为你知道,那些精妙结构,最终要落地成一行model.predict(X_test),而这一行的背后,是数据、是特征、是评估,不是魔法。
3. 实操四步法:从下载数据到模型评估,每一步都经我手把手验证
3.1 数据获取与加载:拒绝“pip install uci-dataset”,用最原始的方式建立信任
很多教程会推荐你用fetch_california_housing()这个API,它确实方便,一行代码搞定:
from sklearn.datasets import fetch_california_housing housing = fetch_california_housing() X, y = housing.data, housing.target但我建议你手动下载、手动加载。原因很简单:fetch_california_housing()内部会自动做标准化、填充缺失值,甚至把latitude和longitude合并成一个AveOccup特征。你还没开始建模,数据就已经被“预处理”过了,这违背了我们“直面真实数据”的初衷。
正确姿势如下:
- 访问UCI官网:打开 https://archive.ics.uci.edu/ml/datasets/California+Housing
- 找到Data Folder链接:页面中部,点击“Data Folder”,进入文件列表页;
- 下载核心文件:右键保存以下两个文件到本地文件夹(比如
./data/california/):housing.data(20640行,8列,空格分隔)housing.names(数据字典,必读!)
- 用pandas加载并命名列:
import pandas as pd import numpy as np # 指定列名,严格按housing.names文件描述 column_names = [ 'longitude', 'latitude', 'housing_median_age', 'total_rooms', 'total_bedrooms', 'population', 'households', 'median_income', 'median_house_value' ] # 加载数据,指定分隔符为空格,跳过首行(如果有的话) df = pd.read_csv( './data/california/housing.data', sep=r'\s+', # 匹配一个或多个空格 names=column_names, engine='python' # 防止警告 ) print(f"原始数据形状: {df.shape}") print(f"缺失值统计:\n{df.isnull().sum()}")运行后你会看到:
原始数据形状: (20640, 9) 缺失值统计: longitude 0 latitude 0 housing_median_age 0 total_rooms 0 total_bedrooms 207 ← 就是这里! population 0 households 0 median_income 0 median_house_value 0注意:
sep=r'\s+'是关键。很多新手用默认的逗号分隔,结果整个文件被读成一列。UCI的.data文件几乎全是空格分隔,这是它的“祖传格式”。另外,engine='python'能避免pandas在处理多空格时的解析警告,属于实操中的“防抖”设置。
3.2 探索性数据分析(EDA):用三张图,看清数据的脾气
加载完数据,别急着train_test_split。先花10分钟,和数据“聊聊天”。我只用三张图,就能抓住California数据集的全部要害:
第一张:目标变量分布直方图
import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize=(10, 6)) sns.histplot(df['median_house_value'], bins=50, kde=True) plt.title('California House Price Distribution') plt.xlabel('Median House Value ($)') plt.show()你会看到一个明显的右偏分布,且在500000处有一个陡峭的“断崖”——这就是数据采集的硬性上限。这意味着:
- 任何回归模型,在预测>500000的房价时,天然存在系统性偏差;
- 如果你用MSE作为损失函数,这个“断崖”会严重拉高整体误差,因为预测500001和500000的差距,在MSE里是1,但实际业务中,它们可能都属于“高端豪宅”同一档。
第二张:关键特征散点图矩阵(只选4个)
# 只选最关键的4个特征,避免图太密 features = ['median_income', 'housing_median_age', 'total_rooms', 'latitude'] sns.pairplot(df[features + ['median_house_value']], hue='median_house_value', palette='viridis', plot_kws={'alpha':0.3}) plt.suptitle('Feature Relationships with House Price', y=1.02) plt.show()重点观察:
median_incomevsmedian_house_value:呈现强正相关,但不是直线,而是类似“指数增长”的曲线——收入从2万涨到4万,房价涨20万;从8万涨到10万,房价可能涨80万。这提示我们,对收入做对数变换(np.log1p)可能比直接用原值效果更好;latitudevsmedian_house_value:能看出明显的地理聚类,北加州(纬度高)房价普遍低于南加州(纬度低),但中间有一段“洼地”,这对应着中央谷地农业区——数据在告诉你,地理位置不能只看数字,还要结合地理常识。
第三张:缺失值热力图
plt.figure(figsize=(10, 4)) sns.heatmap(df.isnull(), cbar=False, yticklabels=False, cmap='viridis') plt.title('Missing Values Heatmap') plt.show()207个total_bedrooms缺失值,在20640行中占比约1%,不算多,但它们的分布是否有规律?我们快速验证:
# 检查缺失值是否集中在某些区域 missing_mask = df['total_bedrooms'].isnull() print("缺失值所在区域的平均收入:", df[missing_mask]['median_income'].mean()) print("非缺失值所在区域的平均收入:", df[~missing_mask]['median_income'].mean())结果可能是:缺失区域平均收入(3.2)显著低于非缺失区域(4.1)。这说明缺失不是随机的,而是低收入社区更可能不统计卧室数——这是典型的“缺失机制为MNAR(Missing Not At Random)”。此时,用全局中位数填充就不太合理,应该用同收入分位数的中位数,或者用KNNImputer基于median_income和latitude来填充。
实操心得:这三张图,我每次拿到新数据集必画。它不产生模型,但能让你在写第一行
model.fit()前,就预判出模型的天花板在哪里。比如,看到median_house_value的断崖,你就该立刻决定:后续评估改用MAE(平均绝对误差)而非MSE,因为MAE对异常值不敏感;看到latitude的聚类,你就该想到:可以构造“是否位于南加州(latitude > 36.5)”这样的二值特征。EDA不是可选项,它是建模的“导航仪”。
3.3 特征工程与数据预处理:不做“炫技”,只做“必要”
很多教程把特征工程讲得神乎其技:PCA降维、多项式特征、Target Encoding……但对于California数据集,真正必要的操作只有四步:
第一步:处理total_bedrooms缺失值
如前所述,简单用df['total_bedrooms'].fillna(df['total_bedrooms'].median())是下策。上策是用KNNImputer,它能利用其他相似样本的信息来填充:
from sklearn.impute import KNNImputer # 构造用于填充的特征矩阵(排除目标变量和无关列) impute_features = ['median_income', 'latitude', 'longitude', 'housing_median_age'] imputer = KNNImputer(n_neighbors=5) # 找5个最相似的邻居 df[impute_features + ['total_bedrooms']] = imputer.fit_transform( df[impute_features + ['total_bedrooms']] )为什么选这四个特征?因为卧室数,本质上由家庭规模(median_income)、地理区位(latitude,longitude)和社区成熟度(housing_median_age)共同决定。KNNImputer会计算每个缺失样本与其他所有样本的欧氏距离,取距离最近的5个,用它们的total_bedrooms均值来填充。实测下来,这比全局中位数填充,能让后续模型的R²提升约0.015。
第二步:对median_income做对数变换
df['median_income_log'] = np.log1p(df['median_income']) # log1p避免log(0) # 同时,移除原始income列,避免信息冗余 df = df.drop('median_income', axis=1)np.log1p是log(x+1),它能有效压缩高收入端的极端值,让分布更接近正态。你可以画图对比:sns.histplot(df['median_income'])vssns.histplot(df['median_income_log']),后者明显更“胖”,更适合线性模型。
第三步:构造地理交互特征
经纬度本身是弱特征,但它们的组合能揭示强信息:
# 计算到旧金山、洛杉矶、圣地亚哥三大城市的球面距离(简化版) def haversine_distance(lat1, lon1, lat2, lon2): # 简化计算,单位:公里 dlat = np.radians(lat2 - lat1) dlon = np.radians(lon2 - lon1) a = np.sin(dlat/2)**2 + np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) * np.sin(dlon/2)**2 c = 2 * np.arcsin(np.sqrt(a)) return 6371 * c # 地球半径6371km sf_lat, sf_lon = 37.7749, -122.4194 la_lat, la_lon = 34.0522, -118.2437 sd_lat, sd_lon = 32.7157, -117.1611 df['dist_to_sf'] = haversine_distance(df['latitude'], df['longitude'], sf_lat, sf_lon) df['dist_to_la'] = haversine_distance(df['latitude'], df['longitude'], la_lat, la_lon) df['dist_to_sd'] = haversine_distance(df['latitude'], df['longitude'], sd_lat, sd_lon) # 再构造一个“是否靠近大城市”的布尔特征 df['is_near_major_city'] = ((df['dist_to_sf'] < 100) | (df['dist_to_la'] < 100) | (df['dist_to_sd'] < 100)).astype(int)这个操作看似复杂,但带来的提升是实打实的:is_near_major_city这个单列,就能让LinearRegression的R²从0.59提升到0.61。因为房价最核心的驱动因素,就是“离工作机会近不近”。
第四步:标准化(仅对线性模型)
注意:StandardScaler只对LinearRegression、Ridge等线性模型必要,对DecisionTree、RandomForest完全无效,因为树模型只关心特征的排序,不关心数值大小。
from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 分离特征和目标 X = df.drop('median_house_value', axis=1) y = df['median_house_value'] # 划分训练集和测试集(必须在标准化前!) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 对训练集标准化,再用同一scaler转换测试集 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 注意:用fit_transform后的scaler.transform!关键提醒:
scaler.fit_transform(X_train)和scaler.transform(X_test)必须成对使用。如果对测试集也用fit_transform,相当于用测试集自己的均值和标准差去标准化,这会造成数据泄露,让模型在测试集上表现虚高。这是新手最高频的错误之一,务必刻在脑子里。
3.4 模型训练与评估:用“交叉验证”代替“一次分割”,建立可信度
现在,终于到了model.fit()环节。但我们不满足于“跑通”,我们要确保结果可信。所以,放弃简单的train_test_split+model.score(),改用5折交叉验证:
from sklearn.linear_model import LinearRegression, Ridge from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score import numpy as np # 定义模型列表 models = { 'Linear Regression': LinearRegression(), 'Ridge Regression': Ridge(alpha=1.0), 'Decision Tree': DecisionTreeRegressor(max_depth=5, random_state=42), 'Random Forest': RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42) } # 存储结果 results = {} for name, model in models.items(): # 对线性模型,用标准化后的数据;对树模型,用原始数据 if name in ['Linear Regression', 'Ridge Regression']: X_use = X_train_scaled else: X_use = X_train # 5折交叉验证,评分用R² cv_scores = cross_val_score(model, X_use, y_train, cv=5, scoring='r2') results[name] = { 'mean_r2': cv_scores.mean(), 'std_r2': cv_scores.std(), 'scores': cv_scores } print(f"{name}: R² = {cv_scores.mean():.3f} (±{cv_scores.std():.3f})")输出类似:
Linear Regression: R² = 0.602 (±0.008) Ridge Regression: R² = 0.603 (±0.007) Decision Tree: R² = 0.721 (±0.012) Random Forest: R² = 0.815 (±0.009)看到没?RandomForest的R²高达0.815,远超线性模型。但这不意味着它“一定更好”。交叉验证的标准差(std_r2)告诉你稳定性:RandomForest的0.009,比DecisionTree的0.012更小,说明它在不同数据子集上表现更一致。而LinearRegression的0.008,虽然均值低,但极其稳定——这在生产环境中,有时比高分更重要。
最后,用测试集做终局检验:
# 选表现最好的RandomForest best_model = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42) best_model.fit(X_train, y_train) # 树模型用原始数据 y_pred = best_model.predict(X_test) from sklearn.metrics import mean_absolute_error, r2_score print(f"Test R²: {r2_score(y_test, y_pred):.3f}") print(f"Test MAE: ${mean_absolute_error(y_test, y_pred):,.0f}")你会得到一个具体的数字,比如Test R²: 0.812,Test MAE: $48,230。这个MAE意味着:模型预测的房价,平均误差是4.8万美元。结合加州房价中位数约21万美元,这个误差在业务上是可接受的(误差率约23%)。如果MAE是15万美元,那这个模型就该回炉重造了。
实操心得:交叉验证不是为了“挑最高分的模型”,而是为了“排除不稳定的模型”。我见过太多人,用单次
train_test_split得到0.85的R²,结果换一组随机种子,分数暴跌到0.5。5折CV的均值±标准差,才是你敢跟老板汇报的底气。记住:在真实项目中,模型的稳定性,永远比单次的峰值性能重要十倍。
4. 十个血泪凝结的“不废话”建议:每一条都来自凌晨三点的debug现场
4.1 建议1:永远先用df.info()和df.describe(),再写任何模型代码
这是最基础、却被90%新手跳过的一步。df.info()能一眼看出:
- 有多少列是
object类型(意味着可能是字符串,需要编码); - 每列的非空计数,比
df.isnull().sum()更直观; - 内存占用,帮你预判是否需要
category类型节省内存。
df.describe()则暴露数据的“性格”:
mean和std差距极大(如total_rooms均值2635,标准差6800),说明存在极端值;25%和50%(中位数)非常接近,但75%和max差距巨大,说明上尾肥厚;min为负数?那一定是数据录入错误,必须查源头。
我曾在一个医疗数据集上,因没看describe(),直接用StandardScaler,结果发现age列的min是-120(显然是录入错误),导致整个标准化失效。花了三小时才定位。
4.2 建议2:train_test_split的random_state,必须设为固定整数
random_state=42不是玄学,是保证可复现性的生命线。如果不设,每次运行,训练集和测试集都不同,你昨天调好的参数,今天跑出来分数差0.1,你会怀疑人生。更可怕的是,当你把代码交给同事,他跑出来的结果和你不一样,协作就此崩盘。
4.3 建议3:特征名里绝不能有空格和特殊符号
df.columns = ['house age', 'rooms count']看着清爽,但会埋雷:
model.predict(X_test)可能报错,因为某些库不支持空格列名;- 用
X_test['house age']取列没问题,但X_test.house age会语法错误; - 后期用SQL或Spark读取时,空格必须用反引号包裹,极其麻烦。
统一用下划线:house_age,rooms_count。这是行业默认规范,照做就行。
4.4 建议4:RandomForest的n_estimators,不要盲目设1000
很多人觉得“越多越好”。错。在California数据集上,我实测:
n_estimators=10:R²=0.792n_estimators=50:R²=0.810n_estimators=100:R²=0.815n_estimators=500:R²=0.816(+0.001),但训练时间从1.2秒涨到5.8秒
收益递减明显。我的经验是:先设100,画出oob_score_曲线,看分数何时收敛,就停在那里。对绝大多数中小数据集,100是黄金数字。
4.5 建议5:DecisionTree的max_depth,宁浅勿深
树太深=过拟合。在California数据上,max_depth=15的树,训练R²=0.99,测试R²=0.72;而max_depth=5,训练R²=0.75,测试R²=0.72——两者测试分一样,但浅树快10倍,且可解释性强(你能画出整棵树)。用tree.plot_tree()看看,5层的树,你一眼就能说出“高房价=高收入+低纬度+近LA”,这就是业务价值。
4.6 建议6:LinearRegression前,务必检查多重共线性
total_rooms和population高度相关(相关系数0.92),同时放入模型,会导致系数估计不稳定(今天是+2.1,明天是-1.8)。用statsmodels的variance_inflation_factor(VIF)检测:
- VIF<5:安全;
- VIF>10:严重共线性,必须剔除一个。
解决方案不是删特征,而是构造新特征:rooms_per_person = total_rooms / population,这个比值更有物理意义。
4.7 建议7:评估指标,永远用业务语言,而非算法术语
老板不关心R²是多少,他问:“模型预测错的房子,平均差多少钱?”——这就是MAE。
销售团队想知道:“预测房价>50万的客户里,真买了房的占多少?”——这就是精确率(Precision)。
风控部门关心:“所有真买了房的客户里,模型成功识别出多少?”——这就是召回率(Recall)。
在代码里,永远同时打印r2_score和mean_absolute_error,前者看相对性能,后者看绝对误差。
4.8 建议8:保存模型,用joblib,别用pickle
joblib专为NumPy数组优化,序列化sklearn模型比pickle快10倍,体积小50%。一行代码:
import joblib joblib.dump(best_model, 'california_rf_model.joblib') # 加载 model = joblib.load('california_rf_model.joblib')4.9 建议9:feature_importance,只信RandomForest,不信LinearRegression的系数
线性回归的系数大小,受特征尺度影响极大。median_income的系数是50000,latitude的系数是-2000,并不意味收入重要性是纬度的25倍——因为收入单位是“万美元”,纬度单位是“度”。而RandomForest的feature_importances_,是基于不纯度减少计算的,单位统一,可直接比较。画图:
feat_imp = pd.Series(best_model.feature_importances_, index=X_train.columns).sort_values(ascending=False) feat_imp.head(10).plot(kind='barh') plt.title('Top 10 Feature Importances') plt.show()你会看到median_income_log、is_near_major_city、dist_to_la稳居前三,这才是数据告诉你的真相。
4.10 建议10:每天结束前,把当天的代码、数据、结果,打包成一个ZIP
文件名:ml_project_20241219_v1.zip。
里面包含:
notebook.ipynb(含所有代码和注释)data/文件夹(含原始.data和处理后的cleaned.csv)models/文件夹(含.joblib模型)results/文件夹(含cv_scores.txt和test_metrics.txt)
这是你个人的知识资产。三个月后,你想复现某个实验,打开这个ZIP,5分钟就能回到当时的状态。没有这个习惯,你所有的努力,都会随着时间蒸发。
5. 常见问题与排查技巧实录:那些让我摔过跤的坑,现在都给你垫好了
5.1 问题:ValueError: Input contains NaN, infinity or a value too large for dtype('float64')
现象:model.fit()直接报错,提示输入有NaN或无穷大。
排查思路:
- 先确认
df.isnull().sum(),看哪些列有NaN; - 再检查
np.isinf(df).sum().sum(),看是否有inf(常由1/0或log(0)产生); - 最后用
df.select_dtypes(include=[np.number]).describe(),看max列是否有天文数字(如1e308)。
根治方案:
NaN:按前述KNNImputer或SimpleImputer(strategy='median')处理;inf:df = df.replace([np.inf, -np.inf], np.nan),再填充;- 天文数字:通常是特征工程错误,如
total_rooms / population时population=0,加一句df['population'] = df['population'].replace(0, 1)兜底。
5.2 问题:RandomForest训练慢得像蜗牛,CPU跑满100%
现象:n_estimators=100,跑了5分钟还没完。
原因:max_features默认是'sqrt',但在高维数据上,它可能仍太大。
速效方案:
- 加
n_jobs=-1,用满所有CPU核心; - 设
max_features='log2',进一步降低单棵树的复杂度; - 或直接
max_depth=10,限制树的生长。
实测:三者叠加,California数据集训练时间从320秒降到22秒。
5.3 问题:测试集R²比训练集还高,模型“作弊”了?
现象:train_r2=0.75,test_r2=0.82,这违反常理。
真相:你用了StandardScaler,但对测试集用了fit_transform,导致测试集标准化参数(均值、标准差)来自自身,而非训练集。这等于让模型“偷看了”测试集的分布,分数必然虚高。
验证方法:打印scaler.mean_和scaler.scale_,看它们是否和X_test.mean(axis=0)接近。如果接近,就是泄露了。
修复:严格遵守scaler.fit_transform(X_train)+scaler.transform(X_test)。
5.4 问题:cross_val_score返回的分数全是nan
现象:cv_scores数组里,5个值全是nan。
