Skip to content
SynthStats
SynthStats
ReportBrief
DemonstrationsResearch resultsSupporting resultsHow one program is scored
Dugong growth (length against age)Disease spread (infected count against time, population 50)
Dugong growth programsDisease spread programs

Files

ResourcesSlidesData

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.

ColumnTypeDescription
agefloatage of the animal, in years
lengthfloatrecorded length

Summary statistics of the provided data, computed from the file itself:

Columnmin25%medianmean75%maxsd
age0.10260.66691.06371.14751.42713.00870.6286
length0.71711.54341.79241.76812.01042.73660.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_model

Checks 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 fittingA program passes it when
Parseits source can be parsed as Python
Syntax-tree inspectionits code contains only the imports and constructs we allow
Model buildcalling model(data) on the visible rows returns a PyMC model
Restricted PyMC subsetits model is built from the distributions and operations we accept
Fabricated-data checkits model declares no more observed values than the data it was given
Data-sensitivity checkthe 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 dugongHow it is obtainedValue
Age and measured lengththe first of the 20 simulated dugongs at ages in the same range, whose lengths are kept out of fitting for scoring0.16 and 0.64
Priorsthe distributions assigned to alpha, beta and lam before fitting, given by the three Normal distributions in the codemeans 2.0, 1.5 and 0.4, standard deviations 0.2, 0.5 and 0.5
Posterior drawsfitting 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 curve200 kept
The first drawits values of alpha, beta and lam2.127, 1.183 and 0.318
Expected length at age 0.16 under the first draw2.127 - 1.183 × 0.318^0.161.142
Smallest and largest expected length at age 0.16over the 200 draws0.88 and 1.37
Simulated lengths at age 0.1620 from each draw, each the draw's expected length plus noise of scale 0.254,000
5th percentile, median and 95th percentileof the 4,000 simulated lengths0.69, 1.12 and 1.55
90 percent predictive intervalfrom the 5th to the 95th percentile0.69 to 1.55
Coveredwhether 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 drawa Normal distribution with mean 1.142 and scale 0.25, evaluated at 0.640.212
Predictive density at 0.64the density at 0.64 averaged over the 200 draws, measuring how concentrated the predicted lengths are around 0.640.283
Log predictive densitythe 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.

AgeLength observed5th to 95th percentileInsideLog predictive density
0.160.640.69 to 1.55no-1.262
0.551.611.13 to 1.96yes+0.420
1.812.331.57 to 2.42yes-0.441
1.862.131.57 to 2.42yes+0.326
1.531.931.54 to 2.37yes+0.455
0.651.131.21 to 2.03no-1.452
0.321.320.90 to 1.75yes+0.442
2.441.541.64 to 2.48no-1.528
2.342.051.63 to 2.45yes+0.450
1.721.611.57 to 2.38yes-0.608
1.261.461.48 to 2.29no-0.984
0.541.701.12 to 1.98yes+0.253
1.631.921.55 to 2.38yes+0.439
0.631.331.19 to 2.03yes-0.142
0.861.631.32 to 2.16yes+0.362
0.891.561.35 to 2.17yes+0.151
2.262.081.63 to 2.47yes+0.438
0.231.040.79 to 1.64yes+0.188
1.212.441.47 to 2.31no-1.991
1.882.281.60 to 2.42yes-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 scoreDugong growthDisease spread
What a program predictsa continuous lengthhow many of 50 individuals are infected, a whole number
What the term is the logarithm ofthe density at the measured length, which is measured per unit of length (changing the unit changes every term) and unlike a probability can exceed 1the probability assigned to the observed count
Sign of the termpositive 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 1negative, 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 ranksonly programs scored on the same held-out rows of this task, where higher is betteronly 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.

ProgramPredictive interval at age 0.16Log predictive density thereHeld-out log score (all 20)MCSE
Reference0.69 to 1.55-1.262-4.60280.1031
Floor model1.16 to 2.34-4.559-14.08980.1042
Single attempt 10.71 to 1.61-1.466-4.62920.0731
Single attempt 20.21 to 1.17+0.278-3.19690.0190
Single attempt 30.71 to 1.62-1.478-4.68760.0539
Single attempt 40.20 to 1.18+0.262-3.22670.0204
Loop proposal 1 (round 1)0.74 to 1.66-1.607-4.91660.1399
Loop proposal 2 (round 1)0.70 to 1.62-1.433-4.66340.0950
Loop proposal 3 (round 2)0.88 to 1.54-4.709-7.43060.1294
Loop proposal 4 (round 2)0.61 to 1.66-1.129-4.51210.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()
SynthStats team

Supporting results

The corpora, training populations and results catalogue.

Dugong growth (length against age)

Next Page

On this page

How one program is scoredThe prompt the model receivedThe reference programChecks before fittingScoring the first held-out dugongAll twenty held-out dugongsReading a held-out log scoreThe reference and the other nine programsRun the reference fit