Ahmet Selim Yılmaz
← Work

Research instrument

Dynamic Hazard Rate BART

An offline desktop instrument that turns the risk structure of the Balloon Analogue Risk Task into an experimental design choice — eleven configurable hazard families, numerically verified optima, and local scoring into forty-plus psychometric metrics.

Hazard families
11
Scored metrics
40+
Network requests
0

What was wrong

In the Balloon Analogue Risk Task, a participant pumps a virtual balloon. Each pump adds money to a temporary bank; each pump also risks bursting the balloon and losing that bank. Collect, and the money is yours. It is a clean paradigm, and it has been used for two decades as a behavioral measure of risk-taking.

The classic version draws the burst point once, uniformly, from the balloon's capacity N. That places the expected-value-optimal stopping point at N/2 — 64 pumps on a 128-pump balloon. Real participants average somewhere in the teens.

The consequence is structural, not statistical. Because every participant sits far below the optimum on the same side of it, pump count and earnings are nearly collinear: pumping more is essentially always better, right up until the point where it isn't, and nobody gets there. The task measures risk exposure. It cannot see risk calibration — whether a person is matching their behavior to the odds — because there is no behavior in the observed range that counts as overshooting.

Two participants who both stop at twelve pumps get the same score. One of themmay have reasoned about the odds; the other may simply be timid. The classictask has no way to tell them apart.

The design change

Replace the one-shot uniform draw with a sequential per-pump hazard. Under the dynamic hazard model, pump k bursts with probability k/N: risk accumulates as the balloon inflates.

The expected value of stopping at s becomes

```
EV(s) = s × ∏(k=1..s) (1 − k/N)
```

whose optimum lies near √N instead of N/2. On that same 128-pump balloon the
target moves from 64 pumps to 11 — squarely inside the range people actually
play. Participants now land on both sides of the optimum, which is what makes
overshoot, undershoot, and accuracy separable quantities rather than one axis.

The default study ships three balloon colors spanning a risk gradient:

| Color | Capacity N | Risk tier | EV-optimal stop s* | Peak EV | P(survive to s*) |
|--------|:------------:|-----------|:---------------------:|:-------:|:-------------------:|
| Purple | 128 | Low | 11 | 6.46 | 0.588 |
| Teal | 32 | Medium | 5 | 3.04 | 0.608 |
| Orange | 8 | High | 2 | 1.31 | 0.656 |

The colors are deliberately neutral — no red — so that learned danger
associations do not do the work the hazard function is supposed to do.

Why eleven families instead of one

Committing the instrument to a single alternative hazard would have traded one
hard-coded assumption for another. Instead the hazard family is a **per-color
configuration choice** drawn from a library grounded in survival analysis:
dynamic (linear), constant (Bernoulli), lejuez (the classic uniform),
Rayleigh, exponential, Weibull, Gompertz, logistic, log-normal, step, and a
validated tabular escape hatch that accepts an arbitrary hazard vector.

This matters practically. A lab can run a faithful classic replication and a
dynamic-hazard condition on the same instrument, scored by the same engine, with
the difference between them recorded in a config file rather than in two
codebases. Parameters only — no free-form code — so every configuration is
validated before a participant ever sees it.

Arbitrary hazards break the closed-form √N result, so the engine computes each
configuration's optimum numerically from its survival curve. Numerical
optima are exactly the kind of thing that is quietly wrong for months, so they
are checked against an independent, seeded Monte Carlo simulation that anyone
can reproduce:

```bash
python -m scoring.verification # per-family PASS table, seeded simulation
```

Building it for people who do not write code

The design is worthless to a psychology lab if using it requires editing Python. The instrument is therefore a desktop application that carries the whole workflow — design, validate, run, export — in one offline window.

A researcher picks colors, capacities, trial counts, reward per pump, interface language (English or Turkish), and a hazard family per color, then saves the design as a portable study.json. Conditions, a real-world currency with a payout conversion rate, and automated quality-control thresholds are all part of the same file.

Validation runs continuously and reports in place, under the control that caused it, and never before the field has been touched — a detail that sounds small and is the difference between a form that helps and a form that nags.

Participants get a conventional, distraction-free flow — consent, ID, gameplay, debrief — and never see their own clinical metrics.

Several affordances exist because running human participants is unforgiving:

  • A passcode-gated kiosk lock forces fullscreen, stays on top, and swallows Escape and F11, so a participant cannot exit the session early.
  • A reproducible seed fixes each participant's random sequence by (seed, participant_id), so a session can be regenerated exactly.
  • Test Run mode lets research assistants rehearse behind a persistent banner without writing anything into the data folder.
  • An optional Generate button issues random nine-digit participant IDs, wide enough that a cross-station collision is an anomaly rather than an expectation. It is off by default and the field stays editable — the participant ID is the join key to consent and payment records, so the app never takes it over.

Every session ends by telling the operator exactly what was written and where.

The scoring engine

The engine is an installable Python package independent of the desktop app. It takes raw pump-level event telemetry and produces more than forty metrics: EV-referenced calibration, explosion penalty, three separate learning estimators, within- and between-balloon consistency, per-color breakdowns, and a narrative behavioral profile.

| Metric | Range | What it captures |
|--------|-------|------------------|
| ev_ratio_score | 0–100 | EV(participant) / EV(optimal) × 100, EV-weighted across colors |
| explosion_penalty | 0–1 | Excess explosion rate against what optimal play would produce |
| rng_normalized_pumps | ≥ 0 | Mean pumps as a ratio of the EV-optimal stop; 1.0 is on target |
| half_split_learning_rate | −1 to 1 | First-half against second-half improvement |
| tercile_learning_rate | −1 to 1 | First against last third — catches late learners |
| post_explosion_sensitivity | ≈ −2 to 2 | Pump change after a same-color explosion; positive is adaptive |
| ev_efficiency_uniformity | 0–1 | 1 − CV of per-color EV efficiencies |
| adaptive_strategy_score | 0–100 | Composite of calibration, learning, uniformity, and earnings |

One methodological decision is load-bearing: every behavioral-intention metric is computed from collected balloons only. An exploded balloon truncates the participant's intended stopping point at a random location, so including explosions would let the random number generator write itself into the estimate of what the participant meant to do.

A validation pipeline flags incomplete, too-fast, non-monotonic, automated, and OS-key-repeat sessions before scoring, so bad sessions are labelled rather than silently averaged in.

The engine installs and runs on its own, which means an existing dataset can be rescored without adopting the application:

```python
from scoring.bart import score_bart

metrics = score_bart(events) # default 128/32/8 linear study
print(f"EV ratio: {metrics.ev_ratio_score:.1f}")
print(f"Risk style: {metrics.behavioral_profile.get('risk_style')}")
```

Core dependencies are numpy and pydantic only — the engine is deliberately scipy-free so the frozen sidecar binary stays small.

Collecting on several machines

Running one study across several stations quietly corrupts the shared master CSV: each machine keeps its own copy, and nothing in the files tells you whether two stations were even running comparable designs.

Standalone Mode — one flag in study.json — has stations write only their per-session files, each stamped with a station label. The operator collects the folders by hand, since the app never touches a network, and the Data Hub rebuilds the study-wide outputs in one pass, re-scoring every session from its raw events so the pooled dataset is uniform even if stations upgraded at different times.

The Hub's governing rule is that it never acts silently. Sneakernet duplicates, two machines sharing a label, participant IDs reused across stations, a station left on a different task configuration, stored scores that disagree with a recomputation from raw events — each is either resolved and itemized or held back and reported. A station that turns out to have been running a genuinely different task is partitioned into its own output set rather than pooled into the others.

It is available as a tab in the app and as a command:

```bash
openbart hub <sources…> --out <dir>
```

Privacy as an architectural constraint

Human-subjects data does not go on the internet, so the application makes zero network requests. The Python scoring engine runs as a loopback-only local process; the Windows installer is per-user and needs no administrator rights; the whole thing works with the network adapter disabled. This is not a configuration option that could be switched on by accident — there is no code path that would send anything anywhere.

Where it stands

Released under MIT, archived on Zenodo with a citable DOI, documented on Read the Docs, and shipping as a per-user Windows installer. A software paper in Journal of Open Source Software format is prepared. The repository includes a worked sample dataset — a real study directory, plus a four-station study with one distinct problem planted in each session and the Hub's report on it — so the failure modes are demonstrable and not just described.

macOS and Linux are development platforms and build from source; the packaged installer is Windows-first because that is what the labs I work with run.