Skip to main content
Rust4PM

Analyze object-centric data

Directly-follows graphs per object type and OC-DECLARE constraints on an OCEL 2.0 log, without flattening it first.

Both techniques work on a SlimLinkedOCEL, the linked form of an OCEL that indexes all event-to-object relations. The event data types page explains the difference to OCEL.

Object-centric directly-follows graph

An object-centric DFG (OC-DFG) has one directly-follows graph per object type. For each type, every object becomes a trace with the events it is involved in.

import r4pm

ocel = r4pm.import_item("SlimLinkedOCEL", "data/order-management.xml.gz")

# One directly-follows graph per object type, each from the log flattened on that type.
ocdfg = r4pm.bindings.discover_dfg_from_ocel(ocel)
for object_type, dfg in sorted(ocdfg["object_type_to_dfg"].items()):
    print(f"{object_type:<10} {len(dfg['activities']):>2} activities, {len(dfg['directly_follows_relations']):>3} edges")
output
customers   2 activities,   4 edges
employees   8 activities,  34 edges
items      11 activities,  56 edges
orders      4 activities,   5 edges
packages    4 activities,   5 edges
products   11 activities, 121 edges

Orders follow a short, regular life cycle (4 activities, 5 edges). Products are involved in every activity and show the most varied behavior. Reference: discover_dfg_from_ocel.

OC-DECLARE constraints

OC-DECLARE describes behavior as constraints between activities, scoped by the objects involved. A constraint such as EF(place order, create package, Each(items), ...) reads: for each item involved in a place order event, a create package event involving that item must occur afterwards. Discovery finds every constraint that holds for all but a noise_threshold share of events.

import r4pm

ocel = r4pm.import_item("SlimLinkedOCEL", "data/order-management.xml.gz")

# These are the defaults. A dict passed here must contain every field.
options = {
    "noise_threshold": 0.2,  # share of events allowed to violate a constraint
    "o2o_mode": "None",
    "acts_to_use": None,  # None: all activities
    "counts_for_generation": [1, None],
    "counts_for_filter": [1, 20],
    "reduction": "None",
    "refinement": False,
    "considered_arrow_types": ["AS", "EF", "EP", "DF", "DP"],
}
arcs = r4pm.bindings.discover_oc_declare(ocel, options)
print(len(arcs), "constraints")


def describe(arc):
    """TYPE(from, to, Each(...), All(...), Any(...)), as the Rust example prints it."""
    involvement = [
        f"{kind.capitalize()}({','.join(o['object_type'] for o in arc['label'][kind])})"
        for kind in ("each", "all", "any")
        if arc["label"][kind]
    ]
    return f"{arc['arc_type']}({arc['from']}, {arc['to']}, {', '.join(involvement)})"


# Strongest first, ties by name, so the output is stable.
scored = sorted(
    ((r4pm.bindings.oc_declare_conformance(ocel, arc), describe(arc)) for arc in arcs),
    key=lambda pair: (-pair[0], pair[1]),
)
for share, text in scored[:5]:
    print(f"{share:.1%}  {text}")
output
116 constraints
100.0%  AS(confirm order, pick item, Each(items), Any(products))
100.0%  AS(confirm order, pick item, Each(products), Any(items))
100.0%  AS(create package, confirm order, Each(items), Any(products))
100.0%  AS(create package, pay order, Each(items), Any(products))
100.0%  AS(create package, pay order, Each(products), Any(items))

The discovery order of constraints can differ between runs, so the snippets sort them by score and then by name. oc_declare_conformance returns the share of events that satisfy a constraint, so discovered constraints score at least 80% with the default noise threshold of 0.2.

Arc typeMeaning (from the crate documentation)
ASAssociation: no temporal restriction
EFEventually-follows: the target occurs after the source event
EPEventually-precedes: the target occurs before the source event
DFDirectly-follows: the target occurs directly after the source event, among events that involve all required objects
DPDirectly-precedes: the target occurs directly before the source event, among events that involve all required objects

Each constraint also carries counts (the minimum and maximum number of matching target events) and the object involvement. Each(t) requires the target events for every object of type t separately, Any(t) for at least one of them, and All(t) for target events that involve all of them. References: discover_oc_declare, oc_declare_conformance.

Back to case-centric

Some questions are easier with a single case notion. Filter and flatten turns the OCEL into an event log per object type, ready for Alpha+++ and alignments.