Python大数据实战(一):Scrapy爬取二手车数据+GBDT价格预测完整项目
文章目录
- Python大数据实战(一):Scrapy爬取二手车数据+GBDT价格预测完整项目
- 前言
- 一、项目概述
-
- [1.1 项目架构总览](#1.1 项目架构总览)
- [1.2 学习目标](#1.2 学习目标)
- [1.3 数据来源](#1.3 数据来源)
- [1.4 项目目标](#1.4 项目目标)
- [二、Scrapy 爬虫:数据采集](#二、Scrapy 爬虫:数据采集)
-
- [2.1 创建爬虫项目](#2.1 创建爬虫项目)
- [2.2 定义数据模型(Item)](#2.2 定义数据模型(Item))
- [2.3 编写爬虫逻辑(Spider)](#2.3 编写爬虫逻辑(Spider))
- [2.4 启动爬虫](#2.4 启动爬虫)
- [三、Pandas 数据分析与特征工程](#三、Pandas 数据分析与特征工程)
-
- [3.1 标签数据预处理](#3.1 标签数据预处理)
- [3.2 标签 One-Hot 编码](#3.2 标签 One-Hot 编码)
- [3.3 价格分析:哪些品牌最贵?](#3.3 价格分析:哪些品牌最贵?)
- [3.4 销量分析:哪些品牌最畅销?](#3.4 销量分析:哪些品牌最畅销?)
- [3.5 价格分布分析(以"大众"为例)](#3.5 价格分布分析(以"大众"为例))
- [3.6 品牌 One-Hot 编码](#3.6 品牌 One-Hot 编码)
- [四、GBDT 建模与评估](#四、GBDT 建模与评估)
-
- [4.0 GBDT 算法原理(一图看懂)](#4.0 GBDT 算法原理(一图看懂))
- [4.1 数据准备](#4.1 数据准备)
- [4.2 模型训练](#4.2 模型训练)
- [4.3 模型评估](#4.3 模型评估)
- [4.4 特征重要性分析](#4.4 特征重要性分析)
- 五、常见错误与解决方案
-
- [错误 1:Scrapy 爬取被反爬拦截](#错误 1:Scrapy 爬取被反爬拦截)
- [错误 2:中文显示乱码](#错误 2:中文显示乱码)
- [错误 3:One-Hot 编码后特征爆炸](#错误 3:One-Hot 编码后特征爆炸)
- 六、总结
前言
"这辆二手车值多少钱?"------这是每个买车人和卖车人都想知道答案的问题。
对于数据从业者来说,二手车价格预测是一个经典的"爬虫 + 数据分析 + 机器学习"综合实战项目。它不像鸢尾花分类那样简单,也不像推荐系统那样复杂,刚好卡在"能学到东西又不至于劝退"的甜点区。
本文带你从零开始,用 Scrapy 爬取人人车真实数据,用 Pandas 做数据清洗和特征工程,最后用 GBDT(梯度提升决策树)建立价格预测模型。全程代码可运行,适合有 Python 基础、想入门大数据项目的同学。
一、项目概述
1.1 项目架构总览
人人车网站 → Scrapy爬虫 → 原始数据CSV → Pandas清洗 → 标签拆分+OneHot
→ 品牌编码+标签编码 → Matplotlib可视化分析 → GBDT回归模型 → 价格预测评估
1.2 学习目标
| 阶段 | 技能 | 工具/库 |
|---|---|---|
| 数据采集 | Scrapy 爬虫框架 | Scrapy |
| 数据处理 | 数据清洗、标签编码、One-Hot | Pandas、NumPy |
| 数据分析 | 价格分布、品牌销量、概率密度 | Matplotlib、Seaborn、SciPy |
| 机器学习 | 回归预测、模型评估 | Scikit-learn(GBDT) |
1.3 数据来源
爬取人人车网站(https://www.renrenche.com/)的二手车信息,包含:
- 品牌(brand):大众、丰田、本田、宝马......
- 价格(price):二手车挂牌价格
- 标签(tag):如"准新车""急售""0过户"等
1.4 项目目标
通过已有数据建立回归模型,对二手车价格进行预测,最终用 MSE、MAE、RMSE、R² 四个指标评估模型效果。
二、Scrapy 爬虫:数据采集
2.1 创建爬虫项目
bash
# 创建 Scrapy 项目
scrapy startproject ershouche
# 进入项目目录,创建爬虫文件
cd ershouche
scrapy genspider car www.renrenche.com
执行后项目结构如下:
ershouche/
├── ershouche/
│ ├── spiders/
│ │ └── car.py # 爬虫逻辑
│ ├── items.py # 数据模型
│ ├── pipelines.py # 数据处理管道
│ └── settings.py # 项目配置
└── scrapy.cfg
2.2 定义数据模型(Item)
python
# items.py
import scrapy
class ErshoucheItem(scrapy.Item):
"""二手车数据模型"""
brand = scrapy.Field() # 品牌,如"大众"
price = scrapy.Field() # 价格,如"8.5万"
tag = scrapy.Field() # 标签,如"准新车_急售"
2.3 编写爬虫逻辑(Spider)
python
# spiders/car.py
import scrapy
from ershouche.items import ErshoucheItem
class CarSpider(scrapy.Spider):
name = 'car'
allowed_domains = ['renrenche.com']
start_urls = ['https://www.renrenche.com/xa/ershouche/']
def parse(self, response):
"""解析当前页的二手车列表"""
# 获取当前页所有二手车列表项
car_list = response.xpath(
"//ul[@class='row-fluid list-row js-car-list']/li"
)
for li in car_list:
item = ErshoucheItem()
# 提取品牌
brand = li.xpath(".//a[@class='brand']/text()").extract_first()
item['brand'] = brand.strip() if brand else ''
# 提取价格
price = li.xpath(".//div[@class='price']/text()").extract_first()
item['price'] = price.strip() if price else ''
# 提取标签(多个标签用下划线连接)
tag_list = li.xpath(".//div[@class='tag']/span/text()").extract()
item['tag'] = '_'.join([t.strip() for t in tag_list])
yield item
# 翻页处理
next_url = response.xpath(
'//ul[@class="pagination js-pagination"]/li[last()]/a/@href'
).extract_first()
# 如果不是最后一页,继续爬取
if next_url and next_url != "javascript:void(0);":
next_url = response.urljoin(next_url)
yield scrapy.Request(next_url, callback=self.parse)
2.4 启动爬虫
bash
# 在项目根目录执行
scrapy crawl car -o cars.csv # 输出为 CSV 文件
⚠️ 注意事项 :爬取前请确认目标网站的 robots.txt 协议,合理设置下载延迟(
DOWNLOAD_DELAY = 2),避免对目标服务器造成压力。
三、Pandas 数据分析与特征工程
3.1 标签数据预处理
爬取到的 tag 字段包含多个标签(如"准新车_急售_0过户"),需要拆分为独立的特征列。
python
import pandas as pd
import numpy as np
# 读取数据
dataset = pd.read_csv('cars.csv')
# 过滤掉标签为空的数据(数据量很少,不影响整体)
dataset = dataset[dataset["tag"].notna()]
# 收集所有唯一标签
tag_list = []
dataset["tag"].apply(lambda x: tag_list.extend(x.split("_")))
tag_list = list(set(tag_list)) # 去重
print(f"共有 {len(tag_list)} 种标签: {tag_list[:10]}")
3.2 标签 One-Hot 编码
将标签转换为 0/1 特征矩阵:
python
# 创建标签特征 DataFrame,初始值全为 0
tag_df = pd.DataFrame(columns=tag_list)
df = pd.concat([dataset, tag_df], sort=False)
df[tag_list] = df[tag_list].fillna(0)
def set_tag_status(series):
"""将当前行的标签设置为 1"""
tags = series["tag"].split("_")
for t in tags:
if t in tag_list:
series[t] = 1
return series
# 对每一行应用标签编码
df[tag_list] = df[["tag", *tag_list]].apply(
lambda x: set_tag_status(x), axis=1
).drop("tag", axis=1)
df = df.drop("tag", axis=1) # 删除原始 tag 列
print(f"特征矩阵形状: {df.shape}")
3.3 价格分析:哪些品牌最贵?
python
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体(避免乱码)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 平均价格最高的前 10 个品牌
num_top = df.groupby("brand")["price"].mean().sort_values(ascending=False)[:10]
# 绘制条形图
fig = plt.figure(figsize=(15, 10))
sns.barplot(x=num_top.index, y=num_top.values)
plt.title("二手车平均价格 Top 10 品牌")
plt.xticks(rotation=90)
plt.ylabel("平均价格(万元)")
plt.tight_layout()
plt.show()
3.4 销量分析:哪些品牌最畅销?
python
# 销量最多的前 10 个品牌
amount_top = df["brand"].value_counts().sort_values(ascending=False)[:10]
# 条形图
fig = plt.figure(figsize=(12, 8))
sns.barplot(x=amount_top.index, y=amount_top.values)
plt.title("二手车销量 Top 10 品牌")
plt.xticks(rotation=90)
plt.ylabel("数量(辆)")
plt.tight_layout()
plt.show()
# 饼图:各品牌占比
fig = plt.figure(figsize=(10, 10))
plt.pie(amount_top.values, labels=amount_top.index, autopct="%1.2f%%")
plt.title("各大品牌车系数量占有比重前 10 位")
plt.show()
3.5 价格分布分析(以"大众"为例)
python
from scipy.stats import norm
# 筛选大众品牌数据
df_dazhong = df[df["brand"] == "大众"]
dazhong_mean = df_dazhong["price"].mean() # 均值
dazhong_std = df_dazhong["price"].std() # 标准差
# 绘制直方图 + 概率密度曲线
num_bins = 20
n, bins, patches = plt.hist(
df_dazhong["price"], num_bins,
facecolor="green", density=True, alpha=0.5
)
# 叠加正态分布概率密度曲线
y = norm.pdf(bins, dazhong_mean, dazhong_std)
plt.plot(bins, y, "r--", linewidth=2, label="正态分布拟合")
plt.xlabel("价格(万元)")
plt.ylabel("概率密度")
plt.title(f"大众二手车价格分布(均值={dazhong_mean:.2f}万,标准差={dazhong_std:.2f}万)")
plt.legend()
plt.show()
3.6 品牌 One-Hot 编码
python
# 对品牌进行 One-Hot 编码
one_hot_df = pd.get_dummies(df["brand"])
df = df.drop("brand", axis=1) # 删除原始 brand 列
# 合并 One-Hot 列
df = pd.merge(df, one_hot_df, left_index=True, right_index=True)
print(f"One-Hot 编码后特征数: {df.shape[1]}")
四、GBDT 建模与评估
4.0 GBDT 算法原理(一图看懂)
GBDT(Gradient Boosting Decision Tree)的核心思想是串行训练多棵决策树,每棵树都在修正前面所有树的残差:
第1棵树: 预测 → 残差1 = 真实值 - 预测值1
第2棵树: 拟合残差1 → 残差2 = 残差1 - 预测值2
第3棵树: 拟合残差2 → 残差3 = 残差2 - 预测值3
...
最终预测 = 树1预测 + 树2预测 + 树3预测 + ...
| 对比维度 | 随机森林 | GBDT |
|---|---|---|
| 训练方式 | 并行(Bagging) | 串行(Boosting) |
| 每棵树关系 | 独立 | 依赖前序树 |
| 偏差 | 较低 | 更低 |
| 方差 | 较低 | 较高(需控制学习率) |
| 适用场景 | 通用 | 追求高精度 |
4.1 数据准备
python
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# 分离特征和标签
X = df[df.columns.difference(["price"])].values # 样本特征
Y = df["price"].values # 目标变量(价格)
# 划分训练集和测试集(7:3)
X_train, X_test, Y_train, Y_test = train_test_split(
X, Y, test_size=0.3, random_state=666
)
print(f"训练集: {X_train.shape[0]} 条, 测试集: {X_test.shape[0]} 条")
4.2 模型训练
python
# 创建 GBDT 回归模型
gbdt = GradientBoostingRegressor(
n_estimators=70, # 70 棵决策树
learning_rate=0.1, # 学习率
max_depth=5, # 树的最大深度
random_state=666
)
# 训练模型
gbdt.fit(X_train, Y_train)
# 预测
pred = gbdt.predict(X_test)
4.3 模型评估
python
# 四大评估指标
mse = mean_squared_error(Y_test, pred)
mae = mean_absolute_error(Y_test, pred)
rmse = np.sqrt(mse)
r2 = r2_score(Y_test, pred)
print("=" * 40)
print("GBDT 二手车价格预测模型评估")
print("=" * 40)
print(f"MSE (均方误差): {mse:.2f}")
print(f"MAE (平均绝对误差): {mae:.2f} 万元")
print(f"RMSE (均方根误差): {rmse:.2f} 万元")
print(f"R² (决定系数): {r2:.4f}")
print("=" * 40)
# 解读
if r2 > 0.8:
print("✅ 模型效果优秀,可以用于实际预测")
elif r2 > 0.6:
print("⚠️ 模型效果一般,建议调参优化")
else:
print("❌ 模型效果较差,需要更多特征或数据")
4.4 特征重要性分析
python
# 查看哪些特征对价格预测影响最大
feature_importance = pd.DataFrame({
'feature': df.columns.difference(["price"]),
'importance': gbdt.feature_importances_
}).sort_values('importance', ascending=False)
print("Top 10 重要特征:")
print(feature_importance.head(10))
五、常见错误与解决方案
错误 1:Scrapy 爬取被反爬拦截
python
# ❌ 错误:直接高频请求,被网站封 IP
scrapy crawl car # 无任何反爬措施
# ✅ 正确:配置下载延迟和 User-Agent
# settings.py
DOWNLOAD_DELAY = 2 # 每次请求间隔 2 秒
RANDOMIZE_DOWNLOAD_DELAY = True # 随机延迟
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36',
}
错误 2:中文显示乱码
python
# ❌ 错误:matplotlib 默认不支持中文
plt.title("二手车价格分布") # 显示为方框
# ✅ 正确:设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
错误 3:One-Hot 编码后特征爆炸
python
# ❌ 问题:品牌有 100+ 种,One-Hot 后特征数暴增
# 解决方案:只保留出现频率最高的 Top N 品牌
top_brands = df["brand"].value_counts().head(20).index.tolist()
df["brand"] = df["brand"].apply(lambda x: x if x in top_brands else "其他")
# 然后再做 One-Hot,特征数从 100+ 降到 21
六、总结
本文完成了一个完整的"爬虫 → 数据分析 → 机器学习"项目实战:
- Scrapy 爬虫:从人人车采集真实二手车数据,含翻页处理
- Pandas 数据处理:标签拆分、One-Hot 编码、品牌编码
- 数据可视化:价格 Top 10、销量占比饼图、价格概率密度分布
- GBDT 建模:70 棵决策树的梯度提升回归,四大指标评估
核心收获:一个完整的数据项目不是"调个包就完事了",数据采集和特征工程往往占 80% 的工作量。爬虫写得好不好、特征做得细不细,直接决定模型效果的上限。
参考链接: