
Диаграммы воронки зачастую используются для представления последовательного процесса. Они помогают смотрящему сравнивать и видеть, как цифры меняются от этапа к этапу.
В этой статье мы рассмотрим, как построить воронку с нуля с помощью 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 с использованием популярных библиотек.
>> РЕГИСТРАЦИЯ
