Время прочтения: 4 мин.
GraphMining (далее –GM) – одно из направлений анализа данных, которое позволяет представить комплексные данные в виде графов.
В Python наиболее популярными библиотеками для GM являются NetworkX, pyviz и graph-tool. С их помощью можно формировать и кастомизировать различные виды графов, а, так же, вычислять множество метрик для анализа. Однако, есть проблема: стандартные библиотеки GM не работают с картами, а библиотеки для работы с картами не формируют графы. На самом деле существует очевидное и простое решение, которое я опишу далее.
В начале – импорт необходимых библиотек:
import folium
import pandas as pd
import numpy as np
Допустим, что у меня имеется датасет с аггрегированной информацией о переводах от одного клиента другому:
data = pd.read_csv('data.csv', sep = ';')
Id_send – идентификатор отправителя;
Id_recei – идентификатор получателя;
lat_send – широта точки местоположения клиента отправителя;
lon_send – долгота точки местоположения клиента отправителя;
lat_rec – широта точки местоположения клиента получателя;
lon_rec – долгота точки местоположения клиента получателя;
opers_cnt – количество операций по направлению от id_send к id_recei;
opers_sum – сумма операций по направлению от id_send к id_recei.
Предлагаю рассмотреть описание датасета:
В 75% строк количество операций от отправителя к получателю 5 или меньше. Отфильтрую данные, оставив наиболее сильные связи:
data_clean = data[data['opers_cnt']>5]
Далее, необходимо получить набор точек (nodes) с идентификаторами клиентов и их координатами и посчитать общую сумму операций у клиента – отправлений и поступлений:
data_senders = data_clean.rename(
columns = {'id_send':'id','lat_send':'lat','lon_send':'lon'})[['id','lat','lon','opers_sum']]
data_receivers = data_clean.rename(
columns = {'id_recei':'id','lat_rec':'lat','lon_rec':'lon'})[['id','lat','lon','opers_sum']]
nodes = (pd.concat([data_senders, data_receivers])
.groupby(['id','lat','lon'])['opers_sum']
.sum()
.reset_index())
Нормализую объем операций, данный столбец будет использоваться в качестве параметра размера точки:
nodes['opers_sum_scaled'] = (nodes['opers_sum']-nodes['opers_sum'].min()) / (nodes['opers_sum'].max()-nodes['opers_sum'].min())*20
Получаю датасет:
Обогащаю информацией о суммах отправлений и поступлений каждого идентификатора:
id_send_opers = (data_clean.groupby(['id_send'])['opers_sum'].sum()
.reset_index()
.rename(columns = {'id_send':'id','opers_sum':'send_sum'}))
id_rec_opers = (data_clean.groupby(['id_recei'])['opers_sum'].sum()
.reset_index()
.rename(columns = {'id_recei':'id','opers_sum':'rec_sum'}))
nodes = nodes.merge(id_send_opers, on ='id', how = 'left')
nodes = nodes.merge(id_rec_opers, on ='id', how = 'left')
nodes = nodes.fillna(0)
Получил всю необходимую информацию для нанесения точек на карту:
Далее эти точки необходимо соединить – формирую список ребер:
edges = (pd.DataFrame(np.unique(np.array(['-'.join(sorted(edge)) for edge in zip(for_edges['id_send'],for_edges['id_recei'])])))[0]
.str.split('-', expand = True).rename(columns=({0:'id_x', 1:'id_y'})))
coords_list = nodes[['id','lat','lon']]
edges = edges.merge(coords_list.rename(columns={'id':'id_x'}), on ='id_x', how = 'left')
edges = edges.merge(coords_list.rename(columns={'id':'id_y'}), on ='id_y', how = 'left')
Для визуализации буду использовать библиотеку folium, создаю карту:
wm = folium.Map(tiles='cartodbpositron', prefer_canvas = True, control_scale = True, disable_3d = True)
Наношу слой с точками на карту:
for index, row in nodes.iterrows():
pers_id = row[0]
lat = row[1]
lon = row[2]
wei = row[4]
op_sum_total = round(row[3]/1000)
op_sum_send = round(row[5]/1000)
op_sum_rec = round(row[6]/1000)
html = (
"ID: {pers_id}</br>"
"Суммарный объем операций: {op_sum_total} т.р.<br>"
"Отправлено: {op_sum_send} т.р.<br>"
"Получено: {op_sum_rec} т.р.<br>"
).format(pers_id=pers_id, op_sum_total=op_sum_total, op_sum_send=op_sum_send, op_sum_rec=op_sum_rec)
folium.CircleMarker([lat, lon],
radius=1,
color="#571e63",
weight=wei,
opacity = 0.6,
fill_opacity = 0,
popup= folium.Popup(html, max_width = 1000)
).add_to(wm)
Цикл бежит по строчкам из датасета, и на карте отмечается точка в зависимости от координат, указанных в строке. Также динамически определяется текст для pop-up, который будет отображаться при нажатии на точку, и размер ноды. В зависимости от задачи можно менять параметры, например, не задавать динамический размер точек (weight).
Результат:
Добавляю ребра:
for index, row in edges.iterrows():
folium.PolyLine([[float(row[2]), float(row[3])], [float(row[4]), float(row[5])]],
color="#571e63",
weight=0.5,
opacity = 0.5
).add_to(wm)
Полученный результат:
Представленное решение позволяет выявить точки концентрации клиентов и проследить связи между ними, однако, по своей природе оно является некой имитацией GM, и в большей степени предназначено для визуализации связей с точки зрения географии. Тем не менее, используя элементарные методы folium (CircleMarker и PolyLine), поставленную задачу удалось выполнить, несмотря на отсутствие необходимого функционала в профильных библиотеках.