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
postgresextension, 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
/serveprints (e.g.http://127.0.0.1:8765/d/<db>/<name>.html) — not from double-clicking the saved.htmlfile, 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 agent —
relml-agentlaunch modes and flags, the full REPL command set, live dashboards (/serve), therelml-servesidecar, 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. Includesschema_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 RelMLdataset_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:
- F1 podium (
dbname=f1) —benchmarks/f1_podium/, task defined inpython/examples/f1_podium.py. - Sunny Side v2 demand (
dbname=sunnyside_v2) —python/examples/sunnyside_v2_demand.py: daily order-count forecast with holiday/weather/lag features, residual modelling, and a strict 20-day backtest (beats the seasonal baseline by ~56%).
Table of contents
- 1. Build
- 2. Data loading
- 3. Task definition
- 4. Training
- 5. Saving and loading a model
- 6. Prediction
- 7. Explanation (LIME)
- 8. Worked example: MovieLens-1M
- 9. Architecture parameters
- 10. Supported task types
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:
| R² | 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, reducechannels. - Underfitting (both losses stay high): increase
channels,gnn_layers, orepochs.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1bd5e6de4cc9cd0d6f3f371bb6c65583fb0b2bb349e5ec126570b5932a31f84b
|
|
| MD5 |
a742b409f61c731a1601b0f32631d134
|
|
| BLAKE2b-256 |
8d45041e0c7017e1474bc4ba19ba9e41c9f01b9e9023e29c86001b9ef7115393
|
File details
Details for the file guepard_relml-0.1.0-cp313-cp313-macosx_15_0_arm64.whl.
File metadata
- Download URL: guepard_relml-0.1.0-cp313-cp313-macosx_15_0_arm64.whl
- Upload date:
- Size: 405.4 kB
- Tags: CPython 3.13, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48dfb3d84522f5659a079a252459abe377880568cf31de13852de672537b370d
|
|
| MD5 |
066ecdd4a1509c4264f85a7af51861c8
|
|
| BLAKE2b-256 |
bfc931b06494a9c28a480a76e230a933381030dc0051f37e5834ced2502c778c
|