Время прочтения: 6 мин.

Порой, полученные графы необходимо передать профильному специалисту, на компьютере которого нет специализированных программ для просмотра (python, graphviz, proM  и т.д), в связи с чем встал вопрос о разработке приложения, поддерживающего методы Process Mining и работающем для просмотра и взаимодействия с полученными графами на любом компьютере. Решение – написать веб-приложение. Лог файл методами Process Mining будет обрабатываться на компьютере IT-специалиста с помощью python, а вот в построении графа процесса нам поможет Дракула! Но не спешите нести осиновые колья, это всего лишь javascript библиотека!

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

В начале работы импортируем необходимую для обработки табличных данных библиотеку Pandas:

import pandas as pd

Считаем данные из нашего excel-файла, указав имя листа для чтения и требуемую кодировку (указание кодировки не является обязательным, но поможет избежать проблем с отображением информации в дальнейшем):

df = pd.read_excel('log.xlsx', sheet_name='data', encoding='utf-8')
df.head()

Далее необходимо отсортировать данные нашего лог-файла по id случая и дате событий для каждого из id (так же необходимо найти список уникальных id, для дальнейшей работы):

sort_df = df.sort_values(['case_id','data'], ascending=True)
unique_ids = sort_df['case_id'].unique().tolist()

Далее сформирмируем из наших данных цепочки событий, как показано на рисунке ниже:

dict_df = {'case_id':[],'event_1':[],'event_2':[],'time_delta':[]}
for unique_id in unique_ids:
    sort = sort_df[sort_df['case_id'].isin([unique_id])]
    events = sort['event'].values
    dates = sort['data'].values
    dict_df['case_id'].append(unique_id)
    dict_df['event_1'].append("начало процесса")
    dict_df['event_2'].append(events[0])
    dict_df['time_delta'].append(pd.Timedelta(dates[0]-dates[0]).total_seconds())
    for i, event in enumerate(events[:-1]):
        dict_df['case_id'].append(unique_id)
        dict_df['event_1'].append(event)
        dict_df['event_2'].append(events[i+1])
        dict_df['time_delta'].append(pd.Timedelta(dates[i+1]-dates[i]).total_seconds())        
    dict_df['case_id'].append(unique_id)
    dict_df['event_1'].append(events[-1])
    dict_df['event_2'].append("конец процесса")
    dict_df['time_delta'].append(pd.Timedelta(dates[-1]-dates[-1]).total_seconds())        
new_df = pd.DataFrame.from_dict(dict_df)

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

dup = new_df.drop_duplicates(subset = ['event_1','event_2'])

Далее, определим количество переходов между нашими событиями:

rows_df = new_df.groupby(['event_1','event_2']).size().reset_index(name="rows_count")
rows_df.head()

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

def to_days(s):
    return s/86400
mean_time = new_df.groupby(['event_1','event_2'])[['time_delta']].mean()
mean_time['time_delta'] = mean_time['time_delta'].apply(to_days)
mean_time.head()

С помощью метода apply мы можем применить указанную функцию to_days, к элементам из указанного столбца (в данном случае преобразуем данные столбца time_delta из секунд в дни).

Сформируем итоговый DataFrame объединив все наши данные с помощью метода merge, в качестве ключей для объединения будем использовать наши «цепочки».

res_df=pd.merge(pd.merge(dup,rows_df,on=['event_1','event_2']), mean_time,on=['event_1','event_2'])

Из полученного DataFrame формируем js-файл с двумя переменными:

json1 – список из событий, которые будут использоваться для рисования графа.
json2 – полная информация о цепочках событий.
json1, json2 = {},{}
json1['events'] = df['event'].unique().tolist()
json2['events'] = []
for index, row in res_df.iterrows():
    obj = {}
    obj['case_id'] = row['case_id']
    obj['event1'] = row['event_1']
    obj['event2'] = row['event_2']
    obj['count_edges'] = row['rows_count']
    obj['mean_time'] = row['time_delta_y']
    json2['events'].append(obj)
json_file = open('events.js', 'w', encoding='utf-8')
json_file.write('json1={0}\njson2={1}'.format(str(json1),str(json2)))
json_file.close()

Итоговый js-файл будет содержать переменные:

Далее, полученный js-файл будет использоваться для построения графа в будущем веб-приложении. Приступим к этапу работы с Dracula.

Dracula.js – это набор инструментов для отображения и компоновки интерактивных связанных графов и сетей, а также различных связанных алгоритмов из области теории графов. Название библиотеки навеяно игрой слов – Граф Дракула (Dracula Graph Library).

 Cоздадим html-файл со структурой, указанной ниже:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script type="text/javascript" src="js/raphael-min.js"></script>
    <script type="text/javascript" src="js/dracula_graffle.js"></script>
    <script type="text/javascript" src="js/dracula_graph.js"></script>
    <script type="text/javascript" src="events.js"></script>
    <script type="text/javascript" src="graph_maker.js"></script>
<style>
    #canvas{
        border-width:2px;
        border-color:black;
        border-style:solid;
    }
</style>
</header>
<body>
    <div id="canvas"></div>
    <button id="redraw" onclick="redraw();">redraw</button>
</body>

Методы, необходимые для формирования графа и его отображения на рабочей области, будут храниться в файле graph_maker.js.

var redraw;
/*

Для отрисовки графа  задаём функцию, которая будет выполняться при загрузке страницы в браузере (window.onload)

*/
window.onload = function() {
/*

Ширина и высота рабочей области в этом примере задается исходя из ширины и высоты страницы в браузере.

*/
width=document.body.clientWidth;  
height=document.body.clientHeight;
//Инициализируем объект будущего графа
var g = new Graph();
//Задаем функцию отрисовки 
var renderer = function(r, n) {
var set = r.set().push(
*/

r.rect задает свойства прямоугольной области, в которой будет храниться информация о событии (размер, текст внутри области, цвет заливки и т.д).

        r.rect(n.point[0]-100, n.point[1]-13, 200, 40).attr({"fill": "#fa8", "stroke-width": 2, r : "9px"})).push(
        r.text(n.point[0], n.point[1] + 10, n.label).attr({"font-size":"10px"}));
    set.items.forEach(function(el) {el.tooltip(r.set().push(r.rect(0, 0, 70, 70).attr({"fill": "#fec", "stroke-width": 1, r : "9px"})))});
    return set; };

В цикле перебираем элементы из списка json1 и создаём вершины нашего графа, используя функцию addNode

for (var i=0; i<json1['events'].length; i++){
    event = json1['events'][i];
    g.addNode(event, {label:event, render:renderer});}

Перебираем элементы из переменной json2, соединяем ребрами вершины графа, согласно цепочкам событий и указываем информацию о них (количество переходов, среднее время переходов)

for (var j=0; j<json2['events'].length; j++)
{
    event1 = json2['events'][j]['event1'];
    event2 = json2['events'][j]['event2'];
    mean_time = json2['events'][j]['mean_time'];
    count_edges = json2['events'][j]['count_edges'];
g.addEdge(event1, event2, { stroke : "#bfa" , fill : "#56f", directed : true, label : count_edges});
}  

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

var layouter = new Graph.Layout.Spring(g);
layouter.layout();
var renderer = new Graph.Renderer.Raphael('canvas', g, width, height);
renderer.draw();
redraw = function() {
    layouter.layout();
    renderer.draw();
};};

Открыв наш html-файл в браузере, мы сможем увидеть результат:

Стоит отметить, что изображенные на холсте объекты поддерживают технологию «Drag-and-drop», что позволяет изменять вид полученного графа в реальном времени. Также код в файле graph_maker.js может быть изменён в зависимости от требований к представлению процесса, при этом формирование нового графа будет произведено сразу после обновления страницы браузера.

В итоге у нас получилось разработать приложение, после работы которого может быть сформирован набор скриптов, которые можно переслать профильному сотруднику для просмотра и анализа, не задумываясь о средствах просмотра на его компьютере.