Пишем приложение на Python для интерактивной визуализации графов с NetworkX, Plotly и Dash

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Говорят, хорошая визуализация данных лучше тысячи слов о них, и с этим трудно спорить.

Эта статья посвящена написанию приложения на Python для интерактивной визуализации графов. В первой части представлен краткий обзор использованных средств и библиотек, а также свойства приложения. Во второй половине — технические детали, касающиеся использования NetworkX, Plotly и Dash, и собственно код.

1. Свойства приложения для визуализации графов

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

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

Кроме того, это интерактивное приложение. Когда пользователь наведёт мышь на вершину или ребро графа, будет показана дополнительная информация об элементе. Также пользователь сможет выбрать аккаунт и временной промежуток, по которым будут отображаться данные.

2. Введение в используемые средства

2.1 Теория графов и NetworkX

Граф, описывающий сеть транзакций, состоит из вершин (которым соответствуют аккаунты-участники транзакций) и рёбер (которым соответствуют сами транзакции). У вершин есть такие свойства, как имя пользователя и тип аккаунта, а у ребер — время совершения и сумма операции. Таким образом, сеть транзакций является ориентированным графом, где каждое ребро направлено от отправителя к получателю.

NetworkX — это Python-библиотека для создания, изменения и изучения структуры графов. Она позволяет построить и визуализировать граф всего за несколько строк кода:

import networkx as nx
import matplotlib.pyplot as plt
G = nx.Graph()
G.add_edge(1,2)
G.add_edge(1,3)
nx.draw(G, with_labels=True)
plt.show()

Помимо построения простых графов с данными, заданными прямо в коде, NetworkX также поддерживает импорт из баз данных и .csv-файлов (что и будет использовано далее).

2.2 Интерактивная визуализация и Plotly

Для Python создано немало полезных библиотек для визуализации данных. Но, в отличие от статичных Matplotlib и Seaborn, Plotly создает интерактивные графики. Этот пакет поддерживает множество распространенных типов графов и диаграмм. При использовании ipywidgets, Plotly также позволяет отображать интерактивные графики прямо в Jupyter Notebook.

2.3 Создание веб-приложения и Dash

Jupyter Notebook — популярный инструмент у аналитиков и data scientist'ов, но в некоторых случаях визуализация данных должна быть доступна менеджерам, заказчикам и другим заинтересованным лицам, которые не обязательно имеют опыт в работе с кодом и необходимое окружение на компьютере. В таком случае хорошим выбором оказывается написание веб-приложения, которое будет доступно прямо в браузере любому пользователю.

Это легко сделать при помощи опенсорс-фреймворка Dash, созданного для быстрого написания реактивных веб-приложений. Dash обеспечивает интеграцию кода для анализа данных с фронтенд-частью на HTML, CSS и JavaScript с минимальными усилиями.

Так как Dash построен на популярных фреймворках Flask для бэкенда и React.js для фронтенда, у него большое комьюнити в Интернете. Отдельно стоит отметить, что Dash полностью совместим с Plotly, поэтому интерактивный график сети транзакций легко станет компонентом Dash-приложения, интерфейс которого будет дополнен другими компонентами для взаимодействия пользователя с кодом для анализа данных.

3. Написание кода

Перейдем же к программированию!

3.1 Инициализация веб-приложения

Так как в основе Dash лежит Flask, совсем не удивительно, что синтаксис для запуска приложения очень похож.

import dash
import dash_core_components as dcc
import dash_html_components as html
import networkx as nx
import plotly.graph_objs as go
import pandas as pd
from colour import Color
from datetime import datetime
from textwrap import dedent as d
import json

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Transaction Network"

if __name__ == '__main__':
    app.run_server(debug=True)

3.2 Написание пользовательского интерфейса приложения

HTML-разметка и интерактивные компоненты для веб-приложения легко интегрируются с Python-кодом для анализа данных с помощью интерфейсов dash_html_components и dash_core_components. В данном веб-приложении для дизайна используется Bootstrap grid system, а из возможных интерактивных элементов задействованы следующие:

  • RangeSlider для задания временного промежутка, за который рассматриваются данные;

  • Input для того, чтобы пользователь мог задать название аккаунта для поиска;

  • собственно Plotly graph для отображения поля с графом сети транзакций;

  • Hover box и Click box для отображения детальной информации при наведении курсора/клике соответственно на элемент графа. На самом деле, как видно из кода ниже, это не отдельные библиотечные компоненты, а два поля типа Markdown, в которые отправляются соответствующие данные.

Пользовательский интерфейс нашего приложения и его элементы
Пользовательский интерфейс нашего приложения и его элементы
app.layout = html.Div([
    html.Div([html.H1("Transaction Network Graph")],
             className="row",
             style={'textAlign': "center"}),
    
    html.Div(
        className="row",
        children=[
            html.Div(
                className="two columns",
                children=[
                    dcc.Markdown(d("""
                            **Time Range To Visualize**
                            Slide the bar to define year range.
                            """)),
                    html.Div(
                        className="twelve columns",
                        children=[
                            dcc.RangeSlider(
                                id='my-range-slider',
                                min=2010,
                                max=2019,
                                step=1,
                                value=[2010, 2019],
                                marks={
                                    2010: {'label': '2010'},
                                    2011: {'label': '2011'},
                                    2012: {'label': '2012'},
                                    2013: {'label': '2013'},
                                    2014: {'label': '2014'},
                                    2015: {'label': '2015'},
                                    2016: {'label': '2016'},
                                    2017: {'label': '2017'},
                                    2018: {'label': '2018'},
                                    2019: {'label': '2019'}
                                }
                            ),
                            html.Br(),
                            html.Div(id='output-container-range-slider')
                        ],
                        style={'height': '300px'}
                    ),
                    html.Div(
                        className="twelve columns",
                        children=[
                            dcc.Markdown(d("""
                            **Account To Search**
                            Input the account to visualize.
                            """)),
                            dcc.Input(id="input1", type="text", placeholder="Account"),
                            html.Div(id="output")
                        ],
                        style={'height': '300px'}
                    )
                ]
            ),
            html.Div(
                className="eight columns",
                children=[dcc.Graph(id="my-graph",
                                    figure=network_graph(YEAR, ACCOUNT))],
            ),
            html.Div(
                className="two columns",
                children=[
                    html.Div(
                        className='twelve columns',
                        children=[
                            dcc.Markdown(d("""
                            **Hover Data**
                            Mouse over values in the graph.
                            """)),
                            html.Pre(id='hover-data', style=styles['pre'])
                        ],
                        style={'height': '400px'}),
                    html.Div(
                        className='twelve columns',
                        children=[
                            dcc.Markdown(d("""
                            **Click Data**
                            Click on points in the graph.
                            """)),
                            html.Pre(id='click-data', style=styles['pre'])
                        ],
                        style={'height': '400px'})
                ]
            )
        ]
    )
])

3.3 Привязка к коду для обработки данных

Когда пользователь меняет значения RangeSlider или Input, соответствующим образом изменяется и граф, который показывается на основном поле для отображения.

Когда пользователь наводит курсор или кликает на вершину или ребро на графе, HoverBox/ClickBox отображает детальную информацию по выбранному аккаунту или транзакции. Для этого используются обратные вызовы.

@app.callback(
    dash.dependencies.Output('my-graph', 'figure'),
    [dash.dependencies.Input('my-range-slider', 'value'), dash.dependencies.Input('input1', 'value')])
def update_output(value,input1):
    YEAR = value
    ACCOUNT = input1
    return network_graph(value, input1)
    
@app.callback(
    dash.dependencies.Output('hover-data', 'children'),
    [dash.dependencies.Input('my-graph', 'hoverData')])
def display_hover_data(hoverData):
    return json.dumps(hoverData, indent=2)
    
@app.callback(
    dash.dependencies.Output('click-data', 'children'),
    [dash.dependencies.Input('my-graph', 'clickData')])
def display_click_data(clickData):
    return json.dumps(clickData, indent=2)

3.4 Отображение графа на поле с NetworkX и Plotly

Теперь перейдем к части кода, которая отвечает за построение графа сети транзакций. Начнем с импорта данных и трансформации строк с датами в питоновский тип Datetime.

edge1 = pd.read_csv('edge1.csv')
node1 = pd.read_csv('node1.csv')
edge1['Datetime'] = "" 
accountSet=set() 
for index in range(0,len(edge1)):
    edge1['Datetime'][index] = datetime.strptime(edge1['Date'][index], '%d/%m/%Y')
    if edge1['Datetime'][index].year<yearRange[0] or edge1['Datetime'][index].year>yearRange[1]:
        edge1.drop(axis=0, index=index, inplace=True)
        continue
    accountSet.add(edge1['Source'][index])
    accountSet.add(edge1['Target'][index])

Строим граф сети с помощью NetworkX:

G = nx.from_pandas_edgelist(edge1, 'Source', 'Target', ['Source', 'Target', 'TransactionAmt', 'Date'], create_using=nx.MultiDiGraph())
nx.set_node_attributes(G, node1.set_index('Account')['CustomerName'].to_dict(), 'CustomerName')
nx.set_node_attributes(G, node1.set_index('Account')['Type'].to_dict(), 'Type')
pos = nx.layout.shell_layout(G, shells)
for node in G.nodes:
    G.nodes[node]['pos'] = list(pos[node])

Код для описания вершин:

traceRecode = []
node_trace = go.Scatter(x=[], y=[], hovertext=[], text=[], mode='markers+text', textposition="bottom center",
                        hoverinfo="text", marker={'size': 50, 'color': 'LightSkyBlue'})

index = 0
for node in G.nodes():
    x, y = G.node[node]['pos']
    hovertext = "CustomerName: " + str(G.nodes[node]['CustomerName']) + "<br>" + "AccountType: " + str(
        G.nodes[node]['Type'])
    text = node1['Account'][index]
    node_trace['x'] += tuple([x])
    node_trace['y'] += tuple([y])
    node_trace['hovertext'] += tuple([hovertext])
    node_trace['text'] += tuple([text])
    index = index + 1
    
traceRecode.append(node_trace)

Теперь определим ребра при помощи Plotly. Это чуть сложнее, чем было с вершинами, поскольку цвет ребра должен отображать время транзакции (чем раньше, тем светлее ребро), а ширина ребра соответствовать сумме транзакции (чем больше, тем шире ребро).

colors = list(Color('lightcoral').range_to(Color('darkred'), len(G.edges())))
colors = ['rgb' + str(x.rgb) for x in colors]

index = 0
for edge in G.edges:
    x0, y0 = G.node[edge[0]]['pos']
    x1, y1 = G.node[edge[1]]['pos']
    weight = float(G.edges[edge]['TransactionAmt']) / max(edge1['TransactionAmt']) * 10
    trace = go.Scatter(x=tuple([x0, x1, None]), y=tuple([y0, y1, None]),
                       mode='lines',
                       line={'width': weight},
                       marker=dict(color=colors[index]),
                       line_shape='spline',
                       opacity=1)
    traceRecode.append(trace)
    index = index + 1

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

middle_hover_trace = go.Scatter(x=[], y=[], hovertext=[], mode='markers', hoverinfo="text",marker={'size': 20, 'color': 'LightSkyBlue'},opacity=0)
index = 0
for edge in G.edges:
    x0, y0 = G.node[edge[0]]['pos']
    x1, y1 = G.node[edge[1]]['pos']
    hovertext = "From: " + str(G.edges[edge]['Source']) + "<br>" + "To: " + str(
        G.edges[edge]['Target']) + "<br>" + "TransactionAmt: " + str(
        G.edges[edge]['TransactionAmt']) + "<br>" + "TransactionDate: " + str(G.edges[edge]['Date'])
    middle_hover_trace['x'] += tuple([(x0 + x1) / 2])
    middle_hover_trace['y'] += tuple([(y0 + y1) / 2])
    middle_hover_trace['hovertext'] += tuple([hovertext])
    index = index + 1
traceRecode.append(middle_hover_trace)

Наконец, опишем вид самого поля с графом:

figure = {
    "data": traceRecode,
    "layout": go.Layout(title='Interactive Transaction Visualization', showlegend=False, hovermode='closest',
                        margin={'b': 40, 'l': 40, 'r': 40, 't': 40},
                        xaxis={'showgrid': False, 'zeroline': False, 'showticklabels': False},
                        yaxis={'showgrid': False, 'zeroline': False, 'showticklabels': False},
                        height=600,
                        clickmode='event+select',
                        annotations=[
                            dict(
                                ax=(G.node[edge[0]]['pos'][0] + G.node[edge[1]]['pos'][0]) / 2,
                                ay=(G.node[edge[0]]['pos'][1] + G.node[edge[1]]['pos'][1]) / 2, axref='x', ayref='y',
                                x=(G.node[edge[1]]['pos'][0] * 3 + G.node[edge[0]]['pos'][0]) / 4,
                                y=(G.node[edge[1]]['pos'][1] * 3 + G.node[edge[0]]['pos'][1]) / 4, xref='x', yref='y',
                                showarrow=True,
                                arrowhead=3,
                                arrowsize=4,
                                arrowwidth=1,
                                opacity=1
                            ) for edge in G.edges]
                        )}

Демонстрация работы получившегося веб-приложения:

Полный код приложения опубликован автором в ее репозитории на GitHub и занимает 303 строки. Далее можно заняться развёртыванием приложения, например, при помощи Heroku.

Источник: https://habr.com/ru/articles/728256/


Интересные статьи

Интересные статьи

Граф — это форма визуализации, позволяющая показывать и анализировать отношения между сущностями. Например, рисунок ниже показывает вклад редакторов Википедии на различных языках энциклопедии в июле...
Статья о том, как не терять статистику по рекламным лидам, которые пришли к вам на сайт, и ушли с сайта в мобильное приложение. Переведем их из "органики" в рекламные лиды.Затронем темы- Генерация QR-...
Меня зовут Саша Хрущев и я технический директор IT-компании WINFOX. Расскажу, почему необходимо адаптировать приложения для Huawei AppGallery и как это сделать.
Все делают это. Ну ладно, не все, но большинство. Пишут скрипты, чтобы симулировать свои проекты на Verilog, SystemVerilog и VHDL. Однако, написание и поддержка таких скр...
После всех вычислений, приведенных в этой и этой публикациях, можно углубиться в статистический анализ и рассмотреть метод наименьших квадратов. Для этой цели используется библиотека statsmodels,...