ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据可视化实战:用Matplotlib和Seaborn助力房价预测与特征分析

数据可视化实战:用Matplotlib和Seaborn助力房价预测与特征分析

做数据挖掘项目,最容易被低估的环节就是数据可视化。我的房价项目预测刚好做到第四阶段,这几天的任务很明确:把 Matplotlib 和 Seaborn 系统过一遍,把已经清洗好的数据全部可视化出来。说实话,一开始我觉得无非就是“画几张图给汇报用”,但真正跑下来才发现,可视化是数据挖掘里最直接的探针,它能让你在建模之前就看到数据的真实形状、异常点、相关关系,甚至推翻你之前的一些假设。

这篇文章不是 API 文档的翻译,是我自己在房价项目预测中实际用过、踩过坑之后的完整记录。内容包括两个库的分工、环境配置、常见图表怎么画、每一张图在项目中怎么解读、以及我在过程中遇到的若干问题。适合正在做数据挖掘课程项目、竞赛或者入门机器学习的朋友,手头没数据也没关系,我会用一份模拟数据把流程跑通,你跟着改字段名就能直接套用到自己的数据集上。

1. 可视化在房价预测项目里的真实位置

1.1 为什么画图这一步不能省

很多初学者做数据挖掘,容易陷入“拿到数据就赶紧跑模型”的状态。我也曾是这么想的,觉得 pandas 里describe()已经给了均值、最值,再打印个corr()就能了解相关性,何必费劲画图?

但这个想法在房价项目里很快就被纠正了。举一个很实在的例子:某个字段的缺失率是 5%,用isnull().sum()看,好像可以接受。但你把它画成分布图之后,会发现缺失值集中在某一类高单价房源上,那就不是随机缺失,而是系统性偏差。这种问题,单纯靠统计数字是很难察觉的。

更关键的是,房价数据这种连续值目标,和面积、房龄、楼层等特征之间往往不是简单线性关系。你看着相关系数只有 0.3,但散点图可能告诉你:面积小于 50 平方米时价格不敏感,超过 120 平方米后曲线突然陡增。这就是所谓的非线性模式,只有可视化才能把这些“隐藏形状”暴露出来。

所以我把可视化看成数据挖掘流程中承上启下的关卡:上游是数据清洗,下游是特征工程和建模。画图不是把清洗结果“证明”给别人看,而是帮你决定下一步该创建什么特征、该处理哪些异常值。

1.2 Matplotlib 和 Seaborn 的分工

这两个库关系紧密,Seaborn 严格来说是建立在 Matplotlib 之上的高级接口。但它们的分工很容易被误解。

Matplotlib 是一个底层的“绘画引擎”,它给你画布、坐标轴、刻度、线条、文字,一切都可控,代价是很多图表需要自己组合。比如你想画一个带均值线的直方图,你至少得写几行代码分别处理直方图和线条。

Seaborn 则是“统计图表工具箱”,它自带数据聚合能力,你只要给它一个整洁的 DataFrame,它就能画出带置信区间的回归图、分组的箱线图、相关性的热力图。它的设计目标不是代替 Matplotlib,而是让“数据探索”这件事少写代码。

用一个生活化的类比:Matplotlib 是毛笔和宣纸,你想怎么画都行,但每一步都得自己构思。Seaborn 是印好的素描本,你只要选对模板,把数据往里一放,它就能给你一个符合统计习惯的底稿,细枝末节再交给 Matplotlib 去调整。

我的实际用法是:90% 的探索性图用 Seaborn,需要精细排版或组合布局时再回到 Matplotlib。两者不是二选一,而是配合关系。

2. 先搭环境,再把数据摆到桌面上

2.1 这一步应该准备什么

在开始画图之前,我建议先把环境准备好。我这里用的是 Python 3.10 环境,核心库版本大致是:

库版本作用
matplotlib3.8+底层绘图,负责坐标轴、画布、保存
seaborn0.13+统计图表封装,依赖 matplotlib
pandas2.0+数据读取、清洗、透视
numpy1.24+数值计算,画图时经常用到

安装命令很简单:

pip install numpy pandas matplotlib seaborn jupyterlab

如果你用 Anaconda,直接用conda install也一样。我推荐在 Jupyter Notebook 里做这阶段工作,因为画图结果就在代码下方,方便反复调整参数。

2.2 画图前的数据检查代码

正式画图前,先做两件事:读入数据、看一眼字段类型。我这份模拟数据没有真实地址信息,字段包括面积、房龄、楼层、房间数、距离市中心的公里数、挂牌价。虽然不是真实项目数据,但结构足够演示房价预测中常见的可视化操作。

import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns np.random.seed(42) n = 300 sample_df = pd.DataFrame({ 'area': np.random.normal(85, 25, n).clip(30, 180), 'age': np.random.randint(0, 40, n), 'floor': np.random.randint(1, 30, n), 'rooms': np.random.randint(1, 6, n), 'distance': np.random.normal(8, 3, n).clip(1, 20), 'price': np.random.normal(180, 55, n) }) # 因为价格要和面积相关,简单调整一下 sample_df['price'] = ( 60 + sample_df['area'] * 1.2 - sample_df['age'] * 0.5 + np.random.normal(0, 20, n) ) print(sample_df.head()) print(sample_df.dtypes)

做完这一步,我通常会调用.info()和.describe()做一次快速体检,确认没有明显的字符型数值混入。很多时候,price列里夹着几个"180万"这样的字符串,你直接画图不会报错,但会把整个坐标轴搞乱。所以画图前最好再跑一条:

sample_df['price'] = pd.to_numeric(sample_df['price'], errors='coerce') sample_df = sample_df.dropna(subset=['price'])

这一步的目的是把脏数据挡在画图之前,而不是等到图上出现奇怪的点再回头查数据。我个人的习惯是:可视化前先清洗一次,可视化中发现异常再重新清洗一次,这是两个互补的过程。

2.3 中文字体设置,最容易翻车的点

用 Matplotlib 画中文,大概率第一次都会遇到方块字。原因很简单,默认字体是英文,中文字符没找到对应字形。我一般在代码开头统一设置:

plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei'] plt.rcParams['axes.unicode_minus'] = False

第一行指定中文字体,第二行解决坐标轴负号显示成方块的问题。如果这几行还是不能解决,可以换一种更稳妥的方式:

import matplotlib.font_manager as fm font_path = "你的字体文件路径.ttf" my_font = fm.FontProperties(fname=font_path) plt.rcParams['font.family'] = my_font.get_name()

这种方式适合在 Linux 服务器上跑任务时用,直接指定字体文件,系统环境和本地不一致也不怕。

3. Matplotlib 实操:把房价变量一个个画明白

3.1 折线图:先看价格随时间的趋势

我的模拟数据里没有明确日期字段,但实际房价项目里时间是最常见的维度。如果你手上数据有month或year列,第一张图我推荐画折线图。

# 模拟一个按月均价数据 month_avg = sample_df.groupby( pd.cut(sample_df['age'], bins=4) )['price'].mean() plt.figure(figsize=(8, 4)) plt.plot(range(len(month_avg)), month_avg.values, marker='o') plt.xticks(range(len(month_avg)), [str(x) for x in month_avg.index]) plt.xlabel('房龄分段') plt.ylabel('平均价格') plt.title('不同房龄段的平均房价变化') plt.grid(True) plt.show()

这张图能快速告诉你要不要做时间相关特征。比如价格在某个月突然下跌,看分布图可能看不出原因,但结合当时政策或市场事件就能找到线索。当然,折线图最忌讳的是数据点太少还硬连线,如果只有两个点,画成柱状图更合适。

3.2 散点图:面积和价格的关系

散点图是房价预测里最有用的单图之一。因为它能同时展示取值范围、趋势方向和离群点。

plt.figure(figsize=(7, 5)) plt.scatter(sample_df['area'], sample_df['price'], alpha=0.5) plt.xlabel('面积(平方米)') plt.ylabel('价格(万元)') plt.title('面积与价格散点图') plt.show()

alpha=0.5是我比较常用的参数,样本多时能避免点重叠成一大团黑块。跑完之后,我会先看三点:第一,趋势是否单调递增;第二,有没有明显偏离主体区域的点;第三,点在价格高端是不是特别稀疏。图如果出现一个面积 10 平方米、价格 3000 万的样本,那基本就是录入错误或者极端豪宅,建模时要单独考虑,不能直接丢进模型里冤枉它。

3.3 多子图布局,一页看清多个维度

分析单变量时经常要同时看面积、房龄、楼层、距离这几个分布。如果一个一个画,会在 notebook 里滚半天。我习惯用plt.subplots()做成 2x2 的布局。

fig, axes = plt.subplots(2, 2, figsize=(12, 8)) axes[0, 0].hist(sample_df['area'], bins=30) axes[0, 0].set_title('面积分布') axes[0, 1].hist(sample_df['age'], bins=20) axes[0, 1].set_title('房龄分布') axes[1, 0].hist(sample_df['floor'], bins=15) axes[1, 0].set_title('楼层分布') axes[1, 1].hist(sample_df['distance'], bins=20) axes[1, 1].set_title('距离分布') plt.tight_layout() plt.show()

plt.tight_layout()非常重要,不加的话子图标题很容易重叠。这种布局不是为了好看,是为了快速对比不同变量的量纲和偏态。比如面积是右偏、房龄是均匀分布、距离是左偏,不同类型变量后面要做的特征变换完全不一样。

4. Seaborn 实操:让统计结论直接长在图上

4.1 用直方图和核密度图看价格分布

Matplotlib 的hist能展示频率,但 Seaborn 的histplot会自带统计美化,而且能叠加 KDE 曲线,更直观地看出分布是否偏态。

sns.histplot(sample_df['price'], bins=30, kde=True, color='steelblue') plt.xlabel('价格(万元)') plt.title('房价分布直方图与核密度曲线') plt.show()

画这张图时我会关注两个点。第一,分布是否接近正态。如果价格明显右偏,后续建模通常会把对数价格log1p(price)当作目标,这样训练时模型不会被少数高价样本带偏。第二,有没有异常的多峰。两个峰值往往意味着数据里混入了两种不同档位的房子,比如普通住宅和高端公寓,这会影响特征工程的思路。

4.2 用箱线图找异常值和分组差异

箱线图是我在房价项目里最依赖的异常值探测器。它把四分位数、中位数、上下须都画出来了,超过须的点会被单独标出。

plt.figure(figsize=(8, 5)) sns.boxplot(data=sample_df, x='rooms', y='price') plt.xlabel('房间数') plt.ylabel('价格(万元)') plt.title('不同房间数的房价分布') plt.show()

这张图能回答一个实际问题:房间越多价格一定越高吗?从箱体分布看,3 房和 4 房的房价中位数可能差不多,那么“房间数”作为特征的重要性就要打个问号。此外,箱线图上方孤零零的点就是异常值候选,我会把这些样本挑出来单独看一次,而不是直接删除。因为房价数据中,面积大、房龄新的高端房源本身就是真实存在的高杠杆点,删除反而会丢失重要信息。

4.3 用热力图找相关性,为建模选特征

相关性热力图大概是数据挖掘项目里存在感最强的一张图。Seaborn 的热力图配合annot参数,能把所有数值特征的相关系数直接标在格子上。

corr = sample_df.corr() plt.figure(figsize=(8, 6)) sns.heatmap(corr, annot=True, fmt='.2f', cmap='coolwarm', square=True, cbar_kws={'shrink': 0.8}) plt.title('特征相关系数热力图') plt.show()

解读热力图时要小心一件事:高相关不代表因果,低相关也不一定说明特征没用。我在这个项目里就看到area和price的相关性明显更高,而floor几乎不相关。但后续建模时我依然没有删除floor,因为它在楼层与朝向交互时可能会影响采光,对部分房源价格有作用。特征筛选不能只看单相关系数,可视化只是给你一个初步方向,真正的取舍要结合业务理解。

我还特别推荐使用sns.pairplot做一次多变量总览,虽然样本大时图会多到离谱,但对 5 个以内的数值特征非常管用:

sns.pairplot(sample_df[['area', 'age', 'rooms', 'price']], diag_kind='kde') plt.show()

这张图能一次性展示所有两两组合的散点图和边际分布,价格预测项目里用于找交互效应非常高效。

5. 我踩过的坑和排查清单

5.1 中文变方块

这个前面说过,解决方案就是设置字体。但还有个容易被忽略的问题:如果你的代码在 Jupyter 里是把rcParams放在import seaborn之后,某些版本的 seaborn 会因为调用主题而重置 matplotlib 参数。所以我后来都把字体设置放在文件最顶部,并且不要在画完第一张图之后再改,否则前面已经生成的图不会自动更新。

5.2 Seaborn 版本带来的接口变化

Seaborn 的版本迭代一直在变,比如早些年常用sns.distplot,现在已经被displot/histplot替代。如果你在实操中报AttributeError: module 'seaborn' has no attribute 'distplot',不是代码写错,是版本太新。解决办法很简单,把distplot改成histplot或kdeplot,参数上稍微调整就行。

还有一个更隐蔽的坑:某些老教程里的sns.lmplot用法可能和新版本冲突。我建议遇到报错先看两个地方,第一是sns.__version__,第二是官方文档对应版本。不要去猜,版本升级改接口太常见了。

5.3 图例重叠、坐标轴刻度太密

画多图时最常遇到的就是标签挤在一起。解决方式有几种:加figsize把画布调大;旋转刻度标签plt.xticks(rotation=45);保存图片时用bbox_inches='tight'。我平时保存图片的固定写法是:

plt.savefig('price_analysis.png', dpi=200, bbox_inches='tight')

dpi=200保证投稿或报告打印不模糊,bbox_inches='tight'能自动裁掉多余空白。如果只用默认保存,经常会有标题被切掉一半的问题。

5.4 常见问题速查表

现象原因解决办法
中文变成方块缺少中文字体设置plt.rcParams['font.sans-serif']
负号显示为方块默认编码问题设置axes.unicode_minus=False
seaborn 找不到distplot新版接口移除改用histplot/kdeplot
图像保存不完整未使用 tight 参数加bbox_inches='tight'
散点全部叠成黑块点过多、透明度低加alpha=0.3~0.6或抽样子集
热力图颜色无区分度cmap 色带不合适换成coolwarm/RdBu_r
图例挡在图上布局重叠用plt.legend(loc='best')或调整figsize

6. 做完这一轮,谈谈我的真实体会

这轮可视化学习给我最大的感受是:Matplotlib 教会我怎么控制“画”,Seaborn 教会我怎么理解“统计”。在房价项目里,真正有价值的东西不是某一张图有多漂亮,而是图背后能引出的动作。比如我原本计划直接用线性回归,但看到价格的右偏分布后,果断把目标变成对数价格;原本打算把所有特征一股脑塞进模型,但热力图告诉我其中两个特征相关性极高,于是提前做了去重处理。

如果你也正在做类似的预测项目,我建议不要只把可视化当成“交作业的配图”。你先拿出每个字段,逐一画分布,再画两两关系,最后把缺失值、异常值位置标注在图上,这个过程花的时间不会白费。一套完整的数据探索图做完,你对数据集的熟悉程度会远超光看几十行输出表格的效果。这也是我在这个项目里最愿意分享的一条经验:建模前多画图,比多调参数更划算。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进