Skip to content

Commit bfdadce

Browse files
Add evidence.csv: flag records by number of distinct public sources
Most enriched records carry 2-10 verifier-checked sources (median ~4), but sites_seed.csv keeps only the single strongest evidence_url. The new normalized table restores the full set — one row per (record, distinct URL) — reconstructed from the verified enrichment JSON. - pipeline/build_evidence.py -> data/evidence.csv (291 source rows; 53/72 sites carry >=2 distinct sources, max 10) - build_map.py: joins per-record counts, adds a 'Sources (#)' tooltip field on pins and arcs (defaults to 1 — every record has >=1 source) - compute_sites.csv gains an evidence_count column (Renaissance and Citadel show as single-source; Verne 8, HRT Lefdal 6) - 6 new tests: evidence schema, referential integrity, url hygiene, per-record url uniqueness, every-site-covered, compute-count sync Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8af4158 commit bfdadce

8 files changed

Lines changed: 541 additions & 31 deletions

File tree

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,28 @@ documented quant/HFT infrastructure, with an evidence grade on every record.
55

66
## Structure
77

8-
- `data/sites_seed.csv` — the site inventory (target ~80-120 records)
8+
- `data/sites_seed.csv` — the site inventory (72 records; target ~80-120)
99
- `data/paths.csv` — Tier-2 arcs (origin/dest site_ids, operator, medium),
1010
rendered as a kepler arc layer
11+
- `data/compute_sites.csv` — the megawatts sheet: research-tier compute
12+
centres only, with GPU counts, power (MW), and build status (generated by
13+
`pipeline/build_compute_sheet.py`)
14+
- `data/evidence.csv` — normalized multi-source table: one row per (record,
15+
distinct source URL), so each pin can show how many independent sources
16+
back it (generated by `pipeline/build_evidence.py`)
1117
- `build_map.py` — CSVs → standalone kepler.gl HTML
12-
(`python build_map.py --out docs/index.html`)
18+
(`python build_map.py --out docs/index.html`); joins evidence.csv to show a
19+
source count per pin
1320
- `pipeline/geocode.py` — Nominatim geocoder (rate-limited, cached) for
1421
verifying coordinates against documented addresses
15-
- `docs/index.html` — output (tracked; servable via GitHub Pages); embeddable
16-
in Ghost via iframe or direct upload
22+
- `docs/index.html` — output (tracked; auto-deployed to GitHub Pages via
23+
`.github/workflows/pages.yml`); embeddable in Ghost via iframe
1724
- `ENRICHMENT.md` — candidate records awaiting research/verification
1825

26+
Regenerate derived files after editing `sites_seed.csv`:
27+
`python pipeline/build_evidence.py && python pipeline/build_compute_sheet.py
28+
&& python build_map.py --out docs/index.html`
29+
1930
## Schema
2031

2132
| column | meaning |

build_map.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"status": "Status",
7171
"coord_precision": "Pin precision",
7272
"confidence": "Confidence",
73+
"evidence_count": "Sources (#)",
7374
"evidence_note": "Evidence",
7475
"evidence_url": "Source URL",
7576
"gmaps_url": "Google Maps",
@@ -80,6 +81,7 @@
8081
"medium": "Medium",
8182
"status": "Status",
8283
"confidence": "Confidence",
84+
"evidence_count": "Sources (#)",
8385
"evidence_note": "Evidence",
8486
"evidence_url": "Source URL",
8587
}
@@ -95,14 +97,31 @@
9597
PATHS_COLOR = [250, 190, 60] # amber, matching the network layer
9698

9799

98-
def load_sites(csv_path: Path) -> pd.DataFrame:
100+
def evidence_counts(evidence_csv: Path) -> pd.Series:
101+
"""ref_id -> number of distinct public sources, from evidence.csv.
102+
103+
Empty Series when the file is absent (evidence_count then defaults to 1
104+
since every mapped record carries at least its own evidence_url)."""
105+
if not evidence_csv.exists():
106+
return pd.Series(dtype="int64")
107+
ev = pd.read_csv(evidence_csv, dtype=str)
108+
return ev.groupby("ref_id").size()
109+
110+
111+
def load_sites(csv_path: Path, counts: pd.Series | None = None) -> pd.DataFrame:
99112
df = pd.read_csv(csv_path)
100113

101114
required = {"site_id", "site_name", "tier", "lat", "lon", "confidence"}
102115
missing = required - set(df.columns)
103116
if missing:
104117
raise ValueError(f"CSV missing required columns: {missing}")
105118

119+
# How many distinct public sources back each pin (defaults to 1: every
120+
# record carries at least its own evidence_url).
121+
counts = pd.Series(dtype="int64") if counts is None else counts
122+
df["evidence_count"] = (
123+
df["site_id"].map(counts).fillna(1).astype(int).astype(str))
124+
106125
# Google Maps handoff, honest about pin precision: coordinate deep link
107126
# only where the pin is placed (exact/approximate); a name+city text
108127
# search for city-level pins (the coords are a centroid, not the site);
@@ -130,7 +149,8 @@ def gmaps(row):
130149
return df.fillna("")
131150

132151

133-
def load_paths(paths_csv: Path, sites: pd.DataFrame) -> pd.DataFrame | None:
152+
def load_paths(paths_csv: Path, sites: pd.DataFrame,
153+
counts: pd.Series | None = None) -> pd.DataFrame | None:
134154
"""Load Tier-2 arcs and join origin/dest coordinates from the sites table.
135155
136156
Returns None when the paths file is absent or has no rows."""
@@ -151,6 +171,10 @@ def load_paths(paths_csv: Path, sites: pd.DataFrame) -> pd.DataFrame | None:
151171
paths[f"{end}_lon"] = joined["lon"]
152172
paths[f"{end}_name"] = joined["site_name"]
153173
paths["route"] = paths["origin_name"] + " → " + paths["dest_name"]
174+
175+
counts = pd.Series(dtype="int64") if counts is None else counts
176+
paths["evidence_count"] = (
177+
paths["path_id"].map(counts).fillna(1).astype(int).astype(str))
154178
return paths
155179

156180

@@ -233,9 +257,11 @@ def kepler_config(df: pd.DataFrame, with_paths: bool = False) -> dict:
233257
}
234258

235259

236-
def build(csv_path: Path, out_path: Path, paths_csv: Path | None = None) -> None:
237-
df = load_sites(csv_path)
238-
paths = load_paths(paths_csv, df) if paths_csv else None
260+
def build(csv_path: Path, out_path: Path, paths_csv: Path | None = None,
261+
evidence_csv: Path | None = None) -> None:
262+
counts = evidence_counts(evidence_csv) if evidence_csv else pd.Series(dtype="int64")
263+
df = load_sites(csv_path, counts=counts)
264+
paths = load_paths(paths_csv, df, counts=counts) if paths_csv else None
239265

240266
m = KeplerGl(config=kepler_config(df, with_paths=paths is not None))
241267
# One dataset per tier so each gets its own layer + legend entry + toggle.
@@ -267,6 +293,7 @@ def build(csv_path: Path, out_path: Path, paths_csv: Path | None = None) -> None
267293
parser = argparse.ArgumentParser()
268294
parser.add_argument("--csv", default="data/sites_seed.csv", type=Path)
269295
parser.add_argument("--paths", default="data/paths.csv", type=Path)
296+
parser.add_argument("--evidence", default="data/evidence.csv", type=Path)
270297
parser.add_argument("--out", default="quant_dc_map.html", type=Path)
271298
args = parser.parse_args()
272-
build(args.csv, args.out, paths_csv=args.paths)
299+
build(args.csv, args.out, paths_csv=args.paths, evidence_csv=args.evidence)

0 commit comments

Comments
 (0)