Press "Enter" to skip to content

如何使用Seaborn和Matplotlib创建美观的年龄分布图(包括动画)

图表教程

可视化国家和地区的人口统计信息

作者创建的图表

今天,我想向您展示如何使用matplotlibseaborn创建美丽的年龄分布图表,就像上面的图表一样。

年龄分布图表非常适合用于可视化国家或地区的人口统计信息。它们非常有趣,但是默认的Seaborn + Matplotlib图表对我们来说不够好看。

在本教程中,您将学到以下内容:

  • 如何创建Seaborn样式
  • 改进轴以使其易于阅读和信息丰富
  • 添加标题和漂亮的图例
  • 将Matplotlib图形转换为PIL图像并添加外部填充
  • 创建多个图像的网格(如上面的示例)
  • 创建时间序列动画以显示人口随时间变化的情况

如果您想跟随本教程,可以在此GitHub存储库中找到数据和我的代码。

让我们开始吧。

数据简介

原始数据来自人口估计和预测数据集,这是一个由世界银行许可的数据集,采用知识共享署名4.0许可证。它包含1960年至2021年的实际值和官方预测,直到2050年。

在GitHub存储库中,我已经处理了数据并创建了四个单独的CSV文件,以便您可以专注于制作图表。

两个文件,一个用于女性,一个用于男性,具有绝对数量的人口。

作者的截图

另外两个文件具有描述总人口比例的值。例如,在下面的屏幕截图中,您可以看到1960年时,巴林只有0.336%的人口在70-74岁之间。

作者的截图

该数据集有17个年龄组,00-0405-0910-1415-1920-2425-2930-3435-3940-4445-4950-5455-5960-6465-6970-7475-7980+

还有250多个国家和地区,因此可以创建您感兴趣的年龄分布图表。

创建第一个年龄分布图表

现在我们了解了数据,我们可以使用Seaborn的默认设置创建一个简单的图表。我为女性使用红色,男性使用蓝色。

这也许有点刻板,但是使您的图表易于理解至关重要,颜色对于第一次解释来说非常重要。

唯一的“技巧”是我将男性的值乘以负一,以便蓝色的条形图向相反的方向移动。

这是创建图表的函数。

def create_age_distribution(female_df, male_df, country, year):    df_f = female_df[female_df.country_name == country].loc[::-1]    df_m = male_df[male_df.country_name == country].loc[::-1]        ax = sns.barplot(y=df_m["indicator_name"], x=df_m[year] * -1, orient="h", color=MALE_COLOR)    ax = sns.barplot(y=df_f["indicator_name"], x=df_f[year], orient="h", color=FEMALE_COLOR)        return ax

以下是我如何使用它的方式。

fig = plt.figure(figsize=(10, 7))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="World",    year="2021")plt.show()

这是2021年世界年龄分布的结果图。它显示了所有数据,但它看起来不太好看,也很难理解。

作者创建的图表

让我们让它变得更好。

创建Seaborn风格

关于seaborn最好的部分是,可以使用sns.set_style()创建自己独特的样式。它接受一个可以有多个不同值的字典。

对于本教程,我创建了以下函数,以便快速尝试不同的样式。

def set_seaborn_style(font_family, background_color, grid_color, text_color):    sns.set_style({        "axes.facecolor": background_color,        "figure.facecolor": background_color,        "axes.labelcolor": text_color,        "axes.edgecolor": grid_color,        "axes.grid": True,        "axes.axisbelow": True,        "grid.color": grid_color,        "font.family": font_family,        "text.color": text_color,        "xtick.color": text_color,        "ytick.color": text_color,        "xtick.bottom": False,        "xtick.top": False,        "ytick.left": False,        "ytick.right": False,        "axes.spines.left": False,        "axes.spines.bottom": True,        "axes.spines.right": False,        "axes.spines.top": False,    })

您可能希望拥有更多的控制。我在这里省略了一些我不关心的选项,并在多个地方重复使用相同的颜色。

我们必须选择背景、网格和文本颜色来运行该函数。我更喜欢具有背景颜色的图表,因为它们与页面更加突出。白色背景看起来不错,但不是我的风格。

在创建新的配色方案时,我通常从找到我喜欢的一种颜色开始。一个好的开始寻找的地方是Canva Color Palettes或ColorHunt。

找到我喜欢的几种颜色后,我使用Coolors生成其他颜色。

这是我在本教程中使用的主要颜色调色板。

作者创建的屏幕截图

现在,我可以使用我们的新颜色运行set_seaborn_style(),并选择PT Mono作为字体。

FEMALE_COLOR = "#F64740"MALE_COLOR = "#05B2DC"set_seaborn_style(    font_family="PT Mono",    background_color="#253D5B",    grid_color="#355882",    text_color="#EEEEEE")

现在图表的样子如下。

作者创建的图表

它是与之前相比的一个明显改进,但缺少信息,仍然很难理解。

让我们继续通过修复轴来解决这个问题。

改善轴

现在,颜色看起来很好,是时候让图表更加信息化。

以下是我想要做的三件事情。

  • 删除轴标签,因为它们不添加信息
  • 格式化x轴上的值,使它们更具信息性
  • 使文本变大,以便在较小的屏幕上显示良好

这个解决方案由两个函数组成。

首先是 create_x_labels() 函数,处理第二个 bullet point,让我可以根据国家的人口快速调整 x 轴,或者使用比率而不是绝对数。

def create_x_labels(ax, xformat):    if xformat == "billions":        return ["{}B".format(round(abs(x / 1e9))) for x in ax.get_xticks()[1:-1]]    elif xformat == "millions":        return ["{}M".format(round(abs(x / 1e6))) for x in ax.get_xticks()[1:-1]]    elif xformat == "thousands":        return ["{}K".format(round(abs(x / 1e3))) for x in ax.get_xticks()[1:-1]]    elif xformat == "percentage":        return ["{}%".format(round(abs(x), 1)) for x in ax.get_xticks()[1:-1]]

其次是 format_ticks() 函数,处理第一个和第三个 bullet points,并调用 create_x_labels()

def format_ticks(ax, xformat, xlim=(None, None)):    ax.tick_params(axis="x", labelsize=12, pad=8)    ax.tick_params(axis="y", labelsize=12)    ax.set(ylabel=None, xlabel=None, xlim=xlim)        plt.xticks(        ticks=ax.get_xticks()[1:-1],        labels=create_x_labels(ax, xformat)    )

如果我们想要比较两个不同年龄分布,xlim 参数是必不可少的。如果我们将其留空,轴会适应数据中的值,条形图将会延伸到整个轴。

我在创建图表时添加了这些函数。与之前完全相同,只是在最后加上了 format_tricks()

fig = plt.figure(figsize=(10, 7))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="World",    year="2021")# 新函数format_ticks(ax, xformat="millions")plt.show()

这是新图的样子。

Graph created by the author

我们还可以通过设置 xformat="percentage" 并使用 population_ratio_malepopulation_ratio_female 来测试百分比格式。我还设置了 xlim=(-10, 10)

Graph created by the author

看起来不错,但我们还可以做更多。

添加标题和图例

现在我想要解决的两个明显的问题是:

  • 添加一个描述图表的标题
  • 添加一个解释柱形条代表什么的图例

为了创建图例,我编写了以下函数,它使用 x 和 y 参数来定义位置。

def add_legend(x, y):     patches = [        Patch(color=MALE_COLOR, label="男性"),        Patch(color=FEMALE_COLOR, label="女性")    ]        leg = plt.legend(        handles=patches,        bbox_to_anchor=(x, y), loc='center',        ncol=2, fontsize=15,        handlelength=1, handleheight=0.4,        edgecolor=background_color    )

然后,我像之前添加 format_tricks() 一样添加这个函数。

fig = plt.figure(figsize=(10, 8))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="World",    year="2021")# 新函数format_ticks(ax, xformat="millions")add_legend(x=0.5, y=1.09)plt.title("2021 年世界年龄分布", y=1.14, fontsize=20)plt.tight_layout()plt.show()

我还添加了 plt.title() 来添加一个标题。

当我运行所有代码时,年龄分布图看起来像这样。

作者创建的图表

看起来很棒。让我们继续。

创建一个 PIL 图像并添加填充

在某个时候,我想把我的图形转换成我可以保存到磁盘并以其他方式自定义的图像。

其中一个自定义是在图表周围添加一些填充,使其看起来不那么挤。

首先,我创建了 create_image_from_figure() 函数,将 Matplotlib 图表转换为 PIL 图像。

def create_image_from_figure(fig):    plt.tight_layout()        fig.canvas.draw()    data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)    data = data.reshape((fig.canvas.get_width_height()[::-1]) + (3,))    plt.close()         return Image.fromarray(data)

这里有一个添加填充的函数。

def add_padding_to_chart(chart, left, top, right, bottom, background):    size = chart.size    image = Image.new("RGB", (size[0] + left + right, size[1] + top + bottom), background)    image.paste(chart, (left, top))    return image

再次将这些函数添加到创建图表的原始代码中。它现在看起来像这样。

fig = plt.figure(figsize=(10, 8))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="World",    year="2021")# 新函数format_ticks(ax, xformat="millions")add_legend(x=0.5, y=1.09)plt.title("2021 年世界年龄分布", y=1.14, fontsize=20)image = create_image_from_figure(fig)image = add_padding_to_chart(image, 20, 20, 20, 5, background_color)

这里是结果图表。

作者创建的图表

在我看来,这看起来非常完美。我还有两件事想要展示,一是如何创建网格和时间间隔可视化。

我们先从创建网格开始。

创建一个带有多个国家的网格

您可以使用 plt.subplots() 创建网格,但在本教程中,我想创建一个图像网格,因为我认为它看起来更好。

以下函数接受一个图像列表,并创建具有 ncols 的网格。它通过创建一个足够大的带有单一背景颜色的空白图像来实现,以容纳所有图像。

def create_grid(figures, pad, ncols):    nrows = int(len(figures) / ncols)    size = figures[0].size    image = Image.new(        "RGBA",        (ncols * size[0] + (ncols - 1) * pad, nrows * size[1] + (nrows - 1) * pad),        "#ffffff00"    )    for i, figure in enumerate(figures):        col, row = i % ncols, i // ncols        image.paste(figure, (col * (size[0] + pad), row * (size[1] + pad)))    return image

在以下代码中,我循环遍历一个国家列表,将生成的图表添加到 figures,并通过在最后运行 create_grid() 创建网格。

figures = []for country in [    "United States", "China", "Japan", "Brazil", "Canada",    "Germany", "Pakistan", "Russian Federation", "Nigeria",     "Sweden", "Cambodia", "Saudi Arabia", "Iceland",    "Spain", "South Africa", "Morocco"]:    fig = plt.figure(figsize=(10, 8))    ax = create_age_distribution(        female_df=population_ratio_female,        male_df=population_ratio_male,        country=country,        year="2021"    )        ax.set(xlim=(-10, 10))    # 新函数    format_ticks(ax, xformat="percentage")    add_legend(x=0.5, y=1.09)    plt.title("2021 年{}的年龄分布".format(country), y=1.14, fontsize=20)    image = create_image_from_figure(fig)    image = add_padding_to_chart(image, 20, 20, 20, 5, background_color)        figures.append(image)    grid = create_grid(figures, pad=20, ncols=4)

请注意,我使用比例而不是绝对数字,并设置 xlim=(-10, 10) 。否则,我将无法在视觉上比较各个国家。

作者创建的图表

让我们继续本教程的最后一部分——如何创建时间-lapse可视化。

创建时间-lapse可视化

静态年龄分布图表看起来很棒,但是看到它们随时间的变化是很有趣的。

由于我们有1960年至2021年的实际值和2050年的预测,因此我们可以为相对较长的时间段创建时间-lapse动画。

在开始之前,我需要告诉你,我使用的字体 PT Mono 并非所有字符的高度都相同。为了使可视化效果好,我需要使用 plt.text() 来代替 plt.title() 来显示年份。如果您使用其他字体,则不需要这样做。

这是代码:

images = []years = list(population_male.columns[4:])for year in years:    fig = plt.figure(figsize=(10, 8))    ax = create_age_distribution(        female_df=population_female,        male_df=population_male,        country="World",        year=year    )    # 新函数    format_ticks(ax, xformat="millions", xlim=(-400000000, 400000000))    add_legend(x=0.5, y=1.09)    plt.title("Age Distribution for the World in      ", y=1.14, fontsize=21)    plt.text(x=0.77, y=1.15, s=str(year), fontsize=21, transform=ax.transAxes)    image = create_image_from_figure(fig)    image = add_padding_to_chart(image, 20, 20, 20, 5, background_color)    images.append(image)

我使用 imageio 从图像列表创建GIF。

# 复制最后几帧以添加延迟 # 在动画重新开始前images = images + [images[-1] for _ in range(20)]imageio.mimwrite('./time-lapse.gif', images, duration=0.2)

让我们看看结果。

作者创建的可视化

太棒了!本教程至此结束;如果您喜欢并学到了有用的东西,请告诉我。

结论

这是一篇有趣的教程,我希望你喜欢它。

年龄分布是一个国家人口统计学的很好的可视化方法,现在你已经看到了一些使它们脱颖而出的方法。

我们已经学会了创建样式、网格和动画。像我在这里所做的那样编写函数也很棒,如果您想快速测试不同的想法和风格,这也很有用。

我希望您学到了一些将来会用到的东西。

谢谢您抽出时间阅读我的教程。如果您喜欢这种类型的内容,请告诉我。

如果有需要,我可以创建更多的教程!:)

下次见。

Leave a Reply

Your email address will not be published. Required fields are marked *