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.
- geoexp-0.1.0/.gitignore +56 -0
- geoexp-0.1.0/CHANGELOG.md +63 -0
- geoexp-0.1.0/CLAUDE.md +124 -0
- geoexp-0.1.0/LICENSE +21 -0
- geoexp-0.1.0/PKG-INFO +181 -0
- geoexp-0.1.0/README.md +127 -0
- geoexp-0.1.0/docs/design-2026-09-05-v0.5-api.md +361 -0
- geoexp-0.1.0/docs/geoexp-bootstrap-spec.md +220 -0
- geoexp-0.1.0/docs/geolift-oracle-characterization.md +365 -0
- geoexp-0.1.0/docs/methodology.md +158 -0
- geoexp-0.1.0/docs/releasing.md +55 -0
- geoexp-0.1.0/pyproject.toml +167 -0
- geoexp-0.1.0/src/geoexp/__init__.py +19 -0
- geoexp-0.1.0/src/geoexp/_version.py +3 -0
- geoexp-0.1.0/src/geoexp/exceptions.py +12 -0
- geoexp-0.1.0/src/geoexp/py.typed +0 -0
- geoexp-0.1.0/src/geoexp/selection.py +650 -0
- geoexp-0.1.0/tests/conftest.py +6 -0
- geoexp-0.1.0/tests/unit/test_package.py +37 -0
- geoexp-0.1.0/tests/unit/test_selection.py +430 -0
- geoexp-0.1.0/tests/validation_against_r/__init__.py +0 -0
- geoexp-0.1.0/tests/validation_against_r/_r_frames.py +25 -0
- geoexp-0.1.0/tests/validation_against_r/conftest.py +121 -0
- geoexp-0.1.0/tests/validation_against_r/test_market_selection.py +145 -0
geoexp-0.1.0/.gitignore
ADDED
|
@@ -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.
|