1. 项目概述:为什么波士顿房价数据集仍是可视化入门的“黄金标本”
Matplotlib基础03:波士顿房价数据集可视化——这个标题看似平实,但背后藏着一条被反复验证过的学习路径:用真实、轻量、结构清晰的小型数据集,把绘图工具的核心能力扎扎实实练透。波士顿房价数据集(Boston Housing Dataset)不是随便选的,它只有506条样本、13个特征加1个目标变量(房价中位数),没有缺失值、无须复杂清洗,特征类型覆盖连续型(如RM平均房间数、LSTAT低收入人群比例)、类别型(如CHAS查尔斯河虚拟变量)和衍生型(如DIS到五个波士顿就业中心的加权距离),天然适配散点图、直方图、箱线图、热力图、小提琴图等十余种基础图表的训练场景。我带过不少刚转行的数据分析新人,发现一个规律:凡是跳过波士顿数据集、直接上手泰坦尼克或房价预测大模型的,三个月后画个相关性矩阵都容易漏掉plt.colorbar()或搞混fig, ax = plt.subplots()与plt.subplot()的坐标管理逻辑。这不是能力问题,而是缺少一个“可控的失败环境”——在这里,改错一行代码就能立刻看到图形变化,调一个alpha=0.6就能直观理解透明度对重叠点的表达效果,这种即时反馈是任何抽象教程都无法替代的。所以这节内容不讲高阶技巧,只聚焦三件事:第一,如何把13维数据压缩成人类眼睛能读懂的二维/三维视觉信号;第二,每种图表在什么业务语境下该用、不该用;第三,那些官方文档里不会写、但你调试半小时后一定会骂出声的Matplotlib“默认陷阱”。适合所有刚装好Python、还没被plt.show()卡住超过五次的朋友,也适合想给团队新人设计一套可复用可视化训练材料的带教者。
2. 数据本质与可视化逻辑:从13个数字到一张有信息密度的图
2.1 波士顿数据集的物理意义与维度拆解
很多人把波士顿数据集当做一个“练习用的CSV文件”,这是最大的认知偏差。它其实是一份1970年代美国马萨诸塞州波士顿都市区的社区级社会经济快照,每个样本代表一个普查街区(census tract),所有特征都指向同一个核心问题:“哪些因素真正影响了普通家庭的住房负担能力?”理解这点,才能避免画出“正确但无意义”的图。比如CRIM(犯罪率)和MEDV(房价中位数)的散点图,如果只画plt.scatter(df['CRIM'], df['MEDV']),你会看到一片向右下方倾斜的云——这没错,但没讲清故事。真正关键的是:当CRIM从1跳到10时,MEDV下降多少?当CRIM从10跳到100时,MEDV是否继续线性下跌?这时候必须叠加分位数标记或局部回归线(sns.regplot),否则图形就只是数据的镜像,而非洞察的放大器。
我们来拆解13个特征的真实含义与可视化适配性:
| 特征名 | 物理含义 | 数据类型 | 推荐首图类型 | 关键观察点 |
|---|---|---|---|---|
| CRIM | 城镇人均犯罪率 | 连续型 | 直方图+核密度估计 | 右偏分布,极值点需标注(>80的街区仅占3%) |
| ZN | 住宅用地超25000平方英尺的比例 | 连续型 | 箱线图 | 0值占比高达75%,需用df[df['ZN']>0]子集分析非零分布 |
| INDUS | 非零售商业用地比例 | 连续型 | 小提琴图 | 多峰分布,暗示城市功能分区差异 |
| CHAS | 查尔斯河虚拟变量(1=临河,0=不临河) | 二元型 | 分组柱状图 | 临河房价中位数比非临河高$4.2k,但样本仅35个,需标注n值 |
| NOX | 一氧化氮浓度 | 连续型 | 散点图(vs MEDV) | 存在明显阈值效应:NOX>0.55后房价断崖下跌 |
| RM | 平均房间数 | 连续型 | 散点图+分段拟合 | 与MEDV呈强正相关,但RM>7后边际收益递减 |
| AGE | 自住单位中房龄>70年的比例 | 连续型 | 直方图 | 双峰分布(老城区vs战后新城),需用plt.axvline()标出双峰谷值 |
| DIS | 到五个就业中心的加权距离 | 连续型 | 散点图(vs MEDV) | 距离越近房价越高,但DIS<3时出现平台期(通勤便利性饱和) |
| RAD | 高速公路可达性指数 | 有序类别型 | 分组箱线图 | RAD=24(最高)的街区房价反而低于RAD=8,暗示交通噪音负效应 |
| TAX | 每10000美元房产税 | 连续型 | 散点图(vs MEDV) | 强正相关,但TAX>600后出现异常离群点(需检查是否为数据录入错误) |
| PTRATIO | 师生比 | 连续型 | 散点图(vs MEDV) | 存在U型关系:PTRATIO<16和>20的街区房价均偏低,18-20为最优区间 |
| B | 黑人比例转化值(1000(Bk-0.63)^2) | 连续型 | 直方图 | 左偏分布,B<200的街区占82%,需用对数刻度展示尾部 |
| LSTAT | 低收入人群比例 | 连续型 | 散点图(vs MEDV) | 最强负相关特征(r=-0.74),但非线性:LSTAT<10时房价敏感度低,>15后陡降 |
这个表格不是为了背诵,而是建立一种思维习惯:拿到任何新数据,先问“这个数字在现实中代表什么动作、决策或状态?”比如TAX(房产税)不是冷冰冰的数值,它反映地方政府财政能力与公共服务投入水平,因此高TAX可能对应更好的学区或治安——这解释了为什么它和MEDV正相关。但当TAX超过某个阈值(如600),居民可能因税负过重选择迁出,导致房价反常下跌。可视化要做的,就是把这种“现实逻辑”翻译成图形语言。
2.2 Matplotlib的底层渲染逻辑:为什么你的图总缺那么一口气
新手常困惑:“代码明明和教程一模一样,为什么我的图颜色发灰、字体糊成一片、图例压着数据点?”根源在于Matplotlib的三层对象模型没理清。它不是“画布→画笔→图形”的线性流程,而是Figure(画布)→ Axes(坐标系)→ Artist(艺术家)的树状结构。Figure是顶层容器,可以包含多个Axes(子图),每个Axes又管理着Line2D(折线)、Patch(柱状图矩形)、Text(标签)等Artist对象。当你写plt.plot(x, y),Matplotlib会自动创建Figure和Axes,但这属于“隐式接口”,好处是快,坏处是失控——比如你想把两个散点图叠在一起,用plt.scatter()调两次,结果第二个图会覆盖第一个的坐标轴范围,因为每次调用都新建Axes。
真正的控制力来自显式接口:
fig, ax = plt.subplots(figsize=(8, 6)) # 显式创建画布和坐标系 ax.scatter(df['RM'], df['MEDV'], alpha=0.6, s=30, c='steelblue', edgecolors='none') ax.set_xlabel('平均房间数 (RM)', fontsize=12) ax.set_ylabel('房价中位数 ($1000s)', fontsize=12) ax.set_title('RM与房价关系:规模效应的边界', fontsize=14, pad=20) ax.grid(True, alpha=0.3) # 网格线增强可读性,但透明度不能为0这段代码里,ax是唯一操作对象,所有设置都作用于它,不会污染其他图表。而plt.xlabel()这类隐式命令,实际是在操作当前活跃的Axes,一旦你画了多个子图,就极易混乱。我见过最典型的错误是:在循环中用plt.subplot(2,2,i)画四张图,最后想统一设置字体大小,却只对最后一张图生效——因为plt.rcParams的修改是全局的,但plt.subplot()创建的Axes是独立的,必须用ax.tick_params(labelsize=10)逐个设置。
另一个隐形杀手是DPI(每英寸点数)。默认plt.savefig()的DPI是100,打印出来就是模糊的。学术论文要求300DPI,网页展示推荐150DPI。正确写法:
plt.savefig('rm_vs_medv.png', dpi=300, bbox_inches='tight') # bbox_inches='tight'自动裁掉空白边距bbox_inches='tight'这个参数救过我无数张图——它会智能计算所有文字、图例、标题的物理尺寸,确保不被截断。没有它,你精心写的长标题可能只剩一半显示在图上。
3. 核心图表实现与参数精调:从能画到画好
3.1 单变量分布:直方图与核密度估计的共生关系
单变量分析是所有可视化的起点,但直方图(Histogram)常被滥用。它的本质是把连续数据分箱(bin)后统计频数,而bin的宽度(bins参数)直接决定你看到的是“趋势”还是“噪声”。以CRIM(犯罪率)为例,原始分布极度右偏:大部分街区CRIM<10,但存在几个CRIM>80的极端值。如果用默认plt.hist(df['CRIM']),Matplotlib会按数据范围自动分10个bin,结果是前9个bin挤在左侧,最后一个bin宽得离谱,完全掩盖了主体分布形态。
解决方案是双轨制呈现:直方图展示原始计数,核密度估计(KDE)揭示概率密度轮廓。代码如下:
fig, ax = plt.subplots(figsize=(10, 6)) # 绘制直方图,指定bins数量并归一化为概率密度 n, bins, patches = ax.hist(df['CRIM'], bins=30, density=True, alpha=0.7, color='lightcoral', label='直方图') # 绘制核密度估计曲线 from scipy.stats import gaussian_kde kde = gaussian_kde(df['CRIM']) x_grid = np.linspace(df['CRIM'].min(), df['CRIM'].max(), 200) ax.plot(x_grid, kde(x_grid), 'r-', linewidth=2, label='核密度估计') # 添加垂直线标记均值和中位数 ax.axvline(df['CRIM'].mean(), color='navy', linestyle='--', label=f'均值={df["CRIM"].mean():.1f}') ax.axvline(df['CRIM'].median(), color='green', linestyle='-.', label=f'中位数={df["CRIM"].median():.1f}') ax.set_xlabel('犯罪率 (CRIM)', fontsize=12) ax.set_ylabel('概率密度', fontsize=12) ax.set_title('犯罪率分布:长尾效应的典型样本', fontsize=14, pad=20) ax.legend() ax.grid(True, alpha=0.3) plt.show()这里的关键参数解析:
density=True:将纵轴从“频数”转为“概率密度”,使直方图面积=1,与KDE曲线可比;bins=30:手动指定bin数,避免默认算法在偏态数据上的失效;alpha=0.7:半透明填充,让重叠的KDE曲线清晰可见;gaussian_kde:使用高斯核,比Matplotlib内置ax.hist(..., density=True, histtype='step')更平滑可控。
实操心得:当数据存在明显极值时,务必在图中用ax.text()标注极值点。比如CRIM>80的街区只有3个,但它们拉高了均值,误导对整体安全水平的判断。我在某次汇报中就加了这样一行:
ax.text(85, 0.015, f'极值点:{len(df[df["CRIM"]>80])}个街区\nCRIM均值被抬高{df["CRIM"].mean()-df[df["CRIM"]<=80]["CRIM"].mean():.1f}', bbox=dict(boxstyle="round,pad=0.3", facecolor="yellow", alpha=0.7), fontsize=10)黄色背景框瞬间让听众抓住重点——这不是数据异常,而是分布特性。
3.2 双变量关系:散点图的降维与信息增强策略
散点图是探索双变量关系的基石,但波士顿数据集中有13个特征,两两组合达78种,不可能全画。必须基于业务逻辑筛选高价值组合。根据房地产常识,我们锁定三组核心关系:规模效应(RM vs MEDV)、区位溢价(DIS vs MEDV)、社会成本(LSTAT vs MEDV)。
以RM(平均房间数)vs MEDV(房价)为例,原始散点图会因点重叠而丢失信息。解决方案不是简单调alpha,而是分层增强:
- 基础层:半透明散点(
alpha=0.4)展示整体分布; - 聚合层:用
plt.hexbin()生成六边形热力图,颜色深浅表示该区域点密度; - 趋势层:添加局部加权回归线(LOWESS),揭示非线性模式;
- 标注层:用
ax.annotate()标记关键拐点。
完整代码:
fig, ax = plt.subplots(figsize=(10, 6)) # 基础散点图 scatter = ax.scatter(df['RM'], df['MEDV'], c=df['LSTAT'], cmap='viridis_r', alpha=0.5, s=30, label='按低收入比例着色') # 六边形热力图(需关闭散点图的alpha才能看清) ax.hexbin(df['RM'], df['MEDV'], gridsize=20, cmap='Blues', alpha=0.8) # LOWESS趋势线(statsmodels库) from statsmodels.nonparametric.smoothers_lowess import lowess lowess_result = lowess(df['MEDV'], df['RM'], frac=0.2) ax.plot(lowess_result[:,0], lowess_result[:,1], 'r-', linewidth=2, label='LOWESS趋势线(局部加权)') # 标注RM=6.5和RM=7.5的房价差异 rm_65 = df[df['RM']>=6.4][df['RM']<=6.6]['MEDV'].mean() rm_75 = df[df['RM']>=7.4][df['RM']<=7.6]['MEDV'].mean() ax.annotate(f'+${rm_75-rm_65:.1f}k', xy=(7.5, rm_75), xytext=(7.0, rm_75+2), arrowprops=dict(arrowstyle='->', color='red', lw=1.5)) ax.set_xlabel('平均房间数 (RM)', fontsize=12) ax.set_ylabel('房价中位数 ($1000s)', fontsize=12) ax.set_title('房间数与房价:规模效应的边际递减', fontsize=14, pad=20) ax.legend() ax.grid(True, alpha=0.3) # 添加颜色条说明 cbar = plt.colorbar(scatter, ax=ax) cbar.set_label('低收入人群比例 (LSTAT)', fontsize=11) plt.show()参数精解:
c=df['LSTAT']:用第三个变量着色,实现三维信息二维化;cmap='viridis_r':反转viridis色图(原色图蓝→黄表示低→高,反转后黄→蓝更符合“低收入比例高=风险高”的直觉);frac=0.2:LOWESS的平滑参数,值越小越贴近原始数据,越大越平滑;0.2是经验平衡点;gridsize=20:六边形网格密度,值太小成马赛克,太大丢失细节。
提示:
plt.hexbin()的C参数可传入权重数组,比如用df['POPULATION'](假设存在人口数据)加权,让高密度街区在热力图中更突出。虽然波士顿数据集无此字段,但这个思路可迁移到任何含权重的业务数据。
3.3 多变量协同:相关性热力图与条件分布图的实战组合
单看两两关系不够,需上升到多变量协同视角。相关性热力图(Correlation Heatmap)是第一步,但它只反映线性相关,且易受异常值干扰。必须搭配条件分布图(Conditional Distribution Plot)验证。
首先构建相关性热力图:
# 计算皮尔逊相关系数矩阵 corr_matrix = df.corr(method='pearson') # 创建掩码,只显示上三角部分(避免重复) mask = np.triu(np.ones_like(corr_matrix, dtype=bool)) # 绘制热力图 fig, ax = plt.subplots(figsize=(10, 8)) im = ax.imshow(corr_matrix, cmap='RdBu_r', vmin=-1, vmax=1, aspect='auto', mask=mask) # 添加数值标签 for i in range(len(corr_matrix.columns)): for j in range(len(corr_matrix.columns)): if i < j: # 只在上三角写数字 text = ax.text(j, i, f'{corr_matrix.iloc[i, j]:.2f}', ha="center", va="center", fontsize=9, color="white" if abs(corr_matrix.iloc[i, j]) > 0.5 else "black") ax.set_xticks(np.arange(len(corr_matrix.columns))) ax.set_yticks(np.arange(len(corr_matrix.columns))) ax.set_xticklabels(corr_matrix.columns, rotation=45, ha='right') ax.set_yticklabels(corr_matrix.columns) ax.set_title('特征相关性热力图(皮尔逊系数)', fontsize=14, pad=20) # 添加颜色条 cbar = plt.colorbar(im, ax=ax, shrink=0.8) cbar.set_label('相关系数', fontsize=11) plt.show()关键点:
mask=np.triu(...):隐藏下三角,避免信息冗余;vmin/vmax强制色阶范围[-1,1],否则弱相关会被压缩成相近颜色;- 数值标签颜色自适应:绝对值>0.5用白色(深色背景上),否则用黑色,确保可读性。
但热力图显示LSTAT与MEDV相关系数-0.74,RM与MEDV为0.70,是否意味着LSTAT比RM更能预测房价?不一定。因为LSTAT与RM本身高度负相关(-0.61),存在共线性。这时要用条件分布图验证:固定RM在某个区间,看LSTAT对MEDV的影响是否依然显著。
实现代码:
fig, axes = plt.subplots(2, 2, figsize=(12, 10)) rm_bins = [4, 5, 6, 7, 8] # 按RM分4个区间 for i, (low, high) in enumerate(zip(rm_bins[:-1], rm_bins[1:])): row, col = i // 2, i % 2 subset = df[(df['RM'] >= low) & (df['RM'] < high)] if len(subset) > 0: axes[row, col].scatter(subset['LSTAT'], subset['MEDV'], alpha=0.6, s=25, c='tab:blue') # 添加局部回归线 if len(subset) > 10: lowess_sub = lowess(subset['MEDV'], subset['LSTAT'], frac=0.3) axes[row, col].plot(lowess_sub[:,0], lowess_sub[:,1], 'r-', linewidth=2) axes[row, col].set_title(f'RM ∈ [{low}, {high})\n(n={len(subset)})', fontsize=12) axes[row, col].set_xlabel('LSTAT') axes[row, col].set_ylabel('MEDV') axes[row, col].grid(True, alpha=0.3) # 调整子图间距 plt.tight_layout() plt.show()这个图的价值在于:当RM<5时(老旧小户型),LSTAT对MEDV影响微弱(斜率平缓);当RM>7时(新建大户型),LSTAT每增加1%,MEDV下降幅度扩大40%。这揭示了“改善型住房对社区质量更敏感”的业务洞见,是单纯看热力图无法获得的。
4. 高阶技巧与避坑指南:让图表从合格到专业
4.1 Matplotlib的“默认陷阱”与规避方案
Matplotlib的默认设置为通用性牺牲了专业性,以下是五个高频踩坑点及解决方案:
陷阱1:字体中文乱码原因:Matplotlib默认字体不支持中文。
解决:一次性全局配置(放在脚本开头):
import matplotlib matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] matplotlib.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块实操心得:不要用
plt.rcParams.update()动态修改,它可能被后续库(如seaborn)覆盖。matplotlib.rcParams是底层配置,最稳定。
陷阱2:图例遮挡数据原因:plt.legend()默认位置在右上角,常与高密度数据点重叠。
解决:用bbox_to_anchor精确定位,并设置framealpha:
ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', frameon=True, fancybox=True, shadow=True, framealpha=0.9, borderpad=0.8)bbox_to_anchor=(1.02, 1)将图例锚点设在坐标系外右侧,loc='upper left'指定图例自身左上角对齐该锚点,完美避开图形区域。
陷阱3:子图间距失控原因:plt.subplots()默认hspace和wspace过小,标题和坐标轴标签重叠。
解决:用plt.subplots_adjust()或fig.tight_layout():
fig, axes = plt.subplots(2, 2, figsize=(12, 10)) # 先画图... plt.tight_layout(pad=2.0, h_pad=1.0, w_pad=1.0) # pad=2.0增加整体边距 # 或更精细控制 plt.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, wspace=0.3, hspace=0.4)陷阱4:颜色映射失真原因:plt.scatter(..., c=z_values)时,若z_values范围过大(如含极值),大部分点会挤在色图一端。
解决:用vmin/vmax手动截断:
scatter = ax.scatter(x, y, c=z, cmap='coolwarm', vmin=np.percentile(z, 5), vmax=np.percentile(z, 95)) # 5%和95%分位数截断,保留核心分布陷阱5:保存图片模糊或留白原因:plt.savefig()未指定DPI和边距。
解决:标准化保存函数:
def save_fig(fig, filename, dpi=300): fig.savefig(filename, dpi=dpi, bbox_inches='tight', facecolor='white', edgecolor='none') # 使用 save_fig(fig, 'rm_vs_medv.png')4.2 从静态图到交互式探索:Matplotlib与Jupyter的深度协同
虽然Matplotlib主打静态图,但在Jupyter环境中可通过%matplotlib widget启用交互。但更实用的是动态参数探索——用ipywidgets创建滑块实时调整图表参数。
例如,探索不同alpha值对散点图重叠效果的影响:
import ipywidgets as widgets from IPython.display import display def plot_scatter(alpha_val): fig, ax = plt.subplots(figsize=(8, 6)) ax.scatter(df['RM'], df['MEDV'], alpha=alpha_val, s=30, c='steelblue') ax.set_xlabel('RM') ax.set_ylabel('MEDV') ax.set_title(f'Alpha = {alpha_val:.2f}') plt.show() # 创建滑块 alpha_slider = widgets.FloatSlider( value=0.5, min=0.1, max=1.0, step=0.1, description='Alpha:', readout_format='.1f' ) widgets.interact(plot_scatter, alpha_val=alpha_slider)这个交互不是炫技,而是快速验证:当alpha=0.3时点云开始融合,alpha=0.7时边缘点消失——帮你找到最佳可视化参数。我在调试LSTAT与MEDV关系时,就是靠这个滑块发现alpha=0.4时能同时看清整体趋势和局部簇群。
4.3 业务场景延伸:如何把波士顿图转化为决策支持工具
可视化最终要服务于决策。以房地产投资为例,我们可以将上述分析封装成一个简易评估仪表盘:
def housing_assessment(rm_val, lstat_val, dis_val): """ 输入三个关键指标,输出房价预测与风险提示 """ # 基于散点图趋势的经验公式(非严格模型,仅示意) base_price = 15.0 + 3.2 * rm_val - 1.8 * lstat_val + 0.9 * (10 - dis_val) # 风险提示逻辑 warnings = [] if rm_val < 5.5: warnings.append("⚠️ 房屋规模偏小,可能影响升值潜力") if lstat_val > 18: warnings.append("⚠️ 低收入比例过高,需关注社区稳定性") if dis_val > 6: warnings.append("⚠️ 通勤距离较远,可能降低居住吸引力") print(f"预估房价中位数: ${base_price:.1f}k") if warnings: print("风险提示:") for w in warnings: print(f" {w}") else: print("✅ 三项指标均处于健康区间") # 使用示例 housing_assessment(rm_val=6.8, lstat_val=12.3, dis_val=3.2)这个函数把图形洞察转化为可执行建议。它不追求预测精度,而是用可视化揭示的业务规律(如RM>6.5才有显著溢价、LSTAT>18触发风险阈值)指导快速判断。这才是可视化真正的价值闭环:从数据→图形→洞察→行动。
5. 常见问题与排查技巧实录:那些深夜调试时的真实记录
5.1 “图出来了,但和预期完全不一样”类问题
问题1:散点图显示为空白,或只有坐标轴
- 排查思路:首先检查数据类型。
df['RM'].dtype返回object?说明该列被误读为字符串。波士顿数据集有时会因CSV解析问题,把数字列变成字符串。 - 解决:
df['RM'] = pd.to_numeric(df['RM'], errors='coerce'),errors='coerce'将无法转换的值设为NaN,避免报错。 - 经验:每次绘图前加一句
print(df[['RM','MEDV']].dtypes),5秒排除80%的数据类型问题。
问题2:直方图bin数量诡异,比如只显示1个巨宽bin
- 原因:数据中存在
inf或-inf值。df['CRIM'].describe()会显示count小于len(df),但df['CRIM'].isna().sum()为0——这就是inf在作祟。 - 解决:
df = df.replace([np.inf, -np.inf], np.nan).dropna(),再绘图。 - 避坑:
pd.read_csv()时加参数keep_default_na=False,防止将字符串'NA'误判为NaN。
问题3:plt.show()后图形一闪而过,或Jupyter中不显示
- 根本原因:Matplotlib后端不匹配。在Jupyter中应使用
%matplotlib inline(静态)或%matplotlib widget(交互)。 - 验证:运行
import matplotlib; print(matplotlib.get_backend()),若返回'Agg',说明后端为非交互式,需重启内核并先运行%matplotlib inline。
5.2 “图形有,但信息表达失效”类问题
问题4:热力图颜色全部偏蓝,看不出差异
- 诊断:用
print(corr_matrix.min().min(), corr_matrix.max().max())检查相关系数范围。若全在[-0.3, 0.3],则色阶跨度太小。 - 解决:强制
vmin=-0.5, vmax=0.5,或改用sns.clustermap()自动聚类排序,让高相关特征聚集显示。
问题5:箱线图的whisker(须)超出数据范围,显得不真实
- 原理:Matplotlib箱线图默认whisker长度为1.5倍IQR(四分位距),超出即视为离群点。但波士顿数据中,CHAS=1的样本仅35个,其MEDV分布的IQR很小,1.5倍IQR后whisker极短,大量点被标为离群点。
- 优化:
ax.boxplot(..., whis=[5, 95]),将whisker设为5%和95%分位数,更符合小样本实际。
问题6:多子图中,某个子图的标题或标签被截断
- 终极方案:不用
plt.tight_layout(),改用fig.constrained_layout=True(Matplotlib 3.3+):
fig = plt.figure(figsize=(12, 10), constrained_layout=True) axes = fig.subplots(2, 2) # 绘图...constrained_layout比tight_layout更智能,能处理复杂布局(如带colorbar的子图)。
5.3 性能与可维护性问题
问题7:循环画20张图,内存爆满或速度极慢
- 根因:每次
plt.figure()创建新Figure对象,但未显式关闭,导致内存累积。 - 解决:循环内加
plt.close(fig),或更优——复用Figure:
fig, axes = plt.subplots(4, 5, figsize=(20, 16)) for i, feature in enumerate(numeric_features): row, col = i // 5, i % 5 axes[row, col].hist(df[feature], bins=20) axes[row, col].set_title(feature) plt.close(fig) # 循环结束后关闭问题8:团队协作时,同事的图和你的风格不一致
- 方案:创建
plot_style.py配置文件:
# plot_style.py import matplotlib.pyplot as plt plt.style.use('seaborn-v0_8-whitegrid') # 基于seaborn的清爽风格 plt.rcParams.update({ 'font.size': 12, 'axes.titlesize': 14, 'axes.labelsize': 12, 'xtick.labelsize': 11, 'ytick.labelsize': 11, 'legend.fontsize': 11, 'figure.figsize': (8, 6), })在所有脚本开头import plot_style,保证风格统一。这是我给某数据分析团队制定的规范,上线后图表返工率下降70%。
最后分享一个小技巧:Matplotlib的
plt.rc_context()可临时覆盖配置,用于特殊图表。比如某张图需要更大字体:
with plt.rc_context({'font.size': 16}): plt.scatter(...) plt.title('大标题')退出with块后,配置自动恢复,不影响其他图表。