5 шагов для создания красивых столбчатых диаграмм с помощью Python
Рассказывать убедительную историю с помощью данных на Python становится намного проще, когда диаграммы, поддерживающие эту самую историю, ясны, не требуют пояснений и визуально приятны для аудитории.
Во многих случаях содержание и форма одинаково важны.
Отличные данные, плохо представленные, не привлекут того внимания, которого они заслуживают, в то время как плохие данные, представленные визуально приятным способом, легко будут дискредитированы.
t.me/pro_python_code – Python для разработчиков.
Мотивация
Matplotlib позволяет быстро и легко выводить данные с помощью готовых функций, но этапы настройки требуют больших усилий.
Я потратил довольно много времени на изучение лучших практик построения убедительных диаграмм с помощью Matplotlib, так что вам не придётся этого делать.
В этой статье я сосредоточусь на столбчатых диаграммах и объясню, как я собрал воедино крупицы знаний, которые я нашел. От этого варианта…
… к этому:
#0 Данные
Чтобы проиллюстрировать методологию, я использовал общедоступный набор данных о задержках авиакомпаниями внутренних рейсов в США 2008 года:
2008, “Data Expo 2009: Airline on time data”, https://doi.org/10.7910/DVN/HG7NV7, Harvard Dataverse, V1
Public domain CC0 1.0
После импорта необходимых пакетов для считывания данных и построения наших графиков, я просто сгруппировал данные по месяцам и рассчитал среднюю задержку, используя приведённый ниже код:
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.ticker import MaxNLocator
df = pd.read_csv('DelayedFlights.csv')
df = df[['Month', 'ArrDelay']] # Let's only keep the columns useful to us
df = df[~df['ArrDelay'].isnull()] # Get rid of cancelled and diverted flights
# Group by Month and get the mean
delay_by_month = df.groupby(['Month']).mean()['ArrDelay'].reset_index()
Набор данных, используемый на протяжении всей статьи для построения различных версий столбчатой диаграммы, выглядит следующим образом:
#1 Основной график
Справедливости ради, с помощью двух строк кода вы уже можете построить столбчатую диаграмму и получить из неё некоторые основные сведения.
По общему признанию, этот график не самый красивый и не самый полезный, поскольку в нём отсутствует ключевая информация, но вы уже можете сказать, что путешествие в декабре, скорее всего, приведёт к задержке рейса.
# Create the figure and axes objects, specify the size and the dots per inches
fig, ax = plt.subplots(figsize=(13.33,7.5), dpi = 96)
# Plot bars
bar1 = ax.bar(delay_by_month['Month'], delay_by_month['ArrDelay'], width=0.6)
#2 Самое необходимое
Давайте добавим несколько важных разделов в нашу таблицу, чтобы сделать её более читаемой для аудитории.
- Сетки
Для улучшения её удобочитаемости необходимы сетки графика. Их прозрачность установлена на 0,5, чтобы они не слишком мешали точкам данных.
- Переформатирование по осям X и Y
Я добровольно добавил здесь больше параметров, чем необходимо, чтобы иметь более полное представление о возможностях настройки. Например, оси x не нужны объекты major_formatter и major_locator, поскольку мы только настраиваем метки, но если ось x считывателя состоит из других фигур, то это может пригодиться.
- Панель меток столбцов
Метки столбцов добавляются поверх каждого столбца, чтобы упростить сравнение между близкими точками данных и предоставить более подробную информацию об их фактических значениях.
# Create the grid
ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1)
ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1)
# Reformat x-axis label and tick labels
ax.set_xlabel('', fontsize=12, labelpad=10) # No need for an axis label
ax.xaxis.set_label_position("bottom")
ax.xaxis.set_major_formatter(lambda s, i : f'{s:,.0f}')
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
ax.xaxis.set_tick_params(pad=2, labelbottom=True, bottom=True, labelsize=12, labelrotation=0)
labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
ax.set_xticks(delay_by_month['Month'], labels) # Map integers numbers from the series to labels list
# Reformat y-axis
ax.set_ylabel('Delay (minutes)', fontsize=12, labelpad=10)
ax.yaxis.set_label_position("left")
ax.yaxis.set_major_formatter(lambda s, i : f'{s:,.0f}')
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
ax.yaxis.set_tick_params(pad=2, labeltop=False, labelbottom=True, bottom=False, labelsize=12)
# Add label on top of each bar
ax.bar_label(bar1, labels=[f'{e:,.1f}' for e in delay_by_month['ArrDelay']], padding=3, color='black', fontsize=8)
#3 Профессиональный вид
Добавление еще нескольких функций к нашему графику сделает его более профессиональным. Они будут располагаться поверх любых графиков (не только столбчатых) и не зависят от данных, которые мы используем в этой статье.
Благодаря приведённому ниже фрагменту кода реализация этих настроек практически не потребует усилий. Совет автора: сохраните его и повторно используйте по желанию.
Читатель может настроить их, чтобы создать свою собственную визуальную идентичность.
- Шипы
Шипы образуют рамку, видимую вокруг графика. Они удаляются, за исключением правого, который должен быть немного толще.
- Красная линия и прямоугольник сверху
Красная линия и прямоугольник добавлены над заголовком, чтобы красиво отделить график от текста над ним.
- Название и подзаголовок
Что такое график без заголовка?
Подзаголовок может быть использован для дальнейшего объяснения содержания.
- Источник
Должен быть во всех когда-либо созданных графиках.
- Корректировка отступов
Поля, окружающие область графика, отрегулированы таким образом, чтобы убедиться, что используется всё доступное пространство.
- Белый фон
Установка белого фона (по умолчанию из прозрачного) будет полезна при отправке диаграммы по электронной почте, ведь прозрачный фон может быть проблематичным.
# Remove the spines
ax.spines[['top','left','bottom']].set_visible(False)
# Make the left spine thicker
ax.spines['right'].set_linewidth(1.1)
# Add in red line and rectangle on top
ax.plot([0.12, .9], [.98, .98], transform=fig.transFigure, clip_on=False, color='#E3120B', linewidth=.6)
ax.add_patch(plt.Rectangle((0.12,.98), 0.04, -0.02, facecolor='#E3120B', transform=fig.transFigure, clip_on=False, linewidth = 0))
# Add in title and subtitle
ax.text(x=0.12, y=.93, s="Average Airlines Delay per Month in 2008", transform=fig.transFigure, ha='left', fontsize=14, weight='bold', alpha=.8)
ax.text(x=0.12, y=.90, s="Difference in minutes between scheduled and actual arrival time averaged over each month", transform=fig.transFigure, ha='left', fontsize=12, alpha=.8)
# Set source text
ax.text(x=0.1, y=0.12, s="Source: Kaggle - Airlines Delay - https://www.kaggle.com/datasets/giovamata/airlinedelaycauses", transform=fig.transFigure, ha='left', fontsize=10, alpha=.7)
# Adjust the margins around the plot area
plt.subplots_adjust(left=None, bottom=0.2, right=None, top=0.85, wspace=None, hspace=None)
# Set a white background
fig.patch.set_facecolor('white')
#4 Цветовой градиент
График в том виде, в каком мы оставили его в предыдущем разделе, аккуратен и готов к включению в презентацию. Игра с цветом полос и добавление градиента для лучшей визуализации вариаций не являются существенными, но добавят ему привлекательности.
Этот вариант использования не обязательно имеет лучшую документацию в интернете, но на самом деле его не так уж сложно реализовать с помощью функций LinearSegmentedColormap и Normalize Matplotlib.
# Colours - Choose the extreme colours of the colour map
colours = ["#2196f3", "#bbdefb"]
# Colormap - Build the colour maps
cmap = mpl.colors.LinearSegmentedColormap.from_list("colour_map", colours, N=256)
norm = mpl.colors.Normalize(delay_by_month['ArrDelay'].min(), delay_by_month['ArrDelay'].max()) # linearly normalizes data into the [0.0, 1.0] interval
# Plot bars
bar1 = ax.bar(delay_by_month['Month'], delay_by_month['ArrDelay'], color=cmap(norm(delay_by_month['ArrDelay'])), width=0.6, zorder=2)
#5 Последний штрих
Чтобы получить конечный результат, представленный в начале статьи, единственное, что осталось сделать, это реализовать эти несколько дополнительных компонентов:
- Средняя линия данных
Отображение средней линии данных на графике – полезный способ помочь аудитории быстро разобраться в том, что происходит.
- Вторая цветовая гамма
Благодаря второй цветовой шкале мы выделяем данные выше среднего (или любого порогового значения), чтобы облегчить восприятие визуализации за короткий промежуток времени.
- Легенда
Когда мы добавили вторую цветовую шкалу, мы ввели необходимость в условных обозначениях на нашей диаграмме.
# Find the average data point and split the series in 2
average = delay_by_month['ArrDelay'].mean()
below_average = delay_by_month[delay_by_month['ArrDelay']<average]
above_average = delay_by_month[delay_by_month['ArrDelay']>=average]
# Colours - Choose the extreme colours of the colour map
colors_high = ["#ff5a5f", "#c81d25"] # Extreme colours of the high scale
colors_low = ["#2196f3","#bbdefb"] # Extreme colours of the low scale
# Colormap - Build the colour maps
cmap_low = mpl.colors.LinearSegmentedColormap.from_list("low_map", colors_low, N=256)
cmap_high = mpl.colors.LinearSegmentedColormap.from_list("high_map", colors_high, N=256)
norm_low = mpl.colors.Normalize(below_average['ArrDelay'].min(), average) # linearly normalizes data into the [0.0, 1.0] interval
norm_high = mpl.colors.Normalize(average, above_average['ArrDelay'].max())
# Plot bars and average (horizontal) line
bar1 = ax.bar(below_average['Month'], below_average['ArrDelay'], color=cmap_low(norm_low(below_average['ArrDelay'])), width=0.6, label='Below Average', zorder=2)
bar2 = ax.bar(above_average['Month'], above_average['ArrDelay'], color=cmap_high(norm_high(above_average['ArrDelay'])), width=0.6, label='Above Average', zorder=2)
plt.axhline(y=average, color = 'grey', linewidth=3)
# Determine the y-limits of the plot
ymin, ymax = ax.get_ylim()
# Calculate a suitable y position for the text label
y_pos = average/ymax + 0.03
# Annotate the average line
ax.text(0.88, y_pos, f'Average = {average:.1f}', ha='right', va='center', transform=ax.transAxes, size=8, zorder=3)
# Add legend
ax.legend(loc="best", ncol=2, bbox_to_anchor=[1, 1.07], borderaxespad=0, frameon=False, fontsize=8)
#6 Заключительные мысли
Цель этой статьи состояла в том, чтобы поделиться знаниями для построения более убедительной гистограммы с использованием Matplotlib. Я постарался сделать это как можно более практичным с помощью повторно используемых фрагментов кода.
Я уверен, что необходимо внести и другие коррективы, о которых я не подумал. Если у вас есть какие-либо идеи по улучшению, не стесняйтесь комментировать и делать эту статью более полезной для всех!
Конечно же красиво, но практичнее было бы что-то подобное реализовать в том же plotly, а не громоздком matplotlib