Python大数据实战(一):Scrapy爬取二手车数据+GBDT价格预测完整项目

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

六、总结

本文完成了一个完整的"爬虫 → 数据分析 → 机器学习"项目实战:

  1. Scrapy 爬虫:从人人车采集真实二手车数据,含翻页处理
  2. Pandas 数据处理:标签拆分、One-Hot 编码、品牌编码
  3. 数据可视化:价格 Top 10、销量占比饼图、价格概率密度分布
  4. GBDT 建模:70 棵决策树的梯度提升回归,四大指标评估

核心收获:一个完整的数据项目不是"调个包就完事了",数据采集和特征工程往往占 80% 的工作量。爬虫写得好不好、特征做得细不细,直接决定模型效果的上限。


参考链接: