Skip to main content

Building an OCEL from CSV tables

Read events, objects, and relationships from CSV tables and build an OCEL 2.0 log. Works the same way for Pandas or Polars dataframes.

The script below builds a SlimLinkedOCEL from five CSVs: one for events, one per object type (here order and item), and one each for E2O and O2O relationships. It reads them with the standard library csv module to keep the example dependency-free. Any source that yields row-dicts works the same way.

locel_add_event and locel_add_object accept an id argument, so the CSV string IDs (e1, o3, i7) go in directly. The relationship loops resolve them back to integer refs via locel_get_ev_by_id and locel_get_ob_by_id, so there's no manual id-to-ref map. The only ordering requirement is that events and objects are added before the relationships that reference them.

Input tables

Download the example CSVs or copy them from the previews. Drop them next to the script before running.

  • events.csvDownload
    event_id,event_type,timestamp,tip
    e1,place order,2026-03-02T10:00:00Z,2.50
    e2,place order,2026-03-02T10:15:00Z,5.00
    e3,place order,2026-03-02T10:30:00Z,3.25
    e4,place order,2026-03-02T10:45:00Z,0.00
    e5,place order,2026-03-02T11:00:00Z,7.50
    
  • orders.csvDownload
    order_id
    o1
    o2
    o3
    o4
    o5
    
  • items.csvDownload
    item_id,price
    i1,4.30
    i2,8.60
    i3,4.30
    i4,12.90
    i5,8.60
    i6,4.30
    i7,12.90
    i8,8.60
    i9,4.30
    i10,4.30
    i11,8.60
    i12,12.90
    
  • e2o.csvDownload
    event_id,object_id,qualifier
    e1,o1,of
    e1,i1,with
    e1,i2,with
    e2,o2,of
    e2,i3,with
    e2,i4,with
    e2,i5,with
    e3,o3,of
    e3,i6,with
    e3,i7,with
    e4,o4,of
    e4,i8,with
    e4,i9,with
    e5,o5,of
    e5,i10,with
    e5,i11,with
    e5,i12,with
    
  • o2o.csvDownload
    source_object_id,target_object_id,qualifier
    i1,o1,is in
    i2,o1,is in
    i3,o2,is in
    i4,o2,is in
    i5,o2,is in
    i6,o3,is in
    i7,o3,is in
    i8,o4,is in
    i9,o4,is in
    i10,o5,is in
    i11,o5,is in
    i12,o5,is in
    

Script

"""
Build a SlimLinkedOCEL from CSV tables.

Inputs (in the same directory as this script):
    events.csv             one row per event
    orders.csv, items.csv  one row per object, one CSV per object type
    e2o.csv, o2o.csv       relationships

Run:     python example.py
Install: pip install r4pm
"""

import csv
from pathlib import Path

import r4pm
import r4pm.bindings as b

HERE = Path(__file__).parent


def read_csv(name):
    with (HERE / name).open(newline="") as f:
        return list(csv.DictReader(f))


# --- Declare types ---

locel = b.locel_new()
b.locel_add_event_type(locel, "place order", [{"name": "tip", "type": "float"}])
b.locel_add_object_type(locel, "order")
b.locel_add_object_type(locel, "item", [{"name": "price", "type": "float"}])

# --- Add events and objects ---
# Pass the CSV string ID as the OCEL `id`. The relationship loops below resolve
# it back to an integer ref via `locel_get_ev_by_id` / `locel_get_ob_by_id`,
# so no manual id-to-ref dict is needed. Order matters: every event/object
# must be added before any relationship that references it.

for row in read_csv("events.csv"):
    # Attribute values are positional, in the order declared above.
    b.locel_add_event(
        locel,
        row["event_type"],
        row["timestamp"],
        row["event_id"],
        [float(row["tip"])],
    )

for row in read_csv("orders.csv"):
    b.locel_add_object(locel, "order", row["order_id"])

for row in read_csv("items.csv"):
    # Object attributes are time-indexed; UNIX epoch stands in for "constant".
    history = [[("1970-01-01T00:00:00Z", float(row["price"]))]]
    b.locel_add_object(locel, "item", row["item_id"], history)

# --- Add relationships ---

for row in read_csv("e2o.csv"):
    b.locel_add_e2o(
        locel,
        b.locel_get_ev_by_id(locel, row["event_id"]),
        b.locel_get_ob_by_id(locel, row["object_id"]),
        row["qualifier"],
    )

for row in read_csv("o2o.csv"):
    b.locel_add_o2o(
        locel,
        b.locel_get_ob_by_id(locel, row["source_object_id"]),
        b.locel_get_ob_by_id(locel, row["target_object_id"]),
        row["qualifier"],
    )

# --- Export ---
# Extension picks the OCEL 2.0 format: .xml / .sqlite / .json
r4pm.export_item(locel, "export.sqlite")
print("Exported.")

r4pm.export_item picks the OCEL 2.0 format from the file extension: .xml, .sqlite, or .json.