Skip to main content
Rust4PM

Python

Install r4pm, load a real event log, discover a process model and check how well it fits the data, in about 40 lines of Python.

Install

pip install "r4pm[polars]"

r4pm needs Python 3.9 or newer. Where a prebuilt wheel exists for your platform, pip installs it and you need nothing else. Otherwise pip builds from source, which needs a Rust toolchain. The polars extra is only needed for DataFrames (item_to_df and the r4pm.df module). More on extras and platforms in installation.

Get the sample data

The quickstart uses the Road Traffic Fine Management log: 150,370 fines, 561,470 events, 11 activities.

The script

Save this as quickstart.py next to the data folder:

python
import r4pm
import r4pm.petri_net

# Load the event log into the item registry. Python only holds its ID.
log = r4pm.import_item("EventLog", "data/road-traffic-fines.xes.gz")

# Directly-follows graph: which activity follows which, and how often.
dfg = r4pm.bindings.discover_dfg(log)
print(len(dfg["activities"]), "activities")

# Alpha+++ works on the activity projection: every trace variant once, with its count.
projection = r4pm.bindings.log_to_activity_projection(log)
print(r4pm.bindings.get_num_variants(projection), "variants")

net = r4pm.bindings.discover_alphaplusplusplus(
    projection,
    {
        "balance_thresh": 0.1,
        "fitness_thresh": 0.8,
        "replay_thresh": 0.0,
        "log_repair_skip_df_thresh_rel": 4.0,
        "log_repair_loop_df_thresh_rel": 4.0,
        "absolute_df_clean_thresh": 1,
        "relative_df_clean_thresh": 0.01,
    },
)
print(len(net["places"]), "places,", len(net["transitions"]), "transitions")

# How well does the model explain the log? Align every variant, then aggregate.
alignments = r4pm.bindings.align_variants(net, projection)
fitness = r4pm.bindings.compute_fitness(alignments, net)
print(f"log fitness {fitness['log_fitness']:.3f}")

r4pm.petri_net.export_pnml(net, "model.pnml")

# The same log as a polars DataFrame, one row per event.
df = r4pm.item_to_df(log)
print(df.shape)

Run it:

python quickstart.py
output
11 activities
231 variants
10 places, 16 transitions
log fitness 0.886
(561470, 16)

Alpha+++ also prints about 20 lines of progress information while it runs, which the output above leaves out.

The script step by step

Load the log

r4pm.import_item(kind, path) reads the file into Rust4PM's item registry and returns an ID string. The kind says which data structure to build (EventLog for a classic XES log), and the file extension selects the format, so .xes, .xes.gz and .json all work. Event logs stay in Rust, and Python only passes the ID around. The item registry page explains why and how.

Discover a directly-follows graph

discover_dfg(log) returns plain Python data: a dict where activities maps each activity to its frequency, directly_follows_relations holds ((from, to), frequency) pairs, and start_activities and end_activities are lists of activity names.

Discover a Petri net with Alpha+++

Alpha+++ works on an activity projection, a compact form of the log that keeps every trace variant once with its count. log_to_activity_projection creates it as a new registry item. The road traffic log has 231 variants, so everything after this step is fast even though the log has 150,370 traces.

The thresholds passed to discover_alphaplusplusplus filter rare behavior before and during discovery. The discovery how-to explains each of them. The resulting Petri net is plain data again (places, transitions, arcs, markings).

Check conformance

align_variants computes an optimal alignment between the model and each variant. compute_fitness combines them, weighted by how often each variant occurs. It returns log_fitness, average_fitness, perfectly_fitting_frac and total_costs. A log fitness of 0.886 means the model explains most of the observed behavior but not all of it.

Export and DataFrames

r4pm.petri_net.export_pnml writes the net as PNML, which ProM, PM4Py and Propel can open. r4pm.petri_net.to_pm4py converts it to PM4Py objects directly. r4pm.item_to_df(log) turns the log into a polars DataFrame with one row per event (an OCEL becomes a dict of DataFrames). Call .to_pandas() on it for pandas.

Next steps