How one program is scored
The program in question is the reference program for a task on the growth of dugongs (sea cows). Every program that received a score went through the same steps on its own task, from the prompt to a held-out log score. Twenty simulated dugongs had their lengths kept out of fitting: each adds one term to that score and counts towards the program's coverage when its length falls inside the interval predicted for it.
The prompt the model received
The framed text is the prompt that the Qwen3.6-35B-A3B language model received for this task, word for word, apart from the block of 80 visible rows at its end, which is shortened here. Every program on the site's dugong pages responds to the prompt's goal paragraph, which asks for a Python function that builds a probabilistic model from the data. The language model wrote its own programs from this text.
Prompt given to the model
Task: dugong growth
Goal
Write a PyMC probabilistic model of dugong length as a function of age. Your model must be a Python function def model(data): that returns a pm.Model instance, reading its data from the data dictionary and declaring an observed likelihood for length.
Data
Lengths of dugongs (sea cows) recorded at known ages. 80 observations are provided.
| Column | Type | Description |
|---|---|---|
age | float | age of the animal, in years |
length | float | recorded length |
Summary statistics of the provided data, computed from the file itself:
| Column | min | 25% | median | mean | 75% | max | sd |
|---|---|---|---|---|---|---|---|
age | 0.1026 | 0.6669 | 1.0637 | 1.1475 | 1.4271 | 3.0087 | 0.6286 |
length | 0.7171 | 1.5434 | 1.7924 | 1.7681 | 2.0104 | 2.7366 | 0.3538 |
Length increases with age and levels off; ages lie between 0 and 5 years.
Observed data (three of the 80 rows shown)
{"age":[1.3369649883162058,2.268225484758763,0.8971725854435216, ... 77 more],
"length":[1.8759828713411015,2.3121314800576096,1.7887715886250162, ... 77 more]}The reference program
We wrote the reference program for the same task, knowing how the environment generates its data. It can be thought of as the growth curve of BoxingGym's dugongs environment turned into a program, with the environment's priors on its three parameters and a noise scale fixed at 0.25. Each line that matters has a comment above it here, while the scored file, 01-reference.py on the programs page, is the same code without the comments.
import numpy as np
import pymc as pm
def model(data):
"""Dugong length against age: the growth curve and priors of BoxingGym's
dugongs environment, with a Normal likelihood and a fixed noise scale.
"""
# data holds the 80 visible rows: their ages and their measured lengths.
age = np.asarray(data["age"], dtype=float)
y = np.asarray(data["length"], dtype=float)
with pm.Model() as dugongs_model:
age_data = pm.Data("age_data", age)
# alpha: the expected length at old ages, when |lam| is below 1.
alpha = pm.Normal("alpha", mu=2.0, sigma=0.2)
# beta: alpha minus the expected length at age zero.
beta = pm.Normal("beta", mu=1.5, sigma=0.5)
# lam: |lam| ** age shrinks with age when |lam| is below 1.
lam = pm.Normal("lam", mu=0.4, sigma=0.5)
# The expected length at each age.
mu = alpha - beta * pm.math.abs(lam) ** age_data
# A measured length is the expected length plus noise of scale 0.25.
pm.Normal("length", mu=mu, sigma=0.25, observed=y)
return dugongs_modelChecks before fitting
A program that fails any one of the six checks gets no score. Before the reference program was fitted, it passed all six. The last two look at how its model uses the 80 visible rows.
| Check before fitting | A program passes it when |
|---|---|
| Parse | its source can be parsed as Python |
| Syntax-tree inspection | its code contains only the imports and constructs we allow |
| Model build | calling model(data) on the visible rows returns a PyMC model |
| Restricted PyMC subset | its model is built from the distributions and operations we accept |
| Fabricated-data check | its model declares no more observed values than the data it was given |
| Data-sensitivity check | the log-density terms of its model change when the supplied input arrays are halved and the model is rebuilt |
Scoring the first held-out dugong
Besides the 80 visible rows, twenty more dugongs were simulated at ages in the same range, their lengths being kept out of fitting so that each program's predictions at those ages could be compared with them. The first of them, aged 0.16, measured 0.64, below the interval that the reference program predicts at that age; its log predictive density, given in the last row of the table below, is this dugong's term in the held-out log score.
| For the first held-out dugong | How it is obtained | Value |
|---|---|---|
| Age and measured length | the first of the 20 simulated dugongs at ages in the same range, whose lengths are kept out of fitting for scoring | 0.16 and 0.64 |
| Priors | the distributions assigned to alpha, beta and lam before fitting, given by the three Normal distributions in the code | means 2.0, 1.5 and 0.4, standard deviations 0.2, 0.5 and 0.5 |
| Posterior draws | fitting turns the priors into the posterior with the 80 visible rows (the joint distribution over alpha, beta and lam after fitting the data), and PyMC's sampler draws sets of values from it, each draw being one growth curve | 200 kept |
| The first draw | its values of alpha, beta and lam | 2.127, 1.183 and 0.318 |
| Expected length at age 0.16 under the first draw | 2.127 - 1.183 × 0.318^0.16 | 1.142 |
| Smallest and largest expected length at age 0.16 | over the 200 draws | 0.88 and 1.37 |
| Simulated lengths at age 0.16 | 20 from each draw, each the draw's expected length plus noise of scale 0.25 | 4,000 |
| 5th percentile, median and 95th percentile | of the 4,000 simulated lengths | 0.69, 1.12 and 1.55 |
| 90 percent predictive interval | from the 5th to the 95th percentile | 0.69 to 1.55 |
| Covered | whether 0.64 lies inside the interval, bounds included. Coverage counts only this, whatever density the program puts on the length: the reference covers 15 of the 20 against a nominal 18 of 20 for 90 percent intervals. The floor model, which uses one distribution for every age, covers 16 yet scores -14.09. | no: 0.64 is below 0.69 |
| Density at 0.64 under the first draw | a Normal distribution with mean 1.142 and scale 0.25, evaluated at 0.64 | 0.212 |
| Predictive density at 0.64 | the density at 0.64 averaged over the 200 draws, measuring how concentrated the predicted lengths are around 0.64 | 0.283 |
| Log predictive density | the logarithm of the predictive density, which is this dugong's term in the held-out log score (the twenty terms add up to -4.6028 before rounding) | -1.262 |
All twenty held-out dugongs
At every held-out age the reference program yields an interval and a log predictive density in the same way, and its coverage, 15 of the 20, counts the measured lengths that fall inside their intervals, while its held-out log score is the sum of the twenty log predictive densities.
| Age | Length observed | 5th to 95th percentile | Inside | Log predictive density |
|---|---|---|---|---|
| 0.16 | 0.64 | 0.69 to 1.55 | no | -1.262 |
| 0.55 | 1.61 | 1.13 to 1.96 | yes | +0.420 |
| 1.81 | 2.33 | 1.57 to 2.42 | yes | -0.441 |
| 1.86 | 2.13 | 1.57 to 2.42 | yes | +0.326 |
| 1.53 | 1.93 | 1.54 to 2.37 | yes | +0.455 |
| 0.65 | 1.13 | 1.21 to 2.03 | no | -1.452 |
| 0.32 | 1.32 | 0.90 to 1.75 | yes | +0.442 |
| 2.44 | 1.54 | 1.64 to 2.48 | no | -1.528 |
| 2.34 | 2.05 | 1.63 to 2.45 | yes | +0.450 |
| 1.72 | 1.61 | 1.57 to 2.38 | yes | -0.608 |
| 1.26 | 1.46 | 1.48 to 2.29 | no | -0.984 |
| 0.54 | 1.70 | 1.12 to 1.98 | yes | +0.253 |
| 1.63 | 1.92 | 1.55 to 2.38 | yes | +0.439 |
| 0.63 | 1.33 | 1.19 to 2.03 | yes | -0.142 |
| 0.86 | 1.63 | 1.32 to 2.16 | yes | +0.362 |
| 0.89 | 1.56 | 1.35 to 2.17 | yes | +0.151 |
| 2.26 | 2.08 | 1.63 to 2.47 | yes | +0.438 |
| 0.23 | 1.04 | 0.79 to 1.64 | yes | +0.188 |
| 1.21 | 2.44 | 1.47 to 2.31 | no | -1.991 |
| 1.88 | 2.28 | 1.60 to 2.42 | yes | -0.120 |
Reading a held-out log score
Some of those twenty terms are positive, since the predictive density at a measured length is measured per unit of length and, unlike a probability, can exceed 1. On the disease-spread task, where a program predicts how many of 50 individuals are infected, every term is the logarithm of a probability and is negative.
| Each term of the held-out log score | Dugong growth | Disease spread |
|---|---|---|
| What a program predicts | a continuous length | how many of 50 individuals are infected, a whole number |
| What the term is the logarithm of | the density at the measured length, which is measured per unit of length (changing the unit changes every term) and unlike a probability can exceed 1 | the probability assigned to the observed count |
| Sign of the term | positive where the density exceeds 1, as for the dugong aged 1.53 that measured 1.93 near the middle of its band from 1.54 to 2.37 (+0.455), and negative where it is below 1 | negative, since even a well-placed count gets a probability well below 1 |
| Held-out log score of the reference | -4.6 | -45.6 |
| What the score ranks | only programs scored on the same held-out rows of this task, where higher is better | only programs scored on the same held-out rows of this task, where higher is better |
The reference and the other nine programs
Ten programs answer the dugong task. For each, the table gives the interval and the log predictive density at age 0.16, where the first held-out dugong measured 0.64, and its held-out log score over all twenty dugongs. Each single attempt was written in a separate call to the language model. In the loop, the language model wrote two proposals, received critiques of them, and then wrote two more.
Monte Carlo standard errors (MCSEs) use batch means of each draw's delta-method influence on the total held-out log score, over ten consecutive batches of 20 draws.
| Program | Predictive interval at age 0.16 | Log predictive density there | Held-out log score (all 20) | MCSE |
|---|---|---|---|---|
| Reference | 0.69 to 1.55 | -1.262 | -4.6028 | 0.1031 |
| Floor model | 1.16 to 2.34 | -4.559 | -14.0898 | 0.1042 |
| Single attempt 1 | 0.71 to 1.61 | -1.466 | -4.6292 | 0.0731 |
| Single attempt 2 | 0.21 to 1.17 | +0.278 | -3.1969 | 0.0190 |
| Single attempt 3 | 0.71 to 1.62 | -1.478 | -4.6876 | 0.0539 |
| Single attempt 4 | 0.20 to 1.18 | +0.262 | -3.2267 | 0.0204 |
| Loop proposal 1 (round 1) | 0.74 to 1.66 | -1.607 | -4.9166 | 0.1399 |
| Loop proposal 2 (round 1) | 0.70 to 1.62 | -1.433 | -4.6634 | 0.0950 |
| Loop proposal 3 (round 2) | 0.88 to 1.54 | -4.709 | -7.4306 | 0.1294 |
| Loop proposal 4 (round 2) | 0.61 to 1.66 | -1.129 | -4.5121 | 0.0879 |
Of the other nine programs, fitted and scored on the same rows, only single attempts 2 and 4 and loop proposal 4 have intervals that contain 0.64 at age 0.16.
Run the reference fit
The fitting script selects the 80 visible rows and fits the reference program, then prints its first posterior draw, posterior means, held-out log score of -4.6028, coverage of 15 of 20 in 90 percent predictive intervals, and MCSE of 0.1031. In an empty directory, run:
curl -fsS https://synthstats.org/programs/dugongs/01-reference.py -o dugong_model.py
curl -fsS https://synthstats.org/data/dugongs/dugongs-observations.csv -o observations.csv
curl -fsS https://synthstats.org/examples/fit-dugong.py -o fit-dugong.py
uv run fit-dugong.py# /// script
# requires-python = ">=3.12,<3.13"
# dependencies = ["pymc==5.21.1", "pytensor==2.28.3", "arviz==0.23.4"]
# ///
import csv
import json
import os
os.environ["PYTENSOR_FLAGS"] = "cxx=,mode=FAST_COMPILE"
import numpy as np
import pymc as pm
import pytensor.tensor as pt
def score_summary(loglik, batch_length=20):
"""Total log posterior-predictive density and batch-means MC error.
Keep chains separate when forming consecutive batches. Each draw's
influence sums over observations before its variance is estimated.
"""
x = np.asarray(loglik, dtype=float)
if x.ndim != 3 or not np.isfinite(x).all():
raise ValueError("Expected finite chain-by-draw-by-observation log densities")
chains, draws, _ = x.shape
if draws % batch_length or draws // batch_length < 2:
raise ValueError("Each chain must contain at least two complete batches")
shift = x.max(axis=(0, 1))
density = np.exp(x - shift)
means = density.mean(axis=(0, 1))
pointwise = shift + np.log(means)
influence = (density / means - 1).sum(axis=2)
batches = influence.reshape(chains, draws // batch_length, batch_length).mean(axis=2)
mcse = float(batches.std(ddof=1) / np.sqrt(batches.size))
return dict(score=float(pointwise.sum()), mcse=mcse,
pointwise=pointwise, influence=influence,
batch_length=batch_length, batches=int(batches.size))
def main():
from dugong_model import model
with open("observations.csv", newline="") as stream:
rows = list(csv.DictReader(stream))
visible = [row for row in rows if row["split"] == "visible"]
heldout = [row for row in rows if row["split"] == "heldout"]
if len(visible) != 80 or len(heldout) != 20:
raise ValueError("Expected 80 visible and 20 held-out observations")
data = {name: np.array([float(row[name]) for row in visible]) for name in ("age", "length")}
with model(data):
fit = pm.sample(
draws=200, tune=200, chains=1, cores=1,
progressbar=False, compute_convergence_checks=False,
random_seed=0, init="adapt_diag", jitter_max_retries=1, n_init=100,
idata_kwargs={"log_likelihood": True}, compile_kwargs={"mode": "FAST_COMPILE"},
)
print(fit.posterior.isel(chain=0, draw=0))
print(fit.posterior.mean())
ages = np.array([float(row["age"]) for row in heldout])
outcomes = np.array([float(row["length"]) for row in heldout])
# The model is built without the held-out outcomes. Bind them only for scoring.
prediction_model = model({"age": ages, "length": np.full(20, data["length"].mean())})
target = prediction_model.observed_RVs[0]
original_value = prediction_model.rvs_to_values[target]
prediction_model.rvs_to_values[target] = pt.as_tensor_variable(outcomes, name=original_value.name)
likelihood = pm.compute_log_likelihood(
fit, model=prediction_model, extend_inferencedata=False,
progressbar=False, compile_kwargs={"mode": "FAST_COMPILE"},
)[target.name].values
report = score_summary(likelihood)
repeated = fit.posterior.isel(draw=np.repeat(np.arange(200), 20)).assign_coords(draw=np.arange(4000))
with prediction_model:
predictions = pm.sample_posterior_predictive(
repeated, var_names=[target.name], random_seed=314,
return_inferencedata=False, progressbar=False,
compile_kwargs={"mode": "FAST_COMPILE"},
)[target.name].reshape(4000, 20)
q05, median, q95 = np.quantile(predictions, [0.05, 0.5, 0.95], axis=0)
covered = (q05 <= outcomes) & (outcomes <= q95)
fields = ["row", "age", "observed_length", "predictive_q05", "predictive_median",
"predictive_q95", "covered", "log_predictive_density", "program_log_score", "program_mcse"]
with open("dugongs-heldout-scored.csv", "w", newline="") as stream:
writer = csv.writer(stream)
writer.writerow(fields)
for i in range(20):
writer.writerow([i, ages[i], outcomes[i], q05[i], median[i], q95[i], bool(covered[i]),
report["pointwise"][i], report["score"], report["mcse"]])
output = {key: report[key] for key in ("score", "mcse", "batch_length", "batches")}
output.update(covered=int(covered.sum()), heldout_observations=20,
predictive_interval=0.9, predictive_seed=314, predictive_replicates_per_draw=20)
with open("dugongs-score.json", "w") as stream:
json.dump(output, stream, indent=2, allow_nan=False)
stream.write("\n")
print(f"Held-out log score: {report['score']:.4f}")
print(f"90% predictive interval coverage: {covered.sum()} of 20")
print(f"Monte Carlo standard error: {report['mcse']:.4f}")
if __name__ == "__main__":
main()