Python+Matplotlib实战:如何用玫瑰图优雅展示月度数据(附完整代码)

如果你已经厌倦了千篇一律的柱状图和饼图,想为你的月度报告、销售数据或周期性分析寻找一种既专业又极具视觉冲击力的表达方式,那么玫瑰图绝对值得你投入时间。这种图表,以其形似绽放玫瑰的花瓣而得名,在数据可视化领域有一个更学术的名字——极坐标柱状图。它并非什么新奇玩意儿,早在一个多世纪前,护理学先驱南丁格尔就曾用它来直观展示战争中的伤亡数据,从而说服当局改善医疗条件。今天,借助Python和Matplotlib,我们每个人都能轻松驾驭这种充满历史感与现代美学的可视化工具。

对于数据分析师、市场运营或任何需要呈现周期性数据(如月度KPI、季度营收、小时流量)的朋友来说,玫瑰图提供了一种独特的视角。它最大的魅力在于,能将线性的时间序列(比如12个月)映射到一个圆形空间里,让你一眼就能看出数据的“波峰”与“波谷”出现在哪个周期节点上。想象一下,在年终汇报中展示一张色彩渐变、数据分明的玫瑰图,其带来的专业印象和视觉焦点,远非普通图表可比。本文将从零开始,手把手带你用Matplotlib绘制出杂志级的玫瑰图,并深入探讨其背后的设计哲学、代码细节以及避坑指南。

1. 理解玫瑰图:不仅仅是“好看的饼图”

在动手写代码之前,我们有必要先厘清玫瑰图的本质。很多人第一眼会觉得它像个被拉长的饼图,但这种理解是片面的,甚至可能导致误用。

1.1 核心原理:极坐标系下的柱状图

玫瑰图的数学基础是极坐标系。与我们熟悉的笛卡尔坐标系(用x和y定位点)不同,极坐标系用**角度(θ)半径(r)**来定位。在玫瑰图中:

  • 每个“花瓣”对应的角度是固定的、均匀的。例如,展示12个月的数据,那么每个花瓣就占据360°/12 = 30°的扇形区域。
  • 花瓣的长度(即半径r)由该类别对应的数值大小决定。数值越大,花瓣向外延伸得越长。

这其实就是一个在极坐标中绘制的、宽度相等的柱状图。理解这一点至关重要,因为它直接决定了玫瑰图的适用场景和解读方式。

注意:正因为半径代表数值,而扇形的面积与半径的平方成正比,所以人眼对面积的感知可能会夸大数值之间的差异。这是玫瑰图一个潜在的“误导”风险,需要在设计时通过标注具体数值来弥补。

1.2 适用场景与禁忌

玫瑰图并非万能钥匙,用对了场景是“神器”,用错了就是“花瓶”。

最适合的场景:

  • 周期性数据对比:月度销售额、每周用户活跃度、一天中不同时段的网站流量。玫瑰图的圆形布局能完美呼应时间的循环特性。
  • 展示趋势与模式:当数据存在明显的周期性波动时(如夏季销量高、冬季低),玫瑰图能清晰呈现这种“波形”。
  • 有限数量的类别:通常建议类别数在6到24个之间,比如12个月、24小时、4个季度。太多会导致花瓣拥挤,难以辨认。

需要谨慎或避免的场景:

  • 需要精确比较数值大小:如前所述,人眼不擅长比较扇形面积。如果精确对比是首要需求,请优先考虑普通柱状图。
  • 类别间没有自然顺序或周期关系:例如,比较“北京、上海、广州、深圳”四个城市的数据。虽然可以画,但圆形的布局暗示了一种循环关系,这可能并不符合数据的内在逻辑。
  • 数据量巨大或类别繁多:超过30个类别会让图表变得像一只“刺猬”,失去可读性。

为了更直观地对比,我们来看一个简单的表格:

图表类型 比较维度 优势 劣势 适用玫瑰图替代?
饼图 部分与整体的比例 直观显示占比 难以比较多个饼图;类别不宜过多 ,当你想强调各部分的绝对数值而非占比时。
柱状图 类别间的数值大小 精确比较,易于理解 视觉上可能单调;表现周期性不强 ,当你希望用更紧凑、更具视觉吸引力的方式展示周期性数据时。
雷达图 多个维度的综合表现 展示多维数据轮廓 面积容易造成误解;轴多时混乱 ,雷达图用于多维属性,玫瑰图用于单一维度在不同类别下的数值。

2. 从零搭建:你的第一朵数据“玫瑰”

理论说得再多,不如一行代码。让我们从一个最基础的例子开始,用虚拟的月度网站访问量数据,绘制一朵朴素的玫瑰。

2.1 环境准备与数据生成

首先,确保你的Python环境已安装matplotlibnumpy。如果没有,通过pip安装即可:

pip install matplotlib numpy

我们模拟一组12个月的网站访问量数据(单位:万次)。在实际项目中,这部分数据通常来自你的pandas DataFrame或数据库查询结果。

import matplotlib.pyplot as plt
import numpy as np

# 设置中文字体(可选,如需显示中文标签)
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

# 生成模拟数据:12个月的网站访问量(万次)
months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
# 假设数据有季节性波动:夏季(6-8月)和年底(11-12月)是高峰
np.random.seed(42) # 固定随机种子,确保结果可复现
base_traffic = np.array([45, 48, 52, 55, 60, 85, 90, 88, 65, 58, 75, 80])
fluctuation = np.random.randn(12) * 5 # 加入一些随机波动
traffic_data = np.maximum(base_traffic + fluctuation, 0).round(1) # 确保非负,并保留一位小数

print("月度访问量数据:", dict(zip(months, traffic_data)))

2.2 绘制基础玫瑰图骨架

核心步骤是创建一个极坐标轴(projection='polar'),然后使用ax.bar方法绘制柱体,只不过这里的“x”位置是角度,“高度”是半径。

# 1. 创建图形和极坐标轴
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={'projection': 'polar'})

# 2. 计算每个扇区的角度(均匀分布,从0到2π)
theta = np.linspace(0, 2 * np.pi, len(months), endpoint=False)

# 3. 绘制极坐标柱状图(即玫瑰图)
# width参数控制花瓣的宽度(弧度制),这里设为相邻角度差值的0.8倍,让花瓣间有缝隙
width = (2 * np.pi / len(months)) * 0.8
bars = ax.bar(theta, traffic_data, width=width, bottom=0.0, linewidth=0)

# 4. 设置月份标签
ax.set_xticks(theta)
ax.set_xticklabels(months)

# 5. 设置半径轴(y轴)的标签和范围,让图表更清晰
ax.set_ylim(0, traffic_data.max() * 1.1) # 留出10%的顶部空间
ax.set_yticks(np.arange(0, traffic_data.max()+20, 20)) # 每20万次一个刻度
ax.set_yticklabels([f'{int(y)}万' for y in np.arange(0, traffic_data.max()+20, 20)])

# 6. 添加标题
ax.set_title('月度网站访问量趋势(玫瑰图)', pad=20, size=14, weight='bold')

plt.tight_layout()
plt.show()

运行这段代码,你将得到一朵拥有12个均匀花瓣的“玫瑰”,花瓣的长度直观反映了各月的访问量。但此时图表看起来还有些简陋,标签可能重叠,色彩单一。别急,美化的步骤马上就来。

3. 深度美化:打造杂志级的专业图表

基础图表只解决了“有没有”的问题,而专业图表追求的是“美不美”和“清不清楚”。下面我们从颜色、标签、网格、样式等多个维度进行深度美化。

3.1 色彩映射与渐变效果

颜色是传递信息层次和增强美感的关键。我们可以根据数据值的大小,为每个花瓣映射不同的颜色,使用Matplotlib的色图(colormap)。

# 续接前面的代码,我们重新绘图并应用美化
fig, ax = plt.subplots(figsize=(9, 9), subplot_kw={'projection': 'polar'})

# 使用viridis色图,根据数据值归一化后映射颜色
norm_data = traffic_data / traffic_data.max() # 将数据归一化到[0,1]
colors = plt.cm.viridis(norm_data) # 获取每个数据对应的RGBA颜色

bars = ax.bar(theta, traffic_data, width=width, bottom=0.0, color=colors, edgecolor='white', linewidth=1.2)

# 美化标签:旋转标签使其对齐,并调整字体
ax.set_xticks(theta)
# 将角度从弧度转换为度,并调整标签旋转角度,使其更易读
ax.set_xticklabels(months, fontsize=10)
# 这是一个常见技巧:让标签沿着圆周方向旋转
for label, angle in zip(ax.get_xticklabels(), np.degrees(theta)):
    # 根据角度调整标签的旋转和水平对齐方式
    if 90 < angle <= 270:
        label.set_horizontalalignment('right')
        label.set_rotation(angle-180)
    else:
        label.set_horizontalalignment('left')
        label.set_rotation(angle)

# 美化半径轴:隐藏默认的网格线,使用更简洁的自定义网格
ax.yaxis.grid(True, linestyle='--', linewidth=0.5, alpha=0.7)
ax.xaxis.grid(False) # 隐藏极坐标的角度网格线(通常太密)
ax.spines['polar'].set_visible(False) # 隐藏极坐标的外圈边框

# 在花瓣末端添加具体数值标签,避免面积误解
for bar, value, angle in zip(bars, traffic_data, theta):
    height = bar.get_height()
    # 计算标签位置(角度,半径)
    ax.text(angle, height + traffic_data.max()*0.02, f'{value:.1f}',
            ha='center', va='bottom', fontsize=9, fontweight='bold')

ax.set_title('2023年度网站月度访问量分析', pad=25, size=16, weight='bold', color='#2E4053')

plt.tight_layout()
plt.show()

3.2 高级定制:多数据系列与嵌套玫瑰

有时我们需要对比两年或更多周期的数据。嵌套玫瑰图(或称多系列玫瑰图)是一个很好的选择,但需要特别注意避免视觉混乱。

实现思路:绘制多个玫瑰图系列,通过调整bottom参数将它们错开,或者使用不同的透明度、图案进行区分。下面是一个对比两年数据的简化示例:

# 生成第二年数据
traffic_data_year2 = np.maximum(base_traffic * 1.15 + np.random.randn(12) * 6, 0).round(1)

fig, ax = plt.subplots(figsize=(10, 10), subplot_kw={'projection': 'polar'})

# 绘制第一年数据(内圈)
width_inner = width * 0.9
bars1 = ax.bar(theta, traffic_data, width=width_inner, bottom=0.0,
               color=plt.cm.Blues(0.6), edgecolor='white', linewidth=1, label='2022年')

# 绘制第二年数据(外圈),底部设置为第一年的高度,形成“嵌套”
bars2 = ax.bar(theta, traffic_data_year2, width=width_inner, bottom=traffic_data,
               color=plt.cm.Oranges(0.6), edgecolor='white', linewidth=1, label='2023年')

# 美化设置(同上,略)
# ...
# 添加图例
ax.legend(loc='upper right', bbox_to_anchor=(1.15, 1.1), frameon=False)

plt.show()

提示:嵌套玫瑰图虽然直观,但容易造成内圈数据被外圈遮挡。更清晰的对比方式可能是并排绘制两个独立的玫瑰图,或者使用“偏移”方式(如将第二年数据的花瓣画在第一年的间隙处),但这需要更复杂的角度计算。

4. 避坑指南与性能优化

在实践中,你可能会遇到一些奇怪的问题或希望图表加载更快。这里总结几个常见“坑”及其解决方案。

4.1 常见报错与解决方案

  • 问题:标签重叠或显示不全

    • 原因:默认的标签位置和旋转可能不适合你的屏幕或图形大小。
    • 解决:如3.1节所示,手动调整标签的旋转角度(set_rotation)和对齐方式(set_horizontalalignment)。也可以尝试调整图形大小(figsize)或标签字体大小(fontsize)。
  • 问题:AttributeError: ‘PolarAxesSubplot‘ object has no attribute ‘bar‘

    • 原因:这是一个笔误。在极坐标轴中,绘制柱状图的方法就是ax.bar,与直角坐标系相同。请检查拼写,确保不是ax.barh(水平柱状图)或其他。
  • 问题:颜色映射失效,所有柱子一个颜色

    • 原因ax.barcolor参数传入了一个单一颜色值,而不是一个颜色列表。
    • 解决:确保你传入的color是一个长度与数据相等的列表。例如:color=[plt.cm.viridis(d/max_data) for d in data]
  • 问题:起始角度不是12点钟方向

    • 原因:极坐标的0度默认是“东”(右侧)方向。
    • 解决:在创建图形后,设置ax.set_theta_zero_location('N') 可以将0度设置为“北”(上方)。使用ax.set_theta_direction(-1)可以设置角度增长方向为顺时针。

4.2 性能与导出优化

当数据量较大或需要生成大量图表时,性能变得重要。

  1. 简化图形元素:对于纯用于分析的图表,可以关闭抗锯齿、简化网格线、减少刻度数量来提升渲染速度。

    plt.rcParams['path.simplify'] = True
    plt.rcParams['path.simplify_threshold'] = 1.0
    ax.yaxis.grid(False) # 关闭网格以加速
    
  2. 高质量导出:用于印刷或海报展示时,需要高DPI的矢量或位图。

    # 保存为SVG矢量图,无限缩放不失真
    plt.savefig('rose_chart.svg', format='svg', dpi=300, bbox_inches='tight')
    # 保存为高分辨率PNG
    plt.savefig('rose_chart.png', format='png', dpi=300, bbox_inches='tight', facecolor='white')
    

    bbox_inches='tight' 参数可以自动裁剪掉图形周围多余的空白区域。

  3. 使用面向对象接口:本文全程使用的fig, ax = plt.subplots()是Matplotlib的面向对象接口,比纯pyplot接口(如plt.bar)更清晰、更易于进行复杂定制,也便于将绘图逻辑封装成函数。

4.3 封装成可复用的函数

将上述所有步骤封装成一个函数,可以极大提高工作效率。这里提供一个高度可配置的玫瑰图绘制函数模板:

def plot_rose_chart(data, labels, title='Rose Chart', cmap_name='viridis',
                    show_values=True, value_format='.1f', figsize=(8,8)):
    """
    绘制美化后的玫瑰图。

    参数:
    data: 数值列表或数组。
    labels: 与data等长的标签列表。
    title: 图表标题。
    cmap_name: Matplotlib色图名称。
    show_values: 是否在花瓣末端显示数值。
    value_format: 数值显示格式。
    figsize: 图形大小。
    """
    fig, ax = plt.subplots(figsize=figsize, subplot_kw={'projection': 'polar'})
    theta = np.linspace(0, 2 * np.pi, len(data), endpoint=False)
    width = (2 * np.pi / len(data)) * 0.8

    # 颜色映射
    cmap = plt.cm.get_cmap(cmap_name)
    norm_data = data / np.max(data)
    colors = cmap(norm_data)

    bars = ax.bar(theta, data, width=width, color=colors, edgecolor='white', linewidth=1)

    # 设置标签
    ax.set_xticks(theta)
    ax.set_xticklabels(labels)
    # ... (此处可添加3.1节中的标签旋转美化代码)

    # 设置半径轴
    ax.set_ylim(0, np.max(data) * 1.15)
    ax.spines['polar'].set_visible(False)

    # 添加数值标签
    if show_values:
        for bar, value, angle in zip(bars, data, theta):
            height = bar.get_height()
            ax.text(angle, height + np.max(data)*0.02, f'{value:{value_format}}',
                    ha='center', va='bottom', fontsize=9)

    ax.set_title(title, pad=20, size=14, weight='bold')
    plt.tight_layout()
    return fig, ax

# 使用函数
fig, ax = plot_rose_chart(traffic_data, months, title='月度访问量玫瑰图', cmap_name='plasma')
plt.show()

掌握了从原理到实践,从基础到美化的全过程后,玫瑰图就不再是一个神秘的“高级图表”,而是你数据可视化工具箱中一件得心应手的利器。关键在于理解其极坐标的本质,并始终记住:可视化是为了更有效地传达信息,美观性应服务于这个目的,而不是掩盖它。下次当你手头有一组月度数据时,不妨暂时放下柱状图,试试用玫瑰图给你的观众带来一点视觉上的惊喜。在实际项目中,我习惯先用这个封装函数快速出图查看效果,然后再根据具体的报告或演示场景,微调颜色、标签和样式,整个过程非常流畅。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐