geoexp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+
7
+ # Distribution / packaging
8
+ build/
9
+ dist/
10
+ wheels/
11
+ *.egg-info/
12
+ *.egg
13
+
14
+ # Virtual environments
15
+ .env
16
+ .venv
17
+ env/
18
+ venv/
19
+ ENV/
20
+
21
+ # Testing / coverage
22
+ .pytest_cache/
23
+ .coverage
24
+ .coverage.*
25
+ htmlcov/
26
+ coverage.xml
27
+ .hypothesis/
28
+
29
+ # Type checking / lint caches
30
+ .mypy_cache/
31
+ .ruff_cache/
32
+
33
+ # IDEs
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ *.swo
38
+ *~
39
+
40
+ # OS
41
+ .DS_Store
42
+ Thumbs.db
43
+
44
+ # R artifacts (validation runs)
45
+ .Rhistory
46
+ .RData
47
+ .Ruserdata
48
+
49
+ # Claude Code (local agent state, settings, scratch plans)
50
+ .claude/
51
+ .claudeignore
52
+ CLAUDE.local.md
53
+ docs/plans/
54
+
55
+ # Git worktrees (local isolated workspaces)
56
+ .worktrees/
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
+ (with the usual 0.x caveat: minor versions may break the API).
8
+
9
+ ## [Unreleased]
10
+
11
+ Nothing yet.
12
+
13
+ ## [0.1.0] - 2026-09-20
14
+
15
+ ### Added
16
+
17
+ - **Public API for single-cell market selection** (project milestone v0.5),
18
+ per `docs/design-2026-09-05-v0.5-api.md`: `rank_designs()` ranks explicit
19
+ candidate treated sets x durations by what each design can detect, and
20
+ `DesignRanking` carries the ranking frame plus the underlying
21
+ `augsynth_py.PowerResults`. Consumes augsynth-py through its public API only,
22
+ with one child generator per candidate via `rng.spawn` and candidate-level
23
+ parallelism.
24
+ - `mde` interpolated off the power curve, with the upstream grid point kept
25
+ alongside as `mde_grid`. The grid value takes only four distinct values
26
+ across the 39 fixture candidates, which makes it useless as a sort key; see
27
+ `docs/methodology.md`.
28
+ - `investment` column when `cpic` is supplied — exactly parity-testable.
29
+ - `enumerate_candidates()`, pulled forward from 0.2.x: ranks units by
30
+ correlation with the rest of the panel and draws capped combinations of the
31
+ requested sizes, with `include`/`exclude` and optional reproducible random
32
+ sampling. A separate function, not a `rank_designs` parameter, so bringing
33
+ your own candidates stays the primary path.
34
+ - `null_bias` and `h1_calibration_error` diagnostics, adapted from
35
+ mrcsvg/augsynth-py#22. `null_bias` is the honest analog of GeoLift's
36
+ `abs_lift_in_zero`; `h1_calibration_error` catches a design that detects at
37
+ the wrong magnitude. The two overlap on well-behaved panels — see
38
+ `docs/methodology.md`.
39
+ - Parity suite against R `GeoLiftMarketSelection` on the power curve at 240
40
+ cells, with self-calibrating tolerances derived from the measured noise
41
+ floor rather than from a single draw.
42
+ - `docs/geolift-oracle-characterization.md`: measured input/output surface of
43
+ the R oracle, which of its columns cannot be reproduced under the clean-room
44
+ rule, and the rejected cross-package benchmark experiment.
45
+
46
+ ### Changed
47
+
48
+ - `CLAUDE.md` validation rule now requires parity only for features with a
49
+ reproducible oracle counterpart; features without one must be listed with a
50
+ reason and covered by unit tests plus a methodology entry.
51
+ - The `validation` extra no longer pulls `pandas` or `pyarrow`: the R bridge
52
+ converts to Polars directly (adapted from mrcsvg/augsynth-py#22).
53
+
54
+ ### Added (skeleton)
55
+
56
+ - Repository skeleton per `docs/geoexp-bootstrap-spec.md`: src layout with
57
+ `py.typed`, tooling ported from augsynth-py (ruff, mypy strict, pytest),
58
+ CI workflows (lint/type/unit + min-deps, GeoLift validation, tag-triggered
59
+ PyPI trusted publishing), and placeholder `selection` module. No public
60
+ API yet — that lands with the v0.5 design doc.
61
+
62
+ [Unreleased]: https://github.com/mrcsvg/geoexp/compare/v0.1.0...HEAD
63
+ [0.1.0]: https://github.com/mrcsvg/geoexp/releases/tag/v0.1.0
geoexp-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,124 @@
1
+ # geoexp
2
+
3
+ Geo-experiment design on top of `augsynth-py`: market selection, power-based
4
+ design ranking, and experiment reporting. This file is the entry point for AI
5
+ coding assistants working on this repo. Read it before starting any task.
6
+
7
+ ---
8
+
9
+ ## Project goal
10
+
11
+ Answer *which design should I run?* for single-cell (and later multi-cell)
12
+ geo-experiments: rank candidate treated sets × experiment durations by what
13
+ each design can detect, with GeoLift-shaped reporting.
14
+
15
+ The **authoritative statement of the package boundary** lives in the
16
+ package-boundary section of
17
+ [augsynth-py's CLAUDE.md](https://github.com/mrcsvg/augsynth-py/blob/main/CLAUDE.md):
18
+ *"given a design"* (augsynth-py: estimators, inference, power/MDE) vs.
19
+ *"choose a design"* (geoexp: market selection, budget/ROI, multi-cell,
20
+ reporting). Do not add estimators, inference, or power simulation here.
21
+
22
+ ## Consumption rule (the one rule augsynth-py doesn't need)
23
+
24
+ geoexp consumes augsynth-py **through its public API only**:
25
+
26
+ - Never import an underscore-prefixed name from `augsynth_py`.
27
+ - If market selection appears to need something the public API cannot
28
+ express, the fix is a proposal against augsynth-py's API — never a
29
+ private-attribute reach-in, never a fork of the logic.
30
+ - Contract patterns (documented in `augsynth_py.simulate_power`'s Notes):
31
+ one child generator per candidate via `rng.spawn(n)`; parallelize across
32
+ candidates with `n_jobs=1` passed down (joblib does not nest workers);
33
+ recommend `AugSynth(lambda_=...)` in docs for large grids.
34
+
35
+ ## Implementation strategy: clean-room from published material
36
+
37
+ Implemented from published material (Abadie 2021 §5 in-time placebos; the
38
+ GeoLift methodology publications; CWZ 2021 via augsynth-py). The R `GeoLift`
39
+ package is a **parity oracle only**, driven via `rpy2` in
40
+ `tests/validation_against_r/` — its source is never translated and never
41
+ copied into this repo. MIT license; no association with Meta's GeoLift is
42
+ implied (the working name `geolift-py` was dropped for exactly that reason).
43
+
44
+ ## Validation rule (non-negotiable)
45
+
46
+ Every public ranking/selection feature **that has a reproducible
47
+ `GeoLiftMarketSelection` counterpart** must have at least one test in
48
+ `tests/validation_against_r/` that runs the same input through this package
49
+ and through R `GeoLiftMarketSelection` via rpy2, asserting numerical
50
+ agreement within a documented tolerance (default `atol=1e-6, rtol=1e-5`;
51
+ relax with justification only), on the `GeoLift_PreTest` fixture.
52
+
53
+ A feature with no reproducible counterpart is **not exempt by default**: it
54
+ must be listed in `docs/geolift-oracle-characterization.md` §6 with the
55
+ reason it cannot be compared, and covered by unit tests plus a
56
+ `docs/methodology.md` entry stating what it is and what it is not. Adding an
57
+ entry to that list is a maintainer decision, not a contributor's.
58
+
59
+ PRs that add a public feature without either a parity test or a recorded
60
+ exclusion should be rejected.
61
+
62
+ ## Version map
63
+
64
+ | Project milestone | geoexp release | Content |
65
+ |---|---|---|
66
+ | v0.5 | 0.1.x | Single-cell market selection, candidate enumeration, cpic |
67
+ | v0.5+ | 0.2.x | Budget-constrained selection |
68
+ | v0.6 | 0.3.x+ | Multi-cell designs |
69
+
70
+ augsynth-py's own releases number independently; geoexp's dependency floor
71
+ is `augsynth-py>=0.5.0` (the release that froze the power-API contract —
72
+ see `docs/power-api-contract-review.md` in that repo).
73
+
74
+ ## Repository layout
75
+
76
+ ```
77
+ geoexp/
78
+ ├── CLAUDE.md <- you are here
79
+ ├── README.md
80
+ ├── CHANGELOG.md <- Keep a Changelog format
81
+ ├── LICENSE <- MIT
82
+ ├── pyproject.toml <- hatchling, PEP 621, src layout
83
+ ├── src/geoexp/
84
+ │ ├── __init__.py <- public exports
85
+ │ ├── _version.py <- single source of truth for the version
86
+ │ ├── py.typed <- PEP 561 marker
87
+ │ ├── exceptions.py <- domain exceptions
88
+ │ └── selection.py <- v0.5 module (split into a package
89
+ │ only when a second module earns it)
90
+ ├── tests/
91
+ │ ├── conftest.py
92
+ │ ├── unit/ <- no R required
93
+ │ └── validation_against_r/ <- GeoLift parity via rpy2
94
+ ├── docs/
95
+ │ ├── methodology.md <- maps code to Abadie 2021 §5 / GeoLift docs
96
+ │ ├── releasing.md <- PyPI release runbook
97
+ │ └── geoexp-bootstrap-spec.md <- the bootstrap spec (relocated here)
98
+ └── .github/workflows/
99
+ ├── ci.yml <- lint, mypy, unit tests, min-deps job
100
+ ├── validation.yml <- R + GeoLift parity
101
+ └── release.yml <- tag-triggered PyPI trusted publishing
102
+ ```
103
+
104
+ ## Code and testing conventions
105
+
106
+ Identical to augsynth-py's — type hints everywhere in `src/` with
107
+ `mypy --strict`; NumPy docstrings citing paper + section for known methods;
108
+ no `print()` in library code; no global mutable state; every stochastic
109
+ function takes `rng: np.random.Generator`; `ValueError` for bad input and
110
+ `geoexp.exceptions` subclasses for domain errors; `tests/unit/` runs without
111
+ R (`pytest.importorskip("rpy2")` guards the validation suite); `ruff` for
112
+ lint + format. When in doubt, match augsynth-py.
113
+
114
+ ## Out of scope (inherited wholesale from augsynth-py)
115
+
116
+ Estimators/inference/power (import them), Bayesian variants, GPU
117
+ acceleration, Rust/C extensions.
118
+
119
+ ## Things to confirm with the maintainer before doing
120
+
121
+ - Adding a dependency.
122
+ - Changing the public API of anything exported from `__init__.py`.
123
+ - Relaxing a validation tolerance.
124
+ - Starting multi-cell work (project v0.6).
geoexp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 geoexp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
geoexp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.5
2
+ Name: geoexp
3
+ Version: 0.1.0
4
+ Summary: Geo-experiment design on top of augsynth-py: market selection, power-based design ranking, and experiment reporting.
5
+ Project-URL: Repository, https://github.com/mrcsvg/geoexp
6
+ Project-URL: Issues, https://github.com/mrcsvg/geoexp/issues
7
+ Project-URL: Changelog, https://github.com/mrcsvg/geoexp/blob/main/CHANGELOG.md
8
+ Author: geoexp contributors
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 geoexp contributors
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: causal-inference,geo-experiments,market-selection,marketing-science,synthetic-control
32
+ Classifier: Development Status :: 2 - Pre-Alpha
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
39
+ Classifier: Typing :: Typed
40
+ Requires-Python: >=3.11
41
+ Requires-Dist: augsynth-py>=0.5.0
42
+ Requires-Dist: joblib>=1.3
43
+ Requires-Dist: numpy>=1.26
44
+ Requires-Dist: polars>=1.0
45
+ Provides-Extra: dev
46
+ Requires-Dist: mypy>=1.10; extra == 'dev'
47
+ Requires-Dist: pre-commit>=3.7; extra == 'dev'
48
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
49
+ Requires-Dist: pytest>=8.0; extra == 'dev'
50
+ Requires-Dist: ruff>=0.5; extra == 'dev'
51
+ Provides-Extra: validation
52
+ Requires-Dist: rpy2>=3.5; extra == 'validation'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # geoexp
56
+
57
+ Geo-experiment **design** on top of
58
+ [augsynth-py](https://github.com/mrcsvg/augsynth-py): market selection,
59
+ power-based design ranking, and experiment reporting.
60
+
61
+ The boundary between the two packages is *"given a design"* vs. *"choose a
62
+ design"*:
63
+
64
+ - **augsynth-py** answers *what can this design detect?* — synthetic-control
65
+ estimators, conformal inference, and power/MDE for a treated set the caller
66
+ already fixed.
67
+ - **geoexp** (this package) answers *which design should I run?* — ranking
68
+ candidate treated sets x durations by what each can detect, with
69
+ GeoLift-shaped reporting. It consumes augsynth-py through its public API
70
+ only.
71
+
72
+ ## Quickstart
73
+
74
+ ```python
75
+ import numpy as np
76
+ import polars as pl
77
+ from augsynth_py import AugSynth
78
+
79
+ from geoexp import enumerate_candidates, rank_designs
80
+
81
+ # Build candidates, or pass your own explicit sets.
82
+ candidates = enumerate_candidates(
83
+ panel,
84
+ unit="location",
85
+ time="time",
86
+ outcome="Y",
87
+ sizes=[1, 2],
88
+ max_per_size=15,
89
+ )
90
+
91
+ res = rank_designs(
92
+ panel, # long format: unit, time, outcome
93
+ estimator=AugSynth(lambda_=1.0), # freeze the penalty for large grids
94
+ unit="location",
95
+ time="time",
96
+ outcome="Y",
97
+ candidates=candidates,
98
+ durations=[15, 30],
99
+ lookback_window=10,
100
+ permutation_type="iid",
101
+ rng=np.random.default_rng(7),
102
+ n_jobs=-1, # parallel across candidates
103
+ )
104
+
105
+ res.ranking # one row per candidate x duration, best first
106
+ res.power("chicago, portland", 15) # the underlying augsynth_py.PowerResults
107
+ ```
108
+
109
+ `res.ranking` carries `candidate`, `duration`, `n_units`, `mde`, `mde_grid`,
110
+ `power_at_target`, `empirical_size`, `rmspe_pre`, `null_bias`,
111
+ `h1_calibration_error`, `proportion_total_y`, `holdout`, `rank`, and
112
+ `investment` when `cpic` is given.
113
+
114
+ Two things worth knowing before you read the output:
115
+
116
+ - **`mde` is interpolated off the power curve**, not a grid point, because the
117
+ grid value leaves most candidates tied. It is a geoexp construct and is not
118
+ R GeoLift's `Average_MDE`. `mde_grid` carries the upstream grid point.
119
+ - **Rank order by `mde` is stable within an implementation, not across them.**
120
+ Small differences near ties carry no information. See
121
+ [`docs/methodology.md`](docs/methodology.md).
122
+ - **`null_bias` and `h1_calibration_error` are near-duplicates** on
123
+ well-behaved panels; they separate only when the estimator's bias depends on
124
+ the effect size.
125
+
126
+ ## Scope
127
+
128
+ 0.1.x is single-cell market selection: candidate enumeration by similarity,
129
+ power-based ranking, and cost per incremental conversion. Budget-constrained
130
+ selection is 0.2.x; multi-cell designs are 0.3.x+.
131
+
132
+ `enumerate_candidates` is a convenience, not a gate — `rank_designs` takes any
133
+ list of sets, so bring your own candidates if you prefer.
134
+
135
+ ## Validation
136
+
137
+ Every feature with a reproducible R `GeoLiftMarketSelection` counterpart is
138
+ asserted against it via rpy2 on the `GeoLift_PreTest` fixture. Parity is
139
+ established on the power curve at 240 cells (40 candidates x 6 effect sizes):
140
+ the two implementations agree with each other as well as either agrees with
141
+ itself, with no systematic bias. Features with no reproducible counterpart are
142
+ listed with their reason in
143
+ [`docs/geolift-oracle-characterization.md`](docs/geolift-oracle-characterization.md).
144
+
145
+ Clean-room: the R `GeoLift` package is a numerical oracle only, driven via
146
+ rpy2. Its source is never read for implementation and never translated.
147
+
148
+ ## Development and AI use
149
+
150
+ This package was written by its maintainer working with
151
+ [Claude Code](https://claude.com/claude-code), Anthropic's coding agent. That
152
+ is disclosed here because it should shape how you read the rest of this page,
153
+ not as a novelty.
154
+
155
+ The working rule throughout was that nothing gets asserted that was not
156
+ measured. The parity figures above come from real runs against the R oracle and
157
+ are reproducible from `tests/validation_against_r/`; the noise floor they are
158
+ judged against was measured rather than assumed, after fixed thresholds
159
+ calibrated on a single draw turned out to flake. Where a quantity could not be
160
+ validated — GeoLift's `Average_MDE` and most of its ranking columns — that is
161
+ recorded as a limitation in
162
+ [`docs/geolift-oracle-characterization.md`](docs/geolift-oracle-characterization.md)
163
+ rather than papered over, and a cross-package benchmark that did not work is
164
+ written up there as a negative result instead of being quietly dropped. The
165
+ public API was designed and signed off before implementation, the
166
+ implementation was written test-first, and the clean-room rule stated above
167
+ held throughout: the R GeoLift source was never read.
168
+
169
+ ## Install
170
+
171
+ ```bash
172
+ pip install geoexp
173
+ ```
174
+
175
+ Requires `augsynth-py>=0.5.0` (the release that froze the power-API contract
176
+ this package is written against).
177
+
178
+ ## License
179
+
180
+ MIT. No association with Meta's GeoLift is implied; the R package is used
181
+ strictly as a numerical validation oracle.
geoexp-0.1.0/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # geoexp
2
+
3
+ Geo-experiment **design** on top of
4
+ [augsynth-py](https://github.com/mrcsvg/augsynth-py): market selection,
5
+ power-based design ranking, and experiment reporting.
6
+
7
+ The boundary between the two packages is *"given a design"* vs. *"choose a
8
+ design"*:
9
+
10
+ - **augsynth-py** answers *what can this design detect?* — synthetic-control
11
+ estimators, conformal inference, and power/MDE for a treated set the caller
12
+ already fixed.
13
+ - **geoexp** (this package) answers *which design should I run?* — ranking
14
+ candidate treated sets x durations by what each can detect, with
15
+ GeoLift-shaped reporting. It consumes augsynth-py through its public API
16
+ only.
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ import numpy as np
22
+ import polars as pl
23
+ from augsynth_py import AugSynth
24
+
25
+ from geoexp import enumerate_candidates, rank_designs
26
+
27
+ # Build candidates, or pass your own explicit sets.
28
+ candidates = enumerate_candidates(
29
+ panel,
30
+ unit="location",
31
+ time="time",
32
+ outcome="Y",
33
+ sizes=[1, 2],
34
+ max_per_size=15,
35
+ )
36
+
37
+ res = rank_designs(
38
+ panel, # long format: unit, time, outcome
39
+ estimator=AugSynth(lambda_=1.0), # freeze the penalty for large grids
40
+ unit="location",
41
+ time="time",
42
+ outcome="Y",
43
+ candidates=candidates,
44
+ durations=[15, 30],
45
+ lookback_window=10,
46
+ permutation_type="iid",
47
+ rng=np.random.default_rng(7),
48
+ n_jobs=-1, # parallel across candidates
49
+ )
50
+
51
+ res.ranking # one row per candidate x duration, best first
52
+ res.power("chicago, portland", 15) # the underlying augsynth_py.PowerResults
53
+ ```
54
+
55
+ `res.ranking` carries `candidate`, `duration`, `n_units`, `mde`, `mde_grid`,
56
+ `power_at_target`, `empirical_size`, `rmspe_pre`, `null_bias`,
57
+ `h1_calibration_error`, `proportion_total_y`, `holdout`, `rank`, and
58
+ `investment` when `cpic` is given.
59
+
60
+ Two things worth knowing before you read the output:
61
+
62
+ - **`mde` is interpolated off the power curve**, not a grid point, because the
63
+ grid value leaves most candidates tied. It is a geoexp construct and is not
64
+ R GeoLift's `Average_MDE`. `mde_grid` carries the upstream grid point.
65
+ - **Rank order by `mde` is stable within an implementation, not across them.**
66
+ Small differences near ties carry no information. See
67
+ [`docs/methodology.md`](docs/methodology.md).
68
+ - **`null_bias` and `h1_calibration_error` are near-duplicates** on
69
+ well-behaved panels; they separate only when the estimator's bias depends on
70
+ the effect size.
71
+
72
+ ## Scope
73
+
74
+ 0.1.x is single-cell market selection: candidate enumeration by similarity,
75
+ power-based ranking, and cost per incremental conversion. Budget-constrained
76
+ selection is 0.2.x; multi-cell designs are 0.3.x+.
77
+
78
+ `enumerate_candidates` is a convenience, not a gate — `rank_designs` takes any
79
+ list of sets, so bring your own candidates if you prefer.
80
+
81
+ ## Validation
82
+
83
+ Every feature with a reproducible R `GeoLiftMarketSelection` counterpart is
84
+ asserted against it via rpy2 on the `GeoLift_PreTest` fixture. Parity is
85
+ established on the power curve at 240 cells (40 candidates x 6 effect sizes):
86
+ the two implementations agree with each other as well as either agrees with
87
+ itself, with no systematic bias. Features with no reproducible counterpart are
88
+ listed with their reason in
89
+ [`docs/geolift-oracle-characterization.md`](docs/geolift-oracle-characterization.md).
90
+
91
+ Clean-room: the R `GeoLift` package is a numerical oracle only, driven via
92
+ rpy2. Its source is never read for implementation and never translated.
93
+
94
+ ## Development and AI use
95
+
96
+ This package was written by its maintainer working with
97
+ [Claude Code](https://claude.com/claude-code), Anthropic's coding agent. That
98
+ is disclosed here because it should shape how you read the rest of this page,
99
+ not as a novelty.
100
+
101
+ The working rule throughout was that nothing gets asserted that was not
102
+ measured. The parity figures above come from real runs against the R oracle and
103
+ are reproducible from `tests/validation_against_r/`; the noise floor they are
104
+ judged against was measured rather than assumed, after fixed thresholds
105
+ calibrated on a single draw turned out to flake. Where a quantity could not be
106
+ validated — GeoLift's `Average_MDE` and most of its ranking columns — that is
107
+ recorded as a limitation in
108
+ [`docs/geolift-oracle-characterization.md`](docs/geolift-oracle-characterization.md)
109
+ rather than papered over, and a cross-package benchmark that did not work is
110
+ written up there as a negative result instead of being quietly dropped. The
111
+ public API was designed and signed off before implementation, the
112
+ implementation was written test-first, and the clean-room rule stated above
113
+ held throughout: the R GeoLift source was never read.
114
+
115
+ ## Install
116
+
117
+ ```bash
118
+ pip install geoexp
119
+ ```
120
+
121
+ Requires `augsynth-py>=0.5.0` (the release that froze the power-API contract
122
+ this package is written against).
123
+
124
+ ## License
125
+
126
+ MIT. No association with Meta's GeoLift is implied; the R package is used
127
+ strictly as a numerical validation oracle.