Три разные единицы измерения на одном графике с библиотекой Plotly

от автора

Будучи сторонницей минимализма во всем, в том числе и в визуализации данных, я избегаю попыток «впихнуть невпихуемое» в одну визуализацию. Лучше построю группу графиков. Но иногда попадается интересный визуал и хочется его воспроизвести.

Эта визуализация одна из них. Но заинтриговал меня даже не сам дизайн, а текст публикации из ТГК Power BI Design: «Три разные единицы измерения на одном графике. Как? Непросто». И я подумала: а как это реализовать на python с использованием plotly?

Референс, взято из ТГК Power BI Design

Референс, взято из ТГК Power BI Design
Скрытый текст

Финал будет такой, если вдруг нет времени читать с начала и до конца) Полный код в конце

Первое, что нужно было сделать, это создать синтетический набор данных.

import numpy as np revenue_fact = np.random.randint(50000000, 100000000, size=12).tolist() #выручка SKU_count = np.random.randint(80, 150, size=12).tolist() #кол-во SKU quarters_1 = pd.date_range('2022Q1', periods=12, freq='Q') quarters = quarters_1.to_period('Q').astype(str) #периоды - кварталы gross_margin = np.random.randint(15, 22, size=12).tolist() #валовая рентабельность #создаю фрейм и добавляю вспомогательные столбцы df = pd.DataFrame({        'quarter': quarters,     'revenue': revenue_fact,     'SKU_count': SKU_count,      'percent': persent,      'gross_margin': gross_margin}) df['revenue_mln'] = round(df['revenue']/1000000,2) df['gross_profit_margin'] = round(df['revenue'] * (df['gross_margin']/100),2) df['gross_profit_margin_mln'] = round(df['gross_profit_margin']/1000000,2)

Далее я объясню, зачем я создаю столбцы с данными о выручке и валовой прибыли в миллионах рублей.

Общая логика графика

Сначала нужно создать объект с двумя осями Y: основной (левая) и дополнительная/вторичная (правая), на которые я и буду добавлять мои графики.

# Инициализирую пространство с подзаголовком, который содержит две оси Y fig = make_subplots(specs=[[{'secondary_y': True}]]) # В ось X кладем значения периодов # Добавляю первый график: динамику изменения количества проданных SCU # secondary_y = True привязывает график ко второй оси Y fig.add_trace(go.Scatter(x=df['quarter'],                           y=df['SKU_count'],                           name='Кол-во SKU',                           mode='lines+markers+text'),               secondary_y = True)  # Добавляю второй график: Значение выручки fig.add_trace(go.Bar(x=df['quarter'], y=df['revenue'],                       name='Выручка'),                secondary_y = False)  # Добавляю третий график: Значение валовой прибыли fig.add_trace(go.Bar(x=df['quarter'], y=df['gross_profit_margin'],                       name='Валовая прибыль'),                secondary_y = False)  # Добавляю четвертый график: Значение валовой рентабельности fig.add_trace(go.Scatter(x=df['quarter'], y=df['gross_margin'],                           name='Рентабельность',                           mode='lines',                           fill='tozeroy'),                secondary_y = True)  fig.update_layout(title=(f'Сколько SKU мы продаем и сколько зарабатываем'), #title_x=0.1, title_y=0.95,                   #title_font=dict(size=16, color=dark_3, weight='bold'),                    height = 350, width = 650), fig.show()
Результат выполнения кода

Результат выполнения кода

Вот такой неказистенький график меня получился: между графиками, визуализирующими количество SKU и значения валовой рентабельности слишком уж большое расстояние, сетки обеих осей Y создают неразбериху и делают график абсолютно нечитаемым. И ох уж эти цвета по умолчанию… Можно и не перечислять все недостатки: визуализация плоха и точка и это очевидно

Изменение расположения элементов на графике

Я планирую поместить легенду в верхней части горизонтально, как на моем референсе, задам положение заголовка и установлю параметры отступов, отключу сетку для правой оси Y. Обновленный код будет выглядеть так:

fig = make_subplots(specs=[[{'secondary_y': True}]])  fig.add_trace(go.Scatter(x=df['quarter'],                           y=df['SKU_count'],                           name='Кол-во SKU',                           mode='lines+markers+text'),               secondary_y = True)  fig.add_trace(go.Bar(x=df['quarter'], y=df['revenue'],                       name='Выручка'),                secondary_y = False)  fig.add_trace(go.Bar(x=df['quarter'], y=df['gross_profit_margin'],                       name='Валовая прибыль'),                secondary_y = False)  fig.add_trace(go.Scatter(x=df['quarter'], y=df['gross_margin'],                           name='Рентабельность',                           mode='lines',                           fill='tozeroy'),                secondary_y = True)  fig.update_layout(title=(f'Сколько SKU мы продаем и сколько зарабатываем'),                    title_x=0.1, title_y=0.95, #определяю положение заголовка                   height = 350, width = 650,                    yaxis2=dict(showticklabels=False, showgrid=False), #отключаю сетку и тики для второй (правой) оси Y                    legend_font=dict(size=11), # Устанавливаю размер шрифта для легенды, чтобы замостить все обозначенрия в один ряд                    #определяю конфигурацию легенды и добавляю прозрачный фон для легенда                   legend=dict(orientation="h", yanchor="bottom", y=0.9, xanchor="left", x=0.02, bgcolor='rgba(255, 255, 255, 0)'),                   #определяю режим отображения столбцов: столбцы будут накладываться друг на друга                                     barmode='overlay',                   # устанавливаю параметры отступов для фигyры                   margin = dict(t=50, l=50, r=10, b=10)) fig.show()

А это результат его выполнения.

Результат выполнения кода

Результат выполнения кода

По моему выглядит уже приличнее. Но все равно еще очень много недостатков: элементы накладывают друг на друга или наоборот между ними очень много пространства, очевидна проблема несоответствия масштабности элементов друг другу.
Я буду решать эту проблему путем изменения масштабов осей Y.
Чтобы сместить графики вниз относительно верхней границы и устранить наложение легенды на график, я увеличу границы отображения значений на оси Y. Дополнительно изменю размер шрифта тиков по осям Y и X.
Чтобы это изменить я добавлю дополнительные параметры в метод update_layout.

#меняю размер шрифта тиков, параметр "range" устанавливает пределы осей xaxis=dict(tickfont_size=10), axis=dict(tickfont_size=10,            side='left', range=[0, df['revenue'].max() + 20000000]), yaxis2=dict(showticklabels=False, side='right', showgrid=False,              range=[df['SKU_count'].min()-20, df['SKU_count'].max() + 20])

После добавления этого блока кода, график будет выглядеть вот так:

Результат выполнения кода

Результат выполнения кода

Уууппппс… А где Area Chart?

Теперь не визуализируется график, показывающий динамику изменения рентабельности.
Это произошло потому, что минимальное значение правой оси Y стало больше, чем максимальное значение рентабельности в моем ряду.
Но это не повод откатить изменения, которые, на мой взгляд, сделали мою визуализацию лучше.

Чтобы решить эту проблему я изменю масштабность данных для нашего Area Chart (График площадей), который показывает изменения значений рентабельности. Изначально в ось Y я положила значения из столбца df[‘gross_margin’]. Среднее значение этого столбца в ~6 раз меньше, чем значения столбца df[‘SKU_count’]. Я создам список синтетических значений для графика рентабельностей, путем увеличения значений рентабельности на n.

Обновленный код выглядит так:

fig = make_subplots(specs=[[{'secondary_y': True}]])  fig.add_trace(go.Scatter(x=df['quarter'],                           y=df['SKU_count'],                           name='Кол-во SKU',                           mode='lines+markers+text'),               secondary_y = True)  fig.add_trace(go.Bar(x=df['quarter'], y=df['revenue'],                       name='Выручка'),                secondary_y = False)  fig.add_trace(go.Bar(x=df['quarter'], y=df['gross_profit_margin'],                       name='Валовая прибыль'),                secondary_y = False)  #создание синтетического значения для графика рентабельностей #значение n = 4.8 выбрано простым подбором;  #мне кажется, что именно такое значение наилучшим образом влияет на композицию графика gross_margin_line = [int(num * 4.8) for num in df['gross_margin'].tolist()] fig.add_trace(go.Scatter(x=df['quarter'], y=gross_margin_line, #Кладем список gross_margin_line в Y                          name='Рентабельность',                           mode='lines',                           fill='tozeroy'),                secondary_y = True)  fig.update_layout(title=(f'Сколько SKU мы продаем и сколько зарабатываем'),                    title_x=0.1, title_y=0.95,                   height = 350, width = 650,                    xaxis=dict(tickfont_size=10),                   yaxis=dict(tickfont_size=10,                               side='left', range=[0, df['revenue'].max() + 20000000]),                   yaxis2=dict(showticklabels=False, side='right',                               showgrid=False, range=[df['SKU_count'].min()-20, df['SKU_count'].max() + 20]),                   legend_font=dict(size=11),                    legend=dict(orientation="h", yanchor="bottom", y=0.9, xanchor="left", x=0.02, bgcolor='rgba(255, 255, 255, 0)'),                   barmode='overlay',                   margin = dict(t=50, l=50, r=10, b=10)) fig.show()

Результат выполнения кода; обратите внимание на содержание всплывающей подсказки

Результат выполнения кода; обратите внимание на содержание всплывающей подсказки

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

Поскольку мне необходимо настраивать всплывающие подсказки для графика Area Chart, я сделаю это для всех графиков, чтобы обеспечить единообразие отображения. И именно тут мне понадобятся созданные столбцы с данными о выручке и валовой прибыли в миллионах рублей. Для этого буду настраивать значения параметров customdata и hovertemplate.

fig = make_subplots(specs=[[{'secondary_y': True}]])  fig.add_trace(go.Scatter(x=df['quarter'], y=df['SKU_count'],                           name='Кол-во SKU',                           mode='lines+markers+text',                           customdata = df['SKU_count'],                           hovertemplate='<b>%{x}</b><br>' + 'Кол-во SKU: %{customdata:.0f}<extra></extra>',),               secondary_y = True)  fig.add_trace(go.Bar(x=df['quarter'], y=df['revenue'],                       name='Выручка',                       customdata = df['revenue_mln'], #столбец с выручкой в миллионах руб.                      hovertemplate='<b>%{x}</b><br>' + 'Выручка: %{customdata:.2f} млн.руб.<extra></extra>'),                secondary_y = False)  fig.add_trace(go.Bar(x=df['quarter'], y=df['gross_profit_margin'],                       name='Валовая прибыль',                       customdata = df['gross_profit_margin_mln'], #столбец с вал. прибылью в миллионах руб.                      hovertemplate='<b>%{x}</b><br>' + 'Вал.прибыль: %{customdata:.2f} млн.руб.<extra></extra>'),                secondary_y = False)  gross_margin_line = [int(num * 4.8) for num in df['gross_margin'].tolist()] fig.add_trace(go.Scatter(x=df['quarter'], y=gross_margin_line,                           name='Рентабельность',                           #именно этот параметр отвечает за то, какте значения будут отображаться во всплывающих подсказках                          customdata=df['gross_margin'],                           hovertemplate='<b>%{x}</b><br>' + 'Маржинальность: %{customdata:.2f}%<extra></extra>',                          mode='lines',                           fill='tozeroy'),                secondary_y = True)  fig.update_layout(title=(f'Сколько SKU мы продаем и сколько зарабатываем'),                    title_x=0.1, title_y=0.95,                   height = 350, width = 650,                    xaxis=dict(tickfont_size=10),                   yaxis=dict(tickfont_size=10,                               side='left', range=[0, df['revenue'].max() + 20000000]),                   yaxis2=dict(showticklabels=False, side='right',                               showgrid=False, range=[df['SKU_count'].min()-20, df['SKU_count'].max() + 20]),                   legend_font=dict(size=11),                    legend=dict(orientation="h", yanchor="bottom", y=0.9, xanchor="left", x=0.02, bgcolor='rgba(255, 255, 255, 0)'),                   barmode='overlay',                   margin = dict(t=50, l=50, r=10, b=10)) fig.show()

Теперь всплывающие подсказки отображаются корректно.

Кастомизация визуализации

Теперь самое время приступить к улучшению внешнего вида моей визуализации и приблилизить ее к референсу. Разумеется, я не ставила себе целью полностью повторить референс (хотя Вы можете это сделать, используя приведенный код), поэтому полного совпадения не будет: выглядеть они будут все-же по-разному.
Мне необходимо сделать более плавными линии Line Chart и Area Chart, добавить для них значения, а также настроить цветовую схему.
Поехали!

Референс цветовой схемы. Найдено в Pinterest

Референс цветовой схемы. Найдено в Pinterest

Начнем с цветовой схемы. В качестве референса я использую идею из Pinterest.
Да-да))) Цветовая палитра очень девчачья. Но я девочка, а за окном весна.
Я задам цветовую палитру из шести цветов путем простого присваивания переменных, где значением переменной будет Hex Color Code нужного мне цвета.

Что я сделаю:
— настрою сглаживание линий Line Chart и Area Chart, добавлю визуализацию их значений и настрою красивый визуал области под графиком Area Chart;
— задам цвета для каждого из графиков, а также для заголовка, шрифтов легенды и тиков, фона;
— задам параметры сетки осей Y;
— настрою расстояние между столбцами Bar Chart.

fig = make_subplots(specs=[[{'secondary_y': True}]])  #Задаю переменные, которые хранят информацию о Hex Color Code используемых цветов. dark_1 = '#c46d86' light_1 = '#eab0bb' light_2 = '#f5f5f5' dark_2 = '#6c6c6c' dark_3 = '#26601c' light_3 = '#31a422' fig.add_trace(go.Scatter(x=df['quarter'], y=df['SKU_count'],                           name='Кол-во SKU',                           mode='lines+markers+text',                           customdata = df['SKU_count'],                           hovertemplate='<b>%{x}</b><br>' + 'Кол-во SKU: %{customdata:.0f}<extra></extra>',                          marker_color=light_3, #задаю цвет линии                          line=dict(shape='spline', #сглаживание линии                                    smoothing=0.9, # настраиваю степень сглаживания)                                    color = light_3), #настраиваю цвет маркера                          text = df['SKU_count'].tolist(), #отображающиеся на графике значения                          textposition='top center', #положение этих значений                          textfont=dict(size=9, color=dark_3, weight='bold')),#размер шрифта, его жирность и цвет                secondary_y = True)  fig.add_trace(go.Bar(x=df['quarter'], y=df['revenue'],                       name='Выручка',                       customdata = df['revenue_mln'],                       hovertemplate='<b>%{x}</b><br>' + 'Выручка: %{customdata:.2f} млн.руб.<extra></extra>',                       marker_color=light_1), #цвет столбца               secondary_y = False)  fig.add_trace(go.Bar(x=df['quarter'], y=df['gross_profit_margin'],                       name='Валовая прибыль',                       customdata = df['gross_profit_margin_mln'],                       hovertemplate='<b>%{x}</b><br>' + 'Вал.прибыль: %{customdata:.2f} млн.руб.<extra></extra>',                       marker_color=dark_1),  #цвет столбца               secondary_y = False)  gross_margin_line = [int(num * 4.8) for num in df['gross_margin'].tolist()] fig.add_trace(go.Scatter(x=df['quarter'], y=gross_margin_line,                           name='Рентабельность',                           marker_color=dark_3, #цвет линии                           customdata=df['gross_margin'],                           hovertemplate='<b>%{x}</b><br>' + 'Маржинальность: %{customdata:.2f}%<extra></extra>',                          mode='lines+text+markers',                           fill='tozeroy',                          text=[f"{val:.0f}%" for val in df['gross_margin']], #отображающиеся на графике значения со знаком '%'                          textposition='top center', #положение этих значений                          textfont=dict(size=9, color=dark_3, weight='bold'),                          #эта часть кода отвечает за настроку отображения области под графиком                          fillpattern=dict(shape='/',  # тип штриховки                                           fgcolor=dark_3,  # цвет линий штриховки                                           bgcolor="rgba(255, 255, 255, 0)"),  # цвет фона, я задаю его полностью прозрачным)                          line=dict(shape='spline', smoothing=0.6)), #эти параметры отвечают за сграживание линий                secondary_y = True) #настройка параметров сетки: настраиваю цвет и толщину линий, визуально убираю саму ось X: она есть, но ее не видно за счет того, что ее цвет совпадает с фоном fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor=dark_2, zerolinecolor=light_2)  fig.update_layout(title=(f'Сколько SKU мы продаем и сколько зарабатываем'),                    title_font=dict(size=16, color=dark_3, weight='bold'), #параметры шрифта заголовка                   title_x=0.1, title_y=0.95, height = 350, width = 650,                    xaxis=dict(tickfont_size=10, tickfont_color=dark_2), #настраиваю цвета тиков                   yaxis=dict(tickfont_size=10, tickfont_color=dark_2, #настраиваю цвета тиков                              side='left', range=[0, df['revenue'].max() + 20000000]),                   yaxis2=dict(showticklabels=False, side='right',                               showgrid=False, range=[df['SKU_count'].min()-20, df['SKU_count'].max() + 20]),                   legend_font=dict(size=11, color = dark_2), #настраиваю цвет текста легенды                   legend=dict(orientation="h", yanchor="bottom", y=0.9, xanchor="left", x=0.02, bgcolor='rgba(255, 255, 255, 0)'),                   barmode='overlay',                   margin = dict(t=50, l=50, r=10, b=10),                   bargroupgap=0.1, #добавлю "воздух" между столбцами                   paper_bgcolor = light_2, plot_bgcolor=light_2) #задам цвет фона fig.show()
Результат выполнения кода

Результат выполнения кода

Готово! Честно говоря, мне категорически не нравится одновременное отображение значений для чартов «Количество SKU» и «Рентабельность»: создается ощущение перегруженности. С учетом того, что на Plotly создаются интерактивные визуализации, необходимость визуализации всех значений непосредственно на графике уже не кажется такой очевидной. Поэтому я бы внесла такие изменения в код:

#удалить text=[f"{val:.0f}%" for val in df['gross_margin']],  textposition='top center', #положение этих значений textfont=dict(size=9, color=dark_3, weight='bold') #заменить mode='lines+text+markers', на mode='lines'

И приняла как финальный такой вариант:

Итоговый вариант визуализации

Итоговый вариант визуализации

Какой вариант с Вашей точки зрения лучше: с обилием отображаемых значений или насколько возможно минималистичный вариант?


ссылка на оригинал статьи https://habr.com/ru/articles/898970/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *