# Rai Predictive Modeling

> Build graph neural network (GNN) models — concepts, Snowflake data loading, task relationships, graph edges, and PropertyTransformer features. Use for node classification, regression, and link prediction tasks; for training, predictions, and evaluation, see `rai-predictive-training`.

- **Type:** Skill
- **Install:** `agentstack add skill-relationalai-rai-agent-skills-rai-predictive-modeling`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [RelationalAI](https://agentstack.voostack.com/s/relationalai)
- **Installs:** 0
- **Category:** [Data & Analytics](https://agentstack.voostack.com/c/data-and-analytics)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [RelationalAI](https://github.com/RelationalAI)
- **Source:** https://github.com/RelationalAI/rai-agent-skills/tree/main/plugins/rai/skills/rai-predictive-modeling
- **Website:** https://relational.ai

## Install

```sh
agentstack add skill-relationalai-rai-agent-skills-rai-predictive-modeling
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Predictive Modeling

> **Early access.** The RAI predictive reasoner (GNN) is in early access — APIs, engine requirements, and behavior may change. Confirm the latest surface with the RelationalAI team before production use.

## Summary

**What:** Data modeling workflow for GNN pipelines -- from imports through graph construction and feature configuration.

**When to use:**
- Defining concepts and loading data from Snowflake
- Building graph structure (edges, self-references)
- Configuring task relationships (train/val/test splits)
- Setting up PropertyTransformer features

**When NOT to use:**
- Training, predictions, evaluation, model management -- see `rai-predictive-training`
- Graph algorithms (centrality, community detection) -- see `rai-graph-analysis`

**Overview:** 6 steps: imports -> concepts -> populate -> task relationships -> graph -> features

---

## Prerequisites

### Experiment schema setup (one-time, ACCOUNTADMIN)

GNN training writes experiment artifacts to a Snowflake schema. Create a database and schema you own, then grant the RELATIONALAI native app the four required privileges:

```sql
CREATE DATABASE IF NOT EXISTS ;
CREATE SCHEMA   IF NOT EXISTS .;

GRANT USAGE             ON DATABASE                          TO APPLICATION RELATIONALAI;
GRANT USAGE             ON SCHEMA   .           TO APPLICATION RELATIONALAI;
GRANT CREATE EXPERIMENT ON SCHEMA   .           TO APPLICATION RELATIONALAI;
GRANT CREATE MODEL      ON SCHEMA   .           TO APPLICATION RELATIONALAI;
```

All four grants are required. Then pass the same database and schema to the GNN constructor:

```python
gnn = GNN(
    exp_database="",
    exp_schema="",
    # ... other args (graph=, property_transformer=, train=, validation=, task_type=)
)
```

### `relationalai` package version

The predictive submodule (`relationalai.semantics.reasoners.predictive`) is not in every published `relationalai` release — `from relationalai.semantics.reasoners.predictive import GNN` raises `ModuleNotFoundError` on releases that pre-date it. Pin a release that ships the submodule (or install from the development branch when iterating against unreleased changes).

The GNN runtime dependencies (PyTorch and the RAI GNN extensions) install as the `[gnn]` **extra**, added in 1.16.0. Declare it with an explicit floor — `relationalai[gnn]>=1.16.0` — rather than a bare `--upgrade`: the floor documents the minimum that carries the extra and keeps a resolver from settling on a release too old to provide it.

### Two-engine model: Logic + Predictive

A GNN workflow runs against **two distinct reasoner engines** that must both be `READY`:

| Reasoner | Handles | Why it matters here |
|----------|---------|---------------------|
| **Logic** | `model.data()` / `Table().to_schema()` ingest, all PyRel queries (including `select(...)` over `Source.predictions`), data exports back to Snowflake | The data pipeline that feeds the GNN and reads predictions back is Logic-engine work |
| **Predictive** | `gnn.fit()` training, `gnn.predictions()` inference, experiment + model-registry writes | Where the actual GNN training and inference happen |

When training "hangs" or queries are slow, the first question is *which engine* — they have separate sizes, separate `STATUS`, separate auto-suspend timers. `rai-health` § Predictive train jobs stuck QUEUED covers the Predictive side; the Logic-engine ladder lives in `rai-health` Steps 1–3.

### Provisioning the Predictive reasoner

**Use a GPU compute type for the Predictive reasoner.** The canonical provisioning shape:

```sql
CALL RELATIONALAI.API.CREATE_REASONER_ASYNC(
    'predictive',
    '',
    'GPU_NV_S',
    OBJECT_CONSTRUCT()        -- {} — accept all defaults; or pass auto_suspend_mins, settings, …
);

-- Poll until STATUS=READY (1–3 minutes typical):
CALL RELATIONALAI.API.GET_REASONER('predictive', '');
```

`GPU_NV_S` is faster per epoch on the GNN training job and is the recommended default for predictive workloads. `HIGHMEM_X64_S` / `_M` / `_L` are also valid sizes for the predictive reasoner, but GPU is the path the platform team recommends; pick it unless you have a specific reason not to.

The `rai reasoners:create --type Predictive --size GPU_NV_S` CLI form may report an allow-list error (`Allowed sizes: HIGHMEM_X64_S, HIGHMEM_X64_M, HIGHMEM_X64_L`) on older client versions — the validation list (`relationalai/services/reasoners/constants.py::REASONER_SIZES_AWS`) trails the backend's `AWSEngineSize` Literal in `config_reasoners_fields.py`. The SQL `CREATE_REASONER_ASYNC` call above is the canonical fall-through; both reach the same backend.

Confirm current sizing options with the RelationalAI team — pool capacity and recommendations evolve.

---

## Quick Reference

```python
# Imports
from relationalai.semantics import Model, select, define, Integer, String, Any
from relationalai.semantics.reasoners.graph import Graph
from relationalai.semantics.reasoners.predictive import PropertyTransformer

model = Model("")
Concept, Table, Relationship = model.Concept, model.Table, model.Relationship
```

| Pattern | Code |
|---------|------|
| Single PK | `User = Concept("User", identify_by={"user_id": Integer})` |
| Composite PK | `Class = Concept("Class", identify_by={"courseid": Integer, "year": Integer})` |
| No PK (e.g. task table) | `TrainTable = Concept("TrainTable")` |

```python
# Graph init
gnn_graph = Graph(model, directed=True, weighted=False)
Edge = gnn_graph.Edge

# PropertyTransformer
pt = PropertyTransformer(
    category=[User.locale, User.gender],
    continuous=[User.birthyear],
    datetime=[User.joinedAt, Event.start_time],
    time_col=[Event.start_time],
)
```

---

## Imports and Model Setup

```python
from relationalai.semantics import Model, select, define, Integer, String, Any
from relationalai.semantics.reasoners.graph import Graph
from relationalai.semantics.reasoners.predictive import PropertyTransformer

model = Model("")
Concept, Table, Relationship = model.Concept, model.Table, model.Relationship
```

Additional type imports as needed: `Date`, `DateTime`, `Float`.

---

## Define and Populate Concepts

> **User-input boundary:** the only things you need from the user are the 3 inputs in [`references/auto-discovery.md`](references/auto-discovery.md) -- source table FQNs, task table FQNs, and the experiment tracking database and schema. Auto-derive PKs, FKs, columns, types, edges, task type, and timestamp candidates from Snowflake schema introspection. Use the in-skill `get_table_schema(table_name, database, schema)` helper in `references/auto-discovery.md` as the default schema source before any manual SQL fallback. Don't ask the user for column-level details.

Two concept categories show up in a GNN pipeline, distinguished by their role in the graph:

| Category | Role |
|----------|------|
| **Graph (node)** | Source, target, or other node entities the GNN reasons over -- can carry features and `time_col` |
| **Task table** | Holds train/val/test split rows, joined to a graph concept by FK -- not used in edges; not a feature source |

`identify_by` is not required by the GNN pipeline. Pass it when you want to declare an explicit primary key for a graph concept (matches a Snowflake column); omit it for task tables and for graph concepts where you don't need an explicit PK.

> If you have an existing ontology from `rai-build-starter-ontology`, create a new `Model` for the GNN pipeline.

### Graph (node) Concepts

The `identify_by` key names must exist as columns in the Snowflake table. Column-name matching is **case-insensitive** in both `identify_by` keys and property accesses -- a Snowflake column `FOO_BAR` can be referenced as `Concept.foo_bar`, `Concept.FOO_BAR`, or any other casing. Spelling still has to match exactly. Check `INFORMATION_SCHEMA.COLUMNS` or run `DESCRIBE TABLE` to confirm the columns before writing `identify_by` or property accesses.

```python
User = Concept("User", identify_by={"user_id": Integer})
Event = Concept("Event", identify_by={"event_id": Integer})
```

### Task Table Concepts

Task table concepts have no `identify_by`:

```python
train_table_concept = Concept("TrainTable")
val_table_concept = Concept("ValidationTable")
test_table_concept = Concept("TestTable")
```

### Populate from Snowflake

```python
define(Customer.new(Table("DB.SCHEMA.CUSTOMERS").to_schema()))
define(train_table_concept.new(Table("DB.TASKS.TRAIN").to_schema()))
```

The GNN pipeline expects pre-existing train/val/test split tables in Snowflake. Each split table must contain: a join key column matching a source concept PK, a label/target column (train/val only), and optionally a timestamp column.

**Split to match the prediction question.** When the task is a forecast — predicting a future period from past data — build the splits **temporally**: train on the earliest periods, validate on the next, test on the most recent, so the model is always scored on a period it has not seen. A random shuffle-and-slice (e.g. 80/10/10 by row) puts the same time range in every split, leaking seasonal and period-level signal into training — the validation metric looks healthy but reflects interpolation within a seen period, and the model degrades on a genuinely future one. A random split is right only when rows are exchangeable (the question has no time dimension).

`has_time_column` and the `at {…}` clause are a separate concern: they make the *relationship* time-aware for the model but do **not** partition the data — the split is decided entirely by which rows you place in each task table. A forecast needs both: temporally-partitioned task tables *and* the time-aware relationship.

`PropertyTransformer` and the task-table pattern also work with concepts populated from local data via `model.data(df)` -- not just `Table(...).to_schema()`. Useful when some concept data lives in local CSVs (e.g. optimizer parameters) while the graph comes from Snowflake.

---

## Task Relationships

Relationships encode the task structure using a template string with three parts:
- **Head** = source concept (the concept being predicted on)
- **"at" clause** = timestamp field (required if the task table contains a time column, otherwise omit)
- **"has" clause** = label (classification/regression) or target concept (link prediction)

For per-task-type Relationship Arity Rules and full code examples, see [references/task-relationships.md](references/task-relationships.md).

---

## Graph and Edges

A GNN earns its added complexity only when the **graph carries signal** — when a node's outcome depends on its neighbors' features or labels propagating across edges. If each row's own columns already explain the target, or the only edges are same-entity temporal lags, a tabular model is simpler and usually as accurate. Before committing to a GNN, confirm the edges are load-bearing: heterogeneous connections across concept types, or relational structure a flat feature table couldn't capture (see Common Pitfalls).

```python
gnn_graph = Graph(model, directed=True, weighted=False)
Edge = gnn_graph.Edge
```

### Standard Edges (FK field equality)

```python
define(Edge.new(src=Interaction, dst=User)).where(
    Interaction.user_id == User.user_id)
```

### Self-Referential Edges (use `.ref()`)

```python
PostRef = Post.ref()
define(Edge.new(src=Post, dst=PostRef)).where(
    PostRef.parent_id == Post.id)
```

### Mediated Self-Reference

```python
PeopleRef = People.ref()
define(Edge.new(src=People, dst=PeopleRef)).where(
    People.Id == Related.person1,
    PeopleRef.Id == Related.person2,
)
```

### Multiple Typed Edges Between Same Pair

```python
BB1Edge = Concept("BB1Edge", extends=[Edge])
BB2Edge = Concept("BB2Edge", extends=[Edge])

Bref = B.ref()
define(BB1Edge.new(src=B, dst=Bref)).where(B.field1 == Bref.id)
define(BB2Edge.new(src=B, dst=Bref)).where(B.field2 == Bref.id)
```

---

## Feature Configuration

The `PropertyTransformer` annotates concept fields with their semantic types for the GNN.

```python
pt = PropertyTransformer(
    category=[User.locale, User.gender, Event.city, Event.state, Event.country],
    datetime=[User.joinedAt, Event.start_time],
    continuous=[User.birthyear],
    time_col=[Event.start_time],
)
```

### Feature Type Guidelines

| Data type | Annotation |
|-----------|-----------|
| Boolean flags, enum/status codes | `category` |
| Ages, prices, ratings | `continuous` |
| Free-form text, names, descriptions | `text` |
| Dates, timestamps | `datetime` |
| Explicit integer values (not IDs) | `integer` |

Status/enum codes used as `category` features must stay `String`-typed properties. A property typed by a `model.Enum` (relationalai>=1.12) is not usable as a feature — leave any property the `PropertyTransformer` consumes as a raw `String` (see `rai-pyrel-coding` § Enums).

The `integer` parameter is a distinct type from `continuous` -- use it for whole-number counts or ordinal values where float precision is not meaningful (e.g. review counts, position ranks):

```python
pt = PropertyTransformer(
    integer=[Review.num_votes, Standing.position],
    continuous=[Review.rating, Result.points],
    # ... other feature lists
)
```

### Feature Selection Strategy

- **Drop all PKs and FKs.** Graph structure already captures relationships; IDs add noise. Example: `drop=[Study.nct_id, Outcome.id, Outcome.nct_id, ...]`
- **Start with minimal `text` fields.** Text embedding is expensive and too many text fields dilute signal. Begin with 3-5 key text fields, add more only if metrics improve.
- **Use `category` for discrete location/status fields.** Fields like city, state, country have limited cardinality.
- **Use `continuous` for numeric measurements.** Counts, scores, percentages.
- **Lean feature sets beat everything-in.** In practice, reducing ~30 text fields to 5 improved AUROC from 57% to 68%.

### Graph metrics as features

Centrality, community labels, and other graph-algorithm outputs from `rai-graph-analysis` can feed the GNN as features once they're materialized as concept properties. Compute the metric on a separate Graph instance (the algorithm graph -- often a different topology from the GNN graph), bind the result, then include in the PropertyTransformer:

```python
# Algorithm graph (often a different topology from the GNN graph)
algo_graph = Graph(model, directed=False)
define(algo_graph.Edge.new(src=Source, dst=SourceRef)).where(...)

# Bind metric output as a Concept property
Source.pagerank = model.Property(f"{Source} has {Float:pagerank}")
model.define(Source.pagerank(graph_algo_result))

# Include as a continuous (or category) feature
pt = PropertyTransformer(
    continuous=[Source.pagerank],
    # ... other feature lists
)
```

Two-graph setups are common (the GNN graph and the algorithm graph have different shapes); name them distinctly to avoid confusion.

PropertyTransformer is optional -- omitting it auto-infers all field types. For production, explicit annotation is recommended. Use `drop` to exclude fields or entire concepts: `drop=[Interaction, Item.internal_code]`.

For the full feature type reference including drop patterns, see [references/property-transformer-types.md](references/property-transformer-types.md).

---

## Common Pitfalls

| Mistake | Cause | Fix |
|---------|-------|-----|
| Concept name is plural (e.g. "Customers") | Naming convention | Use singular names: `Concept("Customer")` |
| Task table concept has `identify_by` | Task tables don't need primary keys | Use plain `Concept("TrainTable")` with no `identify_by` |
| Snowflake table name not fully qualified | Missing database or schema prefix | Use `"DATABASE.SCHEMA.TABLE"` format |
| Test Relationship includes label/target | Test data should not contain the answer | Omit the "has" clause: `f"{Source}"` or `f"{Source} at {Any:ts}"` |
| Positional args in `define(Train(...))` don't match template | Template and population call must align | Match the order: source, [timestamp],

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [RelationalAI](https://github.com/RelationalAI)
- **Source:** [RelationalAI/rai-agent-skills](https://github.com/RelationalAI/rai-agent-skills)
- **License:** Apache-2.0
- **Homepage:** https://relational.ai

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-relationalai-rai-agent-skills-rai-predictive-modeling
- Seller: https://agentstack.voostack.com/s/relationalai
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
