Attention Modeling And GDPR Boundary#

This note documents how Antics should approach Antics Attention modeling without overstating the privacy status of its training data.

Short Version#

  • The current in-app scorer is transitional, not the final modeling architecture.

  • The real model should be fit offline and then distilled into a deterministic production scorer.

  • The production artifact should come from a two-stage offline pipeline:

    • CmdStan for posterior inference over the site-scoped model.

    • HiGHS for constrained post-processing into the runtime artifact.

  • The current extension point for extra predictive power is the checked-in GBT residual layer, not Vowpal Wabbit.

  • Vowpal Wabbit fits contextual-bandit routing or policy selection around attention, not the live attention score itself.

  • Raw pageview training data should be treated as per-site, not pooled across domains by default.

  • Cross-site help, if added later, should come only from sufficiently anonymised aggregate summaries, not pooled raw event rows.

Why Raw Cross-Domain Pooling Is Not The Default#

Antics is privacy-first, but privacy-first does not mean every event row is automatically anonymous under GDPR.

Current pageview and _leave rows can still carry combinations such as:

  • page path

  • timestamp

  • referrer or source

  • device/browser context

  • page-local behavior summaries

Even without names, cookies, or persistent identifiers, these combinations can still leave the data in pseudonymised territory rather than anonymous territory.

The operational rule for Antics should therefore be conservative:

  • do not assume cross-domain raw pageview rows are anonymous enough to merge wholesale for model fitting

  • do train offline models per site

  • if Antics later wants shared priors across sites, derive them only from heavily aggregated, non-linkable summaries

Regulatory Framing#

The relevant distinction is between anonymised and pseudonymised data.

European Data Protection Board:

  • pseudonymised data remains personal data

  • properly anonymised data falls outside GDPR

Source:

UK ICO:

  • pseudonymised data may still be personal data, especially for the party that can still link or single out people

  • anonymised data must leave people not identifiable by means reasonably likely to be used

Sources:

These sources do not create Antics policy by themselves, but they support the cautious implementation rule above.

Modeling Rule For Antics Attention#

The production metric should follow this shape:

  1. Extract pageview-local evidence from a single site.

  2. Build a latent-label training dataset for that site.

  3. Fit the Bayesian model offline.

  4. Distill the fit into a deterministic production scorer.

  5. Version the scorer and keep it reproducible.

For the first serious fit:

  • site: rgoswami.me

  • target: latent attention fraction of visible time

  • training labels: balanced latent-label dataset from real pageviews

  • model family: hierarchical ordered beta

  • likelihood: ordered beta regression (Kubinec 2023, doi:10.1017/pan.2022.20) with a beta interior (Ferrari and Cribari-Neto 2004, doi:10.1080/0266476042000214501)

  • hierarchy: page-path random intercepts only

What Counts As Cross-Site Safe Later#

Potentially acceptable later:

  • aggregate histograms

  • heavily bucketed parameter summaries

  • calibration priors that cannot be traced back to a domain, path, or pageview

Not acceptable by default:

  • pooled raw pageview rows across domains

  • pooled per-path behavioral traces across domains

  • anything that assumes “no cookie” automatically means “anonymous”

Implementation Consequence#

When Antics says a model is “Bayesian,” that should mean:

  • posterior inference happened in the offline fitting pipeline

  • the live app uses a frozen deterministic scorer derived from that fit

  • the model version is explicit

  • the training scope is documented

That keeps the product technically honest and privacy claims defensible.

Concrete Offline Workflow#

The repo now has a concrete offline handoff for fitting a site-scoped model.

1. Export one site’s training evidence#

Export raw pageview-local rows:

cargo run --bin export-attention-training -- \
  --duckdb /data/antics.duckdb \
  --site-id <site-id> \
  --start 2026-01-01 \
  --end 2026-03-31 \
  --format rows > attention-rows.json

By default this export is real-site-only. If a site has no usable non-bot _leave.attention rows yet, the exporter returns an empty corpus instead of silently mixing in bootstrap data.

If you explicitly want the deterministic bootstrap corpus for fitting experiments or dry runs, add --include-synthetic. Synthetic rows are explicitly marked with "synthetic": true.

Export balanced latent labels:

cargo run --bin export-attention-training -- \
  --duckdb /data/antics.duckdb \
  --site-id <site-id> \
  --start 2026-01-01 \
  --end 2026-03-31 \
  --include-synthetic \
  --format latent > attention-latent.json

Export two files:

  • a metadata-rich stan JSON for distillation and audit

  • a numeric-only cmdstan JSON for the sampler itself

cargo run --bin export-attention-training -- \
  --duckdb /data/antics.duckdb \
  --site-id <site-id> \
  --start 2026-01-01 \
  --end 2026-03-31 \
  --include-synthetic \
  --format stan > attention-stan.json
cargo run --bin export-attention-training -- \
  --duckdb /data/antics.duckdb \
  --site-id <site-id> \
  --start 2026-01-01 \
  --end 2026-03-31 \
  --include-synthetic \
  --format cmdstan > attention-cmdstan.json

2. Fit the hierarchical ordered-beta model with CmdStan#

The first checked-in model lives at:

  • models/attention_fraction_hierarchical.stan

Typical CmdStan flow:

make models/attention_fraction_hierarchical
./models/attention_fraction_hierarchical method=sample \
  data file=attention-cmdstan.json \
  output file=attention-fit.csv

If posterior checks or deterministic summaries need a second pass, use CmdStan’s standalone generated quantities flow against the fitted CSV.

Once you have a CmdStan draws CSV, distill it into the production artifact JSON with:

  • posterior draws remain the Bayesian source of truth

  • HiGHS projects those draws onto a production-safe coefficient surface

  • the emitted JSON artifact is the single runtime source of truth

cargo run --bin distill-attention-fit -- \
  --site-id <site-id> \
  --site-domain <site-domain> \
  --model-version attention-<site>-v1 \
  --stan-json attention-stan.json \
  --cmdstan-csv attention-fit.csv \
  --output <site-id>.json

3. Distill the fit into a frozen production artifact#

Production does not run MCMC or Stan.

Instead, the offline fit must be converted into a site-scoped JSON artifact with:

  • schema_version

  • model_version

  • site_id

  • site_domain

  • feature_order

  • coefficients

  • coefficient_sds

  • path_intercepts

  • path_intercept_sds

  • calibration

  • provenance

  • optional cutpoints

  • optional phi

  • optional gbt_residual_path

  • optional gbt_residual_alpha

  • optional gbt_residual_summary

The distillation step should not be a naive “posterior means into JSON” pass. The intended production path is:

  1. read posterior draws from CmdStan

  2. compute posterior summaries and uncertainty metadata

  3. run HiGHS as a constrained projection step

  4. emit one frozen runtime artifact

That projection step is where Antics can enforce production-safe structure such as:

  • monotone positive effects for engagement signals like scroll, click, keydown, pointer, and copy

  • monotone negative effects for idle-heavy signals like idle_ratio and max_idle_s

  • bounded coefficients and path intercepts derived from the posterior support

  • provenance for how much the final runtime artifact had to move away from the posterior target

The live service loads these artifacts from:

  • ANTICS_ATTENTION_MODEL_DIR/<site-id>.json

These artifacts are part of the deployed analytics data plane. Copy, back up, and restore them alongside the DuckDB analytics and redb control snapshots for any site that depends on a fitted model.

If an artifact is present, stats endpoints score pageviews from that frozen model. If no artifact is present yet, Antics uses the transitional in-app scorer. If an artifact is present but fails validation, the request fails loudly instead of silently reverting to the transitional path.

4. Attach the GBT residual layer#

Antics already has the non-linear extension point the attention scorer needs: a pure-Rust gradient-boosted-tree residual model stored alongside the site-scoped JSON artifact.

The residual layer is intentionally narrower than a second independent model:

  1. score each training row with the distilled CmdStan plus HiGHS linear backbone

  2. train the GBT against target - linear_prediction

  3. write the GBT JSON atomically next to the artifact

  4. record the relative model path and fit summary in the artifact

  5. leave gbt_residual_alpha at 0.0 unless validation shows the combined score beats the linear-only baseline

At runtime, ANTICS_ATTENTION_MODEL_DIR/<site-id>.json remains the single control point. If the artifact references a GBT path, the service loads it through the cached attention entry and adds the residual on the eta/linear scale before the expectation. When the artifact has two finite cutpoints c0 and c1, the live score is the ordered-beta unconditional expectation (Kubinec 2023):

eta = linear_dot + alpha * gbt_residual
p_above_zero = logistic(eta - c0)
p_one = logistic(eta - c1)
p_interior = max(p_above_zero - p_one, 0)
E[Y] = p_interior * logistic(eta) + p_one
attention_seconds = clamp(E[Y] * visible_s, 0, visible_s)

Artifacts without cutpoints keep logistic(eta) * visible_s.

This keeps request-time scoring deterministic and local. A missing residual file during artifact rotation falls back to linear-only scoring for that load; artifact schema errors still fail loudly.

The GBT feature order is fixed separately from the linear artifact feature order:

  • log_visible_s

  • scroll

  • click

  • keydown

  • pointer

  • copy

  • idle_ratio

  • max_idle_s

The training row extractor and runtime feature builder must keep that order identical. Treat the GBT residual artifact as part of the same backup/restore surface as the JSON attention artifact.

5. Do not use VW for the current attention scorer#

OokCite’s Vowpal Wabbit work is useful as an operational pattern, but it is not the right scoring primitive for the current Antics Attention metric.

Reuse these ideas from the OokCite VW path:

  • train outside the API request path

  • make model artifacts explicit and versioned

  • keep request-time serving bounded

  • treat malformed policy output as a fault, not as a silent quality downgrade

  • keep a deterministic fallback when a policy daemon is unavailable

Do not reuse the current OokCite VW code directly inside the attention scorer:

  • Antics Attention is a calibrated regression over pageview evidence, not a contextual-bandit action choice.

  • The current runtime score is already deterministic, auditable, and local.

  • Adding a VW daemon to the score path would add native/service complexity without replacing the CmdStan posterior, HiGHS projection, or GBT residual.

A VW-shaped Antics feature belongs in a policy layer around attention, for example:

  • choosing which model family or artifact tier to serve for a site

  • deciding whether a site is ready for residual promotion

  • selecting sampling, refit, or alert-inspection actions under a logged policy

  • ranking workspace jump targets if that UI becomes policy-driven

That policy layer should follow the VW contract used elsewhere: offline ADF examples, a saved model loaded with VW’s normal -i/-t serving shape, and no request-path subprocess execution.

6. Track fit state in redb#

The redb control store keeps per-site model fit status. It uses explicit states so the scheduler can distinguish:

  • fallback

  • eligible

  • queued

  • fitted

  • stale

  • refit_due

The scheduler policy is tiered:

  • paid sites become due every 6 hours

  • free sites use a 30-day cadence

  • free sites that have gone idle past the monthly window stay stale instead of being refit immediately

The admin registry is the only write path for fit state. The worker uses ANTICS_ADMIN_URL and ADMIN_TOKEN; operators can call the same routes:

curl -sS -H "Authorization: Bearer $ADMIN_TOKEN" \
  "$ANTICS_ADMIN_URL/api/v1/admin/registry/queue?limit=25"
curl -sS -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model_version":"<model-version>","fitted_at":"<rfc3339>"}' \
  "$ANTICS_ADMIN_URL/api/v1/admin/registry/<site-id>/mark-fitted"
curl -sS -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"error":"cmdstan failed","fitted_at":"<rfc3339>"}' \
  "$ANTICS_ADMIN_URL/api/v1/admin/registry/<site-id>/mark-failed"

That keeps the refresh policy explicit without a second process opening the control store.

7. Run the fit worker#

Deployed, not retired. Production still runs antics_attention_fit_worker (Quadlet + image ghcr.io/turtletech-ehf/antics-attention-fit-worker). A failed unit after a DuckDB snapshot outage is an ops restart, not a product retirement: golive antics ships the worker as a dependent image consumer, and attention scores need periodic offline fits. After recovering the main analytics store, start the unit and confirm it is active and writing under /data/attention-models/ and /data/fit-debug/.

The registry CLI manages fit state. The dedicated worker performs the fitting loop. Its default registry backend calls the protected Antics admin API, so provide ANTICS_ADMIN_URL and ADMIN_TOKEN through the service’s secret environment. Do not place the admin token in a unit file or source repository.

ANTICS_ADMIN_URL=http://127.0.0.1:8480 \
ADMIN_TOKEN='<admin-token>' \
cargo run --bin attention-fit-worker -- \
  --duckdb /data/antics.duckdb \
  --model-bin /opt/cmdstan/models/attention_fraction_hierarchical \
  --model-dir /data/attention-models \
  --work-dir /tmp/antics-attention-fit \
  run-once

For continuous background fitting, run the same worker under a timer or long-lived service:

ANTICS_ADMIN_URL=http://127.0.0.1:8480 \
ADMIN_TOKEN='<admin-token>' \
cargo run --bin attention-fit-worker -- \
  --duckdb /data/antics.duckdb \
  --model-bin /opt/cmdstan/models/attention_fraction_hierarchical \
  --model-dir /data/attention-models \
  --work-dir /tmp/antics-attention-fit \
  run 240

The worker does the real end-to-end promotion:

  1. sync the registry

  2. queue due sites

  3. export the full available non-bot training window for each queued site

  4. run the CmdStan model

  5. distill the posterior with HiGHS constraints

  6. atomically install ANTICS_ATTENTION_MODEL_DIR/<site-id>.json

  7. mark the site fitted or failed

Operational notes:

  • paid sites become due every 6 hours in the registry

  • free sites refresh on the 30-day cadence in the registry

  • the worker fits on real rows by default; synthetic bootstrap data is opt-in

  • if a site has no usable _leave.attention rows, the worker records a deferred fit and retries it according to the registry policy

  • the worker uses the full site-scoped evidence window, not a short rolling slice, so fitted artifacts can reflect the whole historical corpus available

When a fit fails, the registry now records:

  • last_attempted_at

  • last_error

and releases the site back into a sensible state instead of leaving it stuck in queued forever:

  • unfitted active sites go back to eligible

  • unfitted idle sites go back to stale

  • previously fitted sites go back to refit_due

8. Reproducibility rule#

Every shipped artifact must be attributable to:

  • the site it was trained on

  • the input date range

  • the Stan model version

  • the distilled production model_version

  • the GBT residual path and validation summary when the residual is enabled

Retuning is allowed. Silent mutation is not.