Skip to main content

RelML

RelML is a relational machine learning engine built by Guepard. It trains a heterogeneous graph neural network (HeteroGraphSAGE) directly on a relational database: it detects foreign keys, builds the entity graph, encodes node features, and trains end-to-end without any manual feature joining. Predictions are locally explainable via a built-in LIME module.

The C++20 core handles encoding, message passing, training, and Adam optimization. A Python layer built on DuckDB handles data loading and task definition. A pybind11 binding connects the two. On Apple Silicon the BLAS backend is Apple Accelerate (uses the AMX co-processor); elsewhere it falls back to OpenBLAS.


Quick start

git clone https://github.com/guepard/relml && cd relml
./install.sh                  # builds the C++ extension + installs the Python package
source .venv/bin/activate     # the installer creates this virtualenv
python -c "import guepard.qwery.relml as r; print('relml ok')"   # verify

./install.sh needs CMake, a C++20 compiler, and Python ≥ 3.9 (xcode-select --install && brew install cmake python on macOS; sudo apt-get install build-essential cmake python3-dev python3-venv on Debian/Ubuntu). To build by hand instead, or to configure the natural-language agent, see Installation below.


Repository layout

RelML/
├── README.md                   ← this file (canonical entry point)
├── CMakeLists.txt              ← C++ build
├── benchmarks/                 ← comparisons vs PyG, baselines, applied demos
│   ├── proposal/               ← canonical RelML-vs-PyG suite
│   │   ├── f1_relbench_compare.py    ← F1 driver-top3 comparison
│   │   ├── ml1m_compare.py           ← MovieLens-1M comparison
│   │   ├── f1_compare.py             ← legacy F1 (Postgres-backed) comparison
│   │   ├── sunnyside_compare.py      ← regression comparison
│   │   ├── relml_pyg.py              ← PyG worker (HeteroGraphSAGE mirror)
│   │   ├── _common.py                ← shared helpers (metrics, subprocess tee)
│   │   └── make_plots.py             ← regenerates the docs/figures/ PDFs
│   ├── f1_podium/              ← F1 podium prediction + analysis (Postgres `f1`)
│   │   ├── predict_montreal.py            ← forecast next race (binary podium)
│   │   ├── predict_montreal_position.py   ← forecast via position regression
│   │   ├── eval_podium.py / eval_formonly.py / eval_enriched.py / eval_regression.py
│   │   └── README.md                      ← methodology + findings
│   ├── baselines/              ← simpler baselines (flat MLP, etc.)
│   └── requirements.txt
├── bindings/                   ← pybind11 → _relml_core
├── docs/
│   ├── integration.md          ← detailed integration guide
│   ├── proposal.tex            ← AWS grant proposal
│   └── figures/                ← plot PDFs (regenerated from benchmark CSVs)
├── data/                       ← (gitignored) datasets — see Section 2 below
├── draft/                      ← (gitignored) working artifacts, history CSVs
├── include/relml/              ← public C++ headers
│   ├── encoding/HeteroEncoder.h
│   ├── gnn/HeteroGraphSAGE.h, SAGEKernels.h, MLPHead.h
│   ├── graph/HeteroGraph.h, GraphBuilder.h
│   └── training/Trainer.h, Adam.h, TaskSpec.h
├── python/
│   ├── pyproject.toml          ← package + console scripts (relml-agent/-serve/-schema)
│   ├── examples/               ← end-to-end Python examples
│   │   ├── ml1m_ratings.py            ← MovieLens-1M demo (CSV)
│   │   ├── binary_f1_example.py       ← F1 qualifying top-3 demo (CSV)
│   │   ├── f1_podium.py               ← F1 podium task def (Postgres `f1`)
│   │   ├── sunny_side_demand.py       ← daily-demand regression demo (CSV)
│   │   └── sunnyside_v2_demand.py     ← daily order-count forecast (Postgres
│   │                                    `sunnyside_v2`): holidays + weather +
│   │                                    lags, residual modelling, 20-day backtest
│   └── guepard/
│       ├── qwery/relml/        ← core Python package (TaskSpec, LIMEExplainer, …)
│       └── tools/              ← developer & agent tools (one folder per tool)
│           ├── README.md              ← tools index
│           ├── agent/                 ← NL agent: REPL + orchestrator/subagents,
│           │                            dashboards, live server, JSON-RPC sidecar
│           └── schema_introspect/     ← describe a DB as JSON: schema/PK/FK/
│               │                        stats/samples + RelML dataset_schema
│               ├── core.py            ← describe_database() + CLI
│               ├── api.py             ← FastAPI HTTP wrapper
│               ├── tests.py           ← tests (CSV; PG via env var)
│               └── README.md
├── scripts/                    ← data import helpers (relbench → CSV)
├── src/                        ← C++ implementation
│   ├── encoding/                  HeteroEncoder, type-aware column encoders
│   ├── gnn/                       HeteroGraphSAGE, SAGEKernels, MLPHead
│   ├── graph/                     HeteroGraph, GraphBuilder, FKDetector
│   ├── training/                  Trainer, Adam, Metrics, TaskSpec
│   ├── agent/                     LLM agent layer (TaskParser, RelMLSystem)
│   └── example_tasks/             C++ end-to-end examples
└── tests/                      ← C++ tests + bench
    ├── test_grad_check.cpp        gradient check vs central differences
    ├── test_training.cpp          end-to-end MovieLens-1M training
    ├── test_gnn.cpp / test_encoding.cpp / test_graph.cpp / test_database.cpp
    └── bench_gnn.cpp              microbench of the per-epoch hot path

Installation

RelML has two halves: a C++ core compiled into a Python extension (_relml_core), and a Python package (guepard) that ships the DuckDB data layer, the LIME explainer, and the relml-agent CLI.

Prerequisites

Need macOS Debian / Ubuntu
C++20 compiler + CMake ≥ 3.20 xcode-select --install && brew install cmake sudo apt-get install build-essential cmake
Python ≥ 3.9 with pip brew install python sudo apt-get install python3-pip python3-venv
Ruby (for the uplot plotter) brew install ruby sudo apt-get install ruby
optional: OpenMP + logo tools brew install libomp chafa librsvg sudo apt-get install libomp-dev chafa librsvg2-bin

The first build needs a network connection (CMake fetches pybind11 and nlohmann/json).

One command

git clone https://github.com/guepard/relml && cd relml
./install.sh
source .venv/bin/activate          # the installer creates this virtualenv

install.sh creates a virtualenv (.venv), builds _relml_core against it, installs the Python package (giving you the relml-agent, relml-serve and relml-schema commands), installs the external CLI tools, and seeds a .env. It never touches the system Python and is idempotent — re-run it anytime. Flags: --no-venv, --no-editable, --skip-external, --jobs N; env PYTHON=python3.11 ./install.sh picks the interpreter, VENV=path the location. If a virtualenv is already active, the installer uses it as-is.

Manual install

Always install into a virtualenv so the build, the extension, and the CLI all share one interpreter:

# 0. Create and activate a virtualenv.
python3 -m venv .venv && source .venv/bin/activate

# 1. Build the Python extension against THIS interpreter. RELML_BUILD_EXTRAS=OFF
#    skips the C++ tests, example tasks, and the libcurl-dependent agent lib.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DRELML_BUILD_EXTRAS=OFF \
      -DPython_EXECUTABLE="$(which python)"
cmake --build build --target _relml_core -j

# 2. Install the Python package (editable) — adds the CLI commands.
pip install -e python/

# 3. External CLI tools the agent shells out to (YouPlot, chafa, librsvg).
bash external/install.sh

# 4. Credentials for the agent.
cp .env.example .env   # then edit

The build copies _relml_core.*.so into python/guepard/qwery/relml/ automatically.

Verify

With the virtualenv active:

python -c "import guepard.qwery.relml as r; print('relml ok')"
python -m guepard.tools.agent.tests          # offline test suite

Configure the agent

The agent talks to Claude via AWS Bedrock (bearer token) or the Anthropic API. Put one of these in .env (see .env.example for the full list of options):

AWS_BEARER_TOKEN_BEDROCK=...   # selects the Bedrock backend
# or
ANTHROPIC_API_KEY=...          # first-party Anthropic API

.env is gitignored — never commit it.

Run

With the virtualenv active (source .venv/bin/activate):

relml-agent --source "dbname=sunnyside_v2"                        # connect to a database
relml-agent                                                       # bare REPL, then /connect <db>
relml-agent "who will podium in Montreal?" --source "dbname=f1"   # one-shot question

A source can be a Postgres DSN, a CSV/Parquet file, or a folder of them. The agent, its full REPL command set, and the live dashboards are documented in The natural-language agent below.

Developer build (C++ tests & benchmarks)

Building with extras on (the default) compiles the C++ test suite, gradient check, example tasks, and the standalone agent library (this path also needs libcurl — brew install curl / sudo apt-get install libcurl4-openssl-dev):

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release      # RELML_BUILD_EXTRAS=ON
cmake --build build -j
./build/test_grad_check                             # gradient correctness

# benchmark vs PyTorch Geometric (downloads relbench F1 CSVs once)
OMP_NUM_THREADS=8 python benchmarks/proposal/f1_relbench_compare.py \
    --data-dir data/rel-f1-data --epochs 50 --history-csv draft/f1_proposal.csv

For the full library tutorial (task definition, training, prediction, explanation, a worked MovieLens example), keep reading — or see docs/integration.md.


The natural-language agent

Everything above is the RelML library. RelML also ships a natural-language agent: point it at a database, ask a question in English, and a Claude orchestrator explores the schema, designs a TaskSpec, trains a model, evaluates it, and answers — delegating to schema-analyst, task-designer and evaluator subagents along the way. It is installed by install.sh / pip install -e python/ as three console commands:

Command What it is
relml-agent The interactive CLI / REPL (and one-shot mode). Start here.
relml-serve A headless JSON-RPC sidecar over stdio, for embedding RelML in another host (e.g. the qwery TUI). Not interactive.
relml-schema Describe any database as JSON (schema, PK/FK, per-column stats, samples) — see python/guepard/tools/schema_introspect/.

First, configure a backend — see Configure the agent (one of AWS_BEARER_TOKEN_BEDROCK or ANTHROPIC_API_KEY in .env).

Launching relml-agent

# Interactive REPL, already connected to a database:
relml-agent --source "dbname=sunnyside_v2"

# Bare REPL — start with nothing, then /connect from inside:
relml-agent

# One-shot: answer a single question, then drop into the REPL (TTY) or exit:
relml-agent "who will podium in Montreal?" --source "dbname=f1"

# Truly headless one-shot (no REPL afterward):
relml-agent "forecast next week" --source ./data --once

A --source is a Postgres DSN (dbname=f1, or a full dbname=f1 user=me host=…), a CSV/Parquet file, or a folder of them. A bare Postgres database name is accepted and expanded to dbname=<name>.

All flags:

Flag Default Purpose
question (positional) The question to ask. Omit for an interactive REPL. A one-shot question requires --source (or $RELML_AGENT_SOURCE).
--source <dsn|path> $RELML_AGENT_SOURCE Database/file/folder to connect to.
--pg-schema <name> all schemas Restrict a Postgres source to one schema.
--backend bedrock|anthropic auto Force a backend (otherwise inferred from which credential is set).
--model <id> per backend Override the Claude model id.
--region <aws-region> us-east-1 Bedrock region.
--max-steps <n> 40 Cap the agent's tool-call iterations per question.
--quiet off Don't stream the subagents' thinking / tool calls.
--once off Answer and exit; don't drop into the REPL afterward.
--json off Headless NDJSON mode: one JSON event per line, then a final {"event":"result",…}. No banner/colors/REPL. Requires a one-shot question. For embedding in another tool.

Connecting to a database

Your data does not have to be in Postgres. The agent runs on an in-memory DuckDB engine and can ingest three kinds of source — pick whichever you already have:

Source Example What happens
Postgres (DSN or bare name) dbname=f1 · dbname=f1 user=me host=db.internal port=5432 · postgresql://me@host/f1 DuckDB's postgres extension attaches the database read-only, then copies every base table into the in-memory DuckDB. The Postgres server must be running and reachable at connect time; your DB is never written to.
CSV / Parquet file ./ratings.csv · ./events.parquet The file is read into one DuckDB table named after the file (e.g. ratings).
Folder of files ./data Every *.csv / *.parquet in the folder becomes a table (named after each file).

So the answer to "does it need to be in Postgres first?" is no — Postgres is only one option. If your data lives in Postgres, point the agent at it and it attaches read-only; if it's in flat files, point at the file or folder directly. Either way the agent operates on an in-memory copy, so it can never modify your source, and you can run any read-only SQL against it with /sql.

Two ways to attach:

# 1. At launch, with --source:
relml-agent --source "dbname=sunnyside_v2"
relml-agent --source ./data           # a folder of CSV/Parquet
# 2. From inside the REPL, with /connect (start bare, then attach):
relml ❯ /databases                    # list Postgres databases on the server (works disconnected)
relml ❯ /connect sunnyside_v2         # bare name → expanded to dbname=sunnyside_v2
relml ❯ /connect dbname=f1 user=me    # full DSN
relml ❯ /connect ./ratings.csv        # a file or folder

If you don't know the database name, run /databases first — it probes the local Postgres server (or a DSN you pass, e.g. /databases "dbname=postgres host=…") and lists what's available, then /connect <name>. /connect also switches datasources mid-session, and accepts a bare name, a full DSN, or a file/folder path. On connect, any models previously trained for that source are restored automatically (see /models). To restrict a multi-schema Postgres database to one schema, pass --pg-schema <name> at launch.

Prerequisite for Postgres sources: the Postgres server must be running and the DSN must reach it (host/port/user/password as needed) — connection uses DuckDB's postgres extension, fetched on first use (needs network once). CSV/Parquet sources need nothing beyond the files.

REPL commands

Inside the REPL, type a question in plain English to run the agent, or use a /command. Commands accept unique-prefix matching (/con/connect) and Tab-completion (command names, and model ids for /model, /predict, /evaluate, /plot). Aliases: /? = /help, /source = /connect, /exit = /quit.

Command Arguments Description
/help, /? List commands.
/connect, /source <dsn|path> Attach (or switch) a datasource — Postgres DSN, CSV/Parquet, or folder.
/databases [dsn] List databases on the Postgres server. Works disconnected (probes dbname=postgres, or a DSN you pass).
/tables Tables in the connected DB with row counts, primary keys, FK counts, and a kind classification.
/schema [table] Columns, types, RelML types, and keys — all tables, or one.
/sql <query> Run a read-only SQL query (SELECT / WITH / DESCRIBE / EXPLAIN / PRAGMA / SHOW / SUMMARIZE).
/models Models trained this session (or restored for this DB) with held-out MAE/R² and feature counts.
/model <id> One model's full details: task, target, split, hyperparams, features, dropped edges, held-out metrics.
/delete <id> | all Delete a model (or all) from disk for this database; also removes any dashboards that referenced it.
/predict <id> [k] Predict with a model and show the top-k rows (default 20). Materializes a <task>__pred table.
/evaluate <id> [where] Critique a model: metrics, vs-baseline %, by-segment breakdown, and prioritized suggestions. Optional holdout WHERE filter.
/explain <id> [where] Why a prediction — LIME feature contributions. Supports a date (/explain "date = '2026-05-19'") or a window (/explain each, /explain model_1 last 20 days).
/plot [id [where] | <sql>] ASCII chart of actual vs predicted (defaults to the backtest window / best model), or the numeric columns of a SQL query.
/serve [port | stop] Open dashboards live in the browser — see below.
/reset Clear the conversation (trained models are kept).
/clear Clear the screen.
/quit, /exit Leave (stops the live server if running).

Trained models are persisted per database under ~/.relml/agent_models (override with RELML_MODEL_DIR) and restored automatically when you reconnect to the same source, so /models, /predict, /explain and /plot work without retraining.

Live dashboards (/serve)

A dashboard is a self-contained HTML page built from a manifest of blocks (text, metric, line, bar, scatter, table), each backed by read-only SQL whose results are baked in — so the saved .html opens offline. Ask the agent to build one in natural language:

relml ❯ generate a dashboard for model_0
relml ❯ add a bar chart of total orders by weekday to that dashboard

Dashboards are saved per datasource under ~/.relml/dashboards/<datasource>/ (<name>.json is the source of truth; <name>.html is rendered from it). Override the location with RELML_DASHBOARD_DIR.

/serve makes them interactive:

relml ❯ /serve            # starts a local server (default port 8765) and opens the browser
relml ❯ /serve 8790       # pick a different port
relml ❯ /serve stop       # shut the server down

Served over http, each page becomes editable — an "Add to dashboard" box (natural language → the agent builds a block), an × to remove a block, and drag-to-reorder. The page also has a dark-mode toggle, interactive chart tooltips/crosshair, and CSV export on table cards. The default port is RELML_DASHBOARD_PORT (8765); the server reuses the REPL's warm agent, so no second model load.

Important: the Add/Update dock only works from the http link that /serve prints (e.g. http://127.0.0.1:8765/d/<db>/<name>.html) — not from double-clicking the saved .html file, which is a read-only report.

Headless embedding (relml-serve)

relml-serve is a long-lived sidecar that drives the agent over a framed JSON-RPC channel on stdio — for embedding RelML in a host application. It is not interactive; see python/guepard/tools/agent/rpc/PROTOCOL.md for the wire protocol and method list (including the deterministic, no-LLM explain and dashboard methods). The same --backend / --model / --region / --max-steps flags apply.

Configuration & environment variables

Credentials and overrides are read from the environment or the nearest .env (searched upward from the package). See .env.example for the annotated list.

Variable Default Effect
AWS_BEARER_TOKEN_BEDROCK Use AWS Bedrock; setting it selects the Bedrock backend.
ANTHROPIC_API_KEY Use the first-party Anthropic API (when no Bedrock token is set).
RELML_AGENT_BACKEND auto Force bedrock or anthropic.
RELML_AGENT_MODEL per backend Override the Claude model id.
RELML_AGENT_REGION / AWS_REGION us-east-1 Bedrock region.
RELML_AGENT_SOURCE Default --source if none is passed.
RELML_MODEL_DIR ~/.relml/agent_models Where trained models are persisted per database.
RELML_DASHBOARD_DIR ~/.relml/dashboards Where dashboards are saved.
RELML_DASHBOARD_PORT 8765 Default port for /serve.
RELML_VERBOSE off Show full subagent reasoning + exploratory tables.
RELML_NO_LOGO off Skip the banner logo image in the REPL.

Tests

python -m guepard.tools.agent.tests        # offline agent test suite (no DB, no network)

Documentation

  • This README — install, repo layout, the natural-language agent, end-to-end library tutorial.
  • The natural-language agentrelml-agent launch modes and flags, the full REPL command set, live dashboards (/serve), the relml-serve sidecar, and every environment variable.
  • docs/integration.md — detailed integration guide: TaskSpec, inference schemas for agents, model persistence, LIME.
  • benchmarks/proposal/README.md — how the RelML-vs-PyG comparison works, what each script does, how the CSVs and plots are produced.
  • benchmarks/f1_podium/README.md — applied F1 podium prediction on a live Postgres DB: methodology, validation (≈70% hit-rate), feature-ceiling findings, and how to forecast an upcoming race.
  • python/guepard/tools/ — developer & agent tools, one folder per tool. Includes schema_introspect/: point it at a Postgres DSN or CSV/Parquet folder and get organized JSON of every table (schema, PK/FK, per-column stats, samples) plus a ready-to-use RelML dataset_schema — as a library, CLI, or FastAPI HTTP API.
  • docs/proposal.tex — the AWS grant proposal that cites the benchmark results in this repo.

Applied demos on live Postgres databases

Two examples train directly against local Postgres databases (attached read-only via DuckDB) rather than CSVs — they are not part of CI but are documented, working references:


Table of contents


1. Build

See Installation above for the full guide. The short version, into a virtualenv:

git clone https://github.com/guepard/relml && cd relml
./install.sh && source .venv/bin/activate

That builds _relml_core and installs the Python package (which pulls in scikit-learn for LIME). The build copies _relml_core.*.so into python/guepard/qwery/relml/ automatically.


2. Data loading

RelML uses DuckDB as the data layer. You load your tables into an in-memory DuckDB connection and pass it to relml.train(). DuckDB handles any SQL transformation you need before the data reaches the GNN.

import duckdb

conn = duckdb.connect(":memory:")

conn.execute('CREATE TABLE users   AS SELECT * FROM read_csv_auto("users.csv",   header=true)')
conn.execute('CREATE TABLE movies  AS SELECT * FROM read_csv_auto("movies.csv",  header=true)')
conn.execute('CREATE TABLE ratings AS SELECT * FROM read_csv_auto("ratings.csv", header=true)')

Any DuckDB-compatible source works: Parquet, JSON, remote files, or SQL-derived tables. The only requirement is that the connection exposes the tables by name before you call relml.train().


3. Task definition

A TaskSpec describes what to predict and how to split the data. The sql field defines the task table: the table whose rows will be scored, one prediction per row.

from guepard.qwery.relml import TaskSpec

task = TaskSpec(
    sql             = "SELECT * FROM ratings",
    task_table_name = "ratings",
    target_column   = "rating",
    task_type       = "binary_classification",
    label_transform = {"kind": "threshold", "threshold": 4.0, "inclusive": True},
    split_strategy  = "temporal",
    time_col        = "timestamp",
)

sql

Any SQL query. The result is materialized as task_table_name before training. This is where you can add lag features, rolling aggregates, or any derived columns:

sql = """
    SELECT
        r.*,
        LAG(rating, 1) OVER (PARTITION BY userId ORDER BY timestamp) AS prev_rating
    FROM ratings r
"""

task_type

Value Use case
"binary_classification" Will a user churn? Will an ad be clicked?
"regression" How many orders tomorrow? What price?
"multiclass_classification" Home win / Draw / Away win?

label_transform

Kind Effect
{"kind": "threshold", "threshold": 4.0} Label = 1 if value >= 4.0
{"kind": "normalize"} Standardize to zero mean, unit variance
{"kind": "buckets", "buckets": [0.5, 1.5]} Label = class index (0, 1, 2, ...)

split_strategy

"temporal" sorts rows by time_col before splitting 70/15/15. "random" uses a deterministic shuffle. Temporal split is strongly recommended whenever your data has a time axis.

dataset_schema

When you know your schema, declare it explicitly. This skips heuristic FK detection, guarantees the GNN sees exactly the graph you intend, and is significantly faster:

task = TaskSpec(
    ...
    dataset_schema = {
        "users":   {"pk": "userId",   "fks": []},
        "movies":  {"pk": "movieId",  "fks": []},
        "ratings": {"pk": "ratingId", "fks": [
            {"column": "userId",  "references": "users"},
            {"column": "movieId", "references": "movies"},
        ]},
    }
)

When dataset_schema is None, RelML infers PKs from column names and FKs from name matching and value coverage (≥ 99%). Explicit schema is recommended for production.


4. Training

import guepard.qwery.relml as relml

model = relml.train(
    conn       = conn,
    task       = task,
    channels   = 64,
    gnn_layers = 2,
    hidden     = 64,
    dropout    = 0.3,
    lr         = 3e-4,
    epochs     = 30,
    batch_size = 0,
)

Training prints a per-epoch table with train loss and validation metrics (AP + AUC for classification, RMSE + MAE + R² for regression), then the best validation and final test metrics.

batch_size = 0 means full-batch training. For large datasets set it to a positive integer such as 4096: the encoder and GNN always run on the full graph, but the MLP head loss and backward pass are computed in chunks.


5. Saving and loading a model

# Save after training
relml.save_model(model, "my_model.bin")

# Load on a subsequent run — skips training entirely
model = relml.load_model(
    conn       = conn,       # same connection, same tables
    task       = task,       # same TaskSpec
    path       = "my_model.bin",
    channels   = 64,         # must match training values exactly
    gnn_layers = 2,
    hidden     = 64,
    dropout    = 0.3,
    lr         = 3e-4,
)

load_model rebuilds the encoder vocabularies and graph from the data (required to match parameter shapes), then overwrites the random initialisation with the saved weights. Architecture parameters must be identical to those used during train().


6. Prediction

Score all rows in the task table

predictions = model.predict_all()
# Returns a list of floats, one per row, in task table insertion order.
# Binary classification : probability in [0, 1].
# Regression            : original-scale value (denormalized automatically).
# Multiclass            : predicted class index as a float.

Entity synthesis

Predict for a hypothetical combination of entities that may not exist as a row:

prob = model.predict_entity({"userId": "42", "movieId": "1193"})

The model looks up each entity's GNN embedding, mean-pools them, and runs the MLP head. Useful for recommendation: "would user 42 like movie 1193?"

Row-based aggregation

Filter and aggregate over existing rows:

spec = TaskSpec(
    ...
    inference_mode    = "row_based",
    inference_filters = [{"column": "userId", "op": "=", "value": "42"}],
    inference_agg     = "mean",
)
result = spec.apply_inference(db, predictions)
print(result.aggregate)   # mean predicted rating for user 42

inference_agg options: "mean", "fraction" (positive rate), "count" (predicted positives), "none" (per-row).


7. Explanation (LIME)

RelML includes a LIME explainer that works with any model and any task table. It perturbs features in the task table, runs the full forward pass for each perturbation, and fits a weighted Ridge surrogate to identify which features drove the prediction.

from guepard.qwery.relml.explainer import LIMEExplainer

explainer = LIMEExplainer(
    model           = model,
    task_table      = "ratings",
    target_column   = "rating",
    exclude_columns = ["ratingId", "timestamp"],  # PKs and time columns
    n_samples       = 200,
)

exp = explainer.explain(row_index=500)
print(exp)
exp.save_html("explanation_row500.html")

Explaining multiple rows

explanations = explainer.explain_many([42, 101, 205], num_features=10)

Reliability

The surrogate R² indicates how well the linear approximation captures the model locally:

Interpretation
≥ 0.7 Reliable
0.5 – 0.7 Moderate, interpret with care
< 0.5 Poor fit, highly non-linear region

LIME only perturbs features in the task table. Signal arriving via GNN message passing from neighbour tables is held fixed across all perturbations. R² will be lower when that graph-aggregated signal dominates the prediction.


8. Worked example: MovieLens-1M

This section walks through a complete run on the MovieLens-1M dataset so you can see exactly what the data looks like, what RelML does with it, and what output to expect.

Dataset

Three CSV files, 6040 users, 3706 movies, 1 000 209 ratings.

users.csv — one row per user

userId gender age occupation zip
1 F 1 10 48067
2 M 56 16 70072
3 M 25 15 55117

movies.csv — one row per movie

movieId title genres
1 Toy Story (1995) Animation|Children's|Comedy
2 Jumanji (1995) Adventure|Children's|Fantasy
1193 One Flew Over the Cuckoo's Nest (1975) Drama

ratings.csv — one row per (user, movie) interaction

ratingId userId movieId rating timestamp
1 1 1193 5 978300760
2 1 661 3 978302109
3 1 914 3 978301968

The task is to predict whether a user will give a movie 4 or 5 stars (positive) vs 1, 2, or 3 stars (negative).

Relational graph

RelML builds the following entity graph from the declared schema:

users ←── [userId] ──── ratings ──── [movieId] ──→ movies
              (6 040 nodes)  (1 000 209 nodes)  (3 706 nodes)

Each rating row is a node. Its two FK edges connect it to its user node and its movie node. With 2 GNN layers, information flows:

  • Layer 1: rating nodes absorb user demographics and movie genre. User nodes absorb all their rating history. Movie nodes absorb all ratings they received.
  • Layer 2: rating nodes absorb the now-enriched user and movie embeddings, giving each rating access to collaborative signals (what else this user rated, what other users rated this movie).

Step-by-step code

import duckdb
from pathlib import Path
import guepard.qwery.relml as relml
from guepard.qwery.relml import TaskSpec
from guepard.qwery.relml.explainer import LIMEExplainer

DATA_DIR   = Path("data/ml-1m-data")
MODEL_PATH = "ml1m.bin"

SCHEMA = {
    "users":   {"pk": "userId",   "fks": []},
    "movies":  {"pk": "movieId",  "fks": []},
    "ratings": {"pk": "ratingId", "fks": [
        {"column": "userId",  "references": "users"},
        {"column": "movieId", "references": "movies"},
    ]},
}

# ── 1. Load ────────────────────────────────────────────────────────────────
conn = duckdb.connect(":memory:")
for name in ("users", "movies", "ratings"):
    conn.execute(
        f'CREATE TABLE {name} AS '
        f'SELECT * FROM read_csv_auto("{DATA_DIR / name}.csv", header=true)'
    )

# ── 2. Task ────────────────────────────────────────────────────────────────
task = TaskSpec(
    sql             = "SELECT * FROM ratings",
    task_table_name = "ratings",
    target_column   = "rating",
    task_type       = "binary_classification",
    label_transform = {"kind": "threshold", "threshold": 4.0, "inclusive": True},
    split_strategy  = "temporal",
    time_col        = "timestamp",
    dataset_schema  = SCHEMA,
)

# ── 3. Train or load ───────────────────────────────────────────────────────
if Path(MODEL_PATH).exists():
    model = relml.load_model(
        conn=conn, task=task, path=MODEL_PATH,
        channels=64, gnn_layers=2, hidden=64, dropout=0.3, lr=3e-4,
    )
else:
    model = relml.train(
        conn=conn, task=task,
        channels=64, gnn_layers=2, hidden=64, dropout=0.3,
        lr=3e-4, epochs=30,
    )
    relml.save_model(model, MODEL_PATH)

# ── 4. Predict all ─────────────────────────────────────────────────────────
predictions = model.predict_all()
n_pos = sum(p > 0.5 for p in predictions)
print(f"Predicted positive rate: {n_pos / len(predictions):.2%}")
# → Predicted positive rate: 57.43%

# ── 5. Entity synthesis ────────────────────────────────────────────────────
# Does user 1 like One Flew Over the Cuckoo's Nest (movieId=1193)?
prob = model.predict_entity({"userId": "1", "movieId": "1193"})
print(f"P(user 1 likes movie 1193) = {prob:.4f}")
# → P(user 1 likes movie 1193) = 0.8241
# (user 1 actually gave it 5 stars — the model is confident)

# ── 6. Explain ─────────────────────────────────────────────────────────────
explainer = LIMEExplainer(
    model           = model,
    task_table      = "ratings",
    target_column   = "rating",
    exclude_columns = ["ratingId", "timestamp"],
    n_samples       = 200,
)

exp = explainer.explain(row_index=0)  # first rating in the task table
print(exp)
exp.save_html("ml1m_explanation_row0.html")

What training prints

  Trainable tensors: 214  total floats: 1 847 296

----------------------------------------------------------------------------------------
 Epoch |  Train Loss  |   Val AP  |  Val AUC  |  Val Acc | Time (s)
----------------------------------------------------------------------------------------
    1  | 0.687412     |   0.6823  |   0.7104  |   0.6531 | 3.21
    2  | 0.661083     |   0.7012  |   0.7298  |   0.6714 | 3.18
   ...
   30  | 0.581244     |   0.7489  |   0.7801  |   0.7102 | 3.24
----------------------------------------------------------------------------------------

Best val:
  loss              : 0.5731
  average_precision : 0.7512
  roc_auc           : 0.7834
  accuracy          : 0.7143
  f1                : 0.7089

Test:
  loss              : 0.5884
  average_precision : 0.7401
  roc_auc           : 0.7712
  accuracy          : 0.7089
  f1                : 0.7031

What an explanation looks like

LIMEExplanation
  table      : ratings
  target     : rating
  row        : 0
  prediction : 0.8241
  R²         : 0.871  (reliable)
  n_samples  : 200
  kw         : 2.90

  Feature                          Value              Importance
  -----------------------------------------------------------------
  + genres                         Drama                   +3.1042
  + age                            1                       +1.9231
  + gender                         F                       +0.8812
  - occupation                     10                      -0.7104

Row 0 is user 1 rating movie 1193 (Drama). The model is confident (0.82) because Drama is the genre most correlated with high ratings in the training data, and age group 1 (under 18) has a strong positive prior toward this movie in particular. The LIME surrogate fits well (R² = 0.871), meaning the linear approximation captures the model's local behaviour reliably here.


9. Architecture parameters

Parameter Default Description
channels 64 Embedding dimension for all node types
gnn_layers 2 Number of HeteroGraphSAGE message-passing layers
hidden 64 Hidden dimension of the MLP head
dropout 0.3 Dropout rate in the MLP head (0 = disabled)
lr 3e-4 Adam learning rate
epochs 30 Training epochs
batch_size 0 MLP head mini-batch size (0 = full batch)

Tuning guidance:

  • Small datasets (< 5k rows): channels=32, hidden=64, epochs=200–500, dropout=0.2.
  • Large datasets (> 100k rows): channels=64–128, batch_size=4096–16384, epochs=20–50.
  • Overfitting (train loss drops, val metric flat): increase dropout, reduce channels.
  • Underfitting (both losses stay high): increase channels, gnn_layers, or epochs.

10. Supported task types

Task type Label transform Output Metrics
binary_classification threshold Probability [0, 1] AP, ROC-AUC, accuracy, F1
regression normalize Original-scale value RMSE, MAE, R²
multiclass_classification buckets Class index Accuracy, macro-F1

For multiclass, buckets defines the class boundaries. Three classes require two boundaries:

label_transform = {"kind": "buckets", "buckets": [0.5, 1.5]}
# raw=0 → class 0,  raw=1 → class 1,  raw=2 → class 2

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

guepard_relml-0.1.0.tar.gz (1.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

guepard_relml-0.1.0-cp313-cp313-macosx_15_0_arm64.whl (405.4 kB view details)

Uploaded CPython 3.13macOS 15.0+ ARM64

File details

Details for the file guepard_relml-0.1.0.tar.gz.

File metadata

  • Download URL: guepard_relml-0.1.0.tar.gz
  • Upload date:
  • Size: 1.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for guepard_relml-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1bd5e6de4cc9cd0d6f3f371bb6c65583fb0b2bb349e5ec126570b5932a31f84b
MD5 a742b409f61c731a1601b0f32631d134
BLAKE2b-256 8d45041e0c7017e1474bc4ba19ba9e41c9f01b9e9023e29c86001b9ef7115393

See more details on using hashes here.

File details

Details for the file guepard_relml-0.1.0-cp313-cp313-macosx_15_0_arm64.whl.

File metadata

File hashes

Hashes for guepard_relml-0.1.0-cp313-cp313-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 48dfb3d84522f5659a079a252459abe377880568cf31de13852de672537b370d
MD5 066ecdd4a1509c4264f85a7af51861c8
BLAKE2b-256 bfc931b06494a9c28a480a76e230a933381030dc0051f37e5834ced2502c778c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page