ocelescope 0.1.1b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. ocelescope-0.1.1b1/PKG-INFO +26 -0
  2. ocelescope-0.1.1b1/README.md +13 -0
  3. ocelescope-0.1.1b1/pyproject.toml +32 -0
  4. ocelescope-0.1.1b1/src/ocelescope/__init__.py +60 -0
  5. ocelescope-0.1.1b1/src/ocelescope/ocel/__init__.py +32 -0
  6. ocelescope-0.1.1b1/src/ocelescope/ocel/constants.py +4 -0
  7. ocelescope-0.1.1b1/src/ocelescope/ocel/extension.py +44 -0
  8. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/__init__.py +25 -0
  9. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/apply.py +55 -0
  10. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/base.py +38 -0
  11. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/__init__.py +32 -0
  12. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/attribute.py +100 -0
  13. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/entity_type.py +31 -0
  14. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/relation_count.py +142 -0
  15. ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/time_range.py +30 -0
  16. ocelescope-0.1.1b1/src/ocelescope/ocel/ocel.py +633 -0
  17. ocelescope-0.1.1b1/src/ocelescope/ocel/util/__init__.py +5 -0
  18. ocelescope-0.1.1b1/src/ocelescope/ocel/util/attributes.py +216 -0
  19. ocelescope-0.1.1b1/src/ocelescope/ocel/util/relations.py +150 -0
  20. ocelescope-0.1.1b1/src/ocelescope/plugin/__init__.py +22 -0
  21. ocelescope-0.1.1b1/src/ocelescope/plugin/decorators.py +200 -0
  22. ocelescope-0.1.1b1/src/ocelescope/plugin/input.py +59 -0
  23. ocelescope-0.1.1b1/src/ocelescope/plugin/plugin.py +43 -0
  24. ocelescope-0.1.1b1/src/ocelescope/resource/__init__.py +6 -0
  25. ocelescope-0.1.1b1/src/ocelescope/resource/default/__init__.py +0 -0
  26. ocelescope-0.1.1b1/src/ocelescope/resource/default/dfg.py +124 -0
  27. ocelescope-0.1.1b1/src/ocelescope/resource/default/petri_net.py +131 -0
  28. ocelescope-0.1.1b1/src/ocelescope/resource/resource.py +38 -0
  29. ocelescope-0.1.1b1/src/ocelescope/util/__init__.py +458 -0
  30. ocelescope-0.1.1b1/src/ocelescope/util/cache.py +203 -0
  31. ocelescope-0.1.1b1/src/ocelescope/util/pandas.py +458 -0
  32. ocelescope-0.1.1b1/src/ocelescope/visualization/__init__.py +35 -0
  33. ocelescope-0.1.1b1/src/ocelescope/visualization/default/__init__.py +0 -0
  34. ocelescope-0.1.1b1/src/ocelescope/visualization/default/dot.py +21 -0
  35. ocelescope-0.1.1b1/src/ocelescope/visualization/default/graph.py +73 -0
  36. ocelescope-0.1.1b1/src/ocelescope/visualization/default/svg.py +8 -0
  37. ocelescope-0.1.1b1/src/ocelescope/visualization/default/table.py +18 -0
  38. ocelescope-0.1.1b1/src/ocelescope/visualization/util/color.py +23 -0
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.3
2
+ Name: ocelescope
3
+ Version: 0.1.1b1
4
+ Summary: General tools for working with Object-Centric Event Logs and building Ocelescope plugins
5
+ Requires-Dist: cachetools>=6.1.0
6
+ Requires-Dist: networkx>=3.5
7
+ Requires-Dist: numpy>=2.3.2
8
+ Requires-Dist: pandas>=2.3.1
9
+ Requires-Dist: pm4py~=2.7.15.3
10
+ Requires-Dist: pydantic>=2.11.7
11
+ Requires-Python: ~=3.13
12
+ Description-Content-Type: text/markdown
13
+
14
+ # oceloscope-tools-grkmr
15
+
16
+ General tools and building blocks for working with [Object-Centric Event Logs (OCEL)](https://ocel-standard.org/) and writing plugins for the [OCELoscope](https://github.com/rwth-pads/ocelescope) framework.
17
+
18
+ ---
19
+
20
+ ## 📦 Installation
21
+
22
+ Install directly from Git:
23
+
24
+ ```bash
25
+ pip install git+https://github.com/grkmr/ocelescope-tools.git
26
+
@@ -0,0 +1,13 @@
1
+ # oceloscope-tools-grkmr
2
+
3
+ General tools and building blocks for working with [Object-Centric Event Logs (OCEL)](https://ocel-standard.org/) and writing plugins for the [OCELoscope](https://github.com/rwth-pads/ocelescope) framework.
4
+
5
+ ---
6
+
7
+ ## 📦 Installation
8
+
9
+ Install directly from Git:
10
+
11
+ ```bash
12
+ pip install git+https://github.com/grkmr/ocelescope-tools.git
13
+
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "ocelescope"
3
+ version = "0.1.1b1"
4
+ description = "General tools for working with Object-Centric Event Logs and building Ocelescope plugins"
5
+ readme = "README.md"
6
+ requires-python = "~=3.13"
7
+ dependencies = [
8
+ "cachetools>=6.1.0",
9
+ "networkx>=3.5",
10
+ "numpy>=2.3.2",
11
+ "pandas>=2.3.1",
12
+ "pm4py~=2.7.15.3",
13
+ "pydantic>=2.11.7",
14
+ ]
15
+
16
+ [tool.uv]
17
+ package = true
18
+
19
+ [dependency-groups]
20
+ dev = ["black>=25.1.0", "pre-commit>=4.2.0", "ruff>=0.12.7"]
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.8.4,<0.9.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [tool.black]
27
+ line-length = 100
28
+
29
+ [tool.ruff]
30
+ line-length = 100
31
+ fix = true
32
+ lint.ignore = ["E731", "E501"]
@@ -0,0 +1,60 @@
1
+ from ocelescope.ocel import (
2
+ E2OCountFilter,
3
+ EventAttributeFilter,
4
+ EventTypeFilter,
5
+ O2OCountFilter,
6
+ ObjectAttributeFilter,
7
+ TimeFrameFilter,
8
+ OCELFilter,
9
+ OCELExtension,
10
+ ObjectTypeFilter,
11
+ OCEL,
12
+ RelationCountSummary,
13
+ AttributeSummary,
14
+ )
15
+ from ocelescope.visualization import Visualization
16
+ from ocelescope.resource import PetriNet, Resource, DirectlyFollowsGraph
17
+ from ocelescope.plugin import (
18
+ ResourceAnnotation,
19
+ OCELAnnotation,
20
+ Plugin,
21
+ PluginMeta,
22
+ PluginMethod,
23
+ COMPUTED_SELECTION,
24
+ OCEL_FIELD,
25
+ PluginInput,
26
+ PluginResult,
27
+ plugin_method,
28
+ )
29
+ import matplotlib
30
+
31
+ matplotlib.use("Agg")
32
+
33
+ __all__ = [
34
+ "OCEL",
35
+ "OCELExtension",
36
+ "E2OCountFilter",
37
+ "EventAttributeFilter",
38
+ "EventTypeFilter",
39
+ "O2OCountFilter",
40
+ "ObjectAttributeFilter",
41
+ "ObjectTypeFilter",
42
+ "TimeFrameFilter",
43
+ "OCELFilter",
44
+ "RelationCountSummary",
45
+ "AttributeSummary",
46
+ "Visualization",
47
+ "PetriNet",
48
+ "DirectlyFollowsGraph",
49
+ "Resource",
50
+ "ResourceAnnotation",
51
+ "OCELAnnotation",
52
+ "Plugin",
53
+ "PluginMeta",
54
+ "PluginMethod",
55
+ "COMPUTED_SELECTION",
56
+ "OCEL_FIELD",
57
+ "PluginInput",
58
+ "PluginResult",
59
+ "plugin_method",
60
+ ]
@@ -0,0 +1,32 @@
1
+ from ocelescope.ocel.ocel import OCEL
2
+
3
+ from ocelescope.ocel.extension import OCELExtension
4
+ from ocelescope.ocel.filter import (
5
+ E2OCountFilter,
6
+ EventAttributeFilter,
7
+ EventTypeFilter,
8
+ O2OCountFilter,
9
+ ObjectAttributeFilter,
10
+ TimeFrameFilter,
11
+ OCELFilter,
12
+ ObjectTypeFilter,
13
+ )
14
+ from ocelescope.ocel.util import AttributeSummary, RelationCountSummary
15
+
16
+ from ocelescope.ocel.constants import OCELFileExtensions
17
+
18
+ __all__ = [
19
+ "OCEL",
20
+ "OCELExtension",
21
+ "E2OCountFilter",
22
+ "EventAttributeFilter",
23
+ "EventTypeFilter",
24
+ "O2OCountFilter",
25
+ "ObjectTypeFilter",
26
+ "ObjectAttributeFilter",
27
+ "TimeFrameFilter",
28
+ "AttributeSummary",
29
+ "RelationCountSummary",
30
+ "OCELFilter",
31
+ "OCELFileExtensions",
32
+ ]
@@ -0,0 +1,4 @@
1
+ from typing import Literal
2
+
3
+
4
+ OCELFileExtensions = Literal[".xmlocel", ".jsonocel", ".sqlite"]
@@ -0,0 +1,44 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING, TypeVar
4
+
5
+ from ocelescope.ocel.constants import OCELFileExtensions
6
+
7
+ if TYPE_CHECKING:
8
+ from ocelescope.ocel.ocel import OCEL
9
+
10
+ T = TypeVar("T", bound="OCELExtension")
11
+
12
+
13
+ class OCELExtension(ABC):
14
+ """
15
+ Abstract base class for OCEL extensions that can be imported/exported from a file path.
16
+ """
17
+
18
+ name: str
19
+ description: str
20
+ version: str
21
+ supported_extensions: list[OCELFileExtensions]
22
+
23
+ @staticmethod
24
+ @abstractmethod
25
+ def has_extension(path: Path) -> bool:
26
+ """
27
+ Check if the extension data exists at the given path.
28
+ """
29
+ pass
30
+
31
+ @classmethod
32
+ @abstractmethod
33
+ def import_extension(cls: type[T], ocel: "OCEL", path: Path) -> T:
34
+ """
35
+ Create the extension by reading from the given path.
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ def export_extension(self, path: Path) -> None:
41
+ """
42
+ Write the extension data to the given path.
43
+ """
44
+ pass
@@ -0,0 +1,25 @@
1
+ from ocelescope.ocel.filter.apply import apply_filters
2
+
3
+ from .filters import (
4
+ E2OCountFilter,
5
+ EventAttributeFilter,
6
+ EventTypeFilter,
7
+ O2OCountFilter,
8
+ ObjectAttributeFilter,
9
+ TimeFrameFilter,
10
+ OCELFilter,
11
+ ObjectTypeFilter,
12
+ )
13
+
14
+
15
+ __all__ = [
16
+ "apply_filters",
17
+ "OCELFilter",
18
+ "ObjectTypeFilter",
19
+ "EventTypeFilter",
20
+ "ObjectAttributeFilter",
21
+ "EventAttributeFilter",
22
+ "O2OCountFilter",
23
+ "E2OCountFilter",
24
+ "TimeFrameFilter",
25
+ ]
@@ -0,0 +1,55 @@
1
+ from typing import Optional, cast, TYPE_CHECKING
2
+
3
+ import pandas as pd
4
+ import pm4py
5
+
6
+
7
+ from ocelescope.ocel.filter.base import BaseFilter, FilterResult
8
+
9
+ from ocelescope.ocel.filter.filters import OCELFilter
10
+
11
+ if TYPE_CHECKING:
12
+ from ocelescope.ocel.ocel import OCEL
13
+
14
+
15
+ def compute_combined_masks(ocel: "OCEL", filters: OCELFilter) -> FilterResult:
16
+ combined = FilterResult(
17
+ events=pd.Series(True, index=ocel.events.index),
18
+ objects=pd.Series(True, index=ocel.objects.index),
19
+ )
20
+
21
+ for filter in filters.values():
22
+ filter_list = cast(list[BaseFilter], filter if isinstance(filter, list) else [filter])
23
+
24
+ for filter in filter_list:
25
+ combined = combined.and_merge(filter.filter(ocel))
26
+
27
+ return combined
28
+
29
+
30
+ def apply_filters(ocel: "OCEL", filters: OCELFilter) -> "OCEL":
31
+ from ..ocel import OCEL
32
+
33
+ masks = compute_combined_masks(ocel, filters)
34
+
35
+ filtered_event_ids: Optional[pd.Series] = (
36
+ cast(pd.Series, ocel.events[ocel.ocel.event_id_column][masks.events])
37
+ if masks.events is not None
38
+ else None
39
+ )
40
+
41
+ filtered_object_ids: Optional[pd.Series] = (
42
+ cast(pd.Series, ocel.objects[ocel.ocel.object_id_column][masks.objects])
43
+ if masks.objects is not None
44
+ else None
45
+ )
46
+
47
+ filtered_ocel = ocel.ocel
48
+
49
+ if filtered_event_ids is not None:
50
+ filtered_ocel = pm4py.filter_ocel_events(filtered_ocel, filtered_event_ids, positive=True)
51
+
52
+ if filtered_object_ids is not None:
53
+ filtered_ocel = pm4py.filter_ocel_objects(filtered_ocel, filtered_object_ids, positive=True)
54
+
55
+ return OCEL(filtered_ocel, ocel.id)
@@ -0,0 +1,38 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import Optional, TYPE_CHECKING
4
+
5
+ from pandas import Series
6
+ from pydantic import BaseModel
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from ocelescope import OCEL
11
+
12
+
13
+ @dataclass()
14
+ class FilterResult:
15
+ events: Optional[Series] = None
16
+ objects: Optional[Series] = None
17
+
18
+ def and_merge(self, other: "FilterResult") -> "FilterResult":
19
+ def _and(a, b):
20
+ if a is not None and b is not None:
21
+ return a & b
22
+ elif a is not None:
23
+ return a
24
+ elif b is not None:
25
+ return b
26
+ else:
27
+ return None
28
+
29
+ return FilterResult(
30
+ events=_and(self.events, other.events),
31
+ objects=_and(self.objects, other.objects),
32
+ )
33
+
34
+
35
+ class BaseFilter(ABC, BaseModel):
36
+ @abstractmethod
37
+ def filter(self, ocel: "OCEL") -> FilterResult:
38
+ pass
@@ -0,0 +1,32 @@
1
+ from typing import TypedDict
2
+
3
+
4
+ from ocelescope.ocel.filter.filters.attribute import EventAttributeFilter, ObjectAttributeFilter
5
+ from ocelescope.ocel.filter.filters.entity_type import (
6
+ EventTypeFilter,
7
+ ObjectTypeFilter,
8
+ )
9
+ from ocelescope.ocel.filter.filters.relation_count import E2OCountFilter, O2OCountFilter
10
+ from ocelescope.ocel.filter.filters.time_range import TimeFrameFilter
11
+
12
+
13
+ class OCELFilter(TypedDict, total=False):
14
+ object_types: ObjectTypeFilter
15
+ event_type: EventTypeFilter
16
+ time_range: TimeFrameFilter
17
+ o2o_count: list[O2OCountFilter]
18
+ e2o_count: list[E2OCountFilter]
19
+ event_attributes: list[EventAttributeFilter]
20
+ object_attributes: list[ObjectAttributeFilter]
21
+
22
+
23
+ __all__ = [
24
+ "OCELFilter",
25
+ "ObjectTypeFilter",
26
+ "EventTypeFilter",
27
+ "ObjectAttributeFilter",
28
+ "EventAttributeFilter",
29
+ "O2OCountFilter",
30
+ "E2OCountFilter",
31
+ "TimeFrameFilter",
32
+ ]
@@ -0,0 +1,100 @@
1
+ from typing import Optional, Tuple, Union, cast
2
+
3
+ import pandas as pd
4
+ from pandas.api.types import is_datetime64_any_dtype, is_numeric_dtype
5
+ from pandas.core.frame import DataFrame
6
+ from pandas.core.series import Series
7
+ from pydantic.main import BaseModel
8
+
9
+ from ocelescope.ocel.util.attributes import get_objects_with_object_changes
10
+
11
+ from ..base import BaseFilter, FilterResult
12
+
13
+
14
+ class AttributeFilterConfig(BaseModel):
15
+ target_type: str
16
+ attribute: str
17
+
18
+ # Range filters
19
+ time_range: Optional[Tuple[Optional[str], Optional[str]]] = None
20
+ number_range: Optional[Tuple[Optional[Union[int, float]], Optional[Union[int, float]]]] = None
21
+
22
+ # Nominal filters
23
+ values: Optional[list[Union[str, int, float]]] = None
24
+ regex: Optional[str] = None
25
+
26
+
27
+ def filter_by_attribute(attribute_df: DataFrame, type_column: str, config: AttributeFilterConfig):
28
+ df = attribute_df
29
+ col = config.attribute
30
+
31
+ if col not in df.columns:
32
+ raise ValueError(f"Attribute '{col}' not found in {config.target_type} data")
33
+
34
+ series = cast(Series, df[col])
35
+ mask = pd.Series(True, index=series.index)
36
+
37
+ # Handle numeric filtering
38
+ if config.number_range is not None:
39
+ if is_numeric_dtype(series):
40
+ numeric_series = series
41
+ else:
42
+ numeric_series = pd.to_numeric(series, errors="coerce")
43
+
44
+ if config.number_range[0] is not None:
45
+ mask &= numeric_series >= float(config.number_range[0]) # type:ignore
46
+ if config.number_range[1] is not None:
47
+ mask &= numeric_series <= float(config.number_range[1]) # type:ignore
48
+
49
+ # Handle date filtering
50
+ elif config.time_range is not None:
51
+ if is_datetime64_any_dtype(series):
52
+ date_series = series
53
+ else:
54
+ date_series = pd.to_datetime(series, errors="coerce")
55
+
56
+ if config.time_range[0] is not None:
57
+ mask &= date_series >= pd.to_datetime(config.time_range[0])
58
+ if config.time_range[1] is not None:
59
+ mask &= date_series <= pd.to_datetime(config.time_range[1])
60
+
61
+ # Handle nominal filtering
62
+ if config.values is not None:
63
+ mask &= series.isin(config.values)
64
+
65
+ if config.regex is not None:
66
+ mask &= series.astype(str).str.contains(config.regex, regex=True, na=False)
67
+
68
+ is_not_target_type = attribute_df[type_column] != config.target_type
69
+
70
+ final_mask = cast(Series, is_not_target_type | mask)
71
+ return final_mask
72
+
73
+
74
+ class EventAttributeFilter(BaseFilter, AttributeFilterConfig):
75
+ def filter(self, ocel):
76
+ return FilterResult(
77
+ events=filter_by_attribute(
78
+ ocel.events,
79
+ ocel.ocel.event_activity,
80
+ config=AttributeFilterConfig(**self.model_dump()),
81
+ )
82
+ )
83
+
84
+
85
+ class ObjectAttributeFilter(BaseFilter, AttributeFilterConfig):
86
+ def filter(self, ocel):
87
+ enriched_objects = get_objects_with_object_changes(ocel.ocel)
88
+
89
+ filtered_rows = enriched_objects[
90
+ filter_by_attribute(
91
+ enriched_objects,
92
+ ocel.ocel.object_type_column,
93
+ config=AttributeFilterConfig(**self.model_dump()),
94
+ )
95
+ ]
96
+ valid_ids = filtered_rows[ocel.ocel.object_id_column].unique() # type:ignore
97
+
98
+ return FilterResult(
99
+ objects=ocel.ocel.objects[ocel.ocel.object_id_column].isin(valid_ids) # type:ignore
100
+ )
@@ -0,0 +1,31 @@
1
+ from typing import Literal, cast
2
+
3
+ import pandas as pd
4
+
5
+
6
+ from ocelescope.ocel.filter.base import BaseFilter, FilterResult
7
+
8
+
9
+ class EventTypeFilter(BaseFilter):
10
+ event_types: list[str]
11
+ mode: Literal["exclude", "include"] = "include"
12
+
13
+ def filter(self, ocel):
14
+ mask = cast(pd.Series, ocel.events["ocel:activity"].isin(self.event_types))
15
+ if self.mode == "exclude":
16
+ mask = ~mask
17
+
18
+ return FilterResult(events=mask)
19
+
20
+
21
+ class ObjectTypeFilter(BaseFilter):
22
+ object_types: list[str]
23
+ mode: Literal["exclude", "include"] = "include"
24
+
25
+ def filter(self, ocel):
26
+ mask = cast(pd.Series, ocel.objects["ocel:type"].isin(self.object_types))
27
+
28
+ if self.mode == "exclude":
29
+ mask = ~mask
30
+
31
+ return FilterResult(objects=mask)
@@ -0,0 +1,142 @@
1
+ from typing import Literal, Optional, cast
2
+
3
+ import pandas as pd
4
+ from pandas.core.frame import DataFrame
5
+ from pandas.core.series import Series
6
+ from pydantic import BaseModel
7
+
8
+ from ocelescope.ocel.util.relations import getO2OWithTypes
9
+
10
+ from ocelescope.ocel.filter.base import BaseFilter, FilterResult
11
+
12
+
13
+ class RelationCountFilterConfig(BaseModel):
14
+ source: str
15
+ target: str
16
+ mode: Optional[Literal["include", "exclude"]] = "include"
17
+ range: tuple[Optional[int], Optional[int]]
18
+ qualifier: Optional[str] = None
19
+
20
+
21
+ def filter_by_relation_counts(
22
+ relation_table: pd.DataFrame,
23
+ source_id_column: str,
24
+ source_column: str,
25
+ target_column: str,
26
+ qualifier_column: str,
27
+ entity_id_column: str,
28
+ entity_type_column: str,
29
+ source_df: pd.DataFrame,
30
+ config: RelationCountFilterConfig,
31
+ ):
32
+ # Get o2o with types if not provided
33
+ relation_table = cast(
34
+ DataFrame,
35
+ relation_table[
36
+ (relation_table[source_column] == config.source)
37
+ & (relation_table[target_column] == config.target)
38
+ ],
39
+ )
40
+
41
+ if config.qualifier is not None:
42
+ relation_table = cast(
43
+ DataFrame,
44
+ relation_table[relation_table[qualifier_column] == config.qualifier],
45
+ )
46
+
47
+ # Count how many times each target appears
48
+ entity_counts = cast(Series, relation_table.groupby(source_id_column).size()).reset_index(
49
+ name="entity_count"
50
+ )
51
+
52
+ min_count, max_count = config.range
53
+
54
+ if min_count is not None:
55
+ entity_counts = entity_counts[entity_counts["entity_count"] >= min_count]
56
+ if max_count is not None:
57
+ entity_counts = entity_counts[entity_counts["entity_count"] <= max_count]
58
+ entity_counts = cast(Series, entity_counts[source_id_column])
59
+
60
+ if min_count == 0:
61
+ merged = pd.merge(
62
+ source_df[source_df[entity_type_column] == config.source],
63
+ relation_table,
64
+ left_on=entity_id_column,
65
+ right_on=source_id_column,
66
+ how="left",
67
+ indicator=True,
68
+ )
69
+ entities_with_no_relations = merged.loc[merged["_merge"] == "left_only", entity_id_column]
70
+
71
+ entity_counts = pd.concat([entity_counts, entities_with_no_relations])
72
+
73
+ # Mask for non-target-type objects (always kept)
74
+ is_not_target_type = source_df[entity_type_column] != config.source
75
+
76
+ # Mask for objects meeting the relation count condition
77
+ is_in_filtered_ids = source_df[entity_id_column].isin(entity_counts)
78
+
79
+ # Invert if in exclude mode
80
+ if config.mode == "exclude":
81
+ is_in_filtered_ids = ~is_in_filtered_ids
82
+
83
+ # Final mask: keep non-target-type or qualifying objects
84
+ final_mask = cast(Series, is_not_target_type | is_in_filtered_ids)
85
+
86
+ return final_mask
87
+
88
+
89
+ class E2OCountFilter(BaseFilter, RelationCountFilterConfig):
90
+ direction: Literal["source", "target"] = "source"
91
+
92
+ def filter(self, ocel):
93
+ source_column = (
94
+ ocel.ocel.event_activity if self.direction == "source" else ocel.ocel.object_type_column
95
+ )
96
+ target_column = (
97
+ ocel.ocel.object_type_column if self.direction == "source" else ocel.ocel.event_activity
98
+ )
99
+ source_id_column = (
100
+ ocel.ocel.event_id_column if self.direction == "source" else ocel.ocel.object_id_column
101
+ )
102
+ entity_id_column = source_id_column
103
+ qualifier_column = ocel.ocel.qualifier
104
+ entity_type_column = source_column
105
+
106
+ mask = filter_by_relation_counts(
107
+ relation_table=ocel.relations,
108
+ source_column=source_column,
109
+ target_column=target_column,
110
+ source_id_column=source_id_column,
111
+ qualifier_column=qualifier_column,
112
+ entity_id_column=entity_id_column,
113
+ entity_type_column=entity_type_column,
114
+ source_df=ocel.events if self.direction == "source" else ocel.objects,
115
+ config=RelationCountFilterConfig(**self.model_dump()),
116
+ )
117
+
118
+ return FilterResult(
119
+ events=mask if self.direction == "source" else None,
120
+ objects=mask if self.direction == "target" else None,
121
+ )
122
+
123
+
124
+ class O2OCountFilter(BaseFilter, RelationCountFilterConfig):
125
+ direction: Literal["source", "target"] = "source"
126
+
127
+ def filter(self, ocel):
128
+ o2oTable = getO2OWithTypes(ocel.ocel, direction=self.direction)
129
+
130
+ mask = filter_by_relation_counts(
131
+ relation_table=o2oTable,
132
+ source_column="source_type",
133
+ target_column="target_type",
134
+ source_id_column="source",
135
+ qualifier_column="qualifier",
136
+ entity_id_column=ocel.ocel.object_id_column,
137
+ entity_type_column=ocel.ocel.object_type_column,
138
+ source_df=ocel.objects,
139
+ config=RelationCountFilterConfig(**self.model_dump()),
140
+ )
141
+
142
+ return FilterResult(objects=mask)
@@ -0,0 +1,30 @@
1
+ from typing import Literal, Optional
2
+
3
+ import pandas as pd
4
+
5
+ from ocelescope.ocel.filter.base import BaseFilter, FilterResult
6
+
7
+
8
+ class TimeFrameFilter(BaseFilter):
9
+ time_range: tuple[Optional[str], Optional[str]]
10
+ mode: Literal["exclude", "include"] = "include"
11
+
12
+ def filter(self, ocel):
13
+ start_time, end_time = self.time_range
14
+
15
+ if start_time is not None:
16
+ start_time = pd.Timestamp(start_time, tz="UTC")
17
+ if end_time is not None:
18
+ end_time = pd.Timestamp(end_time, tz="UTC")
19
+
20
+ events_df = ocel.events
21
+
22
+ mask = pd.Series([True] * len(events_df), index=events_df.index)
23
+ if start_time is not None:
24
+ mask &= events_df["ocel:timestamp"] >= start_time
25
+ if end_time is not None:
26
+ mask &= events_df["ocel:timestamp"] <= end_time
27
+ if self.mode == "exclude":
28
+ mask = ~mask
29
+
30
+ return FilterResult(events=mask)