Глаза - наши самые важные органы, так как мы воспринимаем около 80% всех наших впечатлений через зрение. Неудивительно, что визуализации - это самый простой способ собрать и проанализировать информацию. Когда дело доходит до науки о данных, графики и графики различных типов помогают нам понять проблемы различной сложности. Они позволяют нам выявлять закономерности, взаимосвязи и выбросы в наших данных. Итак, независимо от того, какие данные мы хотим анализировать, визуализация данных является важным первым шагом. При работе с Python M atplotlib и соответствующие надстройки, такие как seaborn, являются незаменимыми инструментами для быстрого достижения этой цели.

В этой статье я хочу показать вам пару советов, как улучшить и обогатить вашу фигуру матплолиба. Что наиболее важно, я предоставляю вам удобный способ аннотировать различные виды гистограмм.

Заинтересованы? Итак, приступим!

Предпосылки

Чтобы следовать примеру, вам понадобится Python 3.7+ с M atplotlib, pandas и seaborn. Как всегда, я рекомендую использовать Poetry для управления вашими пакетами Python и средами. Вы можете проверить эту статью о том, как его настроить. В качестве ярлыка я рекомендую использовать pip или pipx для его установки на ваш компьютер.

В качестве предупреждения мы сначала создадим столбчатые диаграммы на основе выборочных данных без дополнительной стилизации. Цель этой статьи - улучшить и обогатить эти диаграммы. Вы можете найти весь пример кода в моем репозитории GitHub.

Настраивать

Сначала мы создаем проект Poetry с именем nice-plots, в котором реализуем пример и добавляем необходимые пакеты.

poetry new nice-plots
cd nice-plots
poetry add pandas matplotlib seaborn
touch nice_plots/bar_charts.py

Теперь у нас есть изолированная среда Python, в которой установлено все, что нам нужно. Отлично, можем приступить к работе!

Данные

В качестве данных мы используем знаменитый набор данных о цветках ириса. К счастью, это идет напрямую с seaborn. Чтобы создать осмысленные гистограммы, я сначала группирую набор данных по видам, используя среднее в качестве функции агрегирования. На основе этих данных я создаю четыре различных столбчатых диаграммы: вертикальные столбчатые диаграммы и горизонтальные столбчатые диаграммы, обе в нормальная и составная версия. В коде это выглядит как

import seaborn as sns
import os
from dataclasses import dataclass
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
from typing import *
sns.set()
# BLOCK 1
# BLOCK 2
# BLOCK 4
# BLOCK 6
data = sns.load_dataset("iris").groupby('species').mean()
fig, axes = plt.subplots(2,2)
data.plot.bar(ax=axes[0][0]) 
data.plot.bar(stacked=True, ax=ax[0][1])
data.plot.barh(ax=axes[1][0])
data.plot.barh(stacked=True, ax=ax[1][1])
# BLOCK 3
# BLOCK 5
# BLOCK 7
plt.show()

Я добавил комментарии # BLOCK N, в которых я пересылаю ссылки на усовершенствования, которые следуют ниже. Надеюсь, это не слишком сбивает с толку. Я также добавил весь импорт, который потребуется позже.

Поскольку вы, возможно, не сидите за компьютером, вот как выглядят полученные гистограммы

Хм, не очень приятно, правда? Думаю, стоит сделать их лучше.

Улучшения

Размеры рисунков и шрифтов

Первое, что бросается в глаза при взгляде на графики, - это то, что они слишком малы по сравнению с остальной частью рисунка. Есть несколько способов изменить это. Я предпочитаю устанавливать глобальные параметры rcParams из pyplot. Глобальные означает, что они применяются ко всем созданным вами фигурам, а не только к определенной. Чтобы изменить как размер рисунка, так и некоторые размеры шрифта, все, что вам нужно сделать, это

# BLOCK 1
def set_sizes(fig_size:Tuple[int,int]=(9, 6), font_size:int=10):
    plt.rcParams["figure.figsize"] = fig_size
    plt.rcParams["font.size"] = font_size
    plt.rcParams["xtick.labelsize"] = font_size
    plt.rcParams["ytick.labelsize"] = font_size
    plt.rcParams["axes.labelsize"] = font_size
    plt.rcParams["axes.titlesize"] = font_size
    plt.rcParams["legend.fontsize"] = font_size
set_sizes((12,8), 10)

Запуск этого производит

Это уже намного лучше, чем раньше. Но все еще есть возможности для улучшения.

Повернуть метки-отметки

Вертикальный текст, как метки x-галочки на горизонтальных столбчатых диаграммах, мне не нравится. Кроме того, этот вертикальный текст также занимает много места на рисунке. Чтобы исправить это, Matplotlib предлагает простой способ поворота меток с помощью

# BLOCK 2
def rotate_xticks(ax: matplotlib.axes, degrees : float = 45):
    ax.set_xticklabels(ax.get_xticklabels(), rotation=degrees)
# BLOCK 3
rotate_xticks(ax=axes[0][0],0)
rotate_xticks(ax=axes[1][0],0)

Поскольку поворот крестиков на 45 градусов очень распространен, я использую это значение по умолчанию. Однако в этом примере печать этикеток по горизонтали имеет наибольший смысл. Вы можете сделать это, установив для градусов значение 0.

Результат, полученный на моей машине, выглядит так:

Аннотировать гистограммы

Гистограммы идеально подходят для быстрого понимания того, как разные группы сравниваются друг с другом. Однако нас может заинтересовать не только относительное сравнение, но и соответствующие абсолютные значения. Мы можем добиться того и другого, добавив к каждому столбцу соответствующее значение.

Для этого я создал класс AnnotateBars, который позволяет аннотировать как вертикальные, так и горизонтальные гистограммы в версии с накоплением и без стека.

# BLOCK 4
#Alias types to reduce typing, no pun intended 
Patch = matplotlib.patches.Patch
PosVal = Tuple[float, Tuple[float, float]] 
Axis = matplotlib.axes.Axes
PosValFunc = Callable[[Patch], PosVal]
@dataclass
class AnnotateBars:
    font_size: int = 10
    color: str = "black"
    n_dec: int = 2
    def horizontal(self, ax: Axis, centered=False):
        def get_vals(p: Patch) -> PosVal:
            value = p.get_width()
            div = 2 if centered else 1
            pos = (
                p.get_x() + p.get_width() / div,
                p.get_y() + p.get_height() / 2,
            )
            return value, pos
        ha = "center" if centered else  "left"
        self._annotate(ax, get_vals, ha=ha, va="center")
    def vertical(self, ax: Axis, centered:bool=False):
        def get_vals(p: Patch) -> PosVal:
            value = p.get_height()
            div = 2 if centered else 1
            pos = (p.get_x() + p.get_width() / 2,
                   p.get_y() + p.get_height() / div
            )
            return value, pos
        va = "center" if centered else "bottom"
        self._annotate(ax, get_vals, ha="center", va=va)
    def _annotate(self, ax, func: PosValFunc, **kwargs):
        cfg = {"color": self.color, 
               "fontsize": self.font_size, **kwargs}
        for p in ax.patches:
            value, pos = func(p)
            ax.annotate(f"{value:.{self.n_dec}f}", pos, **cfg)

Фу, кода много, но не беспокойтесь, я расскажу, как его использовать.

Во-первых, вам нужно создать экземпляр AnnotateBars. Вы можете указать размер шрифта, цвет текста и количество десятичных знаков, которые должны быть напечатаны. Все эти параметры имеют разумные значения по умолчанию.

Затем вам нужно вызвать вертикальный или горизонтальный в зависимости от типа гистограммы, которую вы хотите аннотировать. Этим функциям необходимо передать объект оси, содержащий соответствующую гистограмму. Кроме того, они принимают дополнительный параметр, называемый центрированным. С его помощью вы можете определить, будут ли аннотации напечатаны в центре полосы или вверху / справа от нее. Это особенно полезно при работе с гистограммами с накоплением.

Хватит разговоров, давайте воспользуемся этим классом и аннотируем наши четыре гистограммы с различными конфигурациями.

# BLOCK 5
AnnotateBars().vertical(axes[0][0])
AnnotateBars(color="blue").vertical(axes[1][0], True)
AnnotateBars().horizontal(axes[0][1])
AnnotateBars(font_size=8, n_dec=1).horizontal(axes[1][1], True)

А вот итоговые графики результатов

Теперь мы стали еще умнее и знаем абсолютные значения, а не только отношения. Потрясающий!

В качестве примечания, когда у нас есть очень маленькие столбцы на гистограммах с накоплением, наложение значений становится проблемой. Вы можете видеть это на диаграмме Setosa с накоплением. В таких случаях вы должны выбрать, можете ли вы принять это или требовать чего-то другого.

Сохранить участок

Наконец, когда вы создали потрясающие сюжеты, которыми гордитесь, вы можете поделиться ими со своими коллегами. Для этого вы должны хранить свои графики в формате, например PNG, который вы можете легко распространять. Несмотря на то, что синтаксис выгрузки цифр в изображения довольно прост, я часто его забываю. Вот почему я использую эту вспомогательную функцию

# BLOCK 6
def save_figure(fig : matplotlib.figure.Figure, path : str):
    folder = os.path.dirname(path)
    if folder:
        os.makedirs(folder, exist_ok=True)
    fig.savefig(path, bbox_inches="tight")

Вам нужно только передать ему объект Figure и путь к выходному файлу изображения. Если папка, в которой вы хотите сохранить изображение, не существует, функция автоматически создаст ее для вас. Для меня это очень удобно во многих случаях. Давайте добавим этот последний блок кода, и все готово.

# BLOCK 7
save_figure(fig, "./plots/medium/bar-charts.png")

Именно так я создал все сюжеты, которые вы видели ранее. Я определенно ем собачий корм :)!

Заворачивать

В этой статье я показал вам пару функций Matplotlib для улучшения ваших графиков. Самое главное, я представил вам хороший способ аннотировать различные виды гистограмм.

Спасибо, что подписались на этот пост. Как всегда, не стесняйтесь обращаться ко мне с вопросами, комментариями или предложениями. Удачного построения!