当前位置:首页>python>Python 绘制极坐标分层热力玫瑰图(含代码)

Python 绘制极坐标分层热力玫瑰图(含代码)

  • 2026-09-28 02:41:28
Python 绘制极坐标分层热力玫瑰图(含代码)
案例代码见文末,感谢您关注PFC小姐姐,麻烦您多多对推文点赞、收藏及转发,并衷心希望您多多指教🙏,帮助PFC小姐姐进步提升。

引言

现在很多科学数据并不是简单地沿某一个方向变化,而往往同时具有明显的方向性、层级性和强弱差异。传统的柱状玫瑰图更适合表达不同方向上的数量分布,但一旦数据还包含不同圈层、不同等级或者不同距离带的信息,普通玫瑰图就显得不够用了。极坐标分层热力玫瑰图的价值就在于,它把角度方向、径向层级和颜色强度三类信息整合进同一张图中,使原本需要拆成多张图展示的内容能够在一个统一的极坐标框架下被表达出来。这样绘制出来的图,不仅保留了玫瑰图天然的方向感,还能进一步展示不同方向上的强信号到底集中在哪些层级,从而让图形本身具备更强的结构表达能力。下面这组示例图基于同一份模拟数据,分别展示基础版和进阶版两种极坐标分层热力玫瑰图的绘制方式,用来说明如何在保证图面美观的同时提升单图的信息密度。

1、基础版极坐标分层热力玫瑰图

这张图主要展示这种图的基本构成方式。图中圆周方向表示方位角,从 0° 到 360° 对应不同方向;半径方向表示不同层级,不同圈层从内向外依次展开;颜色则表示每一个“方向—层级”组合位置上的强度大小。这样一来,读图时就不再只是看某个方向上是否有高值,而是可以进一步观察这些高值究竟集中在内层、中层还是外层。与普通玫瑰图相比,这一版最大的特点在于它将“方向分布”和“层级分布”叠加在同一张图中,使图形不仅能表达主方向,还能反映不同方向上的内部结构差异。

图2、进阶版极坐标分层热力玫瑰图

在基础版的基础上进一步强化了结构表达和视觉层次。首先,背景改为深色风格,使热力分层的高值区域在视觉上更加突出;其次,在热力填色之外叠加了白色等值线,用来刻画强度变化的内部轮廓,使热点区的边界和梯度更加清楚。同时,图外侧还增加了一条方向综合强度曲线,用来表示各个方向上的总体响应水平,从而把“某个方向内部哪些层级强”与“这个方向整体强不强”这两个问题放到同一张图中一起表达。图中对主方向峰值进行了单独标注,使读者能够快速识别最值得关注的几个方向。相比基础版,这一版本已经不只是简单的热力玫瑰图,而是一种兼具方向判别、层级分布和热点识别能力的高信息密度可视化图形。

    具体Python如下:

    import osimport numpy as npimport matplotlib.pyplot as pltfrom matplotlib.colors import LinearSegmentedColormap# 0. 输出目录OUTDIR = "polar_rose_output"os.makedirs(OUTDIR, exist_ok=True)# 1. 构造模拟数据def ang_diff_deg(a, b):    """计算角度差(单位:度),结果限制在 [-180, 180]"""    return (a - b + 180) % 360 - 180def gaussian_ang(theta_deg, mu_deg, sigma_deg):    d = ang_diff_deg(theta_deg, mu_deg)    return np.exp(-0.5 * (d / sigma_deg) ** 2)def generate_polar_layer_data(n_theta=180, n_r=10, seed=2026):    """    构造“方向-层级-强度”三维数据    theta: 方向    r: 层级    Z: 每个方向-层级组合对应的强度    """    rng = np.random.default_rng(seed)    theta_centers = np.linspace(0, 360, n_theta, endpoint=False)   # degree    r_centers = np.arange(1, n_r + 1)    T, R = np.meshgrid(theta_centers, r_centers)    # 多个方向主峰    peak1 = 1.8 * gaussian_ang(T, 35, 18) * (0.65 + 0.35 * np.exp(-((R - 3.0) / 2.2) ** 2))    peak2 = 1.5 * gaussian_ang(T, 120, 26) * (0.45 + 0.55 * np.exp(-((R - 6.5) / 1.8) ** 2))    peak3 = 1.9 * gaussian_ang(T, 235, 20) * (0.35 + 0.75 * np.exp(-((R - 8.0) / 1.5) ** 2))    peak4 = 1.2 * gaussian_ang(T, 310, 15) * (0.60 + 0.40 * np.exp(-((R - 4.0) / 1.7) ** 2))    # 条带型起伏    band = 0.35 * (1 + np.cos(np.deg2rad(2.2 * T - 18 * R)))    wave = 0.25 * (1 + np.sin(np.deg2rad(1.3 * T + 25 * R)))    # 径向趋势    radial_trend = 0.15 + 0.08 * R + 0.12 * np.exp(-((R - 5.5) / 3.2) ** 2)    # 局部热点    hotspot = (        0.9 * gaussian_ang(T, 70, 8) * np.exp(-((R - 8.5) / 0.9) ** 2) +        0.8 * gaussian_ang(T, 205, 10) * np.exp(-((R - 2.5) / 0.8) ** 2) +        1.1 * gaussian_ang(T, 280, 7) * np.exp(-((R - 9.0) / 0.7) ** 2)    )    noise = 0.08 * rng.normal(size=(n_r, n_theta))    Z = peak1 + peak2 + peak3 + peak4 + band + wave + radial_trend + hotspot + noise    Z = np.clip(Z, 0, None)    return theta_centers, r_centers, Z# 2. 平滑函数def circular_smooth_along_theta(Z, kernel_size=11, sigma=2.8):    """    对角度方向做环形平滑    Z shape = (n_r, n_theta)    """    x = np.arange(kernel_size) - kernel_size // 2    kernel = np.exp(-0.5 * (x / sigma) ** 2)    kernel /= kernel.sum()    pad = kernel_size // 2    Z_pad = np.concatenate([Z[:, -pad:], Z, Z[:, :pad]], axis=1)    out = np.zeros_like(Z)    for i in range(Z.shape[0]):        out[i] = np.convolve(Z_pad[i], kernel, mode="valid")    return outdef smooth_along_radius(Z, kernel_size=5, sigma=1.1):    """    对径向做一维平滑    """    x = np.arange(kernel_size) - kernel_size // 2    kernel = np.exp(-0.5 * (x / sigma) ** 2)    kernel /= kernel.sum()    pad = kernel_size // 2    Z_pad = np.pad(Z, ((pad, pad), (0, 0)), mode="edge")    out = np.zeros_like(Z)    for j in range(Z.shape[1]):        out[:, j] = np.convolve(Z_pad[:, j], kernel, mode="valid")    return out# 3. 找主方向峰值def find_top_direction_peaks(theta_deg, profile, min_sep_deg=22, top_k=3):    """    在方向强度曲线上找几个主峰,避免彼此过近    """    idx_sorted = np.argsort(profile)[::-1]    chosen = []    for idx in idx_sorted:        ang = theta_deg[idx]        good = True        for c in chosen:            if abs(ang_diff_deg(ang, theta_deg[c])) < min_sep_deg:                good = False                break        if good:            chosen.append(idx)        if len(chosen) >= top_k:            break    return chosen# 4. 作图:基础版def plot_basic(theta_deg, r_centers, Z, out_png):    fig = plt.figure(figsize=(9, 9), dpi=220)    ax = plt.subplot(111, projection="polar")    # 构造边界    theta_edges = np.linspace(0, 2 * np.pi, len(theta_deg) + 1)    r_edges = np.arange(0.5, len(r_centers) + 1.5, 1.0)    mesh = ax.pcolormesh(        theta_edges,        r_edges,        Z,        cmap="YlOrRd",        shading="auto"    )    ax.set_theta_zero_location("N")    ax.set_theta_direction(-1)    ax.set_rlim(0.5, len(r_centers) + 0.5)    ax.set_thetagrids(        np.arange(0, 360, 30),        labels=[f"{d}°" for d in range(0, 360, 30)],        fontsize=10    )    ax.set_rgrids(        np.arange(1, len(r_centers) + 1),        angle=22.5,        labels=[f"L{i}" for i in range(1, len(r_centers) + 1)],        fontsize=9    )    ax.grid(alpha=0.35, linestyle="--", linewidth=0.7)    ax.set_title("Basic Polar Layered Heat Rose", fontsize=18, pad=24)    cbar = plt.colorbar(mesh, ax=ax, pad=0.10, shrink=0.88)    cbar.set_label("Intensity", fontsize=12)    plt.tight_layout()    plt.savefig(out_png, bbox_inches="tight", facecolor="white")# 5. 作图:进阶版def plot_advanced(theta_deg, r_centers, Z, out_png):    # 平滑后的热力场    Zs = circular_smooth_along_theta(Z, kernel_size=13, sigma=3.0)    Zs = smooth_along_radius(Zs, kernel_size=5, sigma=1.0)    # 方向积分强度,用于外环曲线    dir_profile = Zs.sum(axis=0)    # 角度转弧度    theta = np.deg2rad(theta_deg)    theta_edges = np.linspace(0, 2 * np.pi, len(theta_deg) + 1)    r_edges = np.arange(0.5, len(r_centers) + 1.5, 1.0)    # 自定义漂亮一点的配色    colors = ["#14001f", "#3b0f70", "#8c2981", "#de4968", "#fe9f6d", "#fcfdbf"]    cmap_adv = LinearSegmentedColormap.from_list("custom_rose", colors, N=256)    fig = plt.figure(figsize=(10, 10), dpi=240, facecolor="#0b0f1a")    ax = plt.subplot(111, projection="polar", facecolor="#0b0f1a")    # 主热力图    mesh = ax.pcolormesh(        theta_edges,        r_edges,        Zs,        cmap=cmap_adv,        shading="auto"    )    # 叠加等值线    tt, rr = np.meshgrid(theta, r_centers)    levels = np.linspace(np.percentile(Zs, 50), np.percentile(Zs, 96), 7)    cs = ax.contour(        tt, rr, Zs,        levels=levels,        colors="white",        linewidths=0.75,        alpha=0.62    )    # 外环方向强度曲线    prof_norm = (dir_profile - dir_profile.min()) / (dir_profile.max() - dir_profile.min() + 1e-12)    outer_base = len(r_centers) + 0.65    outer_curve = outer_base + 1.15 * prof_norm    ax.plot(theta, outer_curve, color="#8ef9f3", lw=2.4, alpha=0.98)    ax.fill_between(theta, outer_base, outer_curve, color="#38d9d9", alpha=0.16)    # 标出主方向峰值    peak_ids = find_top_direction_peaks(theta_deg, dir_profile, min_sep_deg=26, top_k=3)    for i, idx in enumerate(peak_ids, 1):        ang = theta[idx]        rr_peak = outer_curve[idx]        deg = theta_deg[idx]        ax.scatter(            [ang], [rr_peak],            s=55, c="#ffe66d",            edgecolors="white", linewidths=0.8, zorder=8        )        r_text = rr_peak + 1.45        if 0 <= deg < 90:            ha = "left"        elif 90 <= deg < 180:            ha = "left"        elif 180 <= deg < 270:            ha = "right"        else:            ha = "right"        ax.annotate(            f"Peak {i}\n{deg:.0f}°",            xy=(ang, rr_peak),            xytext=(ang, r_text),            color="white",            fontsize=6,            ha=ha,            va="center",            arrowprops=dict(                arrowstyle="->",                color="white",                lw=0.9,                alpha=0.9,                shrinkA=0,                shrinkB=4            )        )    th_dense = np.linspace(0, 2*np.pi, 500)    ax.plot(th_dense, np.full_like(th_dense, len(r_centers) + 0.5),            color="white", lw=1.1, alpha=0.45)    ax.set_theta_zero_location("N")    ax.set_theta_direction(-1)    ax.set_rlim(0.5, len(r_centers) + 2.2)    ax.set_thetagrids(        np.arange(0, 360, 30),        labels=[f"{d}°" for d in range(0, 360, 30)],        fontsize=10,        color="#d9e2ec"    )    ax.set_rgrids(        np.arange(1, len(r_centers) + 1),        angle=18,        labels=[f"L{i}" for i in range(1, len(r_centers) + 1)],        fontsize=9,        color="#cbd5e1"    )    ax.grid(color="white", alpha=0.18, linestyle="--", linewidth=0.75)    ax.spines["polar"].set_color((1, 1, 1, 0.25))    ax.set_title(        "Advanced Polar Layered Heat Rose",        fontsize=20,        color="white",        pad=28    )    cbar = plt.colorbar(mesh, ax=ax, pad=0.10, shrink=0.6,fraction=0.035 )    cbar.set_label("Intensity", fontsize=12, color="white")    cbar.ax.yaxis.set_tick_params(color="white")    plt.setp(cbar.ax.get_yticklabels(), color="white")    plt.tight_layout()    plt.savefig(out_png, bbox_inches="tight", facecolor=fig.get_facecolor())def main():    theta_deg, r_centers, Z = generate_polar_layer_data()    basic_png = os.path.join(OUTDIR, "polar_rose_basic.png")    adv_png = os.path.join(OUTDIR, "polar_rose_advanced.png")    plot_basic(theta_deg, r_centers, Z, basic_png)    plot_advanced(theta_deg, r_centers, Z, adv_png)if __name__ == "__main__":    main()

    特别声明:

    以上代码与文案均为网上资料整合而成,仅供广大同行们参考学习,如有侵权请联系删除。

    如有其他需要,欢迎关注我的咸鱼号:pfc小姐姐

    最新文章

    随机文章