Skip to content

Commit 47a2bfa

Browse files
Merge remote-tracking branch 'upstream/hotfixes' into release
2 parents a48e7fe + a556b51 commit 47a2bfa

3 files changed

Lines changed: 152 additions & 81 deletions

File tree

examples/prefix_classification.py

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,14 @@
88
from sklearn.metrics import accuracy_score
99
from sklearn.model_selection import train_test_split
1010

11-
12-
def build_prefix_onehot(log, activity_key):
13-
activities = sorted(
14-
{event[activity_key] for trace in log for event in trace if activity_key in event}
15-
)
16-
activity_to_index = {activity: idx for idx, activity in enumerate(activities)}
17-
18-
feature = []
19-
target = []
20-
21-
for trace in log:
22-
if len(trace) < 2:
23-
continue
24-
seen = set()
25-
for idx, event in enumerate(trace):
26-
if activity_key not in event:
27-
continue
28-
activity = event[activity_key]
29-
if idx == 0:
30-
seen.add(activity)
31-
continue
32-
row = [0] * len(activities)
33-
for seen_activity in seen:
34-
row[activity_to_index[seen_activity]] = 1
35-
feature.append(row)
36-
target.append(activity_to_index[activity])
37-
seen.add(activity)
38-
39-
return feature, target, activities, activity_to_index
11+
from prefix_feature_extraction import build_prefix_features_next_activity
4012

4113

4214
def main():
4315
parser = argparse.ArgumentParser(
4416
description=(
45-
"One-hot encode every prefix (length >= 2) of each trace using the "
46-
"activities up to the penultimate event, with the last event as class."
17+
"Encode every prefix of each trace using the activities and paths up to the "
18+
"penultimate event, plus time-based features, with the next event as class."
4719
)
4820
)
4921
parser.add_argument(
@@ -57,6 +29,11 @@ def main():
5729
default=xes_constants.DEFAULT_NAME_KEY,
5830
help=f"Event attribute to use as activity (default: {xes_constants.DEFAULT_NAME_KEY})",
5931
)
32+
parser.add_argument(
33+
"--timestamp-key",
34+
default=xes_constants.DEFAULT_TIMESTAMP_KEY,
35+
help=f"Event attribute to use as timestamp (default: {xes_constants.DEFAULT_TIMESTAMP_KEY})",
36+
)
6037
parser.add_argument(
6138
"--show-sample",
6239
action="store_true",
@@ -65,11 +42,18 @@ def main():
6542
args = parser.parse_args()
6643

6744
log = pm4py.read_xes(args.log_path, return_legacy_log_object=True)
68-
feature, target, activities, activity_to_index = build_prefix_onehot(
69-
log, args.activity_key
45+
(
46+
feature,
47+
target,
48+
activities,
49+
activity_to_index,
50+
paths,
51+
_,
52+
) = build_prefix_features_next_activity(
53+
log, args.activity_key, args.timestamp_key
7054
)
7155
if not feature:
72-
raise SystemExit("No prefixes of length >= 2 found in the log.")
56+
raise SystemExit("No prefixes found in the log.")
7357

7458
class_counts = Counter(target)
7559
min_class = min(class_counts.values()) if class_counts else 0
@@ -92,8 +76,9 @@ def main():
9276
print(f"Log path: {args.log_path}")
9377
print(f"Activity key: {args.activity_key}")
9478
print(f"Activities: {len(activities)}")
79+
print(f"Paths: {len(paths)}")
9580
print(f"Samples (prefixes): {len(feature)}")
96-
print(f"Feature dimension: {len(activities)}")
81+
print(f"Feature dimension: {len(activities) + len(paths) + 3}")
9782
print(f"Target classes: {len(activity_to_index)}")
9883
print(f"Train size: {len(X_train)}")
9984
print(f"Test size: {len(X_test)}")
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env python3
2+
from pm4py.util import xes_constants
3+
4+
5+
def _collect_activities_and_paths(log, activity_key):
6+
activities = set()
7+
paths = set()
8+
for trace in log:
9+
events = [event for event in trace if activity_key in event]
10+
for event in events:
11+
activities.add(event[activity_key])
12+
for idx in range(1, len(events)):
13+
paths.add((events[idx - 1][activity_key], events[idx][activity_key]))
14+
activities = sorted(activities)
15+
paths = sorted(paths)
16+
return activities, paths
17+
18+
19+
def _build_prefix_features(log, activity_key, timestamp_key, target_mode):
20+
activities, paths = _collect_activities_and_paths(log, activity_key)
21+
activity_to_index = {activity: idx for idx, activity in enumerate(activities)}
22+
path_to_index = {path: idx for idx, path in enumerate(paths)}
23+
24+
feature = []
25+
target = []
26+
case_ids = []
27+
28+
for trace_index, trace in enumerate(log):
29+
events = [event for event in trace if activity_key in event]
30+
if len(events) < 2:
31+
continue
32+
trace_id = trace.attributes.get(
33+
xes_constants.DEFAULT_TRACEID_KEY, trace_index
34+
)
35+
end_time = events[-1].get(timestamp_key) if timestamp_key else None
36+
if target_mode == "remaining_time" and end_time is None:
37+
continue
38+
start_time = events[0].get(timestamp_key) if timestamp_key else None
39+
40+
seen_activities = {events[0][activity_key]}
41+
seen_paths = set()
42+
43+
for idx in range(1, len(events)):
44+
prev_event = events[idx - 1]
45+
curr_event = events[idx]
46+
47+
if idx >= 2:
48+
prior_event = events[idx - 2]
49+
seen_paths.add(
50+
(prior_event[activity_key], prev_event[activity_key])
51+
)
52+
53+
row = [0] * (len(activities) + len(paths))
54+
for seen_activity in seen_activities:
55+
row[activity_to_index[seen_activity]] = 1
56+
for seen_path in seen_paths:
57+
row[len(activities) + path_to_index[seen_path]] = 1
58+
59+
prev_to_penultimate = 0.0
60+
if idx >= 2 and timestamp_key:
61+
prev_ts = prev_event.get(timestamp_key)
62+
prior_ts = events[idx - 2].get(timestamp_key)
63+
if prev_ts is not None and prior_ts is not None:
64+
prev_to_penultimate = (prev_ts - prior_ts).total_seconds()
65+
66+
start_to_penultimate = 0.0
67+
if timestamp_key and start_time is not None:
68+
penultimate_ts = prev_event.get(timestamp_key)
69+
if penultimate_ts is not None:
70+
start_to_penultimate = (penultimate_ts - start_time).total_seconds()
71+
72+
path_time_diff = 0.0
73+
if timestamp_key:
74+
prev_ts = prev_event.get(timestamp_key)
75+
curr_ts = curr_event.get(timestamp_key)
76+
if prev_ts is not None and curr_ts is not None:
77+
path_time_diff = (curr_ts - prev_ts).total_seconds()
78+
79+
row.extend([prev_to_penultimate, start_to_penultimate, path_time_diff])
80+
81+
curr_activity = curr_event[activity_key]
82+
if target_mode == "next_activity":
83+
target.append(activity_to_index[curr_activity])
84+
feature.append(row)
85+
else:
86+
curr_ts = curr_event.get(timestamp_key) if timestamp_key else None
87+
if curr_ts is not None:
88+
remaining = (end_time - curr_ts).total_seconds()
89+
target.append(remaining)
90+
feature.append(row)
91+
case_ids.append(trace_id)
92+
93+
seen_activities.add(curr_activity)
94+
95+
return feature, target, case_ids, activities, activity_to_index, paths, path_to_index
96+
97+
98+
def build_prefix_features_next_activity(
99+
log,
100+
activity_key=xes_constants.DEFAULT_NAME_KEY,
101+
timestamp_key=xes_constants.DEFAULT_TIMESTAMP_KEY,
102+
):
103+
feature, target, _, activities, activity_to_index, paths, path_to_index = (
104+
_build_prefix_features(log, activity_key, timestamp_key, "next_activity")
105+
)
106+
return feature, target, activities, activity_to_index, paths, path_to_index
107+
108+
109+
def build_prefix_features_remaining_time(
110+
log,
111+
activity_key=xes_constants.DEFAULT_NAME_KEY,
112+
timestamp_key=xes_constants.DEFAULT_TIMESTAMP_KEY,
113+
):
114+
feature, target, case_ids, activities, activity_to_index, paths, path_to_index = (
115+
_build_prefix_features(log, activity_key, timestamp_key, "remaining_time")
116+
)
117+
return feature, target, case_ids, activities, activity_to_index, paths, path_to_index

examples/prefix_regression.py

Lines changed: 15 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,14 @@
1010
from sklearn.metrics import r2_score
1111
from sklearn.model_selection import train_test_split
1212

13-
14-
def build_prefix_onehot_remaining_time(log, activity_key, timestamp_key):
15-
activities = sorted(
16-
{event[activity_key] for trace in log for event in trace if activity_key in event}
17-
)
18-
activity_to_index = {activity: idx for idx, activity in enumerate(activities)}
19-
20-
feature = []
21-
target = []
22-
case_ids = []
23-
24-
for trace_index, trace in enumerate(log):
25-
if len(trace) < 2:
26-
continue
27-
if timestamp_key not in trace[-1]:
28-
continue
29-
end_time = trace[-1][timestamp_key]
30-
trace_id = trace.attributes.get(
31-
xes_constants.DEFAULT_TRACEID_KEY, trace_index
32-
)
33-
seen = set()
34-
for idx, event in enumerate(trace):
35-
if activity_key not in event:
36-
continue
37-
activity = event[activity_key]
38-
if idx == 0:
39-
seen.add(activity)
40-
continue
41-
if timestamp_key not in event:
42-
seen.add(activity)
43-
continue
44-
row = [0] * len(activities)
45-
for seen_activity in seen:
46-
row[activity_to_index[seen_activity]] = 1
47-
remaining = (end_time - event[timestamp_key]).total_seconds()
48-
feature.append(row)
49-
target.append(remaining)
50-
case_ids.append(trace_id)
51-
seen.add(activity)
52-
53-
return feature, target, case_ids, activities, activity_to_index
13+
from prefix_feature_extraction import build_prefix_features_remaining_time
5414

5515

5616
def main():
5717
parser = argparse.ArgumentParser(
5818
description=(
59-
"One-hot encode every prefix (length >= 2) of each trace using the "
60-
"activities up to the penultimate event, with remaining time to case end as target."
19+
"Encode every prefix of each trace using the activities and paths up to the "
20+
"penultimate event, plus time-based features, with remaining time to case end as target."
6121
)
6222
)
6323
parser.add_argument(
@@ -84,11 +44,19 @@ def main():
8444
args = parser.parse_args()
8545

8646
log = pm4py.read_xes(args.log_path, return_legacy_log_object=True)
87-
feature, target, case_ids, activities, activity_to_index = build_prefix_onehot_remaining_time(
47+
(
48+
feature,
49+
target,
50+
case_ids,
51+
activities,
52+
activity_to_index,
53+
paths,
54+
_,
55+
) = build_prefix_features_remaining_time(
8856
log, args.activity_key, args.timestamp_key
8957
)
9058
if not feature:
91-
raise SystemExit("No prefixes of length >= 2 with timestamps found in the log.")
59+
raise SystemExit("No prefixes with timestamps found in the log.")
9260

9361
X_train, X_test, y_train, y_test, case_train, case_test = train_test_split(
9462
feature, target, case_ids, test_size=0.2, random_state=42
@@ -122,8 +90,9 @@ def main():
12290
print(f"Activity key: {args.activity_key}")
12391
print(f"Timestamp key: {args.timestamp_key}")
12492
print(f"Activities: {len(activities)}")
93+
print(f"Paths: {len(paths)}")
12594
print(f"Samples (prefixes): {len(feature)}")
126-
print(f"Feature dimension: {len(activities)}")
95+
print(f"Feature dimension: {len(activities) + len(paths) + 3}")
12796
print(f"Train size: {len(X_train)}")
12897
print(f"Test size: {len(X_test)}")
12998
print(f"Per-case MAE (hours): {mae_hours:.4f}")

0 commit comments

Comments
 (0)