# -*- coding: utf-8 -*- """Hand-labelled validation of the extracted firm list. The extractor is heuristic, and the strict filter in study500_sensitivity.py is a second heuristic, so neither can say how many extracted names are real businesses. This script uses 400 names labelled by hand, blind to their outcomes, to estimate the headline figures for the population the page actually talks about: real estate agents, teams and brokerages. Why post-stratification. The seeded 400-name draw came out unusually rich in one-area names (355 of 400, against 84.7% in the population; a draw that high happens about 1 time in 125). Raw sample shares would inherit that luck. So every estimate is computed inside two strata, one-area and multi-area names, and re-weighted to the population's known split. The confidence intervals are a stratified bootstrap with a fixed seed, so the output is identical on every run. py -3.10 study500_validation.py # writes study500/VALIDATION.txt build_study500.py imports compute() so the page reads these figures instead of typed ones. """ from __future__ import annotations import io import json import os import random import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import study500_market as M # noqa: E402 (same row loading and firm index as every other script) HERE = os.path.dirname(os.path.abspath(__file__)) LABELS = os.path.join(HERE, "study500", "validation_400.json") OUT = os.path.join(HERE, "study500", "VALIDATION.txt") HAR_MEMBERS = 50000 BOOT = 5000 BOOT_SEED = 7 def _records(): """Join each hand label to the pipeline's own counts for that name.""" lab = json.load(io.open(LABELS, encoding="utf-8")) _, clean = M.load_rows() idx = M.FirmIndex(clean) recs, missing = [], [] for r in lab["records"]: n = r["name"] if n not in idx.mentions: missing.append(n) continue recs.append({"name": n, "label": r["label"], "mentions": idx.mentions[n], "areas": len(idx.firm_areas[n]), "surfaces": len(idx.firm_engines[n])}) if missing: raise SystemExit("labelled names not in the firm index (extractor changed?): %s" % missing[:5]) pop = len(idx.mentions) one = sum(1 for f in idx.mentions if len(idx.firm_areas[f]) == 1) return lab, recs, pop, one def _estimate(recs, w_one, pop): one = [r for r in recs if r["areas"] == 1] multi = [r for r in recs if r["areas"] != 1] W = ((w_one, one), (1 - w_one, multi)) def share(cond): return sum(w * (sum(1 for r in g if cond(r)) / len(g)) for w, g in W if g) a = share(lambda r: r["label"] == "A") return { "oneArea": 100 * w_one * (sum(1 for r in one if r["label"] == "A") / len(one)) / a, "oneSurface": 100 * share(lambda r: r["label"] == "A" and r["surfaces"] == 1) / a, "namedOnce": 100 * share(lambda r: r["label"] == "A" and r["mentions"] == 1) / a, "oneBoth": 100 * share(lambda r: r["label"] == "A" and r["areas"] == 1 and r["surfaces"] == 1) / a, "agentShare": 100 * a, "realShare": 100 * share(lambda r: r["label"] in "AB"), "junkShare": 100 * share(lambda r: r["label"] == "J"), "agents": a * pop, "harShare": 100 * a * pop / HAR_MEMBERS, } def compute() -> dict: lab, recs, pop, one = _records() w_one = one / pop point = _estimate(recs, w_one, pop) strata = ([r for r in recs if r["areas"] == 1], [r for r in recs if r["areas"] != 1]) rng = random.Random(BOOT_SEED) boots = [] for _ in range(BOOT): bs = [rng.choice(strata[0]) for _ in strata[0]] + [rng.choice(strata[1]) for _ in strata[1]] boots.append(_estimate(bs, w_one, pop)) lo, hi = int(0.025 * BOOT) - 1, int(0.975 * BOOT) - 1 ci = {k: (sorted(b[k] for b in boots)[lo], sorted(b[k] for b in boots)[hi]) for k in point} counts = {k: sum(1 for r in recs if r["label"] == k) for k in "ABJ"} return {"n": len(recs), "seed": lab["seed"], "labelled": lab["labelled"], "counts": counts, "population": pop, "oneAreaPop": one, "sampleOneArea": len(strata[0]), "point": point, "ci": ci, "boot": BOOT} def main() -> None: v = compute() p, c = v["point"], v["ci"] L = ["=" * 74, "500-QUERY HOUSTON STUDY - HAND-LABELLED VALIDATION", "generated by study500_validation.py, never hand-edited", "=" * 74, "", " sample %d names, seed %d, labelled blind on %s" % (v["n"], v["seed"], v["labelled"]), " labels A %(A)d agent/team/brokerage | B %(B)d other business | J %(J)d not a business" % v["counts"], " sample one-area %d of %d (population %d of %d, %.1f%%)" % ( v["sampleOneArea"], v["n"], v["oneAreaPop"], v["population"], 100 * v["oneAreaPop"] / v["population"]), " method post-stratified on one-area vs multi-area, stratified bootstrap x%d" % v["boot"], "", " %-50s %9s %s" % ("estimate", "point", "95% interval")] rows = [("named in exactly one area, real agents", "oneArea", "%.1f%%"), ("named by one surface (all queries), real agents", "oneSurface", "%.1f%%"), ("named exactly once, real agents", "namedOnce", "%.1f%%"), ("one area AND one surface, real agents", "oneBoth", "%.1f%%"), ("extracted names that are agents/teams/brokerages", "agentShare", "%.1f%%"), ("extracted names that are any real business", "realShare", "%.1f%%"), ("extracted names that are not a business", "junkShare", "%.1f%%"), ("real agents, teams and brokerages named", "agents", "%.0f"), ("share of %d HAR members named" % HAR_MEMBERS, "harShare", "%.1f%%")] for label, k, f in rows: L.append(" %-50s %9s %s to %s" % (label, f % p[k], f % c[k][0], f % c[k][1])) L += ["", "=" * 74] text = "\n".join(L) io.open(OUT, "w", encoding="utf-8", newline="\n").write(text + "\n") print(text) if __name__ == "__main__": main()