@@ -105,375 +105,6 @@ def __iter__(self):
105
105
yield Trace (list (trace_tuple ))
106
106
107
107
108
- class Explainer :
109
- def __init__ (self ):
110
- """
111
- Initializes an Explainer instance.
112
- """
113
- self .constraints = [] # List to store constraints (regex patterns)
114
- self .adherent_trace = None
115
-
116
- def add_constraint (self , regex ):
117
- """
118
- Adds a new constraint and updates the nodes list.
119
-
120
- :param regex: A regular expression representing the constraint.
121
- """
122
- self .constraints .append (regex )
123
- if self .contradiction ():
124
- self .constraints .remove (regex )
125
- print (f"Constraint { regex } contradicts the other constraints." )
126
-
127
- def remove_constraint (self , idx ):
128
- """
129
- Removes a constraint by index and updates the nodes list if necessary.
130
-
131
- :param idx: Index of the constraint to be removed.
132
- """
133
- if 0 <= idx < len (self .constraints ):
134
- removed_regex = self .constraints .pop (idx )
135
- removed_nodes = set (filter (str .isalpha , removed_regex ))
136
-
137
- # Re-evaluate nodes to keep based on remaining constraints
138
- remaining_nodes = set (filter (str .isalpha , "" .join (self .constraints )))
139
- self .nodes = remaining_nodes
140
-
141
- # Optionally, remove nodes that are no longer in any constraint
142
- for node in removed_nodes :
143
- if node not in remaining_nodes :
144
- self .nodes .discard (node )
145
-
146
- def activation (self , trace , constraints = None ):
147
- """
148
- Checks if any of the nodes in the trace activates any constraint.
149
-
150
- :param trace: A Trace instance.
151
- :return: Boolean indicating if any constraint is activated.
152
- """
153
- if not constraints :
154
- constraints = self .constraints
155
- con_activation = [0 ] * len (constraints )
156
- activated = False
157
- for idx , con in enumerate (constraints ):
158
- if activated :
159
- activated = False
160
- continue
161
- target = self .identify_existance_constraints (con )
162
- if target :
163
- con_activation [idx ] = 1
164
- continue
165
- for event in trace :
166
- if event in con :
167
- con_activation [idx ] = 1
168
- activated = True
169
- break
170
- return con_activation
171
-
172
- def identify_existance_constraints (self , pattern ):
173
- """
174
- Identifies existance constraints within a pattern.
175
-
176
- :param pattern: The constraint pattern as a string.
177
- :return: A tuple indicating the type of existance constraint and the node involved.
178
- """
179
- # Check for AtLeastOne constraint
180
- for match in re .finditer (r"(?<!^)(.)\.\*" , pattern ):
181
- return "ALO, " f"{ match .group (1 )} "
182
-
183
- # Check for End constraint
184
- end_match = re .search (r"(.)\$(?=\Z|\))" , pattern )
185
- if end_match :
186
- return "E" , f"{ end_match .group (1 )} "
187
- # Check for Init constraint
188
- init_match = re .match (r"(?:\A\^|\((?:\?[^)]+\))?\^)(.)" , pattern )
189
- if init_match :
190
- return ("I" , f"{ init_match .group (1 )} " )
191
- return None
192
-
193
- def conformant (self , trace , constraints = None ):
194
- """
195
- Checks if the trace is conformant according to all the constraints.
196
-
197
- :param trace: A Trace instance.
198
- :return: Boolean indicating if the trace is conformant with all constraints.
199
- """
200
- activation = self .activation (trace , constraints )
201
- if any (value == 0 for value in activation ):
202
- new_explainer = Explainer ()
203
- for idx , value in enumerate (activation ):
204
- if value == 1 :
205
- new_explainer .add_constraint (self .constraints [idx ])
206
- return new_explainer .conformant (trace )
207
- trace_str = "" .join (trace )
208
- if constraints :
209
- return all (re .search (constraint , trace_str ) for constraint in constraints )
210
- return all (re .search (constraint , trace_str ) for constraint in self .constraints )
211
-
212
- def contradiction (self ):
213
- """
214
- Checks if there is a contradiction among the constraints.
215
-
216
- :return: Boolean indicating if there is a contradiction.
217
- """
218
- nodes = self .get_nodes_from_constraint ()
219
- max_length = 10 # Set a reasonable max length to avoid infinite loops
220
- nodes = nodes + nodes
221
- for length in range (1 , max_length + 1 ):
222
- for combination in product (nodes , repeat = length ):
223
- test_str = "" .join (combination )
224
- if all (re .search (con , test_str ) for con in self .constraints ):
225
- self .adherent_trace = test_str
226
- return False # Found a match
227
- return True # No combination satisfied all constraints
228
-
229
- def minimal_expl (self , trace ):
230
- """
231
- Provides a minimal explanation for non-conformance, given the trace and constraints.
232
-
233
- :param trace: A Trace instance.
234
- :return: Explanation of why the trace is non-conformant.
235
- """
236
-
237
- # Because constraints that are not activated should not be considered we create a new explainer with the relevant constraints in this case
238
- activation = self .activation (trace )
239
- if any (value == 0 for value in activation ):
240
- new_explainer = Explainer ()
241
- for idx , value in enumerate (activation ):
242
- if value == 1 :
243
- new_explainer .add_constraint (self .constraints [idx ])
244
- return new_explainer .minimal_expl (trace )
245
-
246
- if self .conformant (trace ):
247
- return "The trace is already conformant, no changes needed."
248
- explanations = None
249
-
250
- for constraint in self .constraints :
251
- for subtrace in get_sublists (trace ):
252
- trace_str = "" .join (subtrace )
253
- if not re .search (constraint , trace_str ):
254
- explanations = (
255
- f"Constraint ({ constraint } ) is violated by subtrace: { subtrace } "
256
- )
257
- break
258
-
259
- if explanations :
260
- return "Non-conformance due to: " + explanations
261
- else :
262
- return "Trace is non-conformant, but the specific constraint violation could not be determined."
263
-
264
- def counterfactual_expl (self , trace ):
265
- """
266
- Generates a counterfactual explanation for a given trace.
267
-
268
- :param trace: The trace to be explained.
269
- :return: A string explaining why the trace is non-conformant or a message indicating no changes are needed.
270
- """
271
- activation = self .activation (trace )
272
- if any (value == 0 for value in activation ):
273
- new_explainer = Explainer ()
274
- for idx , value in enumerate (activation ):
275
- if value == 1 :
276
- new_explainer .add_constraint (self .constraints [idx ])
277
- return new_explainer .counterfactual_expl (trace )
278
-
279
- if self .conformant (trace ):
280
- return "The trace is already conformant, no changes needed."
281
- score = self .evaluate_similarity (trace )
282
- # Perform operation based on the lowest scoring heuristic
283
- return self .operate_on_trace (trace , score , "" )
284
-
285
- def counter_factual_helper (self , working_trace , explanation , depth = 0 ):
286
- """
287
- Recursively explores counterfactual explanations for a working trace.
288
-
289
- :param working_trace: The trace being explored.
290
- :param explanation: The current explanation path.
291
- :param depth: The current recursion depth.
292
- :return: A string explaining why the working trace is non-conformant or a message indicating the maximum depth has been reached.
293
- """
294
- if self .conformant (working_trace ):
295
- return f"{ explanation } "
296
- if depth > 100 :
297
- return f"{ explanation } \n Maximum depth of { depth - 1 } reached"
298
- score = self .evaluate_similarity (working_trace )
299
- return self .operate_on_trace (working_trace , score , explanation , depth )
300
-
301
- def operate_on_trace (self , trace , score , explanation_path , depth = 0 ):
302
- """
303
- Finds and applies modifications to the trace to make it conformant.
304
-
305
- :param trace: The trace to be modified.
306
- :param score: The similarity score of the trace.
307
- :param explanation_path: The current explanation path.
308
- :param depth: The current recursion depth.
309
- :return: A string explaining why the best subtrace is non-conformant or a message indicating the maximum depth has been reached.
310
- """
311
- explanation = None
312
- counter_factuals = self .modify_subtrace (trace )
313
- best_subtrace = None
314
- best_score = - float ("inf" )
315
- for subtrace in counter_factuals :
316
- current_score = self .evaluate_similarity (subtrace [0 ])
317
- if current_score > best_score and current_score > score :
318
- best_score = current_score
319
- best_subtrace = subtrace [0 ]
320
- explanation = subtrace [1 ]
321
- if best_subtrace == None :
322
- for subtrace in counter_factuals :
323
- self .operate_on_trace (subtrace [0 ], score , explanation_path , depth + 1 )
324
- explanation_string = explanation_path + "\n " + str (explanation )
325
- return self .counter_factual_helper (best_subtrace , explanation_string , depth + 1 )
326
-
327
- def get_nodes_from_constraint (self , constraint = None ):
328
- """
329
- Extracts unique nodes from a constraint pattern.
330
-
331
- :param constraint: The constraint pattern as a string.
332
- :return: A list of unique nodes found within the constraint.
333
- """
334
- if constraint is None :
335
- all_nodes = set ()
336
- for con in self .constraints :
337
- all_nodes .update (re .findall (r"[A-Za-z]" , con ))
338
- return list (set (all_nodes ))
339
- else :
340
- return list (set (re .findall (r"[A-Za-z]" , constraint )))
341
-
342
- def modify_subtrace (self , trace ):
343
- """
344
- Modifies the given trace to meet constraints by adding nodes where the pattern fails.
345
-
346
- Parameters:
347
- - trace: A list of node identifiers
348
-
349
- Returns:
350
- - A list of potential subtraces each modified to meet constraints.
351
- """
352
- potential_subtraces = []
353
- possible_additions = self .get_nodes_from_constraint ()
354
- for i , s_trace in enumerate (get_iterative_subtrace (trace )):
355
- for con in self .constraints :
356
- new_trace_str = "" .join (s_trace )
357
- match = re .match (new_trace_str , con )
358
- if not match :
359
- for add in possible_additions :
360
- potential_subtraces .append (
361
- [
362
- Trace (s_trace + [add ] + trace .nodes [i + 1 :]),
363
- f"Addition (Added { add } at position { i + 1 } ): "
364
- + "->" .join (s_trace + [add ] + trace .nodes [i + 1 :]),
365
- ]
366
- )
367
- potential_subtraces .append (
368
- [
369
- Trace (s_trace [:- 1 ] + [add ] + trace .nodes [i :]),
370
- f"Addition (Added { add } at position { i } ): "
371
- + "->" .join (s_trace [:- 1 ] + [add ] + trace .nodes [i :]),
372
- ]
373
- )
374
-
375
- potential_subtraces .append (
376
- [
377
- Trace (s_trace [:- 1 ] + trace .nodes [i + 1 :]),
378
- f"Subtraction (Removed { s_trace [i ]} from position { i } ): "
379
- + "->" .join (s_trace [:- 1 ] + trace .nodes [i + 1 :]),
380
- ]
381
- )
382
- return potential_subtraces
383
-
384
- def determine_shapley_value (self , log , constraints , index ):
385
- """Determines the Shapley value-based contribution of a constraint to a the
386
- overall conformance rate.
387
- Args:
388
- log (dictionary): The event log, where keys are strings and values are
389
- ints
390
- constraints (list): A list of constraints (regexp strings)
391
- index (int): The
392
- Returns:
393
- float: The contribution of the constraint to the overall conformance
394
- rate
395
- """
396
- if len (constraints ) < index :
397
- raise Exception ("Constraint not in constraint list." )
398
- contributor = constraints [index ]
399
- sub_ctrbs = []
400
- reduced_constraints = [c for c in constraints if not c == contributor ]
401
- subsets = determine_powerset (reduced_constraints )
402
- for subset in subsets :
403
- lsubset = list (subset )
404
- constraints_without = [c for c in constraints if c in lsubset ]
405
- constraints_with = [c for c in constraints if c in lsubset + [contributor ]]
406
- weight = (
407
- math .factorial (len (lsubset ))
408
- * math .factorial (len (constraints ) - 1 - len (lsubset ))
409
- ) / math .factorial (len (constraints ))
410
- sub_ctrb = weight * (
411
- self .determine_conformance_rate (log , constraints_without )
412
- - self .determine_conformance_rate (log , constraints_with )
413
- )
414
- sub_ctrbs .append (sub_ctrb )
415
- return sum (sub_ctrbs )
416
-
417
- def evaluate_similarity (self , trace ):
418
- """
419
- Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance.
420
-
421
- :param trace: The trace to compare with the adherent trace.
422
- :return: A normalized score indicating the similarity between the adherent trace and the given trace.
423
- """
424
- length = len (self .adherent_trace )
425
- trace_len = len ("" .join (trace ))
426
- lev_distance = levenshtein_distance (self .adherent_trace , "" .join (trace ))
427
- max_distance = max (length , trace_len )
428
- normalized_score = 1 - lev_distance / max_distance
429
- return normalized_score
430
-
431
- def determine_conformance_rate (self , event_log , constraints = None ):
432
- """
433
- Determines the conformance rate of the event log based on the given constraints.
434
-
435
- :param event_log: The event log to analyze.
436
- :param constraints: The constraints to check against the event log.
437
- :return: The conformance rate as a float between 0 and 1, or a message if no constraints are provided.
438
- """
439
- if not self .constraints and not constraints :
440
- return "The explainer have no constraints"
441
- len_log = len (event_log )
442
- if len_log == 0 :
443
- return 1
444
- non_conformant = 0
445
- if constraints == None :
446
- constraints = self .constraints
447
- for trace , count in event_log .log .items ():
448
- for con in constraints :
449
- if not re .search (con , "" .join (trace )):
450
- non_conformant += count
451
- break
452
- return (len_log - non_conformant ) / len_log
453
-
454
- def trace_contribution_to_conformance_loss (
455
- self , event_log , trace , constraints = None
456
- ):
457
- """
458
- Calculates the contribution of a specific trace to the conformance loss of the event log.
459
-
460
- :param event_log: The event log to analyze.
461
- :param trace: The trace to calculate its contribution.
462
- :param constraints: The constraints to check against the event log.
463
- :return: The contribution of the trace to the conformance loss as a float between 0 and 1.
464
- """
465
- if not constraints :
466
- constraints = self .constraints
467
- total_traces = len (event_log )
468
- contribution_of_trace = 0
469
- for t , count in event_log .log .items ():
470
- if not self .conformant (t , constraints ):
471
- if trace .nodes == list (t ):
472
- contribution_of_trace = count
473
-
474
- return contribution_of_trace / total_traces
475
-
476
-
477
108
def determine_powerset (elements ):
478
109
"""Determines the powerset of a list of elements
479
110
Args:
0 commit comments