TA focus: scatter, bar and heatmap. Examples use df2 (Ames housing) from the mock exam. Run the setup cell first.
绘图准备
字体与配色设置:rcParams
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import glob, matplotlib
from matplotlib import font_manager as fm
for f in glob.glob(r'F:\2026fall_skd\练习\画图字体\NotoSerifSC-*.ttf'):
fm.fontManager.addfont(f) # 本机字体;别的电脑没有这个文件夹,自动跳过
have = {f.name for f in fm.fontManager.ttflist}
# ---- WYX Only:粉蓝马卡龙 ----
CN = [f for f in ['Noto Serif SC', 'Source Han Serif SC', 'Songti SC', 'STSong', 'SimSun', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei'] if f in have] # 宋体优先,没有再退到黑体
MAIN, MAIN_D, MAIN_L = '#a9c8ea', '#6f93c6', '#c9dcf2' # 主色:浅蓝 / 深蓝 / 很浅的蓝
ACCENT, ACCENT_D = '#f4b6ca', '#e0819f' # 强调色:浅粉 / 深粉
GRAY, BG = '#a89dab', '#fffbfc' # 注释文字 / 带粉的白底
CMAP = sns.blend_palette([ACCENT_D, '#fbe1ea', BG, MAIN_L, '#86abdb', '#4f7fb8', '#2d4a78'], as_cmap=True) # 负粉 → 白 → 正蓝
sns.set_theme(style='white')
plt.rcParams.update({
'font.family': CN + ['DejaVu Sans'], 'axes.unicode_minus': False,
'figure.facecolor': BG, 'axes.facecolor': BG, 'savefig.facecolor': BG,
'figure.dpi': 110, 'figure.constrained_layout.use': True,
'axes.spines.top': False, 'axes.spines.right': False,
'axes.edgecolor': '#e3d6dc', 'axes.linewidth': 0.9,
'axes.grid': True, 'axes.grid.axis': 'y', 'axes.axisbelow': True,
'grid.color': '#f3e9ee', 'grid.linewidth': 0.8,
'axes.titlesize': 14, 'axes.titleweight': 'bold', 'axes.titlepad': 12, 'axes.titlecolor': '#4a4150',
'axes.labelsize': 11, 'axes.labelcolor': '#7a6f80',
'xtick.color': GRAY, 'ytick.color': GRAY, 'xtick.labelsize': 10, 'ytick.labelsize': 10,
'xtick.major.size': 0, 'ytick.major.size': 0,
'legend.frameon': False, 'legend.fontsize': 10, 'legend.labelcolor': '#6b6070',
'axes.prop_cycle': matplotlib.cycler(color=[MAIN, ACCENT, MAIN_D, ACCENT_D]),
})
图表类型的选择tricky
# 一个数值 → 直方图 histplot(先 describe / skew)
# 一个类别 → 计数图 countplot(先 value_counts)
# 数值 × 数值 → 散点图 scatterplot + regplot(先 corr)
# 类别 × 数值 → 箱线图 boxplot、柱状图 barplot(先 groupby.agg)
# 类别 × 类别 → 堆叠柱状图、热力图(先 crosstab)
# 三个变量 → 加 hue=,或 pivot_table 后画热力图
# 很多数值列 → 相关系数热力图
# 随时间变化 → 折线图
seaborn 与 pandas 绘图的分工:sns.xxx / df.plot
# sns.xxx(data=原始表, x=列名, y=列名) 会帮你算(分组、计数、求平均)
# 算好的表.plot(kind='bar') 不算,表里是什么画什么:索引 → 横轴,每列一组
# ⚠ 算好的表别再交给 sns.barplot:它会再平均一次
# x / y 只传现成的列,图自己能算的不传:
# 直方图、计数图只给 x(高度是数出来的);箱线图 x=分组、y=数值;散点图 x、y 都给;热力图给一张表
画布与子图:plt.subplots / ax=log #30
fig, ax = plt.subplots(1, 2, figsize=(11, 4)) # 1 行 2 列:fig 是整张纸,ax[0]、ax[1] 是两个格子
sns.histplot(data=df2, x='SalePrice', ax=ax[0]) # ✅ ax= 写在括号里
# ❌ ax[0] = sns.histplot(...) 等号是贴标签,图会画到最后一格
# 只有一个格子:fig, ax = plt.subplots(figsize=(7, 4)),后面写 ax=ax,不加 [0]
标题、坐标轴与参考线:set_title / axvline / textlog #35
ax[0].set_title('成交价分布') # 标题写图名
ax[0].set_xlabel('成交价(美元)'); ax[0].set_ylabel('房子数') # 中文 + 单位
ax[0].xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k') # 200000 → 200k;纵轴用 ax.yaxis
ax[0].tick_params(axis='x', rotation=45) # 横轴文字转方向
ax[0].axvline(180000, color=ACCENT_D, ls='--', label='某个值') # 竖线给「数」;横线 axhline
ax[0].legend() # label 只是起名,legend() 才显示
ax[0].text(0.97, 0.9, '偏度 1.88', transform=ax[0].transAxes, # 角上写字:坐标 0–1(右上角 ≈ 0.97, 0.9)
ha='right', color=ACCENT_D)
fig.suptitle('整张图的总标题')
直方图
单变量分布:sns.histplotmock B3
fig, ax = plt.subplots(1, 2, figsize=(11, 4.2))
sns.histplot(data=df2, x='SalePrice', bins=40, kde=True, color=MAIN_D, ax=ax[0]) # kde=True 柱子自动半透明,给深色
med, mean = df2['SalePrice'].median(), df2['SalePrice'].mean()
ax[0].axvline(med, color=ACCENT_D, lw=2.2, label=f'中位数 {med/1000:.0f}k') # 粉色实线
ax[0].axvline(mean, color=MAIN_D, lw=2.2, ls='--', label=f'均值 {mean/1000:.0f}k') # 蓝色虚线
ax[0].set_title('成交价分布')
ax[0].set_xlabel('成交价(美元)'); ax[0].set_ylabel('房子数')
ax[0].xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax[0].legend(loc='upper right')
ax[0].text(0.97, 0.66, f"偏度 {stats.skew(df2['SalePrice']):.2f}", transform=ax[0].transAxes, ha='right', color=ACCENT_D)
sns.histplot(data=df2, x='LogPrice', bins=40, kde=True, color=ACCENT_D, ax=ax[1])
ax[1].set_title('log(1+成交价) 分布')
ax[1].set_xlabel('log(1+成交价)'); ax[1].set_ylabel('')
ax[1].text(0.97, 0.9, f"偏度 {stats.skew(df2['LogPrice']):.2f}", transform=ax[1].transAxes, ha='right', color=ACCENT_D)
# 纵轴想看占比:stat='density'(柱子面积加起来 = 1)
散点图
两个数值变量的关系:sns.scatterplot + regplottricky
fig, ax = plt.subplots(figsize=(7.5, 4.8))
sns.scatterplot(data=df2, x='GrLivArea', y='SalePrice', hue='CentralAir', hue_order=['Y', 'N'],
palette=[MAIN_D, ACCENT_D], s=24, alpha=0.55, edgecolor='white', linewidth=0.4, ax=ax) # 点多:alpha 调透明
sns.regplot(data=df2, x='GrLivArea', y='SalePrice', scatter=False, ci=None, # 只画回归线
color=GRAY, line_kws={'lw': 1.4, 'ls': '--'}, ax=ax)
r, p = stats.pearsonr(df2['GrLivArea'], df2['SalePrice'])
ax.set_title('地上面积与成交价')
ax.set_xlabel('地上面积(平方英尺)'); ax.set_ylabel('成交价(美元)')
ax.yaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax.grid(axis='x') # 散点图横竖网格都要
ax.legend(title='中央空调', loc='upper left')
ax.text(0.97, 0.06, f'r = {r:.2f}', transform=ax.transAxes, ha='right', color=ACCENT_D)
# 纵轴对数刻度:ax.set_yscale('log');hue 给数值列(如 OverallQual)会变成渐变色:palette='Blues'
柱状图
分组汇总:sns.barplot
柱子高度由 seaborn 按组算(estimator)。标数值:ax.bar_label(ax.containers[0]),有 hue 时每种颜色一组、要 for 循环;1×2 子图写 ax[0]。下面三张卡同理。
# 原始数据直接画:seaborn 自己按 x 分组、每组算一个数
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.barplot(data=df2, x='OverallQual', y='SalePrice', estimator='median', errorbar=None, color=MAIN, ax=ax)
ax.set_title('各质量等级的成交价中位数')
ax.set_xlabel('整体质量'); ax.set_ylabel('成交价中位数(美元)')
ax.yaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax.bar_label(ax.containers[0], fmt=lambda v: f'{v/1000:.0f}k', padding=2, fontsize=8, color=GRAY) # 柱顶标数值:50k …
# estimator 不写 = 均值;errorbar 不写 = 柱顶黑线(95% 置信区间)
# 只标数字不改格式:ax.bar_label(ax.containers[0]) # 50150 …;千分位 fmt='{:,.0f}'
# —— 想按高低排序:order 给一串类别 ——
order = df2.groupby('OverallQual')['SalePrice'].median().sort_values(ascending=False).index
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.barplot(data=df2, x='OverallQual', y='SalePrice', estimator='median', errorbar=None, color=MAIN, order=order, ax=ax)
# —— 类别多 / 名字长,想横着放:x、y 对调 ——
fig, ax = plt.subplots(figsize=(7, 7))
sns.barplot(data=df2, x='SalePrice', y='Neighborhood', estimator='median', errorbar=None, color=MAIN, ax=ax)
ax.xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k') # 数值轴变成了 x 轴
# —— 想再分一层:hue;每种颜色是一组柱子,标数值要逐组 ——
fig, ax = plt.subplots(figsize=(9, 4))
sns.barplot(data=df2, x='OverallQual', y='SalePrice', hue='CentralAir', estimator='median', errorbar=None,
palette=[ACCENT, MAIN], ax=ax)
for c in ax.containers:
ax.bar_label(c, fmt=lambda v: f'{v/1000:.0f}k', fontsize=7, color=GRAY)
ax.legend(title='中央空调')
类别计数:sns.countplot
# 只给 x:柱子高度 = 每类几行
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.countplot(data=df2, x='OverallQual', color=ACCENT, ax=ax)
ax.set_title('各质量等级的房子数')
ax.set_xlabel('整体质量'); ax.set_ylabel('房子数')
ax.bar_label(ax.containers[0], padding=2, fontsize=8, color=GRAY) # 柱顶标个数:2, 3, 20 …
# —— 想看占比:stat='percent'(seaborn 0.13 起) ——
fig, ax = plt.subplots(figsize=(7.5, 4))
sns.countplot(data=df2, x='OverallQual', stat='percent', color=ACCENT, ax=ax)
ax.bar_label(ax.containers[0], fmt='{:.1f}%', padding=2, fontsize=8, color=GRAY) # 0.1%, 0.2%, 1.4% …
# —— 想按多少排序 + 横着放 ——
fig, ax = plt.subplots(figsize=(7, 7))
sns.countplot(data=df2, y='Neighborhood', order=df2['Neighborhood'].value_counts().index, color=ACCENT, ax=ax)
ax.bar_label(ax.containers[0], padding=2, fontsize=8, color=GRAY)
# —— 想再分一层:hue,逐组标 ——
fig, ax = plt.subplots(figsize=(9, 4))
sns.countplot(data=df2, x='OverallQual', hue='CentralAir', palette=[ACCENT, MAIN], ax=ax)
for c in ax.containers:
ax.bar_label(c, fontsize=7, color=GRAY)
已算好的表:ax.barh / Series.plot
# 已经 groupby 算好、排好序的 Series:index → 柱子,值 → 长度
med = df2.groupby('Neighborhood')['SalePrice'].median().sort_values()
fig, ax = plt.subplots(figsize=(7.5, 6.8))
ax.barh(med.index, med.values, color=MAIN, height=0.68) # 竖着放:ax.bar
ax.set_title('各街区成交价中位数')
ax.set_xlabel('成交价中位数(美元)'); ax.set_ylabel('')
ax.xaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
ax.grid(axis='y', visible=False); ax.grid(axis='x') # 横柱子配竖网格
ax.bar_label(ax.containers[0], fmt=lambda v: f'{v/1000:.0f}k', padding=3, fontsize=8, color=GRAY)
# —— 想突出前 5 名:color 给一串颜色,和柱子一一对应 ——
colors = [ACCENT_D if v >= med.iloc[-5] else MAIN for v in med] # med 升序,最后 5 个最大
fig, ax = plt.subplots(figsize=(7.5, 6.8))
ax.barh(med.index, med.values, color=colors, height=0.68)
# —— 不想写这么多:pandas 一行 ——
fig, ax = plt.subplots(figsize=(7.5, 6.8))
med.plot(kind='barh', color=MAIN, ax=ax) # 竖着放:kind='bar'
ax.bar_label(ax.containers[0], fmt=lambda v: f'{v/1000:.0f}k', fontsize=8, color=GRAY)
堆叠柱状图:plot(kind='barh', stacked=True)tricky
# crosstab 的结果:index → 一根柱子,列 → 柱子里的一段颜色
tier = pd.cut(df2['OverallQual'], bins=[0, 5, 7, 10], labels=['低', '中', '高'])
ct = pd.crosstab(df2['MSZoning'], tier, normalize='index').sort_values('高') # 每行加起来 = 1
fig, ax = plt.subplots(figsize=(7.5, 3.8))
ct.plot(kind='barh', stacked=True, color=[ACCENT, MAIN_L, MAIN_D], width=0.65, ax=ax) # 竖着放:kind='bar'
ax.set_title('各用地类型的房屋质量构成')
ax.set_xlabel('占比'); ax.set_ylabel('用地类型')
ax.xaxis.set_major_formatter(lambda x, _: f'{x:.0%}') # 0.5 → 50%
ax.grid(axis='y', visible=False); ax.grid(axis='x')
ax.legend(title='质量', bbox_to_anchor=(1, 1), loc='upper left') # 图例放到图外右边
for c in ax.containers: # 每种颜色一组,数字写在段中间
ax.bar_label(c, labels=[f'{v:.0%}' if v > 0.08 else '' for v in c.datavalues], # 太窄的段不写
label_type='center', fontsize=8)
# 看法:比各根柱子里同一种颜色的长短;ct.T 转置换个角度看
# —— 想看人数不看占比:去掉 normalize ——
ct_n = pd.crosstab(df2['MSZoning'], tier)
fig, ax = plt.subplots(figsize=(7.5, 3.8))
ct_n.plot(kind='barh', stacked=True, color=[ACCENT, MAIN_L, MAIN_D], width=0.65, ax=ax)
for c in ax.containers:
ax.bar_label(c, labels=[f'{v:.0f}' if v > 0 else '' for v in c.datavalues], label_type='center', fontsize=8) # 0 不写
热力图
相关矩阵与列联表:sns.heatmapmock C2
cols = ['SalePrice', 'OverallQual', 'GrLivArea', 'GarageCars', 'TotalBsmtSF', 'YearBuilt', 'FullBath']
corr = df2[cols].corr().iloc[1:, :-1] # 去掉对角线那一行一列
mask = np.triu(np.ones_like(corr, dtype=bool), k=1) # 遮住右上角,只留下三角
fig, ax = plt.subplots(figsize=(7, 5.6))
sns.heatmap(corr, mask=mask, annot=True, fmt='.2f', cmap=CMAP, center=0, vmin=-1, vmax=1, # 相关系数:−1 到 1
square=True, linewidths=2, linecolor=BG, cbar_kws={'shrink': 0.7},
annot_kws={'fontsize': 9.5}, ax=ax) # 字色不指定:深底自动白字
ax.set_title('数值变量的相关系数')
ax.grid(False); ax.tick_params(axis='x', rotation=30); ax.tick_params(axis='y', rotation=0)
# 别的表照样喂(都只有正数 → 单色渐变):
# sns.heatmap(pd.crosstab(a, b, normalize='index'), annot=True, fmt='.0%', cmap=sns.light_palette(MAIN_D, as_cmap=True))
# sns.heatmap(confusion_matrix(y_test, pred), annot=True, fmt='d', cmap=sns.light_palette(MAIN_D, as_cmap=True)) # 整数 fmt='d'
折线图
时间趋势:ax.plot
yr = df2.groupby('YrSold')['SalePrice'].median()
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(yr.index, yr.values, color=MAIN_D, lw=2, marker='o', ms=7, mfc='white', mew=2) # 白心圆点
ax.set_title('各年成交价中位数')
ax.set_xlabel('成交年份'); ax.set_ylabel('成交价中位数(美元)')
ax.set_xticks(yr.index) # 横轴只标整数年份
ax.margins(y=0.2) # 上下留白,给数字腾地方
ax.yaxis.set_major_formatter(lambda x, _: f'{x/1000:.0f}k')
for x, y in yr.items(): # 每个点上方标数值:往上挪 8 个点,不随刻度变
ax.annotate(f'{y/1000:.0f}k', (x, y), xytext=(0, 8), textcoords='offset points',
ha='center', fontsize=9, color=GRAY)
# 也可以:sns.lineplot(data=df2, x='YrSold', y='SalePrice', estimator='median', errorbar=None, ax=ax)