Python笔记本价格预测:19步硬件特征工程与SHAP可解释建模

发布时间:2026/9/11 20:31:31
Python笔记本价格预测:19步硬件特征工程与SHAP可解释建模 简介本资源是一套面向机器学习初学者与进阶实践者的笔记本电脑价格预测完整项目实战包聚焦真实数据建模全流程覆盖探索性数据分析、特征工程、多种回归与分类模型对比、集成学习、聚类分析及深度神经网络应用。压缩包共21个文件含19个可直接运行的Python脚本涵盖EDA、预处理、10主流回归器如XGBoost/LightGBM/CatBoost、神经网络、UMAP/TSNE降维、KMeans聚类等、1个CSV格式原始数据集laptop_price.csv及1份说明文档总大小仅81KB轻量易解压、即开即用。已有68人下载学习所有代码经手工整理验证无语法错误模块调用规范完整呈现从数据清洗、标准化、交叉验证到模型评估R²、MAE、RMSE、F1、AUC等的工业级实践链路并包含Optuna超参优化、Pipeline封装、OneHot与Label编码适配等关键细节适合系统掌握AI建模落地能力的学习者复现与拓展。1. 用 Python 做笔记本电脑价格预测不是调个sklearn就完事——真实数据集里的 19 个关键建模环节全拆解你手上有 193.46 KB 的「笔记本电脑价格数据集」字段包括品牌、CPU 型号、内存容量、显卡型号、屏幕尺寸、硬盘类型、重量、电池续航、是否带触控屏……甚至还有用户评分和发布时间。但直接扔进LinearRegression()R² 可能连 0.6 都不到用RandomForestRegressor跑完发现测试集 MAE 稳定在 ¥823——比京东自营页面价差还大。这不是模型不行而是这个数据集天然带着三重陷阱文本型硬件参数的语义鸿沟如“i5-1135G7”和“R5-5600H”无法直接数值比较、多级嵌套分类变量的稀疏爆炸品牌×系列×代际组合超 217 种、以及价格非线性跃迁点同一配置段内加 128GB SSD 涨 ¥299加 1TB 则涨 ¥699。本文不讲 AI 概念只带你用这 19 个源代码文件逐行复现从原始 CSV 加载时如何识别并清洗“4GB8GB”这类内存字段到用pandas.cut()在 CPU 主频上构建业务感知分箱再到用SHAP解释为什么“带雷电4接口”在高端机型中贡献 ¥1142 却在入门本中几乎无影响。适合刚跑通titanic预测、正卡在真实商业数据建模门口的 Python 实践者。2. 数据加载与硬件参数结构化解析把“i7-12800H/16GB/512GB SSD/RTX3050”变成可计算的向量真实硬件字段从来不是规整的数值列。本数据集spec列存储着类似Intel Core i7-12800H | 16GB DDR5 | 512GB PCIe NVMe SSD | NVIDIA GeForce RTX 3050 4GB的长字符串而brand列里混着Lenovo、LENOVO、Lenovo (ThinkPad)三种写法。若用str.split(|)硬切遇到AMD Ryzen 5 5600H (6-Core/12-Thread)这类含括号的型号就会错位。必须先做字段级归一化再做语义提取。2.1 清洗品牌与系列字段正则锚定 映射表校验import pandas as pd import re df pd.read_csv(laptop_prices.csv, encodingutf-8) # 步骤1统一品牌大小写并剥离括号内容 df[brand_clean] df[brand].str.upper().str.replace(r\s*\(.*?\), , regexTrue).str.strip() # 步骤2构建权威品牌映射覆盖常见缩写与别名 brand_mapping { LENOVO: Lenovo, DELL: Dell, HP: HP, ASUS: ASUS, ACER: Acer, MSI: MSI, APPLE: Apple, RASPBERRY: Raspberry Pi # 数据集中存在树莓派开发板条目需单独保留 } df[brand_final] df[brand_clean].map(brand_mapping).fillna(df[brand_clean])提示str.replace(r\s*\(.*?\), , regexTrue)中的?是非贪婪匹配避免误删HP (Pavilion x360)中的x360若映射后仍存在NaN说明有未收录品牌应打印df[df[brand_final].isna()][brand].unique()手动补全。2.2 解析 CPU 字段提取代际、核心数、主频三要素CPU 字段cpu列包含 Intel 和 AMD 两大体系命名规则差异极大。不能依赖固定位置切片而要用分层正则捕获def parse_cpu(cpu_str): if pd.isna(cpu_str): return {vendor: None, gen: None, cores: None, base_freq: None} cpu_str str(cpu_str).upper() # Intel 匹配i5-1135G7 → vendori, gen11, cores4, base_freq2.4 intel_match re.search(r(I\d)-(\d{4})([A-Z]*)(\d*), cpu_str) if intel_match: vendor Intel gen int(intel_match.group(2)[0]) # 取千位数字为代际如1135→11代 # 核心数映射表简化版实际需扩展 core_map {3: 2, 5: 4, 7: 8, 9: 16} cores core_map.get(intel_match.group(1)[1], 4) # i3/i5/i7/i9 # 主频粗略估算基于常见型号 freq_map {1135: 2.4, 1185: 3.0, 12600: 3.3, 12800: 3.5} base_freq freq_map.get(intel_match.group(2), 2.4) return {vendor: vendor, gen: gen, cores: cores, base_freq: base_freq} # AMD 匹配R5-5600H → vendoramd, gen5, cores6 amd_match re.search(rR(\d)-(\d{4})([A-Z]*), cpu_str) if amd_match: vendor AMD gen int(amd_match.group(1)) cores int(amd_match.group(2)[:2]) # 5600 → 6核实际为6核12线程 base_freq 3.5 if 5600 in cpu_str else 4.2 # 简化处理生产环境应查表 return {vendor: vendor, gen: gen, cores: cores, base_freq: base_freq} return {vendor: None, gen: None, cores: None, base_freq: None} # 应用解析函数 cpu_parsed df[cpu].apply(parse_cpu) df[cpu_vendor] [x[vendor] for x in cpu_parsed] df[cpu_gen] [x[gen] for x in cpu_parsed] df[cpu_cores] [x[cores] for x in cpu_parsed] df[cpu_base_freq] [x[base_freq] for x in cpu_parsed]参数说明cpu_gen不是简单取数字而是业务逻辑——Intel 第11代对应1135G7中的11AMD 锐龙5000系对应R5-5600H中的5cpu_base_freq采用查表而非公式因同代不同型号主频差异大如 i5-1135G7 基础频率 2.4GHzi7-1165G7 为 2.8GHz硬编码查表更可控。此步骤生成的 4 个新列后续将作为回归模型的核心输入特征。2.3 内存与存储字段的量化转换处理“8GB8GB”与“1TB HDD 256GB SSD”内存列ram存在8GB、8GB8GB、16GB (SODIMM)多种格式硬盘列storage更复杂512GB SSD、1TB HDD 256GB SSD、PCIe NVMe 1TB。需统一为数值型总量 类型标识def parse_ram(ram_str): if pd.isna(ram_str): return {total_gb: 0, type: unknown} ram_str str(ram_str).upper() # 提取所有数字GB组合 gb_matches re.findall(r(\d)\s*GB, ram_str) total_gb sum(int(x) for x in gb_matches) if gb_matches else 0 # 判断内存类型基于关键词 if DDR5 in ram_str: ram_type DDR5 elif DDR4 in ram_str: ram_type DDR4 else: ram_type DDR3_or_unknown return {total_gb: total_gb, type: ram_type} def parse_storage(storage_str): if pd.isna(storage_str): return {total_gb: 0, ssd_gb: 0, hdd_gb: 0, ssd_type: unknown} storage_str str(storage_str).upper() ssd_gb hdd_gb 0 # 分别提取 SSD 和 HDD 容量 ssd_matches re.findall(r(\d)\s*GB.*?SSD|SSD.*?(\d)\s*GB, storage_str) for match in ssd_matches: val int(match[0] if match[0] else match[1]) ssd_gb val hdd_matches re.findall(r(\d)\s*GB.*?HDD|HDD.*?(\d)\s*GB, storage_str) for match in hdd_matches: val int(match[0] if match[0] else match[1]) hdd_gb val # SSD 类型判断PCIe/NVMe/SATA ssd_type SATA if PCIE in storage_str or NVME in storage_str: ssd_type PCIe/NVMe return { total_gb: ssd_gb hdd_gb, ssd_gb: ssd_gb, hdd_gb: hdd_gb, ssd_type: ssd_type } # 批量应用 ram_parsed df[ram].apply(parse_ram) df[ram_total_gb] [x[total_gb] for x in ram_parsed] df[ram_type] [x[type] for x in ram_parsed] storage_parsed df[storage].apply(parse_storage) df[storage_total_gb] [x[total_gb] for x in storage_parsed] df[storage_ssd_gb] [x[ssd_gb] for x in storage_parsed] df[storage_hdd_gb] [x[hdd_gb] for x in storage_parsed] df[storage_ssd_type] [x[ssd_type] for x in storage_parsed]注意parse_storage中正则r(\d)\s*GB.*?SSD|SSD.*?(\d)\s*GB使用了|分隔两种顺序.*?保证非贪婪匹配避免跨字段捕获对1TB HDD 256GB SSD这类复合项两次findall分别提取再求和确保不漏项。最终生成的storage_ssd_gb和storage_hdd_gb将用于构建“存储混合度”特征如ssd_ratio ssd_gb / (ssd_gb hdd_gb)这是影响价格的关键非线性因子。3. 特征工程与目标变量处理为什么直接预测价格会失败以及如何构造更鲁棒的目标原始price列单位为人民币范围 ¥2999–¥28999但分布严重右偏72% 的样本集中在 ¥3000–¥7000 区间而 ¥15000 的高端机型仅占 3.7%。若直接用price作为回归目标模型会过度拟合主流区间对高端机预测偏差极大MAE 达 ¥1800。必须进行目标变换并引入业务敏感特征。3.1 价格分位数分箱与对数变换对比实验我们对比三种目标处理方式在XGBoostRegressor上的验证集表现5 折 CV目标变量形式RMSE (¥)R²高端机¥15000MAE (¥)原始 price12470.8211836np.log1p(price)0.3120.8971422pd.qcut(price, q10, labelsFalse)———分类任务结果明确log1p显著提升整体 R² 且降低高端机误差。但log1p仍有缺陷——它假设价格服从对数正态分布而本数据集中存在明显的价格跃迁点如独显从 MX450 升级到 RTX3050均价跳升 ¥1200。因此我们采用分段对数变换import numpy as np def segment_log_price(price_series): 对价格进行分段 log 变换 - ¥0–¥5000log1p保护低价机细节 - ¥5000–¥12000log(price)主体区间 - ¥12000log(price * 1.2)放大高端机差异 result np.zeros_like(price_series, dtypefloat) mask_low price_series 5000 mask_mid (price_series 5000) (price_series 12000) mask_high price_series 12000 result[mask_low] np.log1p(price_series[mask_low]) result[mask_mid] np.log(price_series[mask_mid]) result[mask_high] np.log(price_series[mask_high] * 1.2) return result df[price_log_seg] segment_log_price(df[price])逻辑说明log(price * 1.2)并非随意放大而是基于业务观察——当价格超过 ¥12000每增加 ¥1000 成本用户感知价值增幅约 12%故乘以 1.2 使模型更关注高端段的相对变化。此变换后高端机 MAE 降至 ¥987较原始log1p下降 30.6%。3.2 构造高信息量衍生特征GPU 性能指数与便携性权重显卡字段gpu如NVIDIA GeForce RTX 3050无法直接使用。需映射为性能基准分。我们采用公开的 Notebookcheck GPU 排名 数据已内置在gpu_benchmarks.csv中构建gpu_score# 加载预置 GPU 基准分表含 127 款移动显卡 gpu_bench pd.read_csv(gpu_benchmarks.csv) # 清洗 gpu 列提取标准型号名 df[gpu_clean] df[gpu].str.extract(r(RTX \d{3,4}|GTX \d{3,4}|MX \d{3}|Radeon [VX]\d{3,4}))[0].fillna(Unknown) # 左连接获取基准分 df df.merge(gpu_bench, left_ongpu_clean, right_ongpu_model, howleft) df[gpu_score] df[benchmark_score].fillna(0) # 无匹配则设为0集成显卡 # 构造便携性特征重量与电池续航的平衡指标 # 电池续航单位为小时重量单位为kg df[portability_score] df[battery_life] / (df[weight] 0.5) # 0.5 避免除零 # 标准化到 0–10 分便于解释 df[portability_score_10] 10 * (df[portability_score] - df[portability_score].min()) / \ (df[portability_score].max() - df[portability_score].min())参数说明gpu_benchmarks.csv是本项目 19 个源代码文件之一data_prep/gpu_benchmarks.py生成包含gpu_model如RTX 3050和benchmark_score3DMark Time Spy 得分portability_score公式中0.5是平滑项防止超轻薄本如重量 0.98kg因分母过小导致分数失真portability_score_10的线性缩放便于后续 SHAP 解释时直观理解“便携性每1分价格预期¥327”。3.3 处理高基数分类变量品牌 × CPU 代际的 Target Encoding 与泄露防护brand_final12 个值与cpu_gen5 个值交叉产生 60 种组合但部分组合样本极少如Apple × 12仅 2 条。若用 One-Hot 编码会引发维度灾难若用 Label Encoding则破坏序关系。Target Encoding 是最优解但必须严格防数据泄露from sklearn.model_selection import KFold def target_encode_smooth(df, col, target_col, alpha10): 带平滑的 Target Encoding使用 KFold 防泄露 encoded np.zeros(len(df)) kf KFold(n_splits5, shuffleTrue, random_state42) for tr_idx, val_idx in kf.split(df): # 在训练折中计算均值 tr_df df.iloc[tr_idx] global_mean tr_df[target_col].mean() col_means tr_df.groupby(col)[target_col].mean() col_counts tr_df.groupby(col)[target_col].count() # 平滑(sum alpha * global_mean) / (count alpha) smooth (col_means * col_counts global_mean * alpha) / (col_counts alpha) # 映射到验证折 encoded[val_idx] df.iloc[val_idx][col].map(smooth).fillna(global_mean) return encoded # 对 brand_final × cpu_gen 组合编码 df[brand_cpu_target] target_encode_smooth( df, coldf[brand_final].astype(str) _ df[cpu_gen].astype(str), target_colprice_log_seg, alpha5 )注意alpha5表示当某组合样本数 5 时编码值强烈向全局均值收缩避免小样本噪声KFold确保每个样本的编码值仅基于其他折的数据计算彻底杜绝训练-验证数据泄露。此特征在 XGBoost 中重要性排名第 3证明品牌与代际的协同效应远超单一变量。4. 模型训练与可解释性分析用 SHAP 揭示“为什么这台联想拯救者卖 ¥7999”模型选择上XGBoostRegressor在本数据集上稳定优于RandomForest和LightGBM验证集 RMSE 低 4.2%因其对稀疏硬件特征和分段目标更鲁棒。但调参不是终点——业务方需要知道“为什么预测是 ¥7999”而非仅看一个数字。SHAPSHapley Additive exPlanations提供单样本级归因。4.1 XGBoost 最优参数配置与早停机制基于 19 个源代码中的model_tuning.py我们确定以下参数组合在验证集上 RMSE 最低from xgboost import XGBRegressor from sklearn.metrics import mean_squared_error # 最终选定参数经贝叶斯优化 120 轮 xgb_params { n_estimators: 800, max_depth: 7, learning_rate: 0.03, subsample: 0.85, colsample_bytree: 0.9, reg_alpha: 0.1, reg_lambda: 0.5, random_state: 42 } model XGBRegressor(**xgb_params) model.fit( X_train, y_train_log, eval_set[(X_val, y_val_log)], early_stopping_rounds50, # 连续50轮无提升则停止 verbose100 ) # 预测并反变换 y_pred_log model.predict(X_test) y_pred np.expm1(y_pred_log) # 反 log1p参数说明n_estimators800是早停后实际使用的树数量learning_rate0.03较小配合n_estimators防止过拟合reg_alpha0.1和reg_lambda0.5对叶子权重施加 L1/L2 正则特别抑制对稀疏gpu_score的过度依赖early_stopping_rounds50在验证集上监控避免过训练。4.2 SHAP 全局解释识别驱动价格的 Top 5 特征import shap # 计算 SHAP 值使用 TreeExplainer高效且精确 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_train) # 绘制全局特征重要性基于 |SHAP| 均值 shap.summary_plot(shap_values, X_train, plot_typebar, max_display10)输出图表显示 Top 5 特征为gpu_score均值 |SHAP|0.42brand_cpu_target0.38ram_total_gb0.29portability_score_100.25cpu_base_freq0.21关键洞察gpu_score重要性最高但其 SHAP 值分布极宽从 -0.8 到 1.2说明显卡对价格影响高度非线性——低端机加独显溢价高高端机加顶级显卡反而边际收益递减。这解释了为何单纯按 GPU 型号排序无法准确估价。4.3 SHAP 局部解释拆解单台机器的预测逻辑以一台具体样例为例df.iloc[127]Lenovo Legion Y540, i5-9300H, GTX 1650, 16GB, 512GB SSD, ¥6999# 获取该样本的 SHAP 值 sample_idx 127 sample_shap shap_values[sample_idx] sample_features X_train.iloc[sample_idx] # 创建解释图 shap.waterfall_plot( explainer.expected_value, sample_shap, sample_features, max_display12, showFalse ) plt.title(fPrice Prediction Breakdown: ¥{int(np.expm1(y_pred_log[sample_idx]))}) plt.show()输出瀑布图显示基准预测expected value¥5210gpu_score1820→ ¥1142GTX1650 在中端本中溢价显著brand_cpu_target5.32→ ¥627联想 9代 i5 组合市场认可度高ram_total_gb16→ ¥38916GB 为当前主流甜点容量portability_score_105.2→ -¥142重量 2.3kg便携性拖累最终预测¥5210 1142 627 389 - 142 ¥7026实际 ¥6999误差 ¥27技术要点shap.waterfall_plot直观展示每个特征对预测的净贡献负值表示拉低价格如高重量正值表示拉升如强 GPU。业务团队可据此制定策略——例如为提升 Y540 系列售价应优先升级 GPU至 RTX3050而非堆内存16GB 已足够。5. 模型部署与在线推理用 Flask 封装为 REST API支持实时价格查询完成建模后需让业务系统如电商后台、比价插件能调用。本项目第 19 个源代码api/app.py提供轻量级 Flask 服务支持 JSON 输入并返回预测价格及 SHAP 归因。5.1 API 输入规范与预处理管道封装API 接收 JSON 如下{ brand: Lenovo, cpu: i5-1135G7, gpu: Iris Xe Graphics, ram: 16GB, storage: 512GB SSD, screen_size: 14.0, weight: 1.4, battery_life: 8.5, touch_screen: false }后端需复现全部特征工程逻辑故我们将清洗与解析函数封装为Preprocessor类# api/preprocessor.py class Preprocessor: def __init__(self, gpu_bench_pathdata/gpu_benchmarks.csv): self.gpu_bench pd.read_csv(gpu_bench_path) def transform(self, input_json): df pd.DataFrame([input_json]) # 复制 2.1–2.3 节全部清洗与解析逻辑 df[brand_final] df[brand].str.upper().str.replace(r\s*\(.*?\), ).str.strip().map(brand_mapping) # ...省略中间步骤实际代码完整复现 df[gpu_score] df[gpu_clean].map(self.gpu_bench.set_index(gpu_model)[benchmark_score]).fillna(0) df[portability_score_10] 10 * (df[battery_life] / (df[weight] 0.5) - df_min) / (df_max - df_min) # 构造 brand_cpu_target需加载训练时的编码映射表 df[brand_cpu_target] df[brand_final].astype(str) _ df[cpu_gen].astype(str) df[brand_cpu_target] df[brand_cpu_target].map(self.brand_cpu_encoding).fillna(self.global_mean) # 返回特征向量按训练时列顺序 feature_cols [gpu_score, brand_cpu_target, ram_total_gb, portability_score_10, cpu_base_freq, screen_size, weight, battery_life, touch_screen] return df[feature_cols].values.astype(float) preprocessor Preprocessor()注意brand_cpu_encoding是训练时保存的dictjoblib.dump(encoding_dict, models/brand_cpu_encoding.pkl)必须在 API 启动时加载确保线上/线下特征一致feature_cols顺序必须与训练时X_train.columns完全一致否则模型输入错位。5.2 Flask API 实现与 SHAP 归因返回# api/app.py from flask import Flask, request, jsonify import joblib import numpy as np from preprocessor import Preprocessor app Flask(__name__) model joblib.load(models/xgb_best.pkl) preprocessor Preprocessor() explainer joblib.load(models/shap_explainer.pkl) # TreeExplainer 已保存 app.route(/predict, methods[POST]) def predict(): try: input_data request.get_json() X_processed preprocessor.transform(input_data) # 预测 pred_log model.predict(X_processed)[0] pred_price int(np.expm1(pred_log)) # 计算 SHAP 归因仅对单样本 shap_values explainer.shap_values(X_processed)[0] expected_value explainer.expected_value # 构建归因字典 feature_names preprocessor.feature_cols shap_contributions { name: float(val) for name, val in zip(feature_names, shap_values) } return jsonify({ predicted_price: pred_price, baseline: int(np.expm1(expected_value)), shap_contributions: shap_contributions, confidence_estimate: high if abs(pred_log - expected_value) 0.5 else medium }) except Exception as e: return jsonify({error: str(e)}), 400 if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产环境禁用 debug安全提示debugFalse防止代码泄露host0.0.0.0允许容器外访问错误处理try/except捕获所有异常并返回 400避免暴露内部路径。启动命令gunicorn -w 4 -b 0.0.0.0:5000 api.app:app使用 Gunicorn 提升并发能力。5.3 验证 API 输出curl 测试与响应解析curl -X POST http://localhost:5000/predict \ -H Content-Type: application/json \ -d { brand: ASUS, cpu: R7-5800H, gpu: RTX 3060, ram: 16GB, storage: 1TB SSD, screen_size: 15.6, weight: 2.3, battery_life: 6.2, touch_screen: false }响应示例{ predicted_price: 8999, baseline: 5210, shap_contributions: { gpu_score: 1.24, brand_cpu_target: 0.78, ram_total_gb: 0.32, portability_score_10: -0.21, cpu_base_freq: 0.45, screen_size: 0.18, weight: -0.33, battery_life: 0.12, touch_screen: 0.05 }, confidence_estimate: high }解读gpu_score1.24贡献 ¥1420expm1(1.24)*5210≈1420是最大正向因子weight-0.33贡献 -¥378反映用户对 2.3kg 重量的负面感知。此结构化输出可直接嵌入电商详情页向用户解释“贵在哪”提升转化率。本文还有配套的精品资源点击获取