当前位置: 首页 > news >正文

利用 Python 绘制环形热力图

暑假伊始,Coldrain 参加了学校举办的数模集训,集训的过程中,遇到了需要展示 59 个特征与 15 个指标之间的相关性的情况,在常用的图表不大合适的情况下,学到了一些厉害的图表,但是似乎千篇一律都是用 R 语言、MATLAB 和 SPSS 绘制,Python 代码少之又少,遂作此篇,以为模板。

题目地址:
2012 年全国大学生数学建模竞赛 A 题

网络上找到的环形热力图 be like:

在这里插入图片描述

这种图片究竟是如何绘制出来的呢?

接下来,和小生用 Python 手搓一个吧喵 🐱


1. 嵌套饼图(Nested Pie Charts)

一开始,Coldrain 并无一点头绪,于是在 matplotlib 官网上提供的千奇百怪的图表样例里翻找,找到了一个叫做 Nested Pie Charts 的东西,翻译过来叫做嵌套饼图,官网给的嵌套饼图长这个样子:

在这里插入图片描述

官网给出的第一份案例代码如下:

import numpy as np
import matplotlib.pyplot as pltfig, ax = plt.subplots()size = 0.3
vals = np.array([[60., 32.], [37., 40.], [29., 10.]])tab20c = plt.color_sequences["tab20c"]
outer_colors = [tab20c[i] for i in [0, 4, 8]]
inner_colors = [tab20c[i] for i in [1, 2, 5, 6, 9, 10]]ax.pie(vals.sum(axis=1), radius=1, colors=outer_colors,wedgeprops=dict(width=size, edgecolor='w'))ax.pie(vals.flatten(), radius=1-size, colors=inner_colors,wedgeprops=dict(width=size, edgecolor='w'))ax.set(aspect="equal", title='Pie plot with `ax.pie`')
plt.show()

但是!采用这种方法实现嵌套饼图的效率虽然很高,但是灵活性不高,不便于实现精细设计,于是官方又给出了下面这个新的实现代码:

import numpy as np
import matplotlib.pyplot as pltfig, ax = plt.subplots(subplot_kw=dict(projection="polar"))size = 0.3
vals = np.array([[60., 32.], [37., 40.], [29., 10.]])
# Normalize vals to 2 pi
valsnorm = vals/np.sum(vals)*2*np.pi
# Obtain the ordinates of the bar edges
valsleft = np.cumsum(np.append(0, valsnorm.flatten()[:-1])).reshape(vals.shape)cmap = plt.colormaps["tab20c"]
outer_colors = cmap(np.arange(3)*4)
inner_colors = cmap([1, 2, 5, 6, 9, 10])ax.bar(x=valsleft[:, 0],width=valsnorm.sum(axis=1), bottom=1-size, height=size,color=outer_colors, edgecolor='w', linewidth=1, align="edge")ax.bar(x=valsleft.flatten(),width=valsnorm.flatten(), bottom=1-2*size, height=size,color=inner_colors, edgecolor='w', linewidth=1, align="edge")ax.set(title="Pie plot with `ax.bar` and polar coordinates")
ax.set_axis_off()
plt.show()

现在,我们认真读一下上面的这段代码。

⚠️ Coldrain 觉得有必要认真读一下。

>> 1.1 创建极坐标图
fig, ax = plt.subplots(subplot_kw=dict(projection="polar"))
  • 首先创建一个子图(fig, ax),并指定为极坐标投影 projection="polar"
  • 所有角度以弧度制表示,从 0 开始,逆时针增加。
>> 1.2 设置参数和数据
size = 0.3
vals = np.array([[60., 32.], [37., 40.], [29., 10.]])
  • size:每一个圆环的厚度(即扇形外圈半径长度减去内圈半径长度)
  • vals:二维数组,每一行表示外圈的一个扇区,每行中两个数字表示该扇区内部的两个子分类(用于内圈)

❓ 看到这个 vals 的形状和对应的饼图形状,你想到了什么?没错,似乎可以通过改变 vals 的维度来实现环形热力图的形状!

>> 1.3 角度归一化
valsnorm = vals / np.sum(vals) * 2 * np.pi
  • 先将 vals 所有数值加起来,然后把每个值按比例映射到 [0, 2π2\pi2π] 的弧度范围(也就是一整圈的弧度)
  • 得到每个子块对应的角度宽度
>> 1.4 计算起始角度(边界)
valsleft = np.cumsum(np.append(0, valsnorm.flatten()[:-1])).reshape(vals.shape)
  • valsnorm.flatten() 把二维数组拉成一维
  • np.cumsum(...) 计算角度的累积和,也就是每个条形的起始角度
  • reshape(vals.shape) 把它还原为原来二维结构
>> 1.5 设置颜色
cmap = plt.colormaps["tab20c"]
outer_colors = cmap(np.arange(3)*4)
inner_colors = cmap([1, 2, 5, 6, 9, 10])
  • 使用 tab20c 调色板。
  • outer_colors:每个外圈段使用不同颜色(间隔选择索引 0、4、8)。
  • inner_colors:内圈颜色从调色板中挑选不同颜色索引。

🎨 关于 tab20c 调色板:

tab20cmatplotlib 中内置的分类调色板,共有 20 种颜色,包括 5 个颜色组(每组 4 个颜色)。其构成如下:

颜色组索引范围颜色说明
组 10-3蓝绿色系(蓝、浅蓝、灰蓝等)
组 24-7橙色系(橙、浅橙、灰橙等)
组 38-11红紫色系(红、粉红、灰红等)
组 412-15绿色系(绿、浅绿、灰绿等)
组 516-19灰紫色系(紫灰、浅紫等)
>> 1.6 绘制外圈(大类)
ax.bar(x=valsleft[:, 0],width=valsnorm.sum(axis=1), bottom=1-size, height=size,color=outer_colors, edgecolor='w', linewidth=1, align="edge")
  • 每个外圈段的起始角度valsleft[:, 0]
  • width=valsnorm.sum(axis=1):每个大类的角度宽度是该行两个值之和。
  • bottom=1-size:外圈从半径 0.7 开始(1-0.3=0.7)
  • height=size:厚度是 0.3
  • align="edge":从 x 角度开始绘制
>> 1.7 绘制内圈(子类)
ax.bar(x=valsleft.flatten(),width=valsnorm.flatten(), bottom=1-2*size, height=size,color=inner_colors, edgecolor='w', linewidth=1, align="edge")
  • 每个内圈段的起始角度为展平后的 valsleft
  • 每段的角度宽度来自展平后的 valsnorm
  • bottom=1-2*size:从半径 0.4 开始
  • 用不同颜色表示不同子类
>> 1.8 清理图像
ax.set(title="Pie plot with `ax.bar` and polar coordinates")
ax.set_axis_off()
  • 设置标题
  • 去掉极坐标轴的刻度、边框等

2. 着手绘制环形热力图

由于数据采用的是小生本地的数据,所以这部分代码应该只能用作学习、讲解,如果你想要开袋即食的函数,可以根据下面的代码进行调整(

具体讲解咱们以注释的形式写在代码块里喵:

'''
Part1 导入库
'''
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.cm import get_cmap, ScalarMappable
import pandas as pd
from matplotlib.colors import Normalize, mcolors # 用于标准化颜色映射和自定义 colormap
from mpl_toolkits.axes_grid1.inset_locator import inset_axes   # 在极坐标图中嵌入色条
import matplotlib.font_manager as fm      # 支持中文字体加载'''
Part2 读取数据   
这里小生用的是自己的数据,如果需要参考的话,请务必替
换成自己的数据
(其实这部分不需要关注,直接跳转到 Part3 即可)
'''
red_results = pd.read_excel('red_results.xlsx')
df_doc2_1 = pd.read_excel('doc2.xls')
unprocessed_categories = df_doc2_1.columns.tolist()
categories = [item for item in unprocessed_categories if 'Unnamed' not in item][1:]
color = categories.pop(-1)
for i in ['L', 'a', 'b', 'H', 'c']:categories.append(color+i)df_doc2_2 = pd.read_excel('doc2.xls', sheet_name='葡萄酒')unprocessed_categories = df_doc2_2.columns.tolist()
categories_red = [item for item in unprocessed_categories if 'Unnamed' not in item][1:]
color = categories_red.pop(-1)
for i in ['L', 'a', 'b', 'H', 'c']:categories_red.append(color+i)categories_white = deepcopy(categories_red[1:])unprocessed_grape_features = red_results.iloc[:,0].to_list()
grape_features = []
for i in unprocessed_grape_features:if i not in grape_features:grape_features.append(i)unprocessed_wine_features = red_results.iloc[:,1].to_list()
wine_features = []
for i in unprocessed_wine_features:if i not in wine_features:wine_features.append(i)'''
Part3 将相关系数填入 59*15 大小的列表中
(这里只需要生成你自己的数据即可)
'''
feature_value_map = [[0.0 for i in range(59)] for j in range(15)]for i in range(145):line = red_results.iloc[i,:].to_list()# print(line)n_col = categories.index(line[0])n_row = categories_red.index(line[1])# print(n_row, n_col)feature_value_map[n_row][n_col] = line[2]'''
Part4 图片绘制
'''
def truncate_colormap(cmap, minval=0.2, maxval=0.8, n=256):"""这个函数用来实现 cmap 的截取,具体 cmap 操作可参考matplotlib 官网"""new_cmap = mcolors.LinearSegmentedColormap.from_list(f'trunc({cmap.name}, {minval:.2f}, {maxval:.2f})',cmap(np.linspace(minval, maxval, n)))return new_cmap# 手动添加中文字体(请根据实际路径更改)
font_path = '/usr/share/fonts/noto-cjk/NotoSansCJK-Medium.ttc'
my_font = fm.FontProperties(fname=font_path)# 参数设置
num_rings = 15       # 行数(饼图圈数)
num_segments = 59    # 列数(每圈有多少小格)
ring_width = 0.5 / num_rings  # 控制总半径范围在 [0.5, 1]
angle_width = (1.75 * np.pi) / num_segments      # 这里如果调成 2*np.pi 的话是一个完整的圆
angles = np.linspace(0.5 * np.pi, 2.25 * np.pi, num_segments, endpoint=False)       # 设置起始角度和结束角度# 采用蓝-白-红渐变的配色(请根据个人喜好自行调整)
cmap = get_cmap("RdBu").reversed()      # 这里对 cmap 进行取反操作
cmap = truncate_colormap(cmap, minval=0.1, maxval=0.9)
norm = Normalize(vmin=-1, vmax=1)# 创建画布
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(polar=True))
ax.set_axis_off()    # 将坐标轴隐藏
ax.set_title("酿酒红葡萄理化指标与红葡萄酒理化指标之间的关系热力图", fontsize=14, fontproperties=my_font)       # 设置标题# 绘制所有圈(从外向内)
for i in range(num_rings):bottom = 0.8 - (i + 1) * ring_width   # 每一圈的起始位置height = ring_widthfor j in range(num_segments):color = cmap((feature_value_map[i][j] + 1)/2)   # 颜色映射theta = angles[j]   # 当前段(单元格)中心角度radius = bottom + height / 2      # 填入数值的位置(方格上界和下界中间的位置)# 对每个单元格执行操作ax.bar(x=angles[j],    # 中心角度width=angle_width,     # 扇形的角度宽度bottom=bottom,  # 环的底部半径height=height,  # 环的厚度color=color,    # 采用的颜色edgecolor="black",     # 设置分割线颜色linewidth=0.3,  # 设置分割线宽度align="edge"    # 对齐方式(从角度边缘开始))if np.abs(feature_value_map[i][j]) > 0:ax.text(theta + angle_width / 2,  # 移到扇形中间radius,f"{feature_value_map[i][j]:.2f}",            # 保留两位小数ha='center', va='center',   # 水平/垂直居中(horizontal/verticle)fontsize=4.5,color='black' if abs(feature_value_map[i][j]) < 0.7 else 'white',  # 自适应颜色rotation=0  # 不旋转文本)# 在最外圈插入指标名称
label_radius = (0.3 + num_rings * ring_width + 0.02)  # 最外圈外一点点
indicator_labels = [f'HG{i}' for i in range(1, num_segments + 1)]
for j in range(num_segments):theta = angles[j] + angle_width / 2  # 扇形中间角度label = indicator_labels[j]ax.text(theta,label_radius,label,fontsize=8,ha='center',va='center',rotation=np.degrees(theta - np.pi / 2),rotation_mode='anchor')# 在圆环缺口处添加文字
theta_gap = np.deg2rad(90)  # 可调
ring_width = 0.5 / num_rings
for i in range(num_rings):radius = 0.3 + i * ring_width + ring_width / 2ax.text(theta_gap,radius,f"  HW{15-i}",              # 或者你自定义的 label[i]fontsize=6.5,ha='left',               # 靠左对齐,文字朝外va='center',rotation=0,rotation_mode='anchor',color='black')# 在极坐标图的中间嵌入一个小长条色带(纵向)
cbar_ax = inset_axes(ax,width="4%",   # 相对于父图宽度height="25%",  # 相对于父图高度loc='center'   # 放在图中心
)sm = ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
cbar = plt.colorbar(sm, cax=cbar_ax, orientation='vertical')
cbar.set_label("相关系数 R", fontsize=10, fontproperties=my_font)# 保存图片
plt.tight_layout()
# plt.show()
plt.savefig("my_figure2.png", dpi=300, bbox_inches='tight')

运行之后,得到的效果图如下所示:
在这里插入图片描述

效果图的配色等设计可能有欠缺的地方,但由于时间紧迫,并没有太多时间用于色彩、样式设计…


3. 参考

[1] matplotlib 官网嵌套饼图教学(Nested pie charts)

[2] matplotlib 官网 colormaps 一览

[3] Coldrain 最初遇到的环形热力图

http://www.xdnf.cn/news/1442917.html

相关文章:

  • SuperMap GIS基础产品FAQ集锦(20250819)
  • HTML应用指南:利用POST请求获取全国九号电动车体验店服务店位置信息
  • MyBatis 常见错误与解决方案:从坑中爬出的实战指南
  • 时序数据库选型指南:Apache IoTDB快速部署与实战应用
  • powershell实现,user权限下给软件提取。
  • 数学家破解世界难题——拒绝领奖拒绝百万奖金
  • AV-NeRF、AV-GS、AV-Surf论文解读
  • 基于数据挖掘的当代不孕症医案证治规律研究
  • C# Activator.GetObject 原理与示例:理解.NET Remoting远程调用
  • AI 时代零售数据底座怎么建?首份《零售一体化云数据库白皮书》发布
  • 强化微调:以Swift框架进行GRPO多模态模型强化微调为例
  • 【明道云】[工作表控件5] 手机控件的格式化处理
  • 在麒麟 ARM (aarch64)安装OpenJDK11和elasticsearchkibana
  • 云手机中的三大核心技术主要是指什么?
  • Docker部署Lunalytics开源监控工具
  • 开源检索增强生成(UltraRAG)框架
  • Unity2018版本安卓打包环境配置问题
  • 搞定鸿蒙新手 3 大痛点:页面跳转实现、应用标识修改与 Hyper-V 启动故障排查
  • Elasticsearch(text和keyword)区别分析
  • 【教程】IDEA中导入springboot-maven工程
  • Git 别名:用简短命令大幅提升开发效率
  • 企业级AI应用,Dify集成RAGFlow知识库保姆教程
  • 少儿编程C++快速教程之——1. 基础语法和输入输出
  • 【STL源码剖析】从源码看 deque :拆解双端队列的底层实现与核心逻辑
  • 聚焦岗位能力提升:休闲服务与管理虚拟仿真实训室的实训设计与落地
  • 华为卫星对星引导技术深度解析:原理、实现与开源替代方案
  • 从 MMLU 到 HumanEval:为什么评估大型语言模型(LLM)的基准至关重要?
  • 计算机二级C语言操作题(填空、修改、设计题)——真题库(14)附解析答案
  • 医学图像配准的循环推理机|文献速递-深度学习人工智能医疗图像
  • Aerobits-用于 sUAS 和 UTM/U-Space 的微型 ADS-B 技术(收发器/接收器)和无人机跟踪应答器