Skip to content

Commit 6900aa6

Browse files
Merge remote-tracking branch 'upstream/hotfixes' into release
2 parents 661631e + e9ee74e commit 6900aa6

5 files changed

Lines changed: 133 additions & 14 deletions

File tree

pm4py/algo/discovery/inductive/fall_through/activity_once_per_trace.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ def _get_candidate(
5656
return None
5757

5858
# Return any one of the remaining candidates
59-
return next(iter(candidates)) if candidates else None
59+
candidates = sorted(list(candidates))
60+
return candidates[0] if candidates else None

pm4py/algo/filtering/pandas/attributes/attributes_filter.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class Parameters(Enum):
4343
STREAM_FILTER_KEY2 = "stream_filter_key2"
4444
STREAM_FILTER_VALUE2 = "stream_filter_value2"
4545
KEEP_ONCE_PER_CASE = "keep_once_per_case"
46+
KEEP_NAN_VALUES = "keep_nan_values"
4647

4748

4849
def apply_numeric_events(
@@ -66,6 +67,7 @@ def apply_numeric_events(
6667
Possible parameters of the algorithm:
6768
Parameters.ATTRIBUTE_KEY => indicates which attribute to filter
6869
positive => keep or remove events?
70+
Parameters.KEEP_NAN_VALUES -> Specifies if the filter should keep NAN values for the selected attribute. Default is False
6971
7072
Returns
7173
--------------
@@ -81,11 +83,19 @@ def apply_numeric_events(
8183
positive = exec_utils.get_param_value(
8284
Parameters.POSITIVE, parameters, True
8385
)
86+
keep_nan = exec_utils.get_param_value(
87+
Parameters.KEEP_NAN_VALUES, parameters, False
88+
)
8489

8590
if positive:
86-
ret = df[(df[attribute_key] >= int1) & (df[attribute_key] <= int2)]
91+
event_filter = (df[attribute_key] >= int1) & (df[attribute_key] <= int2)
8792
else:
88-
ret = df[(df[attribute_key] < int1) | (df[attribute_key] > int2)]
93+
event_filter = (df[attribute_key] < int1) | (df[attribute_key] > int2)
94+
95+
if keep_nan:
96+
event_filter = event_filter | df[attribute_key].isna()
97+
98+
ret = df[event_filter]
8999

90100
ret.attrs = copy(df.attrs) if hasattr(df, "attrs") else {}
91101
return ret
@@ -112,6 +122,7 @@ def apply_numeric(
112122
Possible parameters of the algorithm:
113123
Parameters.ATTRIBUTE_KEY => indicates which attribute to filter
114124
Parameters.POSITIVE => keep or remove traces with such events?
125+
Parameters.KEEP_NAN_VALUES -> Specifies if the filter should keep NAN values for the selected attribute. Default is False
115126
116127
Returns
117128
--------------
@@ -130,6 +141,9 @@ def apply_numeric(
130141
positive = exec_utils.get_param_value(
131142
Parameters.POSITIVE, parameters, True
132143
)
144+
keep_nan = exec_utils.get_param_value(
145+
Parameters.KEEP_NAN_VALUES, parameters, False
146+
)
133147

134148
# stream_filter_key is helpful to filter on cases containing an event with an attribute
135149
# in the specified value set, but such events shall have an activity in
@@ -147,9 +161,11 @@ def apply_numeric(
147161
Parameters.STREAM_FILTER_VALUE2, parameters, None
148162
)
149163

150-
filtered_df_by_ev = df[
151-
(df[attribute_key] >= int1) & (df[attribute_key] <= int2)
152-
]
164+
event_filter = (df[attribute_key] >= int1) & (df[attribute_key] <= int2)
165+
if keep_nan:
166+
event_filter = event_filter | df[attribute_key].isna()
167+
filtered_df_by_ev = df[event_filter]
168+
153169
if stream_filter_key1 is not None:
154170
filtered_df_by_ev = filtered_df_by_ev[
155171
filtered_df_by_ev[stream_filter_key1] == stream_filter_value1
@@ -189,6 +205,8 @@ def apply_events(
189205
Parameters.ATTRIBUTE_KEY -> Attribute we want to filter
190206
Parameters.POSITIVE -> Specifies if the filter should be applied including traces (positive=True) or
191207
excluding traces (positive=False)
208+
Parameters.KEEP_NAN_VALUES -> Specifies if the filter should keep NAN values for the selected attribute. Default is False
209+
192210
Returns
193211
----------
194212
df
@@ -203,11 +221,18 @@ def apply_events(
203221
positive = exec_utils.get_param_value(
204222
Parameters.POSITIVE, parameters, True
205223
)
224+
keep_nan = exec_utils.get_param_value(
225+
Parameters.KEEP_NAN_VALUES, parameters, False
226+
)
227+
228+
event_filter = df[attribute_key].isin(values)
229+
if keep_nan:
230+
event_filter = event_filter | df[attribute_key].isna()
206231

207232
if positive:
208-
ret = df[df[attribute_key].isin(values)]
233+
ret = df[event_filter]
209234
else:
210-
ret = df[~df[attribute_key].isin(values)]
235+
ret = df[~event_filter]
211236

212237
ret.attrs = copy(df.attrs) if hasattr(df, "attrs") else {}
213238
return ret
@@ -233,6 +258,8 @@ def apply(
233258
Parameters.ATTRIBUTE_KEY -> Attribute we want to filter
234259
Parameters.POSITIVE -> Specifies if the filter should be applied including traces (positive=True) or
235260
excluding traces (positive=False)
261+
Parameters.KEEP_NAN_VALUES -> Specifies if the filter should keep NAN values for the selected attribute. Default is False
262+
236263
Returns
237264
----------
238265
df
@@ -250,13 +277,17 @@ def apply(
250277
positive = exec_utils.get_param_value(
251278
Parameters.POSITIVE, parameters, True
252279
)
280+
keep_nan = exec_utils.get_param_value(
281+
Parameters.KEEP_NAN_VALUES, parameters, False
282+
)
253283

254284
return filter_df_on_attribute_values(
255285
df,
256286
values,
257287
case_id_glue=case_id_glue,
258288
attribute_key=attribute_key,
259289
positive=positive,
290+
keep_nan_values=keep_nan
260291
)
261292

262293

@@ -266,6 +297,7 @@ def filter_df_on_attribute_values(
266297
case_id_glue="case:concept:name",
267298
attribute_key="concept:name",
268299
positive=True,
300+
keep_nan_values=False,
269301
):
270302
"""
271303
Filter dataframe on attribute values
@@ -283,6 +315,8 @@ def filter_df_on_attribute_values(
283315
positive
284316
Specifies if the filtered should be applied including traces (positive=True) or excluding traces
285317
(positive=False)
318+
keep_nan_values
319+
Specifies if the filter should keep NAN values for the selected attribute
286320
287321
Returns
288322
----------
@@ -291,7 +325,12 @@ def filter_df_on_attribute_values(
291325
"""
292326
if values is None:
293327
values = []
294-
filtered_df_by_ev = df[df[attribute_key].isin(values)]
328+
329+
event_filter = df[attribute_key].isin(values)
330+
if keep_nan_values:
331+
event_filter = event_filter | df[attribute_key].isna()
332+
filtered_df_by_ev = df[event_filter]
333+
295334
i1 = df.set_index(case_id_glue).index
296335
i2 = filtered_df_by_ev.set_index(case_id_glue).index
297336
if positive:

pm4py/objects/conversion/ocel/variants/ocel_to_nx.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Parameters(Enum):
3535

3636
def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None):
3737
"""
38-
Converts an OCEL to a NetworkX DiGraph object.
38+
Converts an OCEL to a NetworkX MultiDiGraph object.
3939
The nodes are the events and objects of the OCEL.
4040
The edges are of two different types:
4141
- relation edges, connecting an event to its related objects
@@ -52,7 +52,7 @@ def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None):
5252
Returns
5353
---------------
5454
G
55-
NetworkX DiGraph
55+
NetworkX MultiDiGraph
5656
"""
5757
if parameters is None:
5858
parameters = {}
@@ -64,7 +64,7 @@ def apply(ocel: OCEL, parameters: Optional[Dict[Any, Any]] = None):
6464
Parameters.INCLUDE_OBJECT_CHANGES, parameters, True
6565
)
6666

67-
G = nx_utils.DiGraph()
67+
G = nx_utils.MultiDiGraph()
6868

6969
stream = ocel.events.to_dict("records")
7070
stream = to_event_stream.__postprocess_stream(stream)

pm4py/statistics/attributes/common/get.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,16 @@ def get_kde_numeric_attribute(values, parameters=None):
148148

149149
# linear space including both endpoints
150150
xs1 = np.linspace(min_val, max_val, half, endpoint=True)
151-
# geometric space including both endpoints (avoid zero)
152-
xs2 = np.geomspace(max(min_val, eps), max_val, half, endpoint=True)
151+
# try to enrich the sampling near the distribution tails; fall back when geomspace is not applicable
152+
if min_val > 0 and max_val > 0:
153+
# both bounds positive -> standard geometric spacing
154+
xs2 = np.geomspace(max(min_val, eps), max_val, half, endpoint=True)
155+
elif min_val < 0 and max_val < 0:
156+
# both bounds negative -> mirror geometric spacing on the absolute values
157+
xs2 = -np.geomspace(abs(min_val), max(abs(max_val), eps), half, endpoint=True)
158+
else:
159+
# bounds cross or hit zero -> stick to linear spacing to avoid invalid geometric ranges
160+
xs2 = np.linspace(min_val, max_val, half, endpoint=True)
153161

154162
# combine, add exact endpoints, dedupe & sort
155163
xs = np.unique(

tests/filtering_pandas_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,77 @@ def test_filtering_attr_events(self):
5555
del df1
5656
del df2
5757

58+
def test_filtering_attr_values(self):
59+
# to avoid static method warnings in tests,
60+
# that by construction of the unittest package have to be expressed in such way
61+
self.dummy_variable = "dummy_value"
62+
input_log = os.path.join(INPUT_DATA_DIR, "running-example.csv")
63+
dataframe = pandas_utils.read_csv(input_log)
64+
dataframe = dataframe_utils.convert_timestamp_columns_in_df(dataframe, timest_format=constants.DEFAULT_TIMESTAMP_PARSE_FORMAT)
65+
# add a row with a NAN value in the filter attribute
66+
case_id_of_nan_case = len(dataframe)
67+
dataframe.loc[len(dataframe)] = {"case:concept:name": case_id_of_nan_case,
68+
"case:concept": "register request",
69+
"Resource": None,
70+
"time:timestamp": "2011-01-09 12:02:00+00:00"
71+
}
72+
73+
pos = attributes_filter.apply(dataframe, ["Pete"],
74+
parameters={attributes_filter.Parameters.POSITIVE: True,
75+
attributes_filter.PARAMETER_CONSTANT_ATTRIBUTE_KEY: "Resource"})
76+
neg = attributes_filter.apply(dataframe, ["Pete"],
77+
parameters={attributes_filter.Parameters.POSITIVE: False,
78+
attributes_filter.PARAMETER_CONSTANT_ATTRIBUTE_KEY: "Resource"})
79+
80+
pos_with_nan = attributes_filter.apply(dataframe, ["Pete"],
81+
parameters={attributes_filter.Parameters.POSITIVE: True,
82+
attributes_filter.PARAMETER_CONSTANT_ATTRIBUTE_KEY: "Resource",
83+
attributes_filter.Parameters.KEEP_NAN_VALUES: True})
84+
85+
# We configure the filter to keep NAN values, however because of attributes_filter.Parameters.POSITIVE: False
86+
# the complement of the filter result is taken. This means that the NAN values are not included
87+
neg_without_nan = attributes_filter.apply(dataframe, ["Pete"],
88+
parameters={attributes_filter.Parameters.POSITIVE: False,
89+
attributes_filter.PARAMETER_CONSTANT_ATTRIBUTE_KEY: "Resource",
90+
attributes_filter.Parameters.KEEP_NAN_VALUES: True})
91+
92+
case_identifier = "case:concept:name"
93+
num_cases = len(dataframe[case_identifier].unique())
94+
95+
cases_pos = pos[case_identifier].unique()
96+
num_cases_pos = len(cases_pos)
97+
98+
cases_neg = neg[case_identifier].unique()
99+
num_cases_neg = len(cases_neg)
100+
101+
cases_pos_with_nan = pos_with_nan[case_identifier].unique()
102+
num_cases_pos_with_nan = len(cases_pos_with_nan)
103+
104+
cases_neg_without_nan = neg_without_nan[case_identifier].unique()
105+
num_cases_neg_without_nan = len(cases_neg_without_nan)
106+
107+
self.assertEqual(num_cases_pos + num_cases_neg, num_cases)
108+
self.assertEqual(num_cases_pos_with_nan + num_cases_neg_without_nan, num_cases)
109+
110+
self.assertEqual(num_cases_pos + 1, num_cases_pos_with_nan)
111+
self.assertEqual(num_cases_neg, num_cases_neg_without_nan + 1)
112+
113+
self.assertNotIn(case_id_of_nan_case, cases_pos)
114+
self.assertIn(case_id_of_nan_case, cases_neg)
115+
self.assertIn(case_id_of_nan_case, cases_pos_with_nan)
116+
self.assertNotIn(case_id_of_nan_case, cases_neg_without_nan)
117+
118+
del pos
119+
del neg
120+
del pos_with_nan
121+
del neg_without_nan
122+
123+
del cases_pos
124+
del cases_neg
125+
del cases_pos_with_nan
126+
del cases_neg_without_nan
127+
128+
58129
def test_filtering_paths(self):
59130
# to avoid static method warnings in tests,
60131
# that by construction of the unittest package have to be expressed in such way

0 commit comments

Comments
 (0)