"""Adverse selection in an instant home offer, worked out without random numbers.

A buyer offers every owner its estimate of the home's value (minus a fee).
The estimate is unbiased: on average it is exactly right. Each owner knows
what the home is worth and says yes only when the offer is higher.
Question: what does the buyer end up owning?

No sampling: the N estimate errors are the evenly spaced quantiles of a
normal distribution, so every run gives the same numbers. The error size
comes from Zillow's own published median error rates in 2021 (sources S17, S18):
for a normal error, median absolute error = 0.6745 x sigma.

Run from the repo root:
    python -I episodes/2026-11-zillow-offers/02-research/sim/adverse-selection.py
Writes adverse-selection.json next to this file.
"""
import json
from pathlib import Path
from statistics import NormalDist

N = 1000                # homes offered; also the dots on screen
VALUE = 350_000         # what each home is really worth (illustrative)
MEDIAN_ERRORS = {"on_market": 0.019, "off_market": 0.069}   # S17 (Oct 2021 Zestimate page), S18 (June 15, 2021 release)
FEES = [0.0, 0.025]     # service charge taken off the offer; 2.5% was Zillow's average (S16)
Z = NormalDist()


def run(median_error, fee):
    sigma = median_error / Z.inv_cdf(0.75)
    errors = [sigma * Z.inv_cdf((i + 0.5) / N) for i in range(N)]
    homes = []
    for e in errors:
        estimate = VALUE * (1 + e)
        offer = estimate * (1 - fee)
        homes.append({"error": round(e, 5), "offer": round(offer), "accepted": offer > VALUE})
    bought = [h for h in homes if h["accepted"]]
    paid_over = [h["offer"] - VALUE for h in bought]
    return {
        "median_error": median_error,
        "sigma": round(sigma, 5),
        "fee": fee,
        "homes_offered": N,
        "homes_bought": len(bought),
        "acceptance_rate": round(len(bought) / N, 4),
        "mean_error_all_offers": round(sum(errors) / N, 6),
        "mean_error_homes_bought": round(sum(h["error"] for h in bought) / max(len(bought), 1), 5),
        "mean_overpaid_per_home_bought": round(sum(paid_over) / max(len(paid_over), 1)),
        "total_overpaid": sum(paid_over),
        "homes": homes,
    }


runs = [run(m, f) for m in MEDIAN_ERRORS.values() for f in FEES]
for r, name in zip(runs, [n for n in MEDIAN_ERRORS for _ in FEES]):
    r["error_basis"] = name

# Sanity: the estimate is unbiased overall, but every home bought was over-estimated.
for r in runs:
    assert abs(r["mean_error_all_offers"]) < 1e-9
    assert all(h["error"] > 0 for h in r["homes"] if h["accepted"])

summary = [f'{r["error_basis"]} fee {r["fee"]:.1%}: bought {r["homes_bought"]}/{N}, '
           f'overpaid ${r["mean_overpaid_per_home_bought"]:,} per home bought, '
           f'mean error all {r["mean_error_all_offers"]:+.4f} vs bought {r["mean_error_homes_bought"]:+.4f}'
           for r in runs]

out = {
    "what": "Instant offers at the estimate (minus a fee); owners accept only offers above the home's real value.",
    "value_per_home": VALUE,
    "source_of_error_sizes": "S17/S18: Zillow, nationwide median Zestimate error after the June 15, 2021 update: 1.9% (on-market) and 6.9% (off-market)",
    "method": "N evenly spaced quantiles of a normal error; no random numbers",
    "summary": summary,
    "runs": runs,
}
Path(__file__).with_suffix(".json").write_text(json.dumps(out, indent=1), encoding="utf-8")
print("\n".join(summary))
