Опубликовано: Сентябрь 03, 2024
Мы все привыкли к тому, что основным объектом анализа пользовательского поведения на сайте, как правило, является некая воронка, например воронка продаж:
- просмотрели товар,
- добавили в корзину,
- перешли к чекауту,
- просмотрели апсел,
- обновили корзину,
- перешли к оплате,
- оплатили (совершили транзакцию)
На каком-то этапе часть пользователей уходит с воронки и постепенно она сужается, т. е. количество купивших всегда будет меньше количества просмотревших товар. Это верно, но с другой стороны, такая картина побуждает рассматривать пользовательские траектории, как некую линейную картину, где движение происходит в одном направлении, что в действительности конечно же не так. Это хорошо видно при анализе переходов отдельных пользователей - иногда они могут совершать "странные" действия, например открыть меню сайта, а затем кликнуть по логотипу и перейти на главную, или же, открыть сразу несколько страниц сайта и сравнивать, изучать что-то и т. п. Слово "странные" здесь взято в кавычки, потому что если присмотреться, эти действия могут оказаться совсем не странными, а вполне объяснимыми с точки зрения реакции на то, как устроен сайт или приложение, его пользовательский интерфейс.
Такие переходы пользователей при детальном рассмотрении будут представлять собой не линейную картину, а скорее сеточку – network. Соответственно и анализировать их возможно нужно, как сеть, с помощью соответствующих инструментов. В Python – это пакет Networkx, предлагающий ряд интересных функция для анализа сетей. У него есть ограничения в плане возможностей по визуализации, поэтому нельзя сказать, что задача решается легко, но поэкспериментировать достаточно интересно. Данная статья и предлагает описание такого экспериментирования.
Два базовых понятия Networkx - это node и edge, что в случае пользовательских переходов будет соответственно страницей и переходом между страницами. Чтобы анализировать данные с помощью Networkx, их естественно вначале нужно подготовить. Нам нужен Pandas dataframe данных пользовательских переходов со следующей структурой:
предыдущая страница | страница | количество переходов всего |
"Количество переходов" — это количество всех внутрисессионых переходов, всех пользователей, к примеру за день, по данной траектории, т. е. от конкретной страницы (экрану) к другой конкретной странице (экрану). Повторную загрузку одной и той же страницы в данном примере мы не будем учитывать. Данную колонку мы назовем 'weight', поскольку это название применяется в Networkx для обозначения 'веса' грани (edge weight).
Вполне возможно, что изначально у вас будет таблица данных с такой структурой:
предыдущая страница | текущая страница |
А именно перечень всех отдельных переходов каждого пользователя и вам нужно будет предварительно обработать данные, например привести URL к упрощенному виду, убрав лишние параметры — нужно, чтобы все одинаковые страницы имели одинаковые URL, а затем уже посчитать вес, т. е. общее количество конкретного перехода.
Убрать или заменить ненужные параметры в URL в Pandas можно с помощью функции replace(). Подробнее на stackoverflow.
Сгруппировать одинаковые переходы, подсчитав их количество можно с помощью функции size(). Подробнее на stackoverflow.
Если вы хотите следовать шагам описанным далее, вы можете загрузить подготовленную мной таблицу пользовательских переходов в dataframe из данного файла csv с помощью команды
df = pd.read_csv('users_moves.csv')
Импортируем необходимые библиотеки:
import numpy as np
import pandas as pd
import networkx as nx
import matplotlib as mpl
import matplotlib.pyplot as plt
Основные базовые понятия присутствующие в Networkx, которые мы будем использовать — это node, edge и weight. Нодой (node) в нашем случае будет страница (ее путь в URL), гранью (edge) — переход пользователя со страницы на другую страницу, а параметр weight будет отражать количество переходов всего.
В качестве типа графа мы выберем DiGrath — граф в котором можно задавать направление граней, т. е. в нашем случае указать с кокой ноды на какую был переход.
Граф в Networkx создается на основе данных о нодах и/или гранях, в нашем случае мы создадим граф на основе данных из нашего датафрейма, применив соответствующий метод доступный в Networkx. Наши данные отсортированы так, что вначале идут наиболее заметные переходы. Посмотрим вначале на первые 30 строчек.
Создадим граф:
G = nx.convert_matrix.from_pandas_edgelist(df.head(30), 'prevpath', 'path',
edge_attr='weight', create_using=nx.DiGraph)
Визуализация в Networkx основана на Matplotlib. Выведем граф с использованием кругового расположения нодов. Смотрите комментарии в коде.
pos = nx.circular_layout(G)
fig, ax = plt.subplots(figsize=(8.5,5))
nodes = nx.draw_networkx(G, pos, node_size=150, node_color='0.8', edge_color = '0.5',
connectionstyle='arc3, rad=0.2', font_size = 7)
plt.margins(0.3,0) # чтобы пути страниц поместились в канвасе
plt.axis('off')
plt.savefig('net1.svg')
plt.show()
Как мы видим такой график не очень информативен и не удобен для восприятия.
Для большего удобства восприятия и информативности мы сделаем следующее:
В новом графике мы посмотрим уже на 50 первых строчек нашей таблицы данных, т. е. на 50 самых заметных переходов. Смотрите комментарии к блокам кода.
G = nx.convert_matrix.from_pandas_edgelist(df.head(50), 'prevpath', 'path',
edge_attr='weight', create_using=nx.DiGraph)
Для того, чтобы привязать вес грани, т. е. количество переходов к толщине линии, нам нужно нормализовать этот набор данных. В данном случае я не стремлюсь к точным пропорциям, а скорее к более эстетическому отображению, поэтому применим что-то вроде "псевдонормализации":
edge_width = [G[u][v]['weight'] for u,v in G.edges()]
norm_edge_width = (edge_width / np.linalg.norm(edge_width))*10
width = list(map(lambda x:(x if x>0.7 else 0.7), norm_edge_width))
Подробнее о нормализации массива в NumPy на stackoverfow.
# Сделаем копию нашего графа
G1 = G.copy()
# Выберем переходы за исключением тех, что имеют обратные (обратные убираем)
for v,u in G1.edges():
if G1.has_edge(u,v):
G1.remove_edge(u, v)
# Выберем остальные переходы (т.е. обраные)
G2 = nx.difference(G, G1)
# и добавим им значение их веса
for v,u in G2.edges():
G2[v][u]['weight'] = G[v][u]['weight']
Теперь визуально разделим прямые и обратные переходы. Прямые будут отображаться прямыми линиями, обратные — дугами. Раскрасим их в разные цвета.
connectionstyle = []
edge_colors=[]
for v,u in G.edges():
if G1.has_edge(v,u):
connectionstyle.append('arc3, rad=0')
edge_colors.append('#008b8b')
else:
connectionstyle.append('arc3, rad=0.4')
edge_colors.append('#dc143c')
Arrow = mpl.patches.ArrowStyle.CurveB(head_length=0.3, head_width=.4) #стиль стрелки
pos = nx.shell_layout(G) #расположение нодов
fig, ax = plt.subplots(figsize=(8,7))
edges = nx.draw_networkx_edges(G, pos, node_size=200, arrowsize=15, arrowstyle=Arrow, width=width)
# Выводим значение нодов с помощью отдельной функции plt.text для красоты отображения:
for p in pos:
if pos[p][0] < 0:
pos[p][0] -= 0.02
plt.text(pos[p][0],pos[p][1],s=p, bbox=dict(facecolor='#87CEFA', edgecolor='None', alpha=0.5),
horizontalalignment='right', fontsize=7)
else:
pos[p][0] += 0.02
plt.text(pos[p][0],pos[p][1],s=p, bbox=dict(facecolor='#87CEFA', edgecolor='None', alpha=0.5),
horizontalalignment='left', fontsize=7)
edge_labels = nx.get_edge_attributes(G2,'weight')
# Задаем параметры отображения линий, т.е. прямых и обратных переодов:
M = G.number_of_edges()
for i in range(M):
edges[i].set_connectionstyle(connectionstyle[i])
edges[i].set_color(edge_colors[i])
edges[i].set_alpha(0.7)
# Выводим посередине графика текст с информацией:
xmin, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
s = ' \n'.join([v+'->'+u+' '+str(w['weight']) for v,u,w in G2.edges.data()])
plt.text(0,-0.2, s='Обратные переходы пользователей \n\n'+s,
bbox=dict(facecolor='#dc143c', edgecolor='None', alpha=0.8),
fontsize=8, horizontalalignment='center', color='white')
plt.tight_layout()
plt.axis('off')
plt.savefig('net2.svg')
plt.show()
Очевидно, что данное отображение гораздо более привлекательно, чем первое. Да, если вы смотрите на этот график первый раз, скорее всего вы не увидите каких-либо инсайтов, и он вам будет непонятен. Но если это данные вашего сайта, который вы хорошо знаете, график возможно будет полезен. Хотелось бы еще раз отметить, что статья не является попыткой "конкурировать" с популярными системами визуализации, это скорее проба того, а что же данный инструмент может делать.