# RADIUS session anomalies — Isolation Forest

## What it detects

Every data session a SIM subscription opens through a mobile operator is recorded by RADIUS accounting: how long
it lasted, how many bytes and packets went each way. On a given APN those sessions have a shape — an IoT fleet
sending small periodic uploads looks nothing like a fleet streaming video. This plan learns that shape and scores
each new session against it, so that a SIM that suddenly uploads gigabytes, or holds a session open for days,
stands out.

The algorithm is an **Isolation Forest**: an ensemble of random trees where points that are easy to isolate —
few splits away from everything else — are anomalies. It needs no labelled examples of bad sessions, only enough
normal traffic to learn from.

## Data it needs

The plan reads RADIUS session records and keeps the sessions of **one APN**, given as the `apn` configuration
field. Each record needs these columns; with a time-series source you map them in the trainer, with a file source
they must be present under these names in a Parquet file:

| Column | Type | Meaning |
|---|---|---|
| `APN` | string | Access point name of the session |
| `SessionState` | string | State of the session record |
| `IP_GGSN`, `IP_Device` | string | Gateway and device IP addresses |
| `sbytes`, `dbytes` | integer | Bytes sent and received by the device |
| `spkts`, `dpkts` | integer | Packets sent and received |
| `dur` | integer | Duration of the session |

Records with missing values are dropped, and so are `TERMINATED` records that carry no traffic counters at all.
The plan refuses to train when the APN has fewer sessions than the catalogue's `minDataToTrain` (15 000 when the
plan does not say otherwise): the execution fails with *Not enough data for APN* rather than register a model
nobody should trust.

## How it trains

From the five counters the plan derives eight more features — totals, ratios between directions, rates per
second of duration — and scales them robustly to the `[0, 1]` range. The data is split 75 / 12.5 / 12.5 into
training, validation and test. After fitting, the forest scores the training set and the plan sets the
**decision threshold at the 95th percentile** of those scores: anything scoring above it at inference time is an
anomaly. Two numbers are recorded with the model version and shown as its metrics:

| Metric | Meaning |
|---|---|
| `calculated_threshold` | The score above which a session is called anomalous |
| `training_max_score` | The highest score seen in training; inference scores are divided by it so `anomaly_score` reads as a fraction |

## The prediction request

The inferencer answers `POST /api/predict` with the five counters of one session:

```json
{ "sbytes": 18234, "dbytes": 1203991, "spkts": 210, "dpkts": 980, "dur": 3600 }
```

```json
{
  "prediction": 1,
  "anomaly_score": 1.18,
  "explanation": [
    { "dbytes": 0.071, "received_bytes_rate": 0.044, "total_bytes": 0.031 }
  ]
}
```

| Field | Meaning |
|---|---|
| `prediction` | `1` anomalous, `0` normal |
| `anomaly_score` | The isolation score divided by the training maximum. Above roughly `1` means *more extreme than anything seen in training* |
| `explanation` | Only when `prediction` is `1`: the features that pushed the score up, with their contribution, computed with SHAP on the forest. Empty otherwise |

## The rule it creates

Unless the trainer says `createRule: false`, the first successful training creates an ADVANCED rule named after
the model in `default_channel`, inactive until the inferencer is activated. What it does:

1. Triggers on `device.communicationModules[].subscription.mobile.presence.gprs`, and acts only when the value is
   `STOP` — the session has just closed — and the subscription's session record belongs to the configured `apn`.
2. Sends that session's `sentBytes`, `receivedBytes`, `sentPackets`, `receivedPackets` and `duration` to the
   inferencer.
3. Collects three datastreams on the entity, dated at the session's timestamp:

   | Datastream | Value |
   |---|---|
   | `withAnomaly` | `true` or `false` |
   | `score` | The `anomaly_score` |
   | `explanation` | The `explanation` as text, when the rule parameter `shouldShowAnomalyReason` is `true` |

4. Opens the alarm **`deviceWithAnomaly`** (severity `URGENT`, priority `MEDIUM`) when the entity becomes anomalous
   and was not before, carrying the explanation as extra information; closes it when a later session comes back
   normal.

The rule's parameters — `inferenceServiceURL`, `apn`, `shouldShowAnomalyReason` — are editable in the rules
editor, as is the whole script. The datastreams it collects must exist in the entity's data model.

## Choosing between this plan and the Autoencoder

Both plans take the same data and answer the same request, so a rule written for one works for the other. The
Isolation Forest trains in seconds, needs no tuning and explains its decisions feature by feature; it is the
one to start with. The [Autoencoder](../radius_autoencoder/) is worth a try when the forest flags too much or too
little and you have plenty of data: it learns a smoother notion of *normal* at the price of a longer training.
