Углубленное введение в библиотеку python pm4py

Некоторое время назад я написал статью, описывающую некоторые полезные запросы, которые можно запустить по данным события потока кликов, чтобы собрать некоторые полезные идеи. Эту статью можно считать ее естественным преемником, поскольку (а) мы будем работать над набором данных аналогичного типа (поток кликов) и (б) с той же основной мотивацией (собрать некоторые идеи). Однако мы переключимся с грубой аналитики, такой как maximum_time_spent и most_played_podcasts в приложении, на более целостное представление о задействованном процессе - прямо с момента открытия приложения пользователями, к прослушиванию, оценке, совместному использованию и, наконец, закрытию / удалению приложения. .

Технологический майнинг

Process Mining можно рассматривать как визуальное отображение того, что пользователи делают в вашем приложении.

Process Mining можно использовать, чтобы нарисовать картину того, как пользователи перемещаются в приложении. Это может быть полезно для указания на:

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

и так далее….

Хотя в распоряжении имеется множество алгоритмов интеллектуального анализа процессов, некоторые из которых хорошо резюмированы в этой статье, наша команда из Podurama обнаружила, что Directly Follows Graph (DFG) и Heuristic Miner (HM) лучше всего подходят для определенных случаев использования. выше. Впоследствии это то, на чем мы сосредоточимся в этом уроке!

Непосредственно следует за графиком

DFG - хорошее первое средство для исследования данных о событиях. Основная идея, лежащая в основе них, довольно проста - это граф с узлами, которые соответствуют событиям / действиям, и направленными ребрами, которые соответствуют отношениям прямого следования. Например, событие соединения края e1 и e2 соответствует наблюдению за событием e1, за которым непосредственно следует событие e2. Граница, соединяющая два события, может быть украшена дополнительной информацией, например, сколько экземпляров было обнаружено, следовавших за этой границей, или сколько в среднем времени требуется для перехода от e1 к e2.

Эвристический майнер

HM - это популярный алгоритм интеллектуального анализа процессов, который используется для моделирования потока событий. По сравнению со своей альтернативой, основанной на DFG, HM пытается найти правила, которым следуют события. В частности, алгоритм может отфильтровывать выбросы и даже определять, когда два события (например, sleeping и snoring) являются одновременными. Следовательно, они не связаны на графике , хотя один может напрямую следовать за другим в журналах событий. В каком-то смысле HM лучше подходит для обработки шума в журналах и поиска общих конструкций (зависимости между двумя действиями).

Давайте посмотрим на них в действии!

Примечание. Прежде чем мы начнем кодирование, убедитесь, что библиотека pm4py была установлена ​​с использованием pip install pm4py.

Давайте перейдем к кодированию.

Установки

import pm4py
import pandas as pd
import numpy as np
from pm4py.objects.conversion.log import converter as log_converter
from pm4py.objects.log.util import dataframe_utils
from pm4py.algo.discovery.heuristics import algorithm as heuristics_miner
from pm4py.algo.discovery.dfg import algorithm as dfg_discovery
from pm4py.visualization.heuristics_net import visualizer as hn_visualizer
from pm4py.visualization.dfg import visualizer as dfg_visualization

Набор ежедневных рутинных данных

Прежде чем мы увидим результаты на реальном наборе данных, я собираюсь использовать (очень) небольшой набор данных фиктивных событий, потому что я хочу иметь возможность показывать промежуточные результаты на каждом этапе. Если хотите, то вот Блокнот Jupyter.

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

dummy = pd.DataFrame(np.array([
['A', '06/10/2021' , 'waking' ],
['A', '06/11/2021' , 'eating'],
['A', '06/12/2021' , 'sleeping'],
['B', '06/12/2021' , 'waking'],
['B', '06/13/2021' , 'eating'],
['B', '06/14/2021' , 'sleeping'],
['C', '06/06/2021' , 'waking'], 
['C', '06/11/2021' , 'sleeping'],
['C', '06/12/2021' , 'eating'],
['D', '06/15/2021' , 'eating'],
['D', '06/16/2021' , 'eating'],
['E', '06/15/2021' , 'eating'],
['E', '06/26/2021' , 'eating'],
['F', '06/11/2021' , 'waking'],
['F', '06/15/2021' , 'sleeping']]),
columns=['case:concept:name', 'time:timestamp', 'concept:name'])

Он содержит три основных столбца - case:concept:name, time:timestamp и concept:name. Они соответствуют имени пользователя, метке времени и типу события соответственно. Для простоты мы будем иметь дело с шестью пользователями - _15 _: _ 16_ и тремя типами событий, а именно waking, eating и sleeping.

Теперь вам может быть интересно - почему загадочные имена столбцов! Разве нельзя просто назвать их user_id, timestamp и event_type соответственно? Короткий ответ - потому что pm4py это нравится. Если ваши столбцы названы по-другому, я настоятельно рекомендую переименовать их, прежде чем продолжить дальнейший анализ.

Еще одна важная вещь, о которой следует помнить, - это то, что столбец отметки времени должен соответствовать формату MM/DD/YYYY. То есть пользователь A решил начать wake 10 июня, а затем eat 11 июня, а затем sleep 12 июня. Причина в том, что pm4py применяет некоторую предварительную обработку к столбцу отметки времени в наборе данных, таким образом выполняя автоматическое преобразование в формат YYYY-MM-DD HH:MM:SS+MS:MS. Для правильной работы этой предварительной обработки ожидается формат MM/DD/YYYY в журналах.

dummy = dataframe_utils.convert_timestamp_columns_in_df(dummy)
dummy.head()

Осторожно: не думайте, что pm4py автоматически попытается определить формат метки времени на основе значений в столбце. Он вслепую преобразует все в формат MM-DD-YYYY, что может привести к неправильному преобразованию некоторых временных меток!

Непосредственно следует за графиком

log = log_converter.apply(dummy)
variant=dfg_visualization.Variants.FREQUENCY
dfg = dfg_discovery.apply(log)
gviz = dfg_visualization.apply(dfg, log=log, variant=variant)
# display viz
dfg_visualization.view(gviz)

Несколько замечаний в DFG слева:

  • Поля относятся к типам событий и окрашиваются в тепловую карту в зависимости от значений внутри поля.
  • Значения в квадратных полях указывают, сколько строк в наборе данных содержит это конкретное событие. Например, event_type = waking наблюдается четыре раза в наборе данныхdummy (в строках 0, 3, 6 и 13).
  • Цифры на стрелках / краях указывают частоту, т. Е. Сколько экземпляров наблюдалось для этого конкретного направленного края. Например, цикл вокруг eating с надписью 2 на краю относится к двум экземплярам от пользователей D и E. Аналогично, в журналах есть только один экземпляр, где мы наблюдаем sleeping событие, за которым следует событие eating, то есть от пользователя C . Наконец, более яркая стрелка означает, что было найдено больше экземпляров, следующих по этому пути.

Примечание. Помните, что частота не соответствует уникальному количеству пользователей, следующих по этому пути. Другими словами, если бы у нас были две дополнительные строки данных для пользователя D в журнале, например:

dummy = pd.DataFrame(np.array([
.
.
.
['D', '06/16/2021' , 'eating'],
['D', '06/16/2021' , 'eating'],
.
.
.]),
columns=['case:concept:name', 'time:timestamp', 'concept:name'])

и если бы мы создали новый DFG, он бы выглядел так:

Обратите внимание, как цикл вокруг eating теперь показывает значение частоты как 4 (вместо 2), потому что теперь есть 4 экземпляра событияeating, за которым следует другое eating событие, одно от пользователя E и три от пользователя D.

Если вы хотите вывести среднее время, прошедшее (вместо частоты) поверх краев, требуется небольшая модификация кода, в которой мы вводим параметр вызывается variant для applyметода и устанавливает его значение как PERFORMANCE вместо FREQUENCY:

log = log_converter.apply(dummy)
variant=dfg_visualization.Variants.PERFORMANCE
dfg = dfg_discovery.apply(log, variant=dfg_discovery.Variants.PERFORMANCE)
gviz = dfg_visualization.apply(dfg, log=log, variant=variant)
# display viz
dfg_visualization.view(gviz)

Дерево продуктов выглядит очень похоже на предыдущее, однако края теперь соответствуют времени (в днях D и секундах s). Например, согласно DFG, в среднем требуется 6 дней, чтобы вернуться к еде после того, как вы только что поели, то есть eating->eating (цикл вокруг event = eating). Давайте проверим это с помощью набора данных dummy.

Там было найдено только два экземпляра для этой конкретной трассировки (по одному от пользователей D и E), и для первого пользователя требуется один день (15 июня - 16 июня), а у последнего - одиннадцать дней (15 июня - 26 июня), соответственно. Если взять среднее затраченное время, получается (1 + 11/2) = 6 дней.

Фильтрация краев в DFG

Иногда ваш DFG будет выглядеть как модель спагетти, особенно если вы имеете дело с большим количеством событий. В таких обстоятельствах мне нравится ограничивать количество ребер в дереве вывода, задав параметр MAX_NO_EDGES_IN_DIAGRAM. Кроме того, было бы полезно отметить начало и конец действий на основе журналов событий, установив параметры START_ACTIVITIES и END_ACTIVITIES соответственно.

log = log_converter.apply(dummy)
_, start_activities, end_activities = pm4py.discover_dfg(log)
variant=dfg_visualization.Variants.FREQUENCY
dfg = dfg_discovery.apply(log)
parameters = {
dfg_visualization.Variants.FREQUENCY.value.Parameters.MAX_NO_EDGES_IN_DIAGRAM: 3,    
dfg_visualization.Variants.FREQUENCY.value.Parameters.START_ACTIVITIES: start_activities,
dfg_visualization.Variants.FREQUENCY.value.Parameters.END_ACTIVITIES: end_activities
}
gviz = dfg_visualization.apply(dfg, log=log, variant=variant, parameters= parameters)
dfg_visualization.view(gviz)

Как видите, мы успешно удалили часть беспорядка, сохранив только те края, которые имеют наибольший вес. Есть два дополнительных узла - зеленый и оранжевый. Эти кружки соответствуют начальному и конечному состоянию, соответственно, последовательности операций. Это можно интерпретировать как - 4 случая наблюдались с waking в качестве первого вызванного события. Глядя на dummydataset, мы можем убедиться, что это верно для пользователей A, B, C и F.

Помните: даже если может показаться, что на приведенном выше графике более трех ребер, обратите внимание, что все ребра из start_activity и все ребра в end_activity исключены. от процесса фильтрации. Исключая их, мы можем подтвердить, что присутствует только 3 ребра: waking->eating, waking->sleeping и eating->sleeping.

Эвристический майнер

Чтобы установить, есть ли какой-то поток событий, который можно смоделировать для ежедневных рутинных журналов, давайте сгенерируем эвристическую сеть.

log = log_converter.apply(dummy)
heu_net = heuristics_miner.apply_heu(log)
gviz = hn_visualizer.apply(heu_net)
hn_visualizer.view(gviz)

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

Согласно вышеприведенной эвристической сети, в типичном повседневном рабочем процессе пользователь начинает свой день с waking или eating. В случае последнего, это единственное действие, которое они делают в течение дня до его окончания (оранжевый кружок), тогда как в случае первого они решают либо sleep, либо eat, прежде чем завершить свой распорядок дня.

Если вы внимательно заметили, алгоритм учел тот факт, что sleeping и eating являются параллельными действиями, и, следовательно, между ними нет края.

Фильтрация краев в HM

Часто HN становится слишком беспорядочным для того, чтобы предоставлять какие-либо полезные идеи. В таких обстоятельствах мы можем фильтровать и сохранять только некоторые ребра на основе минимального значения частоты, сохраняя при этом все узлы (квадратные блоки) нетронутыми. Например, чтобы оставить в эвристической сети только те ребра, в которых встречается не менее 4 DFG, мы введем параметрMIN_DFG_OCCURRENCES. Поскольку есть только одно ребро START->waking со стрелкой весом 4 или более, это должно быть единственное ребро, присутствующее в выходном графе HN:

log = log_converter.apply(dummy)
heu_net = heuristics_miner.apply_heu(log, parameters={ heuristics_miner.Variants.CLASSIC.value.Parameters.MIN_DFG_OCCURRENCES: 4})
gviz = hn_visualizer.apply(heu_net)
hn_visualizer.view(gviz)

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

Набор данных подкастов

В Podurama мы были заинтересованы в использовании процесса интеллектуального анализа данных для решения двух особенно интересных проблем: (а) истощение пользователей и (б) рекомендация по холодному запуску. Что касается первого, мы решили провести обнаружение процессов у (около 4 тысяч) пользователей, которые покинули приложение в течение 15 дней после установки, и посмотреть, сможем ли мы сделать некоторые выводы. Для последнего мы создали DFG, используя журналы активных пользователей, чтобы визуально проверить поток подкастов, которые они слушают в рамках определенного жанра.

Убыток пользователей

Вместо того, чтобы исследовать все зарегистрированные события, мы решили сосредоточиться на основных, таких как app_open, search, podcastseries_click и app_remove.

Вот как выглядят первые несколько строк данных о событиях:

Создание DFG без обрезки краев привело к модели спагетти:

Установив максимальное количество ребер равным 10, мы наблюдали:

С первого взгляда стало ясно, что существует подавляющее большинство случаев, когда функция поиска использовалась неоднократно. Однако, несмотря на то, что многие пользователи ищут подкасты, то есть событие search (~ 31k), есть только несколько случаев (~ 8k), где непосредственно следует щелчок по любому из результатов поиска. то есть событие podcastSeriesClick. Точно так же мы наблюдаем очень мало случаев (~ 6k) воспроизведения подкаста (playAudio) после открытия приложения (app_open).

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

Еще одно заслуживающее внимания наблюдение касается достижения конечного состояния (оранжевый кружок) в потоке процесса. Мы заметили, что большинство экземпляров достигают конечного состояния из события playAudio и очень мало - из события subscribePodcast. Это далеко не идеально, потому что это означает, что пользователи не подписываются на подкасты через наше приложение (что может указывать на долгосрочную приверженность нашему приложению) перед тем, как принять решение о выходе, а просто просматривают страницы, чтобы увидеть, приживается ли оно! Наша цель в целом состоит в том, чтобы новые пользователи подписались как минимум на 10 подкастов в первую неделю после присоединения к приложению, поскольку чем больше подкастов они подписываются, тем выше шансы, что они будут продолжать использовать приложение для прослушивания недавно выпущенных эпизодов. за подкасты, на которые они подписаны!

Рекомендации по холодному запуску

Хотя есть определенно более эффективные способы решения проблемы холодного запуска рекомендательных систем с использованием популярных библиотек Python, мы решили использовать DFG для визуализации подкастов, которые слушают пользователи. Основная идея проста: как только мы узнаем, что пользователь слушал подкаст A, попробуйте и порекомендуйте другие подкасты из того же жанра на основе выходного фронта с самой высокой частотой. в DFG.

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

Вот DFG для жанра настоящее преступление:

mask = (~usr_podcast['concept:name'].isna()) & (usr_podcast.genre == 'True Crime')
log = log_converter.apply(usr_podcast.loc[mask])
parameters = {
dfg_visualization.Variants.FREQUENCY.value.Parameters.MAX_NO_EDGES_IN_DIAGRAM: 10
}
variant=dfg_visualization.Variants.FREQUENCY
dfg = dfg_discovery.apply(log)
gviz = dfg_visualization.apply(dfg, parameters=parameters, variant=variant)
# display viz
dfg_visualization.view(gviz)

Из приведенного выше графика видно, что существует гораздо больше случаев, когда Crime Junkie становится первым подкастом о преступлениях, который слушают пользователи. Это понятно, поскольку у нас есть рекомендация на домашней странице для вновь присоединившихся пользователей. Судя по весу краев (чем темнее, тем лучше), кажется, довольно много случаев, когда пользователи слушают Casefile True Crime и Нераскрытые убийства после Crime Junkie. Это полезная информация, так как теперь из всех подкастов о настоящих преступлениях в нашей базе данных мы можем рекомендовать эти два (наряду с другими, такими как My Favorite Murder и The Generation Why) для пользователей с ограниченной историей просмотров, для которых рекомендательные системы могут не давать полезных рекомендаций.

Еще один пример из другого жанра: вот как выглядят подкасты DFG для технологий:

Заключение

Довольно сложно поддерживать взаимодействие пользователей с вашим приложением, особенно когда 80–90% приложений удаляются в течение недели после их использования. Оценка последовательности событий от пользователей даст вам свежий взгляд на то, почему приложение не работает для них. Опросы тоже помогают, но могут показаться навязчивыми для пользователей, которые почти неделю не заходили в ваше приложение!

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

До скорого :)