
很多人问我用 Python 做数据分析到底能做出什么像样的东西我一般都会拿这个二手车数据分析及可视化项目举例。这个系统从数据采集、清洗整理、多维度分析到可视化大屏展示把 Python 数据分析的全流程走了一遍。它不是教科书里那种孤立的 demo而是一套能自己跑起来、能通过网页交互查看结果的完整项目非常适合想系统练手数据分析的初学者也适合准备写简历项目的学生党。整个项目以“二手车”这个具体的业务场景为载体把爬虫、pandas、Pyecharts 这些工具串联起来。前端展示用 Flask 搭建轻量级 Web 服务各维度图表按业务模块切分可以直接在网页上切换查看。文章里我会把选题思路、系统设计、关键代码、踩坑记录一次讲清楚你照着做也能搭出属于自己的数据分析可视化系统。1. 项目整体设计与技术选型1.1 为什么选二手车这个业务场景二手车是一个数据属性非常丰富的行业每辆车都包含品牌、车系、上牌年份、表显里程、排量、变速箱、排放标准、所在城市、价格等多个字段。这些字段天然适合做分类统计、相关性分析、价格对比而且业务逻辑很容易理解——大家买车关心什么分析就做什么。我用二手车做项目还有一个原因数据源好找。国内有很多二手车交易平台公开的列表页就能拿到比较规范的结构化数据爬虫难度适中不像某些平台需要处理复杂的加密参数。对于练手来说这个难度的数据采集刚刚好既能学到真实的反爬处理思路又不会因为门槛高而弃坑。从分析价值上看二手车数据能回答很多有意思的问题什么品牌的车最保值、车龄对价格的影响有多大、里程到了多少万公里价格会明显跳水、哪类车型更好卖。这些问题既有商业意义又有分析深度做出来的成果展示给面试官或者项目评审听很容易讲出亮点。1.2 技术栈和工具链选型这个项目我用了一整套 Python 生态工具每一环的选择都有原因。Python 3.10 Anaconda一装就带 pandas、numpy、jupyter 等常用库省去逐个安装的麻烦。用 VS Code 作为主编辑器配合 jupyter notebook 做探索性分析。requests BeautifulSoup做静态网页数据抓取。requests 负责拿到页面 HTMLBeautifulSoup 负责解析结构、提取字段。pandas numpy数据清洗和统计分析的主力。pandas 处理表格型数据非常顺手尤其是 DataFrame 的筛选、分组、聚合都是强项。Pyecharts可视化首选。它是 Python 封装 ECharts 的库生成的图表是交互式的适合做网页展示。和 matplotlib 相比Pyecharts 做出来的图更适合放在系统里给非技术用户看。Flask搭建 Web 展示层。Flask 极轻几个文件就能把一个可视化系统跑起来比 Django 更适合这种中小型项目。这套组合的好处是“快”和“灵活”。从写爬虫到出结果几乎不需要切换到其他语言或工具。如果换成 Java 来做光环境配置和类结构设计就要耗掉不少时间这对个人项目来说不划算。提示如果你的主要目标是学数据分析而不是做爬虫工程师建议不要把时间耗在破解高强度反爬上。找一个数据字段完整的平台正常控制请求频率就好。1.3 功能模块划分整个系统按数据流转过程拆成了四个模块数据采集模块爬取平台列表页和详情页的数据保存为 CSV 文件。数据预处理模块处理缺失值、异常值统一字段格式构造新特征比如从“上牌日期”计算出“车龄”。数据分析模块从价格分布、品牌保值、里程影响、地区差异等维度做统计聚合。可视化展示模块用 Pyecharts 生成图表通过 Flask 提供网页交互界面。模块之间通过 CSV 文件解耦每个模块都能独立运行。这样设计最大的好处是调试方便哪一步出问题直接单独跑对应的脚本就行不需要从界面点到底。对于个人项目来说这种“半命令行、半系统”的风格最实用。2. 数据采集与清洗从网页到干净的数据表2.1 爬虫思路与反爬应对我选择的采集方式是先请求列表页从列表页提取每一辆车的详情链接再进入详情页补充完整的车辆参数。这样做虽然请求数量增加了一倍但数据质量高很多因为详情页里有车辆的详细配置信息列表页只有概览。爬虫部分有几个反爬细节需要注意都是实际踩过的坑。第一是请求头。只带默认的 User-Agent 很容易被识别建议用一个包含浏览器特征完整的请求头再加一个随机的 User-Agent 池轮换使用。第二是请求频率。我刚开始用 for 循环一秒请求十几次结果没跑多久 IP 就被封了。后来改成每次请求后随机睡 1 到 2 秒并加 try-except 的重试逻辑单次采集能稳定跑完几千条数据。第三是页面编码。很多网站是 GBK 编码直接 requests.get 默认得到的文本会乱码、或者出现“锟斤拷”。我用 r.encoding r.apparent_encoding 来让 requests 自动识别编码实测很稳。爬取下来的原始数据大概长这样字段名示例数据title2020款 大众朗逸 1.5L 自动风尚版price8.90register_date2021-06mileage3.2gearbox自动displacement1.5Lemission_standard国VIcity南京这些字段是后面所有分析的原料所以我在爬虫阶段就尽量做了一次粗校验价格为空的直接跳过标题里没有品牌信息的也先标注一下留到清洗阶段再处理。2.2 数据清洗要做什么爬虫拿下来的是“能用”的数据但离“能分析”还有距离。我用 pandas 做了如下几件事。先看缺失值。有些车源没有标注排放标准有些城市字段为空。我的策略是核心字段价格、里程、上牌年份缺失就删除改行因为这几个字段是分析的根本缺失了没法估算。次要字段缺失就填“未知”保留数据用于其他维度的统计。再处理重复值。同一个平台可能会出现同一辆车的多条记录我以“标题 上牌年份 里程”三列组合判断是否重复重复的行只保留第一条。然后是异常值。最开始画价格分布图时发现有一辆车标价 0.5 万这明显是虚拟测试车源或标价错误会拉低价格均值影响分析。我用 99 分位和 1 分位做截断处理只保留价格在 1 分位到 99 分位之间的数据把极端异常值踢掉。最后是构造新特征。我从“上牌日期”提取出年份再用当前年份相减得到“车龄”。把“里程”统一转成“万公里”单位把“排量”统一成“1.5L”这种字符串格式方便后面按组分析。import pandas as pd df pd.read_csv(car_data_raw.csv) # 删除关键字段缺失的行 df df.dropna(subset[price, mileage, register_date]) # 去除完全重复的记录 df df.drop_duplicates(subset[title, register_date, mileage]) # 从上牌日期提取年份并计算车龄 df[register_year] pd.to_datetime(df[register_date]).dt.year current_year 2025 df[car_age] current_year - df[register_year] # 价格异常值截断只保留1%-99%区间 low_price df[price].quantile(0.01) high_price df[price].quantile(0.99) df df[(df[price] low_price) (df[price] high_price)] # 转换里程为万公里数值 df[mileage] df[mileage].astype(float) df.to_csv(car_data_clean.csv, indexFalse)这段代码看起来简单但每个操作都有明确的业务考量。实际项目里清洗环节占开发时间的 40% 左右千万不要觉得这是浪费时间直接跳过。数据质量不过关后面的可视化再好看也是忽悠自己。2.3 存储方案的取舍数据量不大几万条以内的时候直接用 CSV 文件存储足够了。我第一次做这个项目时用了 MySQL后来发现清洗和探索阶段要频繁改表结构、加字段换 CSV 反而更灵活pandas 一行就能读入。如果是给别人交付一个正式系统再用 SQLite 或 MySQL。如果非要推荐一个折中方案我建议用 SQLite。它是 Python 内置支持的数据库零配置既能体验 SQL 查询又没有搭建服务器的麻烦。我这次选择 CSV有两个原因一是个人的探索分析习惯是“所见即所得”CSV 用 Excel 打开就能看二是给 CSV 做多版本管理比较方便清洗前后各存一份方便回滚对比。3. 数据分析从统计数据到洞察规律3.1 先做整体描述性统计拿到干净数据我习惯先跑一遍 describe()看核心字段的大致分布。这样能在脑子里建立一个数据画像再看具体维度的分析时不会心里没底。price_summary df[price].describe() print(price_summary) # 输出示例 # count 12684.000000 # mean 15.723456 # std 9.821034 # min 1.280000 # 25% 8.500000 # 50% 13.800000 # 75% 20.600000 # max 58.900000中位数 13.8 万均值 15.7 万说明有一批高端车源把均值拉高了价格分布是右偏的。这个发现直接决定了后面可视化时要重点展示价格分位数而不是只看平均值否则很容易做出误导人的图表。我还会同时看里程和车龄的分布。一般二手车平台的准新车源里程都比较低老车源的里程会明显出现双峰分布——一个峰在 2 万公里以内一个峰在 8 万到 10 万公里之间。这个特点也是后续解析“什么里程的车性价比最高”的基础。3.2 多维度交叉分析这是整个项目最核心的部分也是面试场上最容易讲的内容。我做了四个核心分析方向。第一个是价格区间的分布。我把价格切成 0-5万、5-10万、10-20万、20-35万、35万以上五档可以看出消费者的购买力主要集中在哪个区间。结果没有悬念10 到 20 万区间占比最高说明这是最主流的选择。这组数据对理解二手车市场非常直观。第二个是品牌保值率的横向对比。先按品牌分组求平均价格再结合车龄中位数计算“年均保值率”。我用的算法是品牌价格残值率 平均价格 / 该品牌新车指导均价然后用残值率除以平均车龄折算到年均保值率。这里的新车指导均价我用的是 2024 年公布的厂商指导价所以算出来的绝对数字有偏差但排名和相对关系仍然具备参考价值。实测下来日系、德系头部品牌保值率明显高于美系、法系品牌这个结论也和市场认知一致。第三个是车龄与价格的衰减曲线。把车龄分成 1 年以内、1-3 年、3-5 年、5-8 年、8 年以上五组看每个组的平均价格。结果很典型3 年内价格衰减最快5 年以后衰减趋缓。做这个分析时我顺手画了一条拟合曲线R² 到了 0.92说明车龄对价格的影响非常稳定。第四个是里程与价格的散点关系。我特意把“里程”和“价格”放在一起看因为很多新手分析师只看单变量忽略了这两个变量实际上高度相关。用散点图能直观看到价格随里程上升而下降而且出现明显的台阶效应——3 万公里和 5 万公里是两个价格台阶经常让买家多花两万块。# 按车龄分组统计平均价格 age_price df.groupby(pd.cut(df[car_age], [0, 1, 3, 5, 8, 20]))[price].mean() age_price.index [1年内, 1-3年, 3-5年, 5-8年, 8年以上] print(age_price)用 groupby 加 pd.cut 做分箱统计非常快这也是 pandas 分析最常用的套路。整个过程 5 分钟内就能出结果但要看懂结果背后的原因就需要结合行业经验去解读。3.3 建一个简易估价模型增加深度为了让项目更有含金量我加了一个“简易二手车估价模型”。准确来说不是完整的机器学习模型而是用线性回归拟合车龄、里程和排量对价格的影响权重。from sklearn.linear_model import LinearRegression import numpy as np # 构造特征 X df[[car_age, mileage]].copy() # 排量转成数值 X[displacement_num] df[displacement].str.replace(L, ).astype(float) y df[price] model LinearRegression() model.fit(X, y) print(系数, model.coef_) print(截距, model.intercept_) print(R², model.score(X, y))输出大概长这样车龄每增加 1 年价格平均降低 0.6 万里程每增加 1 万公里价格降低 0.2 万排量每增加 0.1L价格升高 0.8 万。R² 大约 0.82说明这三个变量解释了大部分价格变动。这个模型不用调参不用交叉验证主要是为了说明“哪些变量对二手车价格影响最大”而不是真的拿去做精准估值。简历上写“使用线性回归分析影响二手车价格的核心因子”比单纯写“分析了一下数据”有说服力得多。4. 可视化让分析结果自己说话4.1 图表技术选型对比做可视化方案选择时我对比了三个主流方案。方案优点缺点应用场景matplotlib语言底层高度可控画出来是静态图交互功能弱论文插图、探索性分析Pyecharts交互式图表美观代码简单版本更新较快老教程易踩坑Web可视化、大屏展示Tableau/Power BI拖拽式不用写代码不是 Python 生态难嵌入项目业务分析师快速出图考虑到这是 Python 项目、需要在浏览器里展示Pyecharts 是性价比最高的选择。它的图表在网页上可以缩放、悬停查看数据点、点击图例筛选这些交互体验对观众很友好。做探索性分析的时候我依然会用 matplotlib因为它快点击运行就出图适合自己看。但到了系统展示环节一定用 Pyecharts 重新画一套效果完全不是一个量级。个人经验是自己私下分析贪快给用户看的一定要好看这两者不冲突。4.2 系统里的核心图表我做的可视化系统一共包含 7 张主要图表这里挑几张有代表性的说明设计思路。第一张是二手车价格分布直方图。横轴价格区间纵轴车源数量用柱状图展示。价格分布能一眼看出市场主力价位段这是系统的首页图。我用的是 Pyecharts 的 Bar配合 dataZoom 组件方便用户拖拽查看每个价位区间对应的数量。第二张是品牌均价 Top10 柱状图。这里要注意排序我会先按各品牌车源数量过滤一遍只保留车源数量大于等于 50 的品牌否则几台稀有跑车会把保时捷这种品牌的均价顶到天上去图表失真。第三张是车龄与价格衰减折线图。这张图结合了 3.2 节的分组统计同时把拟合出来的趋势线画上去。用户能清晰看到“车龄 3 年是保值分水岭”这个结论比写一大段文字直观太多。第四张是品牌价格分布箱线图。箱线图能同时展示中位数、四分位数和离群点特别适合对比不同品牌的价格波动范围。豪华品牌的价格箱子跨度很大经济型品牌箱子很窄这种差异通过箱线图表达非常准确。第五张是车型热力地图展示各省份二手车平均价格差异。Pyecharts 的 Map 直接支持中国地图只要有个省份名称和均价就能生成效果非常唬人。虽然地图做出来的视觉冲击力强但要注意价格受车龄和车型组成影响省份对比只能看相对水平不能当作绝对结论。from pyecharts.charts import Bar, Line, Scatter, Map from pyecharts import options as opts # 示例品牌均价Top10柱状图 top10 df.groupby(brand)[price].mean().sort_values(ascendingFalse).head(10) bar ( Bar() .add_xaxis(top10.index.tolist()) .add_yaxis(品牌均价(万元), [round(i, 2) for i in top10.values]) .set_global_opts( title_optsopts.TitleOpts(title二手车品牌均价Top10), yaxis_optsopts.AxisOpts(name万元), ) .render(brand_top10.html) )这段代码生成的 HTML 文件可以直接用浏览器打开交互和缩放都正常。如果在 Jupyter 里想内嵌展示把 render() 换成 render_notebook() 即可。4.3 Flask 把图表组装成系统散落的一张张 HTML 图不方便给用户看这时就用 Flask 统一集成。我建了一个 templates 目录放页面模板每个图表通过 iframe 嵌入到主页面的卡片里。Flask 后端定义几个路由首页路由渲染 dashboard.html数据路由读取 CSV 并返回 JSON图表路由直接返回生成的 HTML 内容。核心逻辑不复杂就是把 Pyecharts 生成的 HTML 字符串传给前端模板from flask import Flask, render_template from pyecharts import options as opts from pyecharts.charts import Bar import pandas as pd app Flask(__name__) app.route(/) def index(): return render_template(dashboard.html) app.route(/chart1) def chart1(): df pd.read_csv(car_data_clean.csv) price_bins pd.cut(df[price], [0, 5, 10, 20, 35, 100]) price_dist df.groupby(price_bins).size() labels [0-5万, 5-10万, 10-20万, 20-35万, 35万以上] c ( Bar() .add_xaxis(labels) .add_yaxis(车源数量, [int(v) for v in price_dist.values]) .set_global_opts(title_optsopts.TitleOpts(title价格区间分布)) ) return c.render_embed() if __name__ __main__: app.run(debugTrue, port5000)对这个项目我不建议用组件库做复杂的大屏后台一是工作量大二是对学习数据分析和可视化的核心目标没有帮助。用最简单的 bootstrap 栅格布局 iframe 嵌套就够了。注意Pyecharts 和 Flask 的版本兼容问题很容易踩坑。Pyecharts 1.x 之后支持 render_embed()旧代码里的“Page().add”写法在新版可能会报错。如果遇到图表加载不出来的情况优先检查 Pyecharts 的版本和对应 API 写法。4.4 前端页面的筛选交互除了静态展示我还加了一个实用的筛选功能按品牌、车龄范围、价格区间筛选数据然后图表实时刷新。实现方法是前端提交表单参数到 Flask 路由Flask 根据参数过滤 DataFrame 之后重新生成图表。比如页码右侧加一个下拉框选择品牌“大众”然后图表的品牌均价柱状图会自动更新只分析大众车源。这个动态刷新的体验让系统从“展示大屏”变成了“可交互的数据分析工具”项目档次一下就上去了。实现这个功能只需要一个带提交按钮的表单后端路由加一个 request.args.get() 获取参数再过滤 DataFrame。整个过程大概 30 行代码强烈建议你加上它。5. 实操过程中的常见问题与排查技巧这章是干货里的干货我把几个真实踩过的坑和对应解法整理出来供你参考。5.1 数据采集被封IP怎么办我遇到过最夸张的情况是爬了不到 200 条数据IP 就被平台临时封禁后续所有请求都返回 403。排查思路有这几步先确认是否是请求频率太高。把请求间隔从 0.5 秒提高到 2 秒并加上随机偏移量比如 time.sleep(random.uniform(1, 2))大部分情况可以缓解。再检查 User-Agent 是否被识别为脚本。要带上完整的浏览器请求头包括 Accept、Accept-Language、Connection 等不要只改 User-Agent 一个字段。最后查看是否有 JS 动态参数或接口签名。如果遇到这种情况说明目标站点的防护等级较高个人学习项目就没必要继续硬杠直接换一个数据字段相对丰富的平台采集把精力留给分析阶段。5.2 数据清洗导致分析结果跳变有一版分析结果非常不稳定——品牌均价排名每跑一次都不一样。排查了很久发现是清洗逻辑的问题我按“标题匹配品牌”来过滤品牌为空的记录但有些车辆标题没写品牌名被直接删掉了同时同一品牌下不同车系的命名方式差异较大导致有些品牌被拆分成了多个“伪品牌”。解决办法是统一维护一张“品牌关键词映射表”把标题中出现的关键词映射到正式品牌名。例如“大众朗逸”“大众帕萨特”都映射到“大众”。这样既避免了误删又保证了分组的准确性。brand_map { 大众: 大众, 朗逸: 大众, 帕萨特: 大众, 奔驰: 奔驰, 宝马: 宝马, } def map_brand(title): for key, brand in brand_map.items(): if key in title: return brand return other这个映射表不完美但能覆盖绝大多数数据剩余“other”品牌在分析时剔除不再参与均值计算。数据清洗阶段多一分用心后面的图表才不会闹笑话。5.3 Pyecharts 版本新老不兼容Pyecharts 在 2019 年发布 1.0 版本之后API 发生了重大变化。网上很多教程还在用旧版的pyecharts.xxx导入方式或者Page(page_title...)的写法照抄下来很容易直接报错。我建议所有新版代码都使用from pyecharts.charts import Bar这种方式导入。如果遇到ImportError用pip install pyecharts -U升级到最新稳定版。另外在 Flask 返回图表时必须用render_embed()而不是render()后者会生成一个完整的 HTML 文档嵌入到模板里会出现嵌套的html标签页面展示时容易异常。老教程里常见的add()参数写法在新版部分会被标记废弃但兼容性还在。为了保险起见我全部改用set_global_opts(...)和set_series_opts(...)来设置标题和样式这样至少未来几年内代码不会因 API 刷新而过期。5.4 中文乱码和字体显示成方块这是国产数据项目最常见的场景问题。matplotlib 画图时中文默认显示成方块因为默认字体包里没有中文字体。解决办法是plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] FalsePyecharts 因为是浏览器渲染中文一般不会出问题。但要记得在 HTML 模板的head里加上meta charsetutf-8否则 Flask 渲染的页面中文可能乱码。这个坑很隐蔽因为单独打开 HTML 文件没问题但通过 Flask 返回时编码就不是默认 utf-8 了。5.5 页面图表加载太慢怎么优化数据量到几万条时每次点击筛选都要重新读一遍 CSV、重新聚合、重新渲染图表响应时间可能超过 3 秒体验很差。我的优化思路有两步。第一步在 Flask 启动时就把清洗好的数据加载成全局 DataFrame避免每次请求都读磁盘。第二步把频繁使用的聚合结果缓存到一个 dict 里同一筛选条件在 5 分钟内直接返回缓存结果不再重复计算。这两步做完页面响应从 3 秒降到 300 毫秒以内。如果是更复杂的系统还可以考虑把聚合结果预计算好存入 Redis 做缓存。但那一步涉及额外组件的部署对个人项目来说有点重等真有需要再加不迟。5.6 项目上线时要注意哪些问题如果要在个人服务器上部署这个系统有几个注意点一是 Flask 自带的开发服务器只适合测试生产环境要用 waitress 或者 gunicorn 来跑。比如用 waitress一行命令就可以启动waitress-serve --port8000 app:app。二是端口选择。不要用 5000 端口很多云服务器安全组默认不开放 5000。我建议用 8000 或者 8080同时检查云服务商的安全组规则是否放行了对应端口。三是数据更新策略。二手车数据有时效性。我当时写了一个定时任务每天凌晨自动跑一次爬虫脚本把最新数据追加到 CSV然后重新生成分析图表。这个定时任务用操作系统的 cron 就能实现不用额外装任务队列。6. 项目后续还能怎么扩展当你把上面这套基础版本完整跑通以后可以按自己的精力逐步扩展。我这里分享几个我实际尝试过的方向你挑感兴趣的来。如果对爬虫感兴趣可以给系统加上实时监控功能。通过定时任务每小时采集一次价格数据计算“本周降价二手车”列表并推送到自己的微信或者钉钉机器人。这个功能本质就是把数据分析结果和自动化联动起来效果非常贴近真实业务场景。如果想往机器学习方向延伸可以把简单的线性回归换成更完整的模型。用 XGBoost 训练二手车价格预测模型把品牌、车龄、里程、排量、地区等特征全部塞进去做特征重要性分析和模型调参。这样项目就多了一个“模型预测”模块简历上的技术栈也能多写一行。如果想做更完整的可视化大屏可以引入 Redis 做热点数据的缓存用 Kafka 做数据采集的消息队列前端再配一个企业级可视化工具展示实时指标。但说实话这些组件对二手车这个数据规模和项目场景来说属于“杀鸡用牛刀”如果不是为了学习和展示基础设施技能不建议为了堆技术而上。我会更推荐做“筛选逻辑增强”。比如增加多条件组合筛选同时选“大众品牌”“3 年以内”“8 万公里以内”页面实时展示符合条件的车源数量和价格分布。这个功能更加贴合真实买车的决策场景对数据分析和可视化的技能提升反而帮助更大。个人体会是这类项目最大的收获不是爬了多少页数据、画了多少张图而是学会把“业务问题”翻译成“数据语言”再把“数据结论”翻译回“业务语言”。二手车价格与车龄的衰减规律自己用数据算出来和看新闻上专家说认知深度完全不一样。最后分享一个小技巧调试可视化图表时先把图渲染成本地 HTML 文件用浏览器打开确认没问题再集成到 Flask 里面。这样每一步出问题都能快速定位不会出现页面报错却不知道是图表问题还是框架问题的尴尬情况。