Domain: Science, Environment & Research

Run a deterministic Monte Carlo study for a scalar parameter under three fixed scenarios and compare nine estimators.

Implementation requirements:

- Use Python and `numpy`.
- In this environment, run it with `uv`, for example `uv run --with numpy python ...`.
- Use the exact parameters, scenario definitions, estimator definitions, and output structure below.
- Return a single valid JSON object as the final output.
- Round all numeric outputs to 4 decimal places using round-half-to-even.

To ensure exact reproducibility of outputs:

- Use numpy.random.default_rng for all randomness.
- All quantile computations (including winsorization and q95_ae) must use numpy.quantile with default settings.
- Probability comparisons (e.g., prob_*_better_than_mean) must use strict inequality (<).
- Rounding must be performed using float(numpy.round(x, 4)).

Global parameters:

- `theta = 3.0`
- `N = 50`
- `reps = 10000`
- `seed = 42`
- `base = 0.5`
- `slope = 0.1`
- `quad = 0.0`
- `t_df = 5.0`
- `mix_p = 0.05`
- `mix_scale = 6.0`
- `trim = 0.1`
- `huber_k = 1.345`
- `shrink_prior = 3.0`
- `shrink_lambda = 1.0`

Data-generating process:

- For each repetition and each `i = 1..N`, generate

`X_i = theta + epsilon_i * sigma_i`

- Use 1-based indexing for the variance profile:

`sigma_i^2 = base + slope * i + quad * (i / N)^2`

- Generate `epsilon_i` from the scenario noise type:
  - `normal`: `z ~ N(0, 1)`
  - `t`: `z ~ t_df` Student-t
  - `mixture`: with probability `mix_p`, `z ~ N(0, mix_scale^2)`, otherwise `z ~ N(0, 1)`
- If `rho > 0`, apply the shared-shock mixture:

`z = sqrt(rho) * z_common + sqrt(1 - rho) * z`

with `z_common ~ N(0, 1)` drawn independently per repetition.

Scenarios:

1. `S1`: `noise.type = normal`, `rho = 0.0`, `quad = 0.0`
2. `S2`: `noise.type = t`, `t_df = 3.0`, `rho = 0.2`, `quad = 0.0`
3. `S3`: `noise.type = mixture`, `rho = 0.1`, `quad = 0.2`

RNG rule:

- Use a separate RNG stream per scenario with seed `seed + scenario_index * 1000` where `scenario_index` is `0, 1, 2` for `S1, S2, S3`.

Estimators per repetition:

- `mean`: simple mean
- `weighted`: inverse-variance weighted mean using weights `1 / sigma_i^2`
- `bias_corrected`: weighted mean plus empirical constant `c_opt = mean(theta - weighted_estimate)` computed over all repetitions in that scenario
- `trimmed`: trimmed mean with fraction `trim`, trimming `floor(trim * N)` values from each tail after sorting
- `median`: sample median
- `huber`: iterative Huber location with `huber_k = 1.345` and exactly 8 iterations, initialized at the sample median, with scale `1.4826 * median(abs(residual)) + 1e-8`
- `shrinkage`: `(wsum * weighted + shrink_lambda * shrink_prior) / (wsum + shrink_lambda)` where `wsum = sum(1 / sigma_i^2)`
- `winsorized`: clip each repetition’s sample to its rowwise `trim` and `1-trim` quantiles, then average
- `mom`: median-of-means with equal contiguous blocks of size 5

Metrics per estimator:

- `bias`
- `variance` using population variance (`ddof=0`)
- `mse`
- `mae`
- `median_ae`
- `rmse`
- `q95_ae` using the 95th percentile of absolute error

Per-scenario extras:

- `rankings.by_mse`
- `rankings.by_mae`
- `comparisons.prob_weighted_better_than_mean`
- `comparisons.prob_huber_better_than_mean`
- `comparisons.prob_winsorized_better_than_mean`
- `bias_correction.c_opt`

Aggregate section:

- `aggregate.mse_mean` = mean of scenario MSE values for each estimator
- `aggregate.mae_mean` = mean of scenario MAE values for each estimator
- `aggregate.rank_by_mse`
- `aggregate.rank_by_mae`

Required JSON structure:

- top-level keys must be exactly `settings`, `scenarios`, `aggregate`
- `scenarios` must be an array of exactly `S1`, `S2`, `S3`
- each scenario object must contain exactly:
  - `id`
  - `settings`
  - `estimators`
  - `rankings`
  - `comparisons`
  - `bias_correction`

Grading rubric:
criterion: The final answer is valid JSON with exactly the top-level keys settings, scenarios, and aggregate.
weight: 5
criterion: The scenarios array contains exactly S1, S2, and S3, and each scenario object includes exactly the sections id, settings, estimators, rankings, comparisons, and bias_correction.
weight: 5
criterion: For each scenario, the estimators object contains exactly the following keys: mean, weighted, bias_corrected, trimmed, median, huber, shrinkage, winsorized, and mom.
weight: 5
criterion: Each estimator contains numeric values for bias, variance, mse, mae, median_ae, q95_ae, and rmse, all rounded to 4 decimal places.
weight: 5
criterion: Scenario S1 has huber.rmse = 0.2405, shrinkage.mse = 0.0396, bias_corrected.median_ae = 0.1426, and comparisons.prob_weighted_better_than_mean = 0.5934.
weight: 20
criterion: Scenario S2 has comparisons.prob_weighted_better_than_mean = 0.7175, and Scenario S3 has comparisons.prob_weighted_better_than_mean = 0.6812, comparisons.prob_huber_better_than_mean = 0.6194, and bias_correction.c_opt = 0.0070.
weight: 20
criterion: In aggregate, mae_mean['shrinkage'] = 0.3726, rank_by_mse[0] = 'shrinkage', and rank_by_mae[0] = 'shrinkage'.
weight: 10
criterion: The implementation uses the fixed scenario seeds 42, 1042, and 2042; inverse-variance weighting from sigma_i^2; population variance for estimator variance; exactly 8 Huber iterations; and round-half-to-even to 4 decimals throughout.
weight: 10
criterion: All stochastic and numerical conventions match the prompt requirements: numpy.random.default_rng is used; numpy.quantile with default settings is used for all quantile computations; strict inequality (<) is used for probability comparisons; and rounding is performed via float(numpy.round(x, 4)).
weight: 5
criterion: The data-generating process and estimators are implemented consistently with the prompt: sigma_i^2 uses 1-based indexing; the shared-shock correlation z = sqrt(rho)*z_common + sqrt(1-rho)*z is correctly applied when rho > 0; and trimmed, winsorized, and median-of-means estimators follow the specified definitions (including floor-based trimming and contiguous equal-sized blocks).
weight: 5

Golden answer:
```python
import json

import numpy as np


def round4(value):
    return float(np.round(value, 4))


def compute_stats(estimates, theta):
    # Compute error relative to true parameter theta
    err = estimates - theta
    # Bias = E[estimator - theta]
    bias = float(np.mean(err))
    # Variance = population variance of estimator (ddof=0)
    variance = float(np.var(estimates, ddof=0))
    # MSE = E[(estimator - theta)^2]
    mse = float(np.mean(err ** 2))
    # MAE, median AE, q95 AE computed from absolute errors
    mae = float(np.mean(np.abs(err)))
    median_ae = float(np.median(np.abs(err)))
    q95_ae = float(np.quantile(np.abs(err), 0.95))
    rmse = float(np.sqrt(mse))
    return {
        "bias": round4(bias),
        "variance": round4(variance),
        "mse": round4(mse),
        "mae": round4(mae),
        "median_ae": round4(median_ae),
        "q95_ae": round4(q95_ae),
        "rmse": round4(rmse),
    }


def trimmed_mean(x, trim):
    n = x.shape[1]
    k = int(np.floor(trim * n))
    xs = np.sort(x, axis=1)
    return xs[:, k : n - k].mean(axis=1)


def winsorized_mean(x, trim):
    lo = np.quantile(x, trim, axis=1)
    hi = np.quantile(x, 1 - trim, axis=1)
    clipped = np.clip(x, lo[:, None], hi[:, None])
    return clipped.mean(axis=1)


def median_of_means(x, block_size):
    num_blocks = x.shape[1] // block_size
    blocks = x.reshape(x.shape[0], num_blocks, block_size)
    return np.median(blocks.mean(axis=2), axis=1)


def huber_location(x, k=1.345, iterations=8):
    mu = np.median(x, axis=1)
    # Iterative Huber M-estimator
    # weights downscale large residuals using threshold k
    # scale estimated via MAD each iteration
    for _ in range(iterations):
        resid = x - mu[:, None]
        scale = 1.4826 * np.median(np.abs(resid), axis=1) + 1e-8
        scaled = resid / scale[:, None]
        weights = np.ones_like(scaled)
        mask = np.abs(scaled) > k
        weights[mask] = k / np.abs(scaled[mask])
        mu = np.sum(weights * x, axis=1) / np.sum(weights, axis=1)
    return mu


def make_sigma2(n, base, slope, quad):
    idx = np.arange(1, n + 1, dtype=float)
    frac = idx / n
    return base + slope * idx + quad * frac ** 2


def simulate_x(rng, theta, n, reps, sigma2, noise, rho, t_df, mix_p, mix_scale):
    # Generate noise z according to scenario
    if noise == "normal":
        # normal, Student-t, or mixture distribution
        z = rng.standard_normal((reps, n))
    elif noise == "t":
        z = rng.standard_t(t_df, size=(reps, n))
    elif noise == "mixture":
        base = rng.standard_normal((reps, n))
        outlier = rng.standard_normal((reps, n)) * mix_scale
        mask = rng.random((reps, n)) < mix_p
        z = np.where(mask, outlier, base)
    else:
        raise ValueError("unknown noise")

    if rho > 0:
        # Apply shared-shock correlation
        # z = sqrt(rho)*z_common + sqrt(1-rho)*z
        z_common = rng.standard_normal((reps, 1))
        z = np.sqrt(rho) * z_common + np.sqrt(1 - rho) * z

    sigma = np.sqrt(sigma2)
    # Final samples: X = theta + z * sigma
    return theta + z * sigma


def run_simulation(
    theta=3.0,
    n=50,
    reps=10000,
    seed=42,
    base=0.5,
    slope=0.1,
    quad=0.0,
    noise="normal",
    rho=0.0,
    t_df=5.0,
    mix_p=0.05,
    mix_scale=6.0,
    trim=0.1,
    huber_k=1.345,
    shrink_prior=3.0,
    shrink_lambda=1.0,
):
    rng = np.random.default_rng(seed)
    sigma2 = make_sigma2(n, base, slope, quad)
    x = simulate_x(rng, theta, n, reps, sigma2, noise, rho, t_df, mix_p, mix_scale)

    weights = 1.0 / sigma2
    wsum = float(weights.sum())

    est_mean = x.mean(axis=1)
    est_weighted = (x * weights).sum(axis=1) / wsum
    est_trimmed = trimmed_mean(x, trim)
    est_median = np.median(x, axis=1)
    est_huber = huber_location(x, k=huber_k)
    est_shrinkage = (wsum * est_weighted + shrink_lambda * shrink_prior) / (wsum + shrink_lambda)
    est_winsorized = winsorized_mean(x, trim)
    est_mom = median_of_means(x, block_size=5)
    # Empirical bias correction:
    # c_opt = E[theta - weighted_estimate] over repetitions
    # bias_corrected estimator shifts weighted estimator by this constant
    c_opt = float(np.mean(theta - est_weighted))
    est_bias_corrected = est_weighted + c_opt

    estimators = {
        "mean": est_mean,
        "weighted": est_weighted,
        "bias_corrected": est_bias_corrected,
        "trimmed": est_trimmed,
        "median": est_median,
        "huber": est_huber,
        "shrinkage": est_shrinkage,
        "winsorized": est_winsorized,
        "mom": est_mom,
    }

    stats = {name: compute_stats(values, theta) for name, values in estimators.items()}
    mse_rank = sorted(stats.keys(), key=lambda name: stats[name]["mse"])
    mae_rank = sorted(stats.keys(), key=lambda name: stats[name]["mae"])

    prob_weighted = float(np.mean(np.abs(est_weighted - theta) < np.abs(est_mean - theta)))
    prob_huber = float(np.mean(np.abs(est_huber - theta) < np.abs(est_mean - theta)))
    prob_winsor = float(np.mean(np.abs(est_winsorized - theta) < np.abs(est_mean - theta)))

    # Rank estimators by increasing MSE and MAE
    # Compute probability that alternative estimator has smaller absolute error than mean
    # using strict inequality as required
    return {
        "settings": {
            "theta": theta,
            "n": n,
            "reps": reps,
            "seed": seed,
            "variance": {"base": base, "slope": slope, "quad": quad},
            "noise": {"type": noise, "rho": rho, "t_df": t_df, "mix_p": mix_p, "mix_scale": mix_scale},
            "trim": trim,
            "huber_k": huber_k,
            "shrinkage": {"prior": shrink_prior, "lambda": shrink_lambda},
        },
        "estimators": stats,
        "rankings": {"by_mse": mse_rank, "by_mae": mae_rank},
        "comparisons": {
            "prob_weighted_better_than_mean": round4(prob_weighted),
            "prob_huber_better_than_mean": round4(prob_huber),
            "prob_winsorized_better_than_mean": round4(prob_winsor),
        },
        "bias_correction": {"c_opt": round4(c_opt)},
    }


scenarios = [
    {"id": "S1", "noise": "normal", "rho": 0.0, "quad": 0.0, "t_df": 5.0},
    {"id": "S2", "noise": "t", "rho": 0.2, "quad": 0.0, "t_df": 3.0},
    {"id": "S3", "noise": "mixture", "rho": 0.1, "quad": 0.2, "t_df": 5.0},
]

scenario_results = []
for idx, scenario in enumerate(scenarios):
    result = run_simulation(
        seed=42 + idx * 1000,
        quad=scenario["quad"],
        noise=scenario["noise"],
        rho=scenario["rho"],
        t_df=scenario["t_df"],
    )
    scenario_results.append({"id": scenario["id"], **result})

estimator_names = list(scenario_results[0]["estimators"].keys())
mse_mean = {
    name: round4(float(np.mean([scenario["estimators"][name]["mse"] for scenario in scenario_results])))
    for name in estimator_names
}
mae_mean = {
    name: round4(float(np.mean([scenario["estimators"][name]["mae"] for scenario in scenario_results])))
    for name in estimator_names
}

output = {
    "settings": {
        "theta": 3.0,
        "n": 50,
        "reps": 10000,
        "seed": 42,
        "variance": {"base": 0.5, "slope": 0.1, "quad": 0.0},
        "noise": {"t_df": 5.0, "mix_p": 0.05, "mix_scale": 6.0},
        "trim": 0.1,
        "huber_k": 1.345,
        "shrinkage": {"prior": 3.0, "lambda": 1.0},
    },
    "scenarios": scenario_results,
    "aggregate": {
        "mse_mean": mse_mean,
        "mae_mean": mae_mean,
        "rank_by_mse": sorted(estimator_names, key=lambda name: mse_mean[name]),
        "rank_by_mae": sorted(estimator_names, key=lambda name: mae_mean[name]),
    },
}

print(json.dumps(output, separators=(",", ":")))
```

Execution Validation:

- The task makes the execution environment explicit: use `uv` and `numpy`.
- The simulation output matches exactly the results produced by executing the provided code implementation using uv.
- All numerics are rounded with `np.round(..., 4)`, which follows round-half-to-even.
- Comments in the code provide extra reasoning details

Verified with:

```bash
uv run --with numpy python golden.py
```

Verified result:

```json
{"settings":{"theta":3.0,"n":50,"reps":10000,"seed":42,"variance":{"base":0.5,"slope":0.1,"quad":0.0},"noise":{"t_df":5.0,"mix_p":0.05,"mix_scale":6.0},"trim":0.1,"huber_k":1.345,"shrinkage":{"prior":3.0,"lambda":1.0}},"scenarios":[{"id":"S1","settings":{"theta":3.0,"n":50,"reps":10000,"seed":42,"variance":{"base":0.5,"slope":0.1,"quad":0.0},"noise":{"type":"normal","rho":0.0,"t_df":5.0,"mix_p":0.05,"mix_scale":6.0},"trim":0.1,"huber_k":1.345,"shrinkage":{"prior":3.0,"lambda":1.0}},"estimators":{"mean":{"bias":-0.0014,"variance":0.0604,"mse":0.0604,"mae":0.1963,"median_ae":0.1665,"q95_ae":0.479,"rmse":0.2457},"weighted":{"bias":-0.0004,"variance":0.0431,"mse":0.0431,"mae":0.1664,"median_ae":0.1427,"q95_ae":0.4056,"rmse":0.2075},"bias_corrected":{"bias":-0.0,"variance":0.0431,"mse":0.0431,"mae":0.1664,"median_ae":0.1426,"q95_ae":0.4054,"rmse":0.2075},"trimmed":{"bias":-0.0015,"variance":0.0577,"mse":0.0577,"mae":0.1917,"median_ae":0.1625,"q95_ae":0.4734,"rmse":0.2402},"median":{"bias":-0.0017,"variance":0.0738,"mse":0.0738,"mae":0.2167,"median_ae":0.1858,"q95_ae":0.5272,"rmse":0.2716},"huber":{"bias":-0.0014,"variance":0.0578,"mse":0.0578,"mae":0.192,"median_ae":0.163,"q95_ae":0.474,"rmse":0.2405},"shrinkage":{"bias":-0.0004,"variance":0.0396,"mse":0.0396,"mae":0.1595,"median_ae":0.1368,"q95_ae":0.3888,"rmse":0.1989},"winsorized":{"bias":-0.0015,"variance":0.0598,"mse":0.0598,"mae":0.1949,"median_ae":0.165,"q95_ae":0.4817,"rmse":0.2446},"mom":{"bias":-0.0001,"variance":0.0687,"mse":0.0687,"mae":0.2081,"median_ae":0.172,"q95_ae":0.5144,"rmse":0.2622}},"rankings":{"by_mse":["shrinkage","weighted","bias_corrected","trimmed","huber","winsorized","mean","mom","median"],"by_mae":["shrinkage","weighted","bias_corrected","trimmed","huber","winsorized","mean","mom","median"]},"comparisons":{"prob_weighted_better_than_mean":0.5934,"prob_huber_better_than_mean":0.5231,"prob_winsorized_better_than_mean":0.5138},"bias_correction":{"c_opt":0.0004}},{"id":"S2","settings":{"theta":3.0,"n":50,"reps":10000,"seed":1042,"variance":{"base":0.5,"slope":0.1,"quad":0.0},"noise":{"type":"t","rho":0.2,"t_df":3.0,"mix_p":0.05,"mix_scale":6.0},"trim":0.1,"huber_k":1.345,"shrinkage":{"prior":3.0,"lambda":1.0}},"estimators":{"mean":{"bias":0.0081,"variance":0.7183,"mse":0.7183,"mae":0.6717,"median_ae":0.5624,"q95_ae":1.6738,"rmse":0.8475},"weighted":{"bias":0.0066,"variance":0.4927,"mse":0.4927,"mae":0.5554,"median_ae":0.465,"q95_ae":1.3701,"rmse":0.702},"bias_corrected":{"bias":-0.0,"variance":0.4927,"mse":0.4927,"mae":0.5554,"median_ae":0.4663,"q95_ae":1.3715,"rmse":0.7019},"trimmed":{"bias":0.0075,"variance":0.6061,"mse":0.6062,"mae":0.6196,"median_ae":0.5227,"q95_ae":1.5379,"rmse":0.7786},"median":{"bias":0.0061,"variance":0.556,"mse":0.556,"mae":0.5917,"median_ae":0.4917,"q95_ae":1.4872,"rmse":0.7457},"huber":{"bias":0.0074,"variance":0.6003,"mse":0.6003,"mae":0.6159,"median_ae":0.5195,"q95_ae":1.5351,"rmse":0.7748},"shrinkage":{"bias":0.0063,"variance":0.4527,"mse":0.4527,"mae":0.5324,"median_ae":0.4457,"q95_ae":1.3132,"rmse":0.6728},"winsorized":{"bias":0.0078,"variance":0.6394,"mse":0.6395,"mae":0.6368,"median_ae":0.5371,"q95_ae":1.5822,"rmse":0.7997},"mom":{"bias":0.0074,"variance":0.613,"mse":0.6131,"mae":0.6185,"median_ae":0.5102,"q95_ae":1.5623,"rmse":0.783}},"rankings":{"by_mse":["shrinkage","weighted","bias_corrected","median","huber","trimmed","mom","winsorized","mean"],"by_mae":["shrinkage","weighted","bias_corrected","median","huber","mom","trimmed","winsorized","mean"]},"comparisons":{"prob_weighted_better_than_mean":0.7175,"prob_huber_better_than_mean":0.6083,"prob_winsorized_better_than_mean":0.5658},"bias_correction":{"c_opt":-0.0066}},{"id":"S3","settings":{"theta":3.0,"n":50,"reps":10000,"seed":2042,"variance":{"base":0.5,"slope":0.1,"quad":0.2},"noise":{"type":"mixture","rho":0.1,"t_df":5.0,"mix_p":0.05,"mix_scale":6.0},"trim":0.1,"huber_k":1.345,"shrinkage":{"prior":3.0,"lambda":1.0}},"estimators":{"mean":{"bias":-0.0021,"variance":0.4583,"mse":0.4584,"mae":0.5393,"median_ae":0.4548,"q95_ae":1.3351,"rmse":0.677},"weighted":{"bias":-0.007,"variance":0.3121,"mse":0.3121,"mae":0.4446,"median_ae":0.3768,"q95_ae":1.1021,"rmse":0.5587},"bias_corrected":{"bias":0.0,"variance":0.3121,"mse":0.3121,"mae":0.4446,"median_ae":0.3766,"q95_ae":1.1018,"rmse":0.5587},"trimmed":{"bias":-0.001,"variance":0.3445,"mse":0.3445,"mae":0.4698,"median_ae":0.4015,"q95_ae":1.147,"rmse":0.5869},"median":{"bias":0.0005,"variance":0.3276,"mse":0.3276,"mae":0.4574,"median_ae":0.3841,"q95_ae":1.112,"rmse":0.5723},"huber":{"bias":-0.0007,"variance":0.343,"mse":0.343,"mae":0.4689,"median_ae":0.4008,"q95_ae":1.1456,"rmse":0.5857},"shrinkage":{"bias":-0.0067,"variance":0.2865,"mse":0.2865,"mae":0.426,"median_ae":0.361,"q95_ae":1.0559,"rmse":0.5353},"winsorized":{"bias":-0.0016,"variance":0.3632,"mse":0.3632,"mae":0.4823,"median_ae":0.4114,"q95_ae":1.1753,"rmse":0.6027},"mom":{"bias":-0.0029,"variance":0.3607,"mse":0.3607,"mae":0.4769,"median_ae":0.4022,"q95_ae":1.1714,"rmse":0.6006}},"rankings":{"by_mse":["shrinkage","weighted","bias_corrected","median","huber","trimmed","mom","winsorized","mean"],"by_mae":["shrinkage","weighted","bias_corrected","median","huber","trimmed","mom","winsorized","mean"]},"comparisons":{"prob_weighted_better_than_mean":0.6812,"prob_huber_better_than_mean":0.6194,"prob_winsorized_better_than_mean":0.6003},"bias_correction":{"c_opt":0.007}}],"aggregate":{"mse_mean":{"mean":0.4124,"weighted":0.2826,"bias_corrected":0.2826,"trimmed":0.3361,"median":0.3191,"huber":0.3337,"shrinkage":0.2596,"winsorized":0.3542,"mom":0.3475},"mae_mean":{"mean":0.4691,"weighted":0.3888,"bias_corrected":0.3888,"trimmed":0.427,"median":0.4219,"huber":0.4256,"shrinkage":0.3726,"winsorized":0.438,"mom":0.4345},"rank_by_mse":["shrinkage","weighted","bias_corrected","median","huber","trimmed","mom","winsorized","mean"],"rank_by_mae":["shrinkage","weighted","bias_corrected","median","huber","trimmed","mom","winsorized","mean"]}}
```