Process Mining sans PM4PY





Il est très facile de créer un graphique à partir des journaux de processus. Les analystes ont actuellement à leur disposition une variété suffisante de développements professionnels, tels que Celonis, Disco, PM4PY, ProM, etc., destinés à faciliter l'étude des processus. Il est beaucoup plus difficile de trouver des écarts dans les graphiques, d'en tirer des conclusions correctes.



Que faire si certains développements professionnels qui ont fait leurs preuves et présentent un intérêt particulier ne sont pas disponibles pour une raison ou une autre, ou si vous souhaitez plus de liberté dans les calculs lorsque vous travaillez avec des graphiques? Est-il difficile d'écrire soi-même un mineur et de mettre en œuvre certaines des fonctionnalités nécessaires pour travailler avec des graphiques? Nous le ferons en pratique en utilisant les bibliothèques Python standard, implémenterons les calculs et donnerons, avec leur aide, des réponses à des questions détaillées qui pourraient intéresser les propriétaires de processus.



Je voudrais tout de suite faire une réserve sur le fait que la solution donnée dans l’article n’est pas une mise en œuvre industrielle. Il s'agit d'une tentative de commencer à travailler avec les journaux par vous-même à l'aide d'un code simple qui fonctionne clairement et qui facilite donc l'adaptation. Cette solution ne doit pas être utilisée sur le Big Data; cela nécessite un raffinement significatif, par exemple en utilisant des calculs vectoriels ou en modifiant l'approche de collecte et d'agrégation d'informations sur les événements.



Avant de créer un graphique, vous devez effectuer des calculs. Le calcul réel du graphique sera le même mineur mentionné précédemment. Pour effectuer le calcul, il est nécessaire de collecter des connaissances sur les événements - les sommets du graphe et les connexions entre eux et de les noter, par exemple, dans des livres de référence. Les références sont remplies en utilisant la procédure de calcul calc ( code sur github). Les références complétées sont passées en paramètres à la procédure de dessin des graphes (voir le code du lien ci-dessus). Cette procédure formate les données comme indiqué ci-dessous:



digraph f {"Permit SUBMITTED by EMPLOYEE (6255)" -> "Permit APPROVED by ADMINISTRATION (4839)" [label=4829 color=black penwidth=4.723857205400346] 
"Permit SUBMITTED by EMPLOYEE (6255)" -> "Permit REJECTED by ADMINISTRATION (83)" [label=83 color=pink2 penwidth=2.9590780923760738] 
"Permit SUBMITTED by EMPLOYEE (6255)" -> "Permit REJECTED by EMPLOYEE (231)" [label=2 color=pink2 penwidth=1.3410299956639813] 
…
start [color=blue shape=diamond] 
end [color=blue shape=diamond]}


et le transmet au moteur graphique Graphviz pour le rendu.



Commençons par créer et examiner des graphiques à l'aide du mineur implémenté. Nous répéterons les procédures de lecture et de tri des données, de calcul et de dessin de graphiques, comme dans les exemples ci-dessous. Par exemple, les journaux d'événements sont tirés des déclarations internationales du concours BPIC2020. Lien vers la compétition.



Nous lisons les données du journal, les trions par date et heure. Le format .xes était auparavant converti en .xlsx.



df_full = pd.read_excel('InternationalDeclarations.xlsx')
df_full = df_full[['id-trace','concept:name','time:timestamp']]
df_full.columns = ['case:concept:name', 'concept:name', 'time:timestamp']
df_full['time:timestamp'] = pd.to_datetime(df_full['time:timestamp'])
df_full = df_full.sort_values(['case:concept:name','time:timestamp'], ascending=[True,True])
df_full = df_full.reset_index(drop=True)


Calculons le graphique.



dict_tuple_full = calc(df_full)


Dessinons le graphique.



draw(dict_tuple_full,'InternationalDeclarations_full')


Après avoir terminé les procédures, nous obtenons le graphe de processus:







Puisque le graphe résultant n'est pas lisible, nous le simplifions.



Il existe plusieurs approches pour améliorer la lisibilité ou simplifier le graphique:



  1. utiliser le filtrage par poids de sommets ou de liens;
  2. se débarrasser du bruit;
  3. regrouper les événements par similitude de nom.


Prenons l'approche 3.



Créons un dictionnaire pour combiner des événements:



_dict = {'Permit SUBMITTED by EMPLOYEE': 'Permit SUBMITTED',
 'Permit APPROVED by ADMINISTRATION': 'Permit APPROVED',
 'Permit APPROVED by BUDGET OWNER': 'Permit APPROVED',
 'Permit APPROVED by PRE_APPROVER': 'Permit APPROVED',
 'Permit APPROVED by SUPERVISOR': 'Permit APPROVED',
 'Permit FINAL_APPROVED by DIRECTOR': 'Permit FINAL_APPROVED',
 'Permit FINAL_APPROVED by SUPERVISOR': 'Permit FINAL_APPROVED',
 'Start trip': 'Start trip',
 'End trip': 'End trip',
 'Permit REJECTED by ADMINISTRATION': 'Permit REJECTED',
 'Permit REJECTED by BUDGET OWNER': 'Permit REJECTED',
 'Permit REJECTED by DIRECTOR': 'Permit REJECTED',
 'Permit REJECTED by EMPLOYEE': 'Permit REJECTED',
 'Permit REJECTED by MISSING': 'Permit REJECTED',
 'Permit REJECTED by PRE_APPROVER': 'Permit REJECTED',
 'Permit REJECTED by SUPERVISOR': 'Permit REJECTED',
 'Declaration SUBMITTED by EMPLOYEE': 'Declaration SUBMITTED',
 'Declaration SAVED by EMPLOYEE': 'Declaration SAVED',
 'Declaration APPROVED by ADMINISTRATION': 'Declaration APPROVED',
 'Declaration APPROVED by BUDGET OWNER': 'Declaration APPROVED',
 'Declaration APPROVED by PRE_APPROVER': 'Declaration APPROVED',
 'Declaration APPROVED by SUPERVISOR': 'Declaration APPROVED',
 'Declaration FINAL_APPROVED by DIRECTOR': 'Declaration FINAL_APPROVED',
 'Declaration FINAL_APPROVED by SUPERVISOR': 'Declaration FINAL_APPROVED',
 'Declaration REJECTED by ADMINISTRATION': 'Declaration REJECTED',
 'Declaration REJECTED by BUDGET OWNER': 'Declaration REJECTED',
 'Declaration REJECTED by DIRECTOR': 'Declaration REJECTED',
 'Declaration REJECTED by EMPLOYEE': 'Declaration REJECTED',
 'Declaration REJECTED by MISSING': 'Declaration REJECTED',
 'Declaration REJECTED by PRE_APPROVER': 'Declaration REJECTED',
 'Declaration REJECTED by SUPERVISOR': 'Declaration REJECTED',
 'Request Payment': 'Request Payment',
 'Payment Handled': 'Payment Handled',
 'Send Reminder': 'Send Reminder'}


Regroupons les événements et dessinons à nouveau le graphique de processus.



df_full_gr = df_full.copy()
df_full_gr['concept:name'] = df_full_gr['concept:name'].map(_dict)
dict_tuple_full_gr = calc(df_full_gr)
draw(dict_tuple_full_gr,'InternationalDeclarations_full_gr'




Après avoir regroupé les événements par similitude de nom, la lisibilité du graphique s'est améliorée. Essayons de trouver des réponses aux questions. Lien vers la liste des questions. Par exemple, combien de déclarations n'ont pas été précédées d'une autorisation pré-approuvée?



Pour répondre à la question posée, nous filtrons le graphique par événements d'intérêt et dessinons à nouveau le graphique de processus.



df_full_gr_f = df_full_gr[df_full_gr['concept:name'].isin(['Permit SUBMITTED',
                                                            'Permit APPROVED',
                                                            'Permit FINAL_APPROVED',
                                                            'Declaration FINAL_APPROVED',
                                                            'Declaration APPROVED'])]
df_full_gr_f = df_full_gr_f.reset_index(drop=True)
dict_tuple_full_gr_f = calc(df_full_gr_f)
draw(dict_tuple_full_gr_f,'InternationalDeclarations_full_gr_isin')






À l'aide du graphique qui en résulte, nous pouvons facilement répondre à la question posée - les déclarations 116 et 312 n'ont pas été précédées d'un permis pré-approuvé.



Vous pouvez en plus «échouer» (filtrer par «cas: concept: nom», participer à la connexion souhaitée) pour les connexions 116 et 312 et vous assurer qu'il n'y aura pas d'événements liés aux permissions sur les graphiques.



"Échouons" pour la communication 116:



df_116 = df_full_gr_f[df_full_gr_f['case:concept:name'].isin(d_case_start2['Declaration FINAL_APPROVED'])]
df_116 = df_116.reset_index(drop=True)
dict_tuple_116 = calc(df_116)
draw(dict_tuple_116,'InternationalDeclarations_full_gr_isin_116')






"Échouons" pour la connexion 312:



df_312 = df_full_gr_f[df_full_gr_f['case:concept:name'].isin(d_case_start2['Declaration APPROVED'])]
df_312 = df_312.reset_index(drop=True)
dict_tuple_312 = calc(df_312)
draw(dict_tuple_312,'InternationalDeclarations_full_gr_isin_312')






Puisqu'il n'y a aucun événement lié aux autorisations sur les graphiques reçus, l'exactitude des réponses 116 et 312 est confirmée.



Comme vous pouvez le voir, écrire un mineur et implémenter les capacités nécessaires pour travailler avec des graphiques n'est pas une tâche difficile, que les fonctions intégrées de Python et Graphviz ont traitées avec succès en tant que moteur graphique.



All Articles