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.
- ocelescope-0.1.1b1/PKG-INFO +26 -0
- ocelescope-0.1.1b1/README.md +13 -0
- ocelescope-0.1.1b1/pyproject.toml +32 -0
- ocelescope-0.1.1b1/src/ocelescope/__init__.py +60 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/__init__.py +32 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/constants.py +4 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/extension.py +44 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/__init__.py +25 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/apply.py +55 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/base.py +38 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/__init__.py +32 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/attribute.py +100 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/entity_type.py +31 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/relation_count.py +142 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/filter/filters/time_range.py +30 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/ocel.py +633 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/util/__init__.py +5 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/util/attributes.py +216 -0
- ocelescope-0.1.1b1/src/ocelescope/ocel/util/relations.py +150 -0
- ocelescope-0.1.1b1/src/ocelescope/plugin/__init__.py +22 -0
- ocelescope-0.1.1b1/src/ocelescope/plugin/decorators.py +200 -0
- ocelescope-0.1.1b1/src/ocelescope/plugin/input.py +59 -0
- ocelescope-0.1.1b1/src/ocelescope/plugin/plugin.py +43 -0
- ocelescope-0.1.1b1/src/ocelescope/resource/__init__.py +6 -0
- ocelescope-0.1.1b1/src/ocelescope/resource/default/__init__.py +0 -0
- ocelescope-0.1.1b1/src/ocelescope/resource/default/dfg.py +124 -0
- ocelescope-0.1.1b1/src/ocelescope/resource/default/petri_net.py +131 -0
- ocelescope-0.1.1b1/src/ocelescope/resource/resource.py +38 -0
- ocelescope-0.1.1b1/src/ocelescope/util/__init__.py +458 -0
- ocelescope-0.1.1b1/src/ocelescope/util/cache.py +203 -0
- ocelescope-0.1.1b1/src/ocelescope/util/pandas.py +458 -0
- ocelescope-0.1.1b1/src/ocelescope/visualization/__init__.py +35 -0
- ocelescope-0.1.1b1/src/ocelescope/visualization/default/__init__.py +0 -0
- ocelescope-0.1.1b1/src/ocelescope/visualization/default/dot.py +21 -0
- ocelescope-0.1.1b1/src/ocelescope/visualization/default/graph.py +73 -0
- ocelescope-0.1.1b1/src/ocelescope/visualization/default/svg.py +8 -0
- ocelescope-0.1.1b1/src/ocelescope/visualization/default/table.py +18 -0
- 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,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)
|