Время прочтения: 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), поставленную задачу удалось выполнить, несмотря на отсутствие необходимого функционала в профильных библиотеках.