如何使用Python和Matplotlib创建美丽的极性直方图
#教程 #python #datascience #datavisualization

嗨,欢迎来到这个python + matplotlib教程,我将向您展示如何创建上面看到的美丽的极性直方图。

极性直方图对于标准条形图有太多值时,极好的直方图非常好。每个条形在中间变细的圆形形状使我们能够将更多信息塞入同一区域。

我正在使用来自World Happiness Report的数据以及有关World Bank的收入水平的信息。

Screenshot by the author

您可以在此GitHub repository中找到我使用的代码和数据。

让我们开始。


步骤1:准备

让我们从一些预言开始。

导入库

我们只需要大家熟悉的标准Python库即可。 PIL不是强制性的,但这是我首选的选择,可以选择添加标志时要做的图像。

import math
import numpy as np
import pandas as pd

import seaborn as sns
import matplotlib.pyplot as plt

from PIL import Image
from matplotlib.lines import Line2D
from matplotlib.patches import Wedge
from matplotlib.offsetbox import OffsetImage, AnnotationBbox

唯一突出的是最后几个特定的​​matplotlib导入。我将在教程中介绍这些组件。

我使用熊猫加载数据。

df = pd.read_csv("./hapiness_report_2022.csv", index_col=None)
df = df.sort_values("score").reset_index(drop=True)

海洋风格的环境

接下来,我使用Seaborn来定义背景,文本颜色和字体来创建基本样式。

font_family = "PT Mono"
background_color = "#F8F1F1"
text_color = "#040303"

sns.set_style({
    "axes.facecolor": background_color,
    "figure.facecolor": background_color,
    "font.family": font_family,
    "text.color": text_color,
})

set_style还有更多参数,但是这四个是我在本教程中唯一需要的参数。

我使用诸如ColorhuntCoolors之类的网站来创建美丽的调色板。

全局设置

我还添加了一些全局设置来控制一般外观。前四个定义了直方图中楔形的范围,大小和宽度。

START_ANGLE = 100 # At what angle to start drawing the first wedge
END_ANGLE = 450 # At what angle to finish drawing the last wedge
SIZE = (END_ANGLE - START_ANGLE) / len(df) # The size of each wedge
PAD = 0.2 * SIZE # The padding between wedges

INNER_PADDING = 2 * df.score.min()
LIMIT = (INNER_PADDING + df.score.max()) * 1.3 # Limit of the axes

内部填充物在每个楔子的起点和开始之间创造距离。它在图表的中间打开一个空间,我可以添加标题。

样板代码

作为一名软件工程师,我努力编写可重复使用的代码,并且当我从事数据可视化时一样。

这就是为什么我总是从创建几行的样板代码开始,可以通过可重复使用的功能扩展。

fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(30, 30))
ax.set(xlim=(-LIMIT, LIMIT), ylim=(-LIMIT, LIMIT))

for i, row in df.iterrows():
    bar_length = row.score
    name = row.country
    length = bar_length + INNER_PADDING
    start = 100 + i*SIZE + PAD
    end = 100 + (i+1)*SIZE
    angle = (end + start) / 2

    # Create variables here

    # Add wedge functions here

# Add general functions here

plt.axis("off")
plt.tight_layout()
plt.show()

在本教程的其余部分中,我将在三个注释之一下创建并添加函数和变量。


步骤2:绘制楔子

为了在matplotlib中获得更大的功能,它有助于使用基础组件而不是内置图形功能。

画一个楔子

例如,您可以使用plt.patches.Wedge()来绘制各个零件。

这就是为什么我创建以下功能,该功能根据角度,长度,条长和颜色绘制楔子。

def draw_wedge(ax, start_angle, end_angle, length, bar_length, color):
    ax.add_artist(
        Wedge((0, 0),
            length, start_angle, end_angle,
            color=color, width=bar_length
        )
    )

在样板代码中,我在此处的添加函数下添加draw_wedge(),如下所示。

bar_length = row.score
length = bar_length # + INNER_PADDING
start = 100 + i*SIZE + PAD
end = 100 + (i+1)*SIZE
.
.
.

# Add functions here    
draw_wedge(ax, start, end, length, bar_length, "#000")

我使用row.score来定义bar_length,以便杆的可见部分相互准确。
现在,我删除了INNER_PADDING,向您展示了它的作用。

运行代码时,我将获得以下图。

Initial chart with wedges

您可以看到,我们还有很长的路要走,直到我们得到类似于您一开始看到的极性直方图的东西,但至少我们设法绘制了楔子。

我们在中间有很多视觉伪像,所以让我们的uncomment INNER_PADDING

这是我们得到的。

Chart with inner padding

好多了。

添加颜色

接下来,我有一个简单的颜色功能,可以根据该国家的收入水平来决定每个楔子的颜色。

def color(income_group):
    if income_group == "High income":
        return "#468FA8"
    elif income_group == "Lower middle income":
        return "#E5625E"
    elif income_group == "Upper middle income":
        return "#62466B"
    elif income_group == "Low income":
        return "#6B0F1A"
    else:
        return "#909090"

我将该函数用作draw_wgeded函数的输入。

# Add functions here    
draw_wedge(ax, start, end, length, bar_length, color(row.income))

这是结果。

Initial chart with wedges and color

使用INNER_PADDINGcolor(),没有奇怪的文物。是时候添加信息来解释我们看的内容。


步骤3:添加标签

让S极直方图中的每个条添加标签。我希望每个酒吧都显示国家的旗帜,名称和幸福分数。

定义位置

当您将标志和文本添加到Matplotlib的图表中时,您需要计算正确的位置。

这通常很棘手,尤其是当您像极地直方图中的异常形状一样。

下面的函数采用楔形的长度及其角度来计算位置。填充将位置从栏上推开以增加一些视觉空间。

def get_xy_with_padding(length, angle, padding):
    x = math.cos(math.radians(angle)) * (length + padding)
    y = math.sin(math.radians(angle)) * (length + padding)
    return x, y

我们可以将此功能用于标志和文本。

添加标志

对于标志,我使用这些来自弗拉提顿的rounded ones

他们需要许可证,因此,不幸的是,我可以分享它们,但是您可以在其他地方找到类似的标志。

在这里,我的功能是在图表中添加标志。它采用该位置,国家的名称(对应于正确文件的名称),Zoom和旋转。

def add_flag(ax, x, y, name, zoom, rotation):
    flag = Image.open("<location>/{}.png".format(name.lower()))
    flag = flag.rotate(rotation if rotation > 270 else rotation - 180)
    im = OffsetImage(flag, zoom=zoom, interpolation="lanczos", resample=True, visible=True)

    ax.add_artist(AnnotationBbox(
        im, (x, y), frameon=False,
        xycoords="data",
    ))

i如果角度超过270度,则更改标志的旋转方式。当我们开始在图表的右侧添加条时,就会发生这种情况。那时,标志在文本的左侧,改变旋转使阅读更自然。

现在,我们可以计算角度,使用get_xy_with_padding()并将标志放在图表上。

bar_length = row.score
length = bar_length + INNER_PADDING
start = START_ANGLE + i*SIZE + PAD
end = START_ANGLE + (i+1)*SIZE

# Add variables here
angle = (end + start) / 2
flag_zoom = 0.004 * length
flag_x, flag_y = get_xy_with_padding(length, angle, 0.1 * length)

# Add functions here
...
add_flag(ax, flag_x, flag_y, row.country, flag_zoom, angle)

flag_zoom参数决定标志的大小并取决于得分。如果一个国家的分数较低,那么旗帜的空间就会少,我们需要使其更小一点。

Polar histogram with flags

很棒。

添加国家名称和分数

要添加该国的名称和分数,我写了以下功能。

与标志一样,如果角度超过270度,我会更改旋转。否则,文本将颠倒。

def add_text(ax, x, y, country, score, angle):
    if angle < 270:
        text = "{} ({})".format(country, score)
        ax.text(x, y, text, fontsize=13, rotation=angle-180, ha="right", va="center", rotation_mode="anchor")
    else:
        text = "({}) {}".format(score, country)
        ax.text(x, y, text, fontsize=13, rotation=angle, ha="left", va="center", rotation_mode="anchor")

我们以与标志相同的方式计算文本的位置。

唯一的区别是我们添加了更多的填充物,因为我们希望它从楔子中进一步。

bar_length = row.score
length = bar_length + INNER_PADDING
start = START_ANGLE + i*SIZE + PAD
end = START_ANGLE + (i+1)*SIZE

# Add variables here
angle = (end + start) / 2
flag_zoom = 0.004 * length
flag_x, flag_y = get_xy_with_padding(length, angle, 0.1 * length)
text_x, text_y = get_xy_with_padding(length, angle, 16*flag_zoom)

# Add functions here
...
add_flag(ax, flag_x, flag_y, row.country, flag_zoom, angle)
add_text(ax, text_x, text_y, row.country, bar_length, angle)

现在我们有以下图表,并且开始看起来好多了。

Polar histogram with flags and text

现在该告诉用户他们在看什么。


步骤4:添加信息

我们添加了所有数据。是时候通过添加有用的信息和指导来使图表可读。

图参考线

出色的视觉辅助器类型是参考线。它们在这里和标准条形图一样工作。

这个想法是在特定分数上绘制一条线,这间接帮助我们比较不同的国家。

这是我绘制参考线的功能。我要重复使用draw_wedge()函数以从0到360度绘制一个楔子。

def draw_reference_line(ax, point, size, padding, fontsize=18):
    draw_wedge(ax, 0, 360, point+padding+size/2, size, background_color)
    ax.text(-0.6, padding + point, point, va="center", rotation=1, fontsize=fontsize)

我运行一次函数以供每个分数绘制多个参考线。

# Add general functions here
draw_reference_line(ax, 2.0, 0.05, INNER_PADDING)
draw_reference_line(ax, 4.0, 0.05, INNER_PADDING)
draw_reference_line(ax, 6.0, 0.05, INNER_PADDING)

这是结果。

Polar histogram with reference lines

这有很大的不同。

添加标题

图表中心的间隙的目的是为标题创建一个自然的位置。在中心拥有标题是不寻常的,可以立即捕捉观众的兴趣。

添加标题的代码是标准matplotlib功能。

# Add general functions here
...
plt.title(
  "World Happiness Report 2022".replace(" ", "\n"), 
  x=0.5, y=0.5, va="center", ha="center", 
  fontsize=64, linespacing=1.5
)

这里的外观。

Polar histogram with title

它越来越近,但是我们还有一件事情要做。

添加传奇

观看者无法理解颜色的含义,但是我们可以通过添加传奇来解决。

要添加一个传奇,我创建了以下功能,该功能需要添加标签,它们的颜色和标题。

def add_legend(labels, colors, title):
    lines = [
        Line2D([], [], marker='o', markersize=24, linewidth=0, color=c) 
        for c in colors
    ]

    plt.legend(
        lines, labels,
        fontsize=18, loc="upper left", alignment="left",
        borderpad=1.3, edgecolor="#E4C9C9", labelspacing=1,
        facecolor="#F1E4E4", framealpha=1, borderaxespad=1,
        title=title, title_fontsize=20,
    )

我在此处添加一般函数并将其与其他所有内容一起运行。

# Add general functions here
...

add_legend(
    labels=["High income", "Upper middle income", "Lower middle income", "Low income", "Unknown"],
    colors=["#468FA8", "#62466B", "#E5625E", "#6B0F1A", "#909090"],
    title="Income level according to the World Bank\n"
)

最终结果看起来像这样。

Polar histogram by the author

就是这样。我们已经重新创建了您在顶部看到的美丽的极性直方图。

您的整个主要代码现在应该看起来像这样。

fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(30, 30))
ax.set(xlim=(-LIMIT, LIMIT), ylim=(-LIMIT, LIMIT))

for i, row in df.iterrows():
    bar_length = row.score
    length = bar_length + INNER_PADDING
    start = START_ANGLE + i*SIZE + PAD
    end = START_ANGLE + (i+1)*SIZE
    angle = (end + start) / 2

    # Add variables here
    flag_zoom = 0.004 * length
    flag_x, flag_y = get_xy_with_padding(length, angle, 8*flag_zoom)
    text_x, text_y = get_xy_with_padding(length, angle, 16*flag_zoom)

    # Add functions here
    draw_wedge(ax, start, end, length, bar_length, color(row.income))
    add_flag(ax, flag_x, flag_y, row.country, flag_zoom, angle)
    add_text(ax, text_x, text_y, row.country, bar_length, angle)

ax.text(1-LIMIT, LIMIT-2, "+ main title", fontsize=58)

# Add general functions here
draw_reference_line(ax, 2.0, 0.06, INNER_PADDING)
draw_reference_line(ax, 4.0, 0.06, INNER_PADDING)
draw_reference_line(ax, 6.0, 0.06, INNER_PADDING)
plt.title("World Happiness Report 2022".replace(" ", "\n"), x=0.5, y=0.5, va="center", ha="center", fontsize=64, linespacing=1.5)

add_legend(
    labels=["High income", "Upper middle income", "Lower middle income", "Low income", "Unknown"],
    colors=["#468FA8", "#62466B", "#E5625E", "#6B0F1A", "#909090"],
    title="Income level according to the World Bank\n"
)

plt.axis("off")
plt.tight_layout()
plt.show()

这是本教程的;恭喜您达到末尾。


结论

今天,我们学会了使用Matplotlib和Python创建美丽的极性直方图。

极性直方图非常容易创建,使我们可以将更多信息塞入单个图表中。

我在本教程中使用了世界幸福报告,但是您可以将其更改为另一个鼓舞人心的数据集。

我希望您学到了一些技术来帮助您将图表的想法栩栩如生。