做数据分析的朋友们应该都遇到过这样的尴尬场景:当你精心准备了一组数据想要用柱状图展示时,突然发现其中一两个数值比其他数据高出几十倍甚至上百倍。这时候直接画出来的图表要么小数值被压缩成一条线,要么大数值直接顶破天花板,根本看不出数据之间的对比关系。
我去年在做电商销售分析时就踩过这个坑。当时想对比不同商品的日销量,结果爆款商品一天卖出了5000多件,而普通商品平均只有几十件。用普通柱状图展示时,其他商品的柱子几乎看不见,整个图表完全失去了分析价值。这时候就需要坐标轴截断(Broken Axis)技术来救场了。
坐标轴截断的核心思路很简单:把y轴分成两个不同量级的区间,中间用断裂符号表示省略的部分。这样既能展示小数值的细节差异,又能完整呈现大数值的情况。就像我们用望远镜看风景时,既需要广角镜头看全景,又需要长焦镜头看细节。
Matplotlib实现坐标轴截断的核心技巧是创建两个重叠的坐标系。下面这段代码展示了具体做法:
python复制import matplotlib.pyplot as plt
import numpy as np
# 创建画布
fig = plt.figure(figsize=(10,6))
# 添加两个坐标系,注意位置参数要重叠
ax1 = fig.add_axes([0.15,0.15,0.8,0.35]) # 下部分坐标系
ax2 = fig.add_axes([0.15,0.55,0.8,0.35]) # 上部分坐标系
这里的关键点在于:
接下来需要为两个坐标系设置不同的y轴范围,这是实现截断效果的关键:
python复制# 生成示例数据
np.random.seed(42)
y = np.random.randint(1,10,10)
y[3] = 150 # 故意设置一个异常大值
y[7] = 180 # 另一个异常大值
# 设置不同的y轴范围
ax1.set_ylim(0, 15) # 下部分显示小数值
ax2.set_ylim(100, 200) # 上部分显示大数值
实测发现,上下部分的y轴范围最好保留20%左右的重叠区,这样断裂处的过渡会更自然。比如这里下部分到15,上部分从100开始,中间85的间隔用断裂符号表示。
为了让读者直观理解这里存在截断,我们需要添加视觉标记。最常用的方法是画两条斜线:
python复制def add_break_symbol(ax1, ax2):
d = 0.8 # 斜线倾斜度
kwargs = dict(
marker=[(-1, -d), (1, d)], # 定义斜线样式
markersize=12,
linestyle="none",
color="k",
mec="k",
mew=1,
clip_on=False
)
# 在上坐标系底部画斜线
ax2.plot([0, 1], [0, 0], transform=ax2.transAxes, **kwargs)
# 在下坐标系顶部画斜线
ax1.plot([0, 1], [1, 1], transform=ax1.transAxes, **kwargs)
这个函数会在两个坐标系的连接处添加对称的斜线标记,看起来就像被撕开的纸张边缘,非常直观。
为了让两个坐标系视觉上更像一个整体,我们需要隐藏多余的边框:
python复制# 隐藏上坐标系的下边框
ax2.spines['bottom'].set_visible(False)
# 隐藏下坐标系的上边框
ax1.spines['top'].set_visible(False)
# 隐藏上坐标系的x轴
ax2.set_xticks([])
这样处理后,两个坐标系就完美衔接成一个整体了。我在实际项目中发现,如果保留这些边框线,图表会显得很杂乱,读者容易误以为是两个独立的图表。
使用坐标轴截断时,数据标注要特别注意:
python复制# 标注异常值示例
for i, val in enumerate(y):
if val > 100:
ax2.text(i+1, val, str(val), ha='center', va='bottom')
else:
ax1.text(i+1, val, str(val), ha='center', va='bottom')
根据我的经验,坐标轴截断最适合以下场景:
但不适合:
最近帮客户分析广告点击率数据时,发现有些异常高的点击率其实是爬虫行为。用坐标轴截断可以清晰区分正常用户和爬虫的点击模式,效果非常好。
下面是一个可直接运行的完整示例,包含了所有最佳实践:
python复制import matplotlib.pyplot as plt
import numpy as np
# 创建画布
plt.style.use('seaborn')
fig = plt.figure(figsize=(10, 8))
# 设置两个重叠的坐标系
ax1 = fig.add_axes([0.15, 0.15, 0.7, 0.4]) # 下部分
ax2 = fig.add_axes([0.15, 0.55, 0.7, 0.4]) # 上部分
# 生成数据
np.random.seed(2023)
categories = [f"产品{i}" for i in range(1,11)]
values = np.random.randint(5, 20, 10)
values[2] = 150 # 异常值
values[6] = 180 # 异常值
# 绘制柱状图
ax1.bar(categories, values, color='skyblue', alpha=0.7)
ax2.bar(categories, values, color='skyblue', alpha=0.7)
# 设置y轴范围
ax1.set_ylim(0, 25)
ax2.set_ylim(100, 200)
# 添加断裂标记
def add_break_markers(ax1, ax2):
d = 0.7
kwargs = dict(
marker=[(-1, -d), (1, d)],
markersize=15,
linestyle="none",
color="gray",
mec="gray",
mew=1.5,
clip_on=False
)
ax2.plot([0, 1], [0, 0], transform=ax2.transAxes, **kwargs)
ax1.plot([0, 1], [1, 1], transform=ax1.transAxes, **kwargs)
add_break_markers(ax1, ax2)
# 美化图表
ax2.spines['bottom'].set_visible(False)
ax1.spines['top'].set_visible(False)
ax2.set_xticks([])
ax1.set_xlabel("产品类别", fontsize=12)
ax1.set_ylabel("正常销量", fontsize=12)
ax2.set_ylabel("异常销量", fontsize=12)
# 添加数据标签
for i, (cat, val) in enumerate(zip(categories, values)):
if val > 100:
ax2.text(i, val+5, str(val), ha='center', fontsize=10)
else:
ax1.text(i, val+0.5, str(val), ha='center', fontsize=10)
# 添加标题和说明
plt.suptitle("产品销量分析(含异常值)", y=0.95, fontsize=14)
ax1.text(5, -8, "注:灰色斜线表示省略了25-100的区间",
ha='center', fontsize=10, color='gray')
plt.tight_layout()
plt.show()
这段代码生成的图表既美观又专业,直接复制到你的项目中就能用。我特别添加了注释说明和异常值标注,这在业务汇报中特别有用。