Hedronite · Archmagus-Stack · Polyglot-Dev / R · Dev Lesson · 2026-06-25

R and Python for
Security Telemetry Analysis

Baseline Profiling with Control Charts and the Explore-in-R-Enforce-in-Python Discipline

LanguagesR + Python (Data Science tier)
Ops pairβ-Trust — Behavioral Anomaly Detection (2026-06-25)
Cert pairAWS AIP-C01 + GitHub GH-600
Tome refsR for Data Science Part I pp30, 66–67 (Wickham + Grolemund)
Prior arc2026-06-18 R+Python SLI Telemetry · 2026-06-11 R+Python On-Chain Events

§I Frame

The explore-in-R-enforce-in-Python discipline has appeared in two prior Data Science lessons. This lesson applies it to the security domain: the same two-language workflow that served on-chain event analysis and operational SLI monitoring now serves behavioral anomaly detection for multi-agent service accounts.

R — Explore

Training-window baseline profiling. Load the audit log, summarise to 5-minute windows, compute rolling statistics, render control charts, select N-sigma with visual evidence, export baseline parameters as JSON.

Python — Enforce

Production anomaly gate. Read the JSON baseline parameters at startup, score each completed observation window against the gate, pass the score to the alert-to-isolate pipeline.

Two languages. Two problem shapes. One discipline. The boundary between them is the exported JSON — the compact between the R analyst and the Python runtime. When baselines refresh, the R session runs, produces a new JSON, and the Python gate loads it at the next startup without any change to the gate code.

§II Language Idiom

R Side — dplyr and ggplot2 for Training-Window Analysis

The Shewhart control chart is the analytic instrument at the center of today's R work. Wickham and Grolemund's grammar of graphics separates the chart into three composable layers: data, aesthetic mapping, and geometric representation (Wickham, Grolemund, Part I Explore, p30). A control chart adds a center line (mean), UCL (mean + N × sigma), and LCL (mean − N × sigma). Points outside the control limits are breach events; color and shape encode breach state using the aesthetic mapping discussed at pp66–67 — color-redundant shape encoding so the chart is readable in greyscale.

The data structure is a tibble: one row per 5-minute observation window per service account, four behavioral dimension columns (call_volume, fan_out, cross_ns_calls, active_hour_flag), and two index columns (sa_name, window_start as POSIXct).

R — compute_baseline with across() The across() call applies both mean and sd to all four behavioral columns in one pass, producing output columns named call_volume_mean, call_volume_sigma, etc. The result is a one-row tibble — the baseline contract for one service account.
library(tidyverse)
library(slider)

compute_baseline <- function(df, sa) {
  df |>
    filter(sa_name == sa) |>
    arrange(window_start) |>
    summarise(
      across(
        c(call_volume, fan_out, cross_ns_calls, active_hour_flag),
        list(mean = mean, sigma = sd),
        .names = "{.col}_{.fn}"
      )
    )
}

Rolling Statistics and the Signal Check

Before committing to a threshold, check whether each dimension is stable under known-good conditions. The coefficient of variation (cv = sigma / mean) is the quick diagnostic: cv below 0.3 means the dimension is tight and worth gating at N=3; cv above 0.8 means the dimension is too noisy for reliable gating at that threshold.

R — rolling_cv with slider::slide_dbl The slide_dbl function applies mean and sd over a 12-period sliding window (one hour at 5-minute intervals). The output cv column shows how the coefficient of variation evolves over time — rising cv indicates a dimension whose baseline is drifting.
rolling_cv <- function(df, sa, dimension) {
  df |>
    filter(sa_name == sa) |>
    arrange(window_start) |>
    mutate(
      roll_mean = slide_dbl(.data[[dimension]], mean, .before = 11),
      roll_sd   = slide_dbl(.data[[dimension]], sd,   .before = 11),
      cv        = roll_sd / roll_mean
    ) |>
    select(window_start, roll_mean, roll_sd, cv)
}

Building the Control Chart

The control chart plots each 5-minute observation; UCL and LCL are mean ± 3 × sigma; breached points are colored orange-red and shaped as triangles for redundant encoding.

R — plot_control_chart with ggplot2 The breach aesthetic maps to both color (grey vs orange-red) and shape (circle vs triangle). This follows Wickham + Grolemund pp66–67: redundant aesthetic encoding ensures the chart communicates under greyscale printing or color-vision differences. UCL and LCL render as dashed cyan lines; the center line renders as solid sea-green.
plot_control_chart <- function(obs_df, baseline_row, dimension, sa) {
  mu    <- baseline_row[[paste0(dimension, "_mean")]]
  sigma <- baseline_row[[paste0(dimension, "_sigma")]]
  ucl   <- mu + 3 * sigma
  lcl   <- max(0, mu - 3 * sigma)

  obs_df |>
    filter(sa_name == sa) |>
    arrange(window_start) |>
    mutate(breached = .data[[dimension]] > ucl | .data[[dimension]] < lcl) |>
    ggplot(aes(x = window_start, y = .data[[dimension]])) +
    geom_line(color = "#4a6dd0", linewidth = 0.6) +
    geom_point(aes(color = breached, shape = breached), size = 2) +
    geom_hline(yintercept = mu,  color = "#2E8B57", linewidth = 0.8) +
    geom_hline(yintercept = ucl, color = "#5ad6e0", linetype = "dashed") +
    geom_hline(yintercept = lcl, color = "#5ad6e0", linetype = "dashed") +
    scale_color_manual(values = c("FALSE" = "#a8b0c0", "TRUE" = "#d97757")) +
    scale_shape_manual(values = c("FALSE" = 16, "TRUE" = 17)) +
    labs(title = paste(sa, "—", dimension), x = NULL, y = dimension,
         color = "Breached", shape = "Breached") +
    theme_minimal(base_size = 13)
}

Exporting Baseline Parameters to Python

R — JSON export with jsonlite The baseline list is a named list of named lists: outer names are service account names, inner names are metric parameters. auto_unbox = TRUE prevents single-value scalars from being JSON-wrapped in arrays, keeping the Python parse path simple.
library(jsonlite)

export_baselines <- function(baseline_list, path) {
  baseline_list |>
    toJSON(pretty = TRUE, auto_unbox = TRUE) |>
    writeLines(path)
}

§III Code Worked Example

Loading the Audit Stream into R

R — Loading JSON lines audit log The stream function reads the JSON lines file lazily. floor_date from lubridate snaps each timestamp to its 5-minute interval. The result is one row per API call, which the next function summarises by service account and window.
library(tidyverse)
library(jsonlite)
library(lubridate)

load_audit_log <- function(path) {
  stream(path, handler = function(x) {
    tibble(
      window_start = floor_date(ymd_hms(x$requestReceivedTimestamp), "5 minutes"),
      sa_name      = x$user$username,
      verb         = x$verb,
      resource     = x$objectRef$resource,
      namespace    = x$objectRef$namespace
    )
  }) |>
    bind_rows()
}

summarise_windows <- function(calls_df) {
  calls_df |>
    group_by(sa_name, window_start) |>
    summarise(
      call_volume      = n(),
      fan_out          = n_distinct(paste(resource, namespace)),
      cross_ns_calls   = sum(namespace != first(namespace), na.rm = TRUE),
      active_hour_flag = as.integer(hour(first(window_start)) %in% 6:22),
      .groups = "drop"
    )
}

Python Side — Production Gate

Python — load_baselines and anomaly_score load_baselines reads the JSON exported from the R session and builds the nested Baseline dict. The outer key is service account name; the inner key is dimension name. anomaly_score counts breached dimensions — identical to the Ops lesson's gate function, now reading from R-produced parameters.
import json
from pathlib import Path
from dataclasses import dataclass

@dataclass
class Baseline:
    mean: float
    sigma: float
    n_sigma: float = 3.0

    def breached(self, value: float) -> bool:
        return abs(value - self.mean) > self.n_sigma * self.sigma


def load_baselines(path: str) -> dict[str, dict[str, Baseline]]:
    raw = json.loads(Path(path).read_text())
    return {
        sa: {
            dim: Baseline(
                mean=params[f"{dim}_mean"],
                sigma=params[f"{dim}_sigma"],
            )
            for dim in ("call_volume", "fan_out", "cross_ns_calls", "active_hour_flag")
        }
        for sa, params in raw.items()
    }


def anomaly_score(obs: dict, baselines: dict[str, Baseline]) -> int:
    return sum(1 for key, bl in baselines.items() if bl.breached(obs[key]))

§IV Connection to Today's Ops Lesson

The Ops lesson defined the baseline contract and alert-to-isolate pipeline at the architectural level. This lesson implements the statistical machinery that fills in the baseline contract's parameters and validates them visually before production deployment.

The workflow is sequential: first the R session, then the Python gate. The R session runs once per baseline refresh. The Python gate runs continuously. The disciplines have different error modes: the R session can fail analytically — wrong sigma choice — but the failure is caught at the chart stage before deployment; the Python gate can fail operationally — stale baselines — but the R session is not in the operational path.

§V Prior-Lesson Reach

The June 11 on-chain event analysis lesson coined the explore-in-R-enforce-in-Python pattern against blockchain data. The dplyr and ggplot2 vocabulary introduced there reappears here with a security-domain data structure rather than a blockchain event stream.

The June 18 operational telemetry analysis lesson applied the same pattern to SLI time-series data. The slide_dbl rolling-window pattern from that lesson is the direct precedent for today's rolling_cv function. Across three lessons, the discipline is consistent: R for statistical exploration, Python for production enforcement, JSON as the boundary.

§VI Closing

Two languages. Two problem shapes. R handles the shape of exploratory statistical analysis: load the training data, compute rolling statistics, visualize the distribution, choose the threshold with visual evidence, export the parameters. Python handles the shape of production enforcement: read the parameters at startup, score each observation window in the live stream, hand the score to the action pipeline.

The explore-in-R-enforce-in-Python discipline is not about language preference. It is about matching tools to the shape of the problem. The R session produces a decision; the Python gate executes that decision at scale, at speed, without second-guessing it. Examine this well.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
Filed 2026-06-25 · Fajr Anchor · Polyglot-Dev / R · Data Science Tier · W2 / C2
Tome: R for Data Science Part I pp30, 66–67 — Wickham + Grolemund (grounded-in, domain-canonical)