Skip to content

init

eventlog ¤

ts-shape canonical event-log package.

A lightweight, pm4py-compatible (XES + OCEL 2.0) representation of detector output. ts-shape itself adds no process-mining dependencies — the columns match the specs verbatim so users can hand the resulting DataFrames to pm4py / Disco / Celonis / OCEL viewers directly.

Typical use::

from ts_shape.eventlog import (
    to_event_log, concat, to_event_log_xes, to_event_log_ocel,
)
from ts_shape.events.production.machine_state import MachineStateEvents
from ts_shape.events.quality.outlier_detection import OutlierDetectionEvents

state_log = to_event_log(
    MachineStateEvents(df, run_state_uuid="m").detect_run_idle(),
    detector="MachineStateEvents.detect_run_idle",
)
outlier_log = to_event_log(
    OutlierDetectionEvents(df, value_column="value_double").detect_outliers_zscore(),
    detector="OutlierDetectionEvents.detect_outliers_zscore",
)
log = concat(state_log, outlier_log)
xes_df = to_event_log_xes(log, case_object_type="asset")
tables = to_event_log_ocel(log)  # OCEL2Tables(events, objects, relations, ...)

LambdaDetector ¤

LambdaDetector(spec: RuleSpec)

Runnable form of a :class:RuleSpec.

evaluate ¤

evaluate(df: DataFrame) -> pd.DataFrame

Apply the rule to df and return a legacy-shaped DataFrame.

RuleSpec dataclass ¤

RuleSpec(
    id: str,
    class_name: str,
    method_name: str,
    pack: str,
    shape: str,
    archetype: str,
    template: str,
    trigger: TriggerSpec,
    produces_objects: tuple[str, ...] = ("asset",),
    severity_field: str | None = None,
    value_field: str | None = None,
    standard_attrs: Mapping[str, object] = dict(),
)

A complete lambda-rule definition.

Required fields mirror the built-in REGISTRY contract — class_name is synthesized (must start with Lambda) so the rule slots into the same (class, method) -> LabelRule table as the 290 built-ins.

TriggerSpec dataclass ¤

TriggerSpec(
    expression: str,
    min_duration_s: float | None = None,
    group_by: tuple[str, ...] = (),
)

When does the rule fire?

expression is evaluated against the input DataFrame by the AST-restricted compiler in :mod:.expression. It must produce a boolean Series aligned with the input rows.

min_duration_s and group_by are only meaningful for shape="interval" rules: consecutive True rows are coalesced (per group) and the resulting interval is dropped if shorter than min_duration_s seconds.

UnsafeExpression ¤

Bases: ValueError

Raised when a trigger expression contains a disallowed AST node.

EventLog dataclass ¤

EventLog(
    events: DataFrame = schema.empty_events(),
    objects: DataFrame = schema.empty_objects(),
    relations: DataFrame = schema.empty_relations(),
    o2o: DataFrame = schema.empty_o2o(),
    object_changes: DataFrame = schema.empty_object_changes(),
)

Canonical ts-shape event log in OCEL 2.0 shape.

Five relational tables, mirroring the OCEL 2.0 standard:

  • events — one row per detected event (+ event attributes),
  • objects — the objects events refer to (ocel:oid / ocel:type),
  • relations — event-to-object (E2O) relations with qualifiers,
  • o2o — object-to-object relations with qualifiers,
  • object_changes — time-varying object attribute values.

objects / relations are empty when the source detector has no natural object association (e.g. a global cross-signal correlation statistic); o2o / object_changes are empty unless supplied. Use :attr:has_objects to check before calling :func:to_event_log_xes.

ObjectSpec dataclass ¤

ObjectSpec(
    uuid: str,
    object_type: str,
    value_column: str = "value_string",
    min_duration: str | None = None,
    id_template: str = "{value}",
    attributes: Mapping[str, str] = dict(),
)

Declares that one signal carries the identity of one object type.

Attributes:

Name Type Description
uuid str

The signal (uuid value) whose value column is the object id.

object_type str

OCEL object type, e.g. "batch", "coil", "serial", "recipe". Auto-registered via :func:~ts_shape.eventlog.schema.register_object_type if unknown.

value_column str

Column carrying the id ("value_string" or "value_integer").

min_duration str | None

Optional minimum presence (e.g. "1min"); shorter blips are dropped.

id_template str

How to render the object id from the raw value. Supports {value} and {type} — default "{value}"; use "{type}:{value}" to namespace ids across types.

attributes Mapping[str, str]

{attribute_name: signal_uuid} — values captured at each presence start (backward merge_asof) and emitted as object_changes.

OCEL2Tables dataclass ¤

OCEL2Tables(
    events: DataFrame,
    objects: DataFrame,
    relations: DataFrame,
    o2o: DataFrame,
    object_changes: DataFrame,
)

The five OCEL 2.0 relational tables produced from an :class:EventLog.

register_adapter ¤

register_adapter(
    class_name: str, method_name: str
) -> Callable[[AdapterFn], AdapterFn]

Register a custom adapter for a specific (class, method).

The function will be called with (legacy_df, *, rule, detector, objects, qualifiers).

align_columns ¤

align_columns(*logs: EventLog) -> list[EventLog]

Reindex every log's events table to the union of their columns.

Each returned :class:EventLog exposes an identical, identically-ordered set of event columns — the canonical core first (in emit order), then the remaining attribute columns sorted for determinism. Columns absent from a given log are added and filled with NA, so the frames can be appended or stacked directly without a column mismatch. objects and relations already have fixed schemas and are returned unchanged.

Returns a list parallel to the inputs. An empty call returns [].

to_event_log_xes ¤

to_event_log_xes(
    eventlog: EventLog,
    *,
    case_object_type: str = "asset",
    lifecycle: str = "single"
) -> pd.DataFrame

Flatten the event log into an XES-style DataFrame.

A trace is built per object of case_object_type — each event linked to that object becomes one (or two) rows in the trace.

lifecycle="single" — interval events become one row with lifecycle:transition="complete". time:timestamp is the interval-end; start_timestamp is exposed verbatim.

lifecycle="two_row" — interval events expand into a start row (using ts_shape:start_timestamp) and a complete row, paired by concept:instance.

compile_expression ¤

compile_expression(
    expression: str,
) -> Callable[[pd.DataFrame], pd.Series]

Compile a trigger expression to a vectorized boolean mask function.

The returned callable accepts a :class:pandas.DataFrame and returns a :class:pandas.Series of booleans (NaN → False) aligned with the frame's rows.

load_dicts ¤

load_dicts(
    entries: Iterable[Mapping[str, Any]],
) -> list[LambdaDetector]

Compile + register an iterable of dict rules.

load_yaml ¤

load_yaml(path: str | Path) -> list[LambdaDetector]

Load and register every rule under a YAML file's rules: key.

YAML is imported lazily so :mod:ts_shape.eventlog does not gain a hard runtime dependency on PyYAML for users who only use built-in detectors. Install pyyaml to enable this loader.

register_lambda_rule ¤

register_lambda_rule(spec: RuleSpec) -> LambdaDetector

Register spec in REGISTRY + ARCHETYPE_BY_METHOD; return runnable detector.

Raises :class:ValueError if the spec is malformed, the standard_attrs mapping uses unknown keys, the archetype's required keys are missing, or a rule with the same (class_name, method_name) is already registered.

run_backtest ¤

run_backtest(
    detector: LambdaDetector,
    df: DataFrame,
    *,
    objects: Mapping[str, object] | None = None,
    qualifiers: Mapping[str, str] | None = None
) -> BacktestResult

Run detector over df and summarize hit counts.

unregister_lambda_rule ¤

unregister_lambda_rule(
    class_name: str, method_name: str
) -> None

Remove a rule previously installed by :func:register_lambda_rule.

Used by tests to keep the global REGISTRY clean between cases. No-op if the rule was not registered.

to_event_log ¤

to_event_log(
    df: DataFrame,
    *,
    detector: str,
    objects: Mapping[str, object] | None = None,
    qualifiers: Mapping[str, str] | None = None,
    o2o: TableInput = None,
    object_changes: TableInput = None,
    validate: bool = True
) -> EventLog

Normalize a legacy detector DataFrame into the canonical event log.

detector is "ClassName.method_name" — the same key used in the taxonomy registry and the value written to ts_shape:detector.

objects binds OCEL object types to either:

  • a string column name in df (e.g. {"asset": "source_uuid"}),
  • a callable taking a row dict and returning an oid,
  • a pd.Series aligned with df rows,
  • a scalar broadcast to every row.

Caller-supplied bindings are always honored. Types listed in the adapter's LabelRule.produces_objects are also auto-extracted from standard legacy columns (e.g. source_uuid -> asset) when no explicit binding is given.

o2o and object_changes optionally attach the OCEL 2.0 object-to-object relations and time-varying object attributes. Each accepts a DataFrame (or an iterable of dict rows) with the canonical columns — see :func:ts_shape.eventlog.schema.empty_o2o / :func:~ts_shape.eventlog.schema.empty_object_changes.

attach_objects ¤

attach_objects(
    event_log: EventLog,
    df: DataFrame,
    specs: Sequence[ObjectSpec],
    *,
    qualifiers: Mapping[str, str] | None = None,
    uuid_column: str = "uuid",
    time_column: str = "systime",
    hierarchy: Mapping[str, str] | None = None,
    infer_o2o: bool = True,
    validate: bool = True
) -> EventLog

Link an existing event log to detected objects by temporal containment.

For every event, any object whose presence interval contains the event timestamp is linked with an event-to-object (E2O) relation. The detected objects, o2o and object_changes are merged in. This enriches the output of any event detector — no per-detector changes required. See :func:detect_objects for hierarchy (declared part_of edges).

This is a convenience over :func:relate: it first segments df into an object-interval table via :func:object_intervals, then relates the events to it. When you already hold an interval/object table, call :func:relate directly and skip the raw signals.

derive_o2o ¤

derive_o2o(
    intervals: DataFrame,
    *,
    hierarchy: Mapping[str, str] | None = None
) -> pd.DataFrame

Derive object-to-object (O2O) relations from an interval table.

Public entry point over the overlap kernel: a declared hierarchy (child type -> parent type) yields part_of along overlapping presence intervals; without one, overlapping objects of different types get the symmetric co_occurs qualifier. Returns the OCEL 2.0 o2o table.

detect_objects ¤

detect_objects(
    df: DataFrame,
    specs: Sequence[ObjectSpec],
    *,
    uuid_column: str = "uuid",
    time_column: str = "systime",
    hierarchy: Mapping[str, str] | None = None,
    infer_o2o: bool = True,
    validate: bool = True
) -> EventLog

Detect object instances and build their OCEL 2.0 tables.

Produces an :class:EventLog with no events — only objects, o2o (object-to-object relations), and object_changes (presence lifecycle + captured attributes). Compose it with event-detector logs via :func:~ts_shape.eventlog.concat.concat.

hierarchy maps a child object type to its parent type (e.g. {"serial": "batch", "batch": "work_order"}). A compositional part_of relation is asserted only along these declared edges, when a child's presence overlaps a parent's — because part_of cannot be inferred from time alone (a long-running tool temporally contains every batch without owning it). Without a declared hierarchy, overlapping objects of different types get the honest, symmetric co_occurs qualifier instead. Set infer_o2o=False to skip object-to-object relations entirely.

link_events_to_objects(
    events: DataFrame,
    intervals: DataFrame,
    *,
    qualifiers: Mapping[str, str] | None = None,
    key_columns: Sequence[str] | None = None,
    contain: bool = True
) -> pd.DataFrame

Derive all event-to-object (E2O) relations from two lists.

Takes an OCEL events table and an object/interval table (oid, type and, for temporal linking, start / end) and returns the OCEL 2.0 relations table -- with no access to the raw signals. Two complementary strategies are unioned and de-duplicated:

  • temporal containment (contain, default on): link an event to every object whose presence interval [start, end] contains the event timestamp. Requires start / end columns.
  • key match (opt-in via key_columns): link an event to the object whose oid equals the value in one of the event's key_columns -- for events that already name their object (e.g. a batch attribute).

qualifiers maps an object type to the relation qualifier to stamp (e.g. {"batch": "processed_in"}); unmapped types get <NA>.

object_intervals ¤

object_intervals(
    df: DataFrame,
    specs: Sequence[ObjectSpec],
    *,
    uuid_column: str = "uuid",
    time_column: str = "systime"
) -> pd.DataFrame

Extract one presence interval per object instance from id signals.

Returns a tidy frame with columns oid, type, start, end plus one column per captured attribute. Each id signal is run-length encoded into contiguous constant-value segments.

object_specs_from_metadata ¤

object_specs_from_metadata(
    metadata: DataFrame,
    *,
    type_field: str = "object_type",
    value_column_field: str = "object_value_column",
    uuid_column: str = "uuid"
) -> list[ObjectSpec]

Build :class:ObjectSpec list from per-signal metadata tags.

Scans a metadata table (e.g. MetadataJsonLoader.to_df()) for a column naming the object type per signal — so new object types are onboarded by tagging metadata, not by writing code. The uuid may be the index or a column; the type/value-column fields are matched directly or with a config_ / config. prefix (mkdocs metadata flattening).

relate ¤

relate(
    event_log: EventLog,
    intervals: DataFrame,
    *,
    hierarchy: Mapping[str, str] | None = None,
    qualifiers: Mapping[str, str] | None = None,
    key_columns: Sequence[str] | None = None,
    infer_o2o: bool = True,
    validate: bool = True
) -> EventLog

Assemble a full OCEL 2.0 log from an event list and an object list.

This is the automated relationship step: given an existing event_log (with events) and an intervals table describing objects -- columns oid, type and, for temporal linking, start / end (plus any attribute columns) -- it derives all relations and returns a validated :class:EventLog whose five tables are ready to ingest into an OCEL 2.0 database:

  • objects from the distinct (oid, type) pairs,
  • relations (E2O) via :func:link_events_to_objects (temporal containment, plus optional key_columns matching),
  • o2o via :func:derive_o2o (declared part_of hierarchy or symmetric co_occurs),
  • object_changes (lifecycle + captured attributes).

Unlike :func:attach_objects, no raw signals or :class:ObjectSpec are needed -- you bring the two lists, ts-shape derives the relationships.

to_event_log_ocel ¤

to_event_log_ocel(eventlog: EventLog) -> OCEL2Tables

Return the OCEL 2.0 tables as an :class:OCEL2Tables bundle.

The canonical schema already uses OCEL 2.0 column names, so this is a pass-through with defensive copy() calls to prevent caller mutation.

register_object_type ¤

register_object_type(name: str) -> None

Register a custom OCEL object type so adapters can emit it.

validate ¤

validate(eventlog: EventLog) -> None

Raise ValueError if the event log violates the canonical schema.