Как создать красивую полярную гистограмму с помощью Python и Matplotlib
Приветствую вас в этом учебном пособии по Python + Matplotlib, в котором я покажу вам, как создать красивую полярную гистограмму, которую вы видите выше.
Полярные гистограммы отлично подходят для тех случаев, когда у вас слишком много значений для стандартной гистограммы. Круговая форма, где каждая полоса становится тоньше к середине, позволяет вместить больше информации в ту же область.
Я использую данные из Всемирного доклада о счастье и информацию об уровне доходов из Всемирного банка.
Код и данные, которые я использую, можно найти в этом репозитории GitHub.
Давайте приступим.
Шаг 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 в конце. Я расскажу об этих компонентах позже в учебнике.
Для загрузки данных я использую pandas.
df = pd.read_csv("./hapiness_report_2022.csv", index_col=None)
df = df.sort_values("score").reset_index(drop=True)
Настройки в стиле Seaborn
Затем с помощью 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, но в данном уроке нужны только эти четыре.
Для создания красивых цветовых палитр я использую такие сайты, как Colorhunt и Coolors.
Глобальные настройки
Я также добавляю несколько глобальных настроек для управления общим видом. Первые четыре определяют диапазон, размер и ширину клиньев в гистограмме.
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.pie() для создания круговой диаграммы можно использовать функцию 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() под комментарием “Add functions here”, как показано ниже.
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, чтобы показать, что он делает.
При выполнении кода я получаю следующий рисунок.
Как видите, до получения чего-то похожего на полярную гистограмму, которую вы видели в начале, нам еще далеко, но, по крайней мере, клинья нарисовать удалось.
Ближе к середине мы получаем много визуальных артефактов, поэтому давайте отменим 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_wedge.
# Add functions here
draw_wedge(ax, start, end, length, bar_length, color(row.income))
Вот результат.
При использовании INNER_PADDING и color() никаких странных артефактов не остается. Пришло время добавить информацию, объясняющую, что мы видим.
Шаг 3: Добавление этикеток
Добавим метки для каждого столбика полярной гистограммы. Я хочу, чтобы каждый столбец отображал флаг страны, ее название и показатель счастья.
Определение позиций
При добавлении флагов и текста на график в 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
Мы можем использовать эту функцию как для флагов, так и для текста.
Добавление флагов
Для флагов я использую эти округлые флажки от FlatIcon.
Они требуют лицензии, поэтому, к сожалению, я не могу поделиться ими, но вы можете найти похожие флаги в других местах.
Вот моя функция для добавления флага на график. Она принимает позицию, название страны (которое соответствует имени правильного файла), масштаб и поворот.
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",
))
Я меняю способ поворота флага, если угол превышает 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 определяют размер флага и зависят от количества баллов. Если страна имеет низкий балл, то места для флага меньше, и его нужно сделать немного меньше.
Фантастика.
Добавление названий и оценок стран
Чтобы добавить название и счет страны, я написал следующую функцию.
Как и в случае с флагами, я изменяю поворот, если угол превышает 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)
Теперь мы имеем следующий график, и он начинает выглядеть гораздо лучше.
Теперь пришло время рассказать пользователям о том, что они видят.
Шаг 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)
Вот результат.
Это существенно меняет ситуацию.
Добавление заголовка
Пробел в центре графика призван создать естественное место для заголовка. Расположение заголовка в центре необычно и может сразу заинтересовать зрителя.
Код для добавления заголовка представляет собой стандартную функциональность 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
)
Вот как это выглядит.
Все ближе и ближе, но нам еще предстоит сделать еще одно дело.
Добавление легенды
У зрителя нет возможности понять, что означают те или иные цвета, но это можно исправить, добавив легенду.
Для добавления легенды я создал следующую функцию, которая принимает метки, которые нужно добавить, их цвета и заголовок.
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"
)
Конечный результат выглядит так.
Вот и все. Мы воссоздали красивую полярную гистограмму, которую вы видели вверху.
Теперь весь ваш основной блок кода должен выглядеть следующим образом.
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.
Полярные гистограммы удивительно просты в создании и позволяют вместить больше информации в один график.
В этом уроке я использовал World Happiness Report, но вы можете заменить его на другой вдохновляющий набор данных.
Надеюсь, вы узнали несколько приемов, которые помогут вам воплотить в жизнь ваши идеи по созданию диаграмм.