Exhaustive project memory so a fresh instance can be productive in one read. Authored May 2026 (Opus 4.8) from a full code+data+docs sweep. If something here disagrees with the code, the code wins — verify before trusting. Companion long-form docs:
docs/repo_state.md(current state),docs/roadmap.md(next steps). This file is the compressed union of all fourdocs/*.mdplus things only visible in the code/data.
The "fundamental result beneath Beta-Prime" exists and is proven on the data:
- The eps-dependence factorizes:
R(eps) = scale(eps)·X,scale ∝ eps^(−2.205),Xeps-invariant to ~1%. RescalingR̃=R·eps²collapses cross-eps spread 52%→5%. (−2=geometryS'∝eps²;−0.205residual = fixed-min_sampleseffect, ~44% N'-occupancy broadening + ~56% per-n area loosening.) - Beta-Prime is derived & demoted:
R|N'=n = n/(λ₀S')is exactly scaled-inverse-gamma with shape = Gamma-shape of hull areaS'|n. Marginal = N'-mixture of these (mode n=10), fits as well as Beta-Prime. Beta-Prime params are non-identifiable (two basins both fit KS~0.02) — never read mechanism offb. The robust quantity is hull-shapek(10)≈20.5. - First-principles geometry:
S'|n= convex-hull area ofnuniform points in an eps-disk (CSR+Poisson conditioning); matches data ~1%, chaining +8%. Leading constant derived:C = N_min/f·k/(k−1) ≈ 25,f≈0.42fill fraction, no free params. - eps-independent scorer shipped — master corrected 2026-06-07 (
docs/RCA.md§7): the master onR̃=R·eps^α(ε)(α(ε)=2.031525+0.258273·ln ε) is a SHIFTED inverse-gamma, integer shape 10 = min_samples:SF(R̃)=P(10, 157.7035/(R̃−7.5091)). The location shift is essential —R|nis bounded away from 0 (hull area of an eps-connected cluster bounded above), so zero-loc families (the earlier inv-gamma(20.5), the log-logistic webapp master) mis-centre z by ~0.07σ. Shape 10 = thea→∞limit of legacy Beta-Prime(a≈46,b≈10). Pooled KS≈0.005 over eps 1.00–1.60; |median_z|≤0.02 per eps. Same constants inanalysis/scorer.py/scorer_master.json,modules/cluster_detector.py(master_sf,score_clusters),webapp/master.js(exact Poisson-sum SF). Never fitfloc=0toR̃. Evidence:analysis/zcal_*.py/csv. - Master parameters decoded (
docs/RCA.md§8): the loc is DBSCAN's certification floor — an n=min_samples cluster fits in one core's eps-ball, max hull = regular (m−1)-gon ⇒R̃_floor = m·(1+2π²/3(m−1)²) ≈ 10.87; observed global min 10.92 over 1.2M clusters. Master meanloc+scale/(shape−1) = 25.03= the derived no-free-paramC≈25. Shape 10 is the only effective param (N'-mixture roughly halves k(10)≈20.5; shape=min_samples link unproven — test with a min_samples sweep). N-invariance confirmed (analysis/ncheck.py): at fixed λ₀ (radius √N), N=10k/20k/40k → identical master & calibration, yield ∝ N.R̃is a purely local observable of (λ₀, eps, min_samples). - Look-elsewhere quantified: typical CSR cluster = "~6σ" under naive Kulldorff/Wilks (overstated ~10⁸×); only MC-replay calibrates. LR beats raw
R(size-weights: equal-R, larger-N'is rarer). - Bug fixes (committed): mixture PDF-used-as-CDF KS; missing NLL data arg; 4-vs-5 stride in
betaprime_mixture_pdf— likely the cause of the repo's degenerate-mixture results.
The two genuinely fundamental objects: the hull-area law of n uniform disk points + the N'-occupancy distribution P(N'=n). Everything else is downstream. This supersedes the framing below where it says "beta-prime parameters drift linearly with eps" (true, but it's just the eps² rescaling in awkward coordinates).
- Problem. Detect dense clusters in a random (Poisson/CSR) point field and assign them an honest null probability. Naive Poisson Z-scores are invalid once a density-seeking algorithm (DBSCAN) chooses the region — that's post-selection / look-elsewhere bias. Originally posed by Newton in 2020 (math.SE 3626685).
- Method to date. Monte-Carlo the null: uniform points in a disk → DBSCAN → record each cluster's
(S'=hull area, N'=count)→ study the density ratioR = (N'/S')/λ₀. Sweep DBSCANeps0.80→2.86. - Headline empirical result.
R | eps≈ Beta-Prime(a,b,loc,scale), KS ~0.003–0.006, with parameters drifting quasi-linearly ineps(arises ~+50/eps,locfalls ~−11/eps,b≈10flat,scaleshrinks). - Headline open problem. Convert per-
epsP(R|eps)into one calibratedeps-independent rare-event scoreP(R≥r)+Z_equiv=Φ⁻¹(1−p). - Biggest gotcha I found. The observed "tail bound"
max R ≈ 60–70is not physics — it'sR_max = N_min/(min_area·λ₀) = 10/(0.5·0.31831) = 62.83, a censoring artifact of themin_area/min_cluster_sizefilters. Don't fit extreme-value tails to current data. - Biggest theory gap. The data records pooled per-cluster
R, but the statistically correct object (per the design doc) isΛ = max over windowsof the Kulldorff LR, obtained by MC-replaying the whole pipeline. LR is never computed; only rawR. simdata/v2/= ~60 h compute on this machine. Precious artifact. Don't casually regenerate; when you do, add geometry columns (see §Roadmap).
| quantity | value | note |
|---|---|---|
| N (points) | 10 000 | hard-coded |
| radius R | 100 | disk |
| S₀ = πR² | 31 415.93 | domain area |
| λ₀ = N/S₀ | 0.318310 (=1/π) | baseline intensity |
| min_samples (DBSCAN) | 10 | core-point threshold |
| min_cluster_size | 10 | post-filter |
| min_area | 0.5 | post-filter (hull area) |
| ratio cap | 62.83 = N_min/(min_area·λ₀) | censoring artifact, matches observed max |
| useful eps range | ~0.9–1.4 | modelled window; <0.84 no clusters, >2.86 hits area cap |
| stable-fit cluster count | ≳10 000 (≥2000 min) | load_data requires ≥2000 |
| sample size for fits | 100 000 (subsample) | sample_data, seed 42 |
| R ≈ λ'·π | since R=λ'/λ₀ and λ₀=1/π | handy conversion |
Beta-Prime fit (results/regular_fit.csv, single-component, free loc):
| eps | a | b | loc | scale | KS |
|---|---|---|---|---|---|
| 1.10 | 40.9 | 10.0 | 7.42 | 2.91 | 0.0025 |
| 1.20 | 45.9 | 10.0 | 6.12 | 2.16 | 0.0027 |
| 1.30 | 50.9 | 10.6 | 4.97 | 1.78 | 0.0054 |
| 1.40 | 55.9 | 10.9 | 4.03 | 1.46 | 0.0060 |
regression_params.csv: a: slope 50.06 / int −14.18 · b: 3.78 / 5.69 · loc: −11.44 / 19.87 (R²≈0.99) · scale: −4.45 / 7.61.
(Two regression vintages exist — the per-eps regression_params.csv above, and the merged-likelihood merged_fit_params.csv with very different coeffs, e.g. scale_slope≈0.005. They optimize different objectives; don't conflate.)
- Why a ratio, not a rate. Raw
λ'is scale-uninformative (3 points in a tiny triangle → huge λ', meaningless). DimensionlessR = λ'/λ₀is the right observable (scale-invariance of Poisson). TheN'≥3/triangulation guard removesλ'∈{0,∞}but not the selection bias. - Why Beta-Prime. Ratio of two independent Gammas
Γ(a,θ)/Γ(b,θ) ~ BetaPrime(a,b).Rbehaves like (count-accumulation Gamma)/(hull-area/support Gamma).b≈10pinned nearmin_samples=10is consistent with the denominator being threshold-governed. Same family as F-distribution and the large-count limit of the Poisson LR — so rate-ratio, variance-ratio, and LR framings are mutually consistent. Diagnostic: Beta-Prime fitting well ⇒ the operative statistic is a ratio/contrast, not a bare rate. A bare selectedλ'would trend to Fréchet/GEV instead. - Why selection breaks naive scoring. DBSCAN returns (near) the max of
λ'over the family of windows it could draw, and hugs the boundary to the points. SoN'/S'is not a draw fromPoisson(λ₀S')/S', andS'is itself random/data-adaptive. Per-window Poisson-Z and per-window Bayesian Beta posteriors are both overconfident by the same mechanism. - Correct null object.
Λ = max_{Z∈W} T(Z)under CSR, withTthe Kulldorff scan LR andW= windows DBSCAN can actually return. Condition on totalN⇒ inside-countN' ~ Binomial(N, |Z|/|S|).- LR:
LR(Z) = (n/μ)^n · ((N−n)/(N−μ))^(N−n) · 1[n/μ>1],μ = N|Z|/|S|.
- LR:
- How to get the null (three routes, design doc §2.3):
- (A) MC-replay the full pipeline under CSR, record per-field
max LR, rank observed against it. Field standard (SaTScan). Valid for any selector because selection is replayed. ← primary recommendation. - (B) Analytic trials factor (Gross–Vitells / Euler-characteristic / Poisson-clumping): only for structured window families; no clean closed form for DBSCAN blobs.
- (C) Bayesian generative mixture: homogeneous-Poisson + cluster component, with cluster location/shape/count as latent vars integrated over (RJMCMC / DP mixture). The prior over where a cluster could be is the multiplicity correction. Gamma conjugate for rate; Beta-Binomial for inside-fraction
Pr(n'|a,b)=C(N,n')B(a+n',b+N−n')/B(a,b). Most principled, heaviest. - Moral: selection must be replayed (A) or integrated over (C). Per-window calc is only valid for pre-registered windows (which is exactly why the coarse fixed-grid method was legitimate).
- (A) MC-replay the full pipeline under CSR, record per-field
- Tails. Beta-Prime is Fréchet-domain (polynomial right tail). For deep-tail p-values the design doc says fit GPD / peaks-over-threshold, not brute 10⁶ sims. BUT — see the censoring caveat: current data's tail is clipped at 62.83 by
min_area, so GPD on it measures the filter, not the process. Must relaxmin_area/ use LR / model the censoring first. - Scoring. Don't use Gaussian
(x−μ)/σ(distribution is skewed, censored). Usep = SF(r)thenZ_equiv = Φ⁻¹(1−p). For an eps range: mixtureΣ wᵢ SFᵢ(r)or conservative envelopemaxᵢ SFᵢ(r).
P(R) = Σᵢ wᵢ P(R|epsᵢ). The weights encode what question you're asking:
- uniform-eps: "eps drawn uniformly from search range, then a cluster."
- cluster-count weighted: "a random detected cluster pooled across all eps runs" (matches how the data was pooled).
- conservative envelope
S_env(r)=maxᵢSᵢ(r): weighting-free, never overstates significance — recommended safe default. - max-over-eps EVD: "detector sweeps eps, reports the most extreme" — the true look-elsewhere null if eps is tuned after seeing data → but then just use route (A) replay over (eps, windows).
These are genuinely different answers; any deliverable must state which it computes.
(Analysis scripts moved 2026-06-07 from repo root into scripts/{sim,fit,stats,viz}/ packages; main.py lazy-imports them as scripts.<group>.<name>.)
Sim (scripts/sim/): simulate.py (parallel DBSCAN MC + eps sweep; writes simdata/v2/*.csv; resumable; 16 procs; per-worker seeded RNG) · plotter.py (S'×N' 2D hist).
Fit (scripts/fit/): beta_mix_vs_regular.py (CLI fit; per-eps single + optional --mix mixture → results/regular_fit.csv, regression_params.csv) · mixure_of_betas.py (CLI fit-mixture; one component per eps; has LL-arg bug, see Gotchas) · mixture_of_betas2.py (CLI fit-merged; eps-linear global model → merged_fit_params.csv) · floc.py (floc exploration; its floc.png artifact lives in results/).
Stats (scripts/stats/): stat_tests.py (CLI stats; normality + Poisson → analysis_summary.csv, plots) · stat_test2.py (Gamma/LogN/Weibull/BetaPrime ECDF compare) · stat_test3.py (Gamma GoF per eps; mixture-by-N') · anova.py (ANOVA + regression of Gamma params vs eps; 3 methods).
Viz (scripts/viz/): visualize.py (3D QQ, heatmap, surface) · beta_plot.py (CLI plot; overlay saved fits) · plot_hist.py (refit+overlay).
Modules: modules/simv2_data.py (data I/O: load_data, sample_data, load_fit_parameters, load_density_ratio) · modules/beta_stats.py (mixture PDF/NLL, eps-linear betaprime, perform_linear_regression) · modules/common_stats.py (compute_aic_bic, compute_ks_statistic).
Entry: main.py (Typer; uv run cluster-distribution {simulate,fit,fit-mixture,fit-merged,stats,visualize,plot}). Several analysis scripts (anova, stat_test2/3, floc, plot_hist, visualize-internals) are run directly, not all via CLI.
Data: simdata/v2/ (207 CSVs, cols S_prime,N_prime,iteration, ~4.3 GB, LFS, ~60 h compute) · simdata/v1/convert.py (legacy v1→v2; v1 dir empty).
Results: regular_fit.csv (core table, eps 1.10–1.40), regression_params.csv, merged_fit_params.csv, analysis_summary.csv, eps_experiment_results.csv, *_vs_eps.png, ratio_plots/, N_prime_plots/.
Docs: repo_state.md, roadmap.md (these two are the curated pair), clusters_problem.md (=theory north star, dup of cluster-detection-handoff's intent but different content now), cluster-detection-handoff.md (richest empirical handoff; content is inside a Python heredoc stub), cluster_density_null_model_handoff_v2.md (empirical notebook), overview.md (catalog).
- Columns:
S_prime(float, convex-hull area),N_prime(int),iteration(int). - Placeholder rows
S_prime=-1.0, N_prime=-1mark iterations with no valid cluster. Always filter(S_prime!=-1)&(N_prime!=-1)first. Most rows at low eps are placeholders. - Multiple clusters per iteration are possible and all are recorded → data is "all detected clusters pooled" (≈ random-detected-cluster), not max-per-field.
R = (N'/S')/λ₀computed downstream;λ₀=N/(πR²).- Some scripts apply a
ratio≥5body filter (floc.py,stat_test2.py); thelocparam (~4–7) reflects this soft floor. load_data/load_density_ratioskip eps with <2000 valid clusters and subsample to 100 000 (seed 42).
- DBSCAN-on-noise produces structured cluster distributions, not chaos.
epsstrongly changes distribution shape (peak shifts left + grows taller as eps↑), not just yield.S'mean & max grow monotonically with eps (bigger neighborhoods absorb more points/area).N'concentrated at the threshold: P(10)≈0.68, P(11)≈0.15, P(12)≈0.08, decaying; mean ≈10.2–10.6. DBSCAN sees only the upper tail of an underlying count process.Ris right-skewed: steep left rise, long right tail, censored above at 62.83.- Normality rejected (p~1e-40…1e-55). Poisson-on-N' rejected (truncated/selection-conditioned).
- Gamma = decent but systematically biased (peak too low, skewed right). Exponential/Weibull ruled out. Lognormal close, usually loses to Beta-Prime with free loc.
- Beta-Prime per-eps = excellent. Free/fitted
locbeatsfloc=0(andfloc=5beatsfloc=0). - 2-component Beta-Prime mixture collapses to identical components (degenerate; see
mixture_fit_eps1.10.csv, KS≈1.0 from the PDF-as-CDF bug). Single component suffices. The earlier "mixture over N' modes" idea is superseded. - Per-eps params strongly correlate with eps (linear, high R²).
- Cluster yield: ~35k @eps1.10 → ~1.2M @eps1.40 (
eps_experiment_results.csv/analysis_summary.csv). eps_experiment_results.csvcovers eps 0.81–2.16;avg_ratiofalls 40→4.4 monotonically;max_ratiopinned ~61–68 (the censoring artifact).- Rarefactions ≠ mirror of compressions (old grid Z-score gave asymmetric +3 ↔ −4); needs separate treatment & localiser.
min_areacensors the tail at 62.83 — the #1 misread-as-physics result. (docs/repo_state.md§4.2; answers handoff §18.2.)mixure_of_betas.pyLL bug:negative_log_likelihood(fitted_params)missing thedataarg → AIC/BIC there unreliable.- Mixture PDF used as CDF:
kstest(data, lambda x: betaprime_mixture_pdf(...))is wrong; KS for mixtures needsΣwᵢ Fᵢ. Present in mixture branches ofbeta_mix_vs_regular.py&plot_hist.py. Single-component fits use the real CDF and are fine. - KS p-values ≈ 0 at large n — meaningless; judge by KS statistic + eyeball, not p.
- AIC/BIC not comparable across different datasets (e.g. all-data vs N'=10-only).
- ANOVA needs replicates: one fit per eps ⇒ ANOVA across eps is ill-posed; do K-subsample fits per eps for dispersion (
anova.pyhas this limitation; handoff §8.1). - Merging eps ≠ a single distribution — it's a mixture; weights must be declared.
mixture_of_betas2.pyglobal KS≈0.43 is not a like-for-like KS (each datum has its own conditional CDF); the model isP(R|eps), not the wanted marginal.stat_tests.pywrites underdata_dir(simdata/v2) though committed plots live inresults/. Cosmetic.- Two regression-param vintages (
regression_params.csvvsmerged_fit_params.csv) — different objectives; keep straight. .python-versionsays 3.14, pyproject says ≥3.11 — uv-managed env reconciles.
- (now, cheap) Survival/
Z_equivscorer fromregular_fit.csv: mixture SF and conservative envelopemaxᵢSFᵢ, overlaid on empirical merged SF. Outputratio→{p_mix,z_mix,p_env,z_env,p_emp}. Body-only (don't extrapolate past ~55 due to censoring). ← closes the named "immediate next step". - (now, cheap) Bake the censoring caveat into all tail reporting; add Tobit-style or LR-based handling.
- ★ (compute, high value) Re-simulate with geometry columns (centroid, r_center, dist-to-boundary, edge-safe flag, r_eff=√(S'/π), hull perimeter, compactness, PCA axes/aspect, LR & per-field max-LR) + a run-manifest (params/seed/git-hash/wall-clock). Budget vs 60 h baseline.
- ★ Compute the Kulldorff LR and per-field max-LR; calibrate detection p-values by MC-replay over (eps, windows). This dissolves the eps-weighting puzzle for detection.
- (after data) Edge-effect split by dist-to-boundary; refit. Compare hull vs miniball vs α-shape vs best-circular-aperture for
S'(each = a different null, must be replayed). - (optional, heavy) Bayesian latent-configuration model (route C) as the principled capstone.
- Rarefactions: separate object, separate localiser (grid/KDE deficit, OPTICS reachability), separate null — out of current scope.
Tried & don't relitigate: 2-comp Beta-Prime mixture (degenerate) · N'-mode mixture (superseded) · Exponential/Weibull (ruled out) · Gaussian Z / normality (rejected) · eps-conditioned global regression (models P(R|eps), not the marginal) · "max ratio 60–70 as physical bound" (it's the min_area artifact).
uv sync
uv run cluster-distribution simulate # regenerate simdata/v2 — ~60 h on this machine (avoid; add geometry first)
uv run cluster-distribution fit # per-eps Beta-Prime → results/regular_fit.csv
uv run cluster-distribution stats # normality/Poisson summary
uv run cluster-distribution plot # overlay saved fits
uv run python scripts/stats/anova.py # (and scripts/stats/stat_test2|3.py, scripts/fit/floc.py, scripts/viz/*.py) run directly
# clone w/o 4.3GB: GIT_LFS_SKIP_SMUDGE=1 git clone <url>
# push code w/o LFS upload: GIT_LFS_SKIP_PUSH=1 git push -u origin HEADStack: numpy, pandas, scipy, scikit-learn (DBSCAN), matplotlib, seaborn, statsmodels, typer. Python ≥3.11.
Repo born 2024, hand-edited from o3 output (pre-agentic-tooling). Author: Newton Winter (winternewt / isoutthere@gmail.com / nikolay.usanov@uni-rostock.de). The four handoff docs are AI-assisted summaries of long o3/GPT + Claude dialogues; clusters_problem.md=cluster-detection-handoff.md were intended duplicates but the latter was re-salvaged with richer content May 2026. When in doubt about a "fact" from a handoff, check it against simdata//results/ or the code — the handoffs occasionally state superseded hypotheses as if current (e.g. the 60–70 tail, the N'-mode mixture).