Диаграммы воронки зачастую используются для представления последовательного процесса. Они помогают смотрящему сравнивать и видеть, как цифры меняются от этапа к этапу.

В этой статье мы рассмотрим, как построить воронку с нуля с помощью Matplotlib, а затем рассмотрим более простую реализацию с помощью Plotly.

Диаграмма воронки. Иллюстрация автора
Диаграмма воронки. Иллюстрация автора

Matplotlib

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

import matplotlib.pyplot as plt
y = [5,4,3,2,1]
x = [80,73,58,42,23]
plt.barh(y, x)

Выглядит довольно близко к тому, чего мы добиваемся, так почему бы не остаться здесь и не отделаться простой гистограммой?

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

Хорошо, теперь нужно выстраивать по одному столбцу за раз и использовать параметр «left», чтобы отрегулировать их положение на диаграмме. Давайте проверим, как это будет работать.

y = [5,4,3,2,1]
x = [80,73,58,42,23]
x_max = 100
x_min = 0
for idx, val in enumerate(x):
    plt.barh(y[idx], x[idx], left = idx+5)
plt.xlim(x_min, x_max)

Теперь у нас есть размер горизонтальных столбцов, который равен x, и диапазон оси x, который равен 100.

Разница между этими значениями – это пустое пространство. Чтобы отцентрировать столбцы нам нужно одинаковое количество пустого пространства с каждой из сторон.

Итак, получается, что: 

left = (размер столбца – диапазон оси x) / 2

Давайте посмотрим, как это выглядит:

y = [5,4,3,2,1]
x = [80,73,58,42,23]
x_max = 100
x_min = 0
for idx, val in enumerate(x):
    left = (x_max - val)/2
    plt.barh(y[idx], x[idx], left = left, color='grey')
plt.xlim(x_min, x_max)

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

y = [5,4,3,2,1]
x = [80,73,58,42,23]
x_max = 100
x_min = 0
fig, ax = plt.subplots(1, figsize=(12,6))
for idx, val in enumerate(x):
    left = (x_max - val)/2
    plt.barh(y[idx], x[idx], left = left, color='grey', height=1, edgecolor='black')
    # value
    plt.text(50, y[idx], x[idx], ha='center', fontproperties=font,
             fontsize=16, color='#2A2A2A')
plt.axis('off')
plt.xlim(x_min, x_max)

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

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

Во-первых, давайте определим все необходимые нам переменные:

from matplotlib import font_manager as fm
# funnel chart
y = [5,4,3,2,1]
x = [80,73,58,42,23]
labels = ['Hot Leads', 'Samples Sent', 'Quotes', 'Negotiations', 'Sales']
x_max = 100
x_min = 0
x_range = x_max - x_min
fpath = "fonts/NotoSans-Regular.ttf"
font = fm.FontProperties(fname=fpath)

И добавим немного деталей.

fig, ax = plt.subplots(1, figsize=(12,6))
for idx, val in enumerate(x):
    left = (x_range - val)/2
    plt.barh(y[idx], x[idx], left = left, color='#808B96',
             height=.8, edgecolor='black')
    # label
    plt.text(50, y[idx]+0.1, labels[idx], ha='center', 
             fontproperties=font, fontsize=16, color='#2A2A2A')
    # value
    plt.text(50, y[idx]-0.3, x[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    
plt.xlim(x_min, x_max)
plt.axis('off')
plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')
plt.show()

*Шрифт взят отсюда: https://fonts.google.com/specimen/Noto+Sans

Проведем линию от одной стороны столбца к другой. Мы можем использовать значение left, чтобы найти начало и конец столбца, а значение y, чтобы найти его центр.

fig, ax = plt.subplots(1, figsize=(12,6))
for idx, val in enumerate(x):
    left = (x_range - val)/2
    plt.barh(y[idx], x[idx], left = left, color='#808B96', height=.8, edgecolor='black')
    # label
    plt.text(50, y[idx]+0.1, labels[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    # value
    plt.text(50, y[idx]-0.3, x[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    
    plt.plot([left, 100-left], [y[idx], y[idx]])
plt.xlim(x_min, x_max)
plt.axis('off')
plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')
plt.show()

А теперь подвинем эту линию в нижнюю часть столбца, а для последнего – ее не будем рисовать вовсе, поскольку там соединения не будет. 

Высота столбца будет равна 0.8, поэтому, чтобы переместить линию вниз, нужно уменьшить y до 0.4.

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

fig, ax = plt.subplots(1, figsize=(12,6))
for idx, val in enumerate(x):
    left = (x_range - val)/2
    plt.barh(y[idx], x[idx], left = left, color='#808B96', height=.8, edgecolor='black')
    # label
    plt.text(50, y[idx]+0.1, labels[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    # value
    plt.text(50, y[idx]-0.3, x[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    
    if idx != len(x)-1:
        next_left = (x_range - x[idx+1])/2
        plt.plot([left, 100-left], 
                 [y[idx]-0.4, y[idx]-0.4])
        plt.plot([next_left, 100-next_left], 
                 [y[idx+1]+0.4, y[idx+1]+0.4])
plt.xlim(x_min, x_max)
plt.axis('off')
plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')
plt.show()

Мы нашли все точки, которые нужно отрисовать. Теперь можно соединить эти точки и посмотреть, получим ли мы нужную форму.

Не забудьте продублировать первую точку в самом конце, чтобы замкнуть многоугольник.

fig, ax = plt.subplots(1, figsize=(12,6))
for idx, val in enumerate(x):
    left = (x_range - val)/2
    plt.barh(y[idx], x[idx], left = left, color='#808B96', height=.8, edgecolor='black')
    # label
    plt.text(50, y[idx]+0.1, labels[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    # value
    plt.text(50, y[idx]-0.3, x[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    
    if idx != len(x)-1:
        next_left = (x_range - x[idx+1])/2
        shadow_x = [left, next_left, 
                    100-next_left, 100-left, left]
        
        shadow_y = [y[idx]-0.4, y[idx+1]+0.4, 
                    y[idx+1]+0.4, y[idx]-0.4, y[idx]-0.4]
        plt.plot(shadow_x, shadow_y)
plt.xlim(x_min, x_max)
plt.axis('off')
plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')
plt.show()

Идеально. Осталось поменять .plot на .fill и диаграмма готова.

fig, ax = plt.subplots(1, figsize=(12,6))
for idx, val in enumerate(x):
    left = (x_range - val)/2
    plt.barh(y[idx], x[idx], left = left, 
             color='#808B96', height=.8)
    # label
    plt.text(50, y[idx]+0.1, labels[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    # value
    plt.text(50, y[idx]-0.3, x[idx], ha='center',
             fontproperties=font, fontsize=16, color='#2A2A2A')
    
    if idx != len(x)-1:
        next_left = (x_range - x[idx+1])/2
        shadow_x = [left, next_left, 
                    100-next_left, 100-left, left]
        
        shadow_y = [y[idx]-0.4, y[idx+1]+0.4, 
                    y[idx+1]+0.4, y[idx]-0.4, y[idx]-0.4]
        plt.fill(shadow_x, shadow_y, color='grey', alpha=0.6)
plt.xlim(x_min, x_max)
plt.axis('off')
plt.title('Beskar Forging Services Inc.', fontproperties=font, loc='center', fontsize=24, color='#2A2A2A')
plt.show()

Вот и оно!

Рисование воронки с Matplotlib может очень быстро перерасти из простой задачи в сложную. 

Но в этом и есть особая прелесть Matplotlib, ведь мы можем нарисовать практически все, что душе угодно.

Plotly

А теперь перейдем к более простому способу достичь тех же результатов, и нарисуем воронку с помощью Plotly.

import plotly.express as px
data = dict(values=[80,73,58,42,23],
            labels=['Hot Leads', 'Samples Sent', 
                    'Quotes', 'Negotiations', 'Sales'])
fig = px.funnel(data, y='labels', x='values')
fig.show()

Потрясающе. У Plotly есть метод для построения диаграмм-воронок, поэтому нам нужны лишь данные, все остальное сделают за нас.

Также у Plotly есть множество опций для кастомизации диаграмм.

Возможно, здесь вы не так хорошо контролируете визуализацию, как до этого, но строить воронку так гораздо удобнее.

data = dict(Quantity=[80, 73, 58, 42, 23,
                      180, 120, 82, 51, 33,
                      109, 78, 62, 44, 22],
    
            Stage=['Hot Leads', 'Samples Sent', 'Quotes',
                   'Negotiations', 'Sales']*3,
    
            Location=['Tatooine']*5 + ['Mandalore']*5 + ['Nevarro']*5)
    
fig = px.funnel(data, y='Stage', x='Quantity', color='Location',
                color_discrete_map={"Tatooine": "#374B53", 
                                    "Mandalore": "#617588",
                                    "Nevarro": "#A4B7C8"},
                template="simple_white",
                title='Beskar Forging Services Inc.',
                labels={"Stage": ""})
fig.show()

И все! Мы посмотрели, как построить воронку с нуля с помощью Matplotlib, и увидели насколько можно усложнить простую визуализацию. А еще мы проверили более удобный способ построения диаграммы-воронки с помощью Plotly и познакомились с некоторыми вариациями ее кастомизации.

Спасибо, что прочитали. Надеюсь, вам понравилось.


Материал подготовлен в рамках курса «Python для аналитики».

Всех желающих приглашаем на demo-занятие «Построение графиков при помощи популярных Python библиотек». Чтобы построить наиболее информативный аналитический отчет, иногда требуются использование множества графиков. На открытом уроке мы рассмотрим построение различных видов графиков на python с использованием популярных библиотек.
>> РЕГИСТРАЦИЯ

Комментарии (0)