bayesian-pv-census 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.
- bayesian_pv_census-0.1.0/.gitignore +12 -0
- bayesian_pv_census-0.1.0/CITATION.cff +28 -0
- bayesian_pv_census-0.1.0/LICENSE +21 -0
- bayesian_pv_census-0.1.0/PKG-INFO +233 -0
- bayesian_pv_census-0.1.0/README.md +207 -0
- bayesian_pv_census-0.1.0/SPEC.md +298 -0
- bayesian_pv_census-0.1.0/pyproject.toml +56 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/__init__.py +50 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/aggregation.py +168 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/audit.py +230 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/budgeting.py +93 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/correction.py +276 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/data/france_2024.csv +94 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/datatypes.py +159 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/figures.py +108 -0
- bayesian_pv_census-0.1.0/src/bayesian_pv_census/io.py +163 -0
- bayesian_pv_census-0.1.0/tests/test_bayesian_pv_census.py +411 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.egg-info/
|
|
3
|
+
dist/
|
|
4
|
+
build/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
|
|
9
|
+
# Audited reference values from the French transmission system operator.
|
|
10
|
+
# Redistribution is pending their clearance; see issue #1. Remove this entry,
|
|
11
|
+
# drop the file back in, and bump the minor version once it is granted.
|
|
12
|
+
src/bayesian_pv_census/data/france_2024_reported.csv
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
title: "bayesian-pv-census: turn a detector's output into a census, then audit a register against it"
|
|
3
|
+
message: "If you use this software, please cite both the software and the paper it implements."
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Kasmi
|
|
7
|
+
given-names: Gabriel
|
|
8
|
+
email: gabkasmi@gmail.com
|
|
9
|
+
orcid: "https://orcid.org/0000-0002-7774-4302"
|
|
10
|
+
affiliation: "Mines Paris-PSL"
|
|
11
|
+
version: 0.1.0
|
|
12
|
+
license: MIT
|
|
13
|
+
repository-code: "https://github.com/gabrielkasmi/bayesian-pv-census"
|
|
14
|
+
abstract: >-
|
|
15
|
+
Reference implementation of the statistical core of a registry-audit protocol:
|
|
16
|
+
Beta posteriors on detector precision and recall, propagation to a corrected
|
|
17
|
+
quantity, a credible-interval decision rule against a reported value, a
|
|
18
|
+
specification battery and its hard core, a closed-form annotation budget, and
|
|
19
|
+
the cost of forming the correction factor at a coarser spatial scale. Ships
|
|
20
|
+
the paper's French dataset, 93 reporting units, as a demo.
|
|
21
|
+
references:
|
|
22
|
+
- type: article
|
|
23
|
+
title: "Auditing photovoltaic registries with remote sensing"
|
|
24
|
+
authors:
|
|
25
|
+
- family-names: Kasmi
|
|
26
|
+
given-names: Gabriel
|
|
27
|
+
journal: Joule
|
|
28
|
+
year: 2026
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Kasmi
|
|
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.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bayesian-pv-census
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn a detector's output into a census with credible intervals, then audit a register against it
|
|
5
|
+
Project-URL: Homepage, https://github.com/gabrielkasmi/bayesian-pv-census
|
|
6
|
+
Project-URL: Paper, https://doi.org/10.5281/zenodo.21534856
|
|
7
|
+
Author-email: Gabriel Kasmi <gabkasmi@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: audit,bayesian,census,measurement error,photovoltaic,registry,remote sensing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: numpy>=1.23
|
|
18
|
+
Requires-Dist: pandas>=1.5
|
|
19
|
+
Requires-Dist: scipy>=1.9
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: matplotlib>=3.6; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
23
|
+
Provides-Extra: figures
|
|
24
|
+
Requires-Dist: matplotlib>=3.6; extra == 'figures'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# bayesian-pv-census
|
|
28
|
+
|
|
29
|
+
**Turn a detector's raw count into a census with credible intervals, then audit a register against it.**
|
|
30
|
+
|
|
31
|
+
A detector that finds objects in imagery misses some and invents others, at rates
|
|
32
|
+
that vary from place to place. Its raw total is therefore not a measurement, and
|
|
33
|
+
comparing it directly to an official register says as much about the detector as
|
|
34
|
+
about the register. This package closes that gap: given a validation sample, it
|
|
35
|
+
turns the raw total into a posterior over the true quantity, and turns that
|
|
36
|
+
posterior into a verdict on whatever the register reports.
|
|
37
|
+
|
|
38
|
+
Nothing here knows about photovoltaics, France, or geometry. A *unit* is anything
|
|
39
|
+
with a raw total and a validation sample — a department, a grid cell, a utility
|
|
40
|
+
service area. `raw` is whatever you chose to count: installed capacity, number of
|
|
41
|
+
installations, roof area.
|
|
42
|
+
|
|
43
|
+
This is the statistical core of *Nationally Consistent, Locally Incomplete: A Bayesian Remote-Sensing Audit of Rooftop Photovoltaic Registries* (Kasmi et al., 2026, pending peer review) , extracted as a
|
|
44
|
+
library. Every public function corresponds to a named component of the paper.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install bayesian-pv-census # numpy, scipy, pandas
|
|
50
|
+
pip install 'bayesian-pv-census[figures]' # adds matplotlib
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## One minute
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from bayesian_pv_census import UnitRecord, correct_unit, evaluate
|
|
57
|
+
|
|
58
|
+
unit = UnitRecord(
|
|
59
|
+
unit_id="dept_86",
|
|
60
|
+
raw=26_800, # what the detector found, any consistent unit
|
|
61
|
+
precision_tp=116, precision_fp=4, # 120 detections checked by hand
|
|
62
|
+
recall_tp=68, recall_fn=33, # 101 real objects located independently
|
|
63
|
+
reported_value=26_820, # what the register reports
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
result = correct_unit(unit)
|
|
67
|
+
print(f"{result.mean:,.0f} 99% CI {result.ci(0.99)}")
|
|
68
|
+
print(evaluate(result, unit.reported_value).status) # below | within | above
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The verdict is decided by the interval, not the gap. A large discrepancy inside a
|
|
72
|
+
wide interval is not a finding; a small one outside a tight interval is.
|
|
73
|
+
|
|
74
|
+
## The paper's data
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from bayesian_pv_census import correct_batch, load_demo
|
|
78
|
+
|
|
79
|
+
units = load_demo() # 93 French reporting units, kWp
|
|
80
|
+
correct_batch(units, prior="empirical_bayes")["corrected_mean"].sum()
|
|
81
|
+
# 4_033_003 kWp, the paper's national estimate
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`load_demo()` returns the 93 continental French reporting units behind the
|
|
85
|
+
published audit: detected rooftop capacity below 36 kWp, the manual validation
|
|
86
|
+
counts (31,853 annotations), and the size-weighted rates of specification B.
|
|
87
|
+
|
|
88
|
+
**One column is not yet included.** The reference values under audit are the
|
|
89
|
+
French transmission system operator's grid-connection registry, and
|
|
90
|
+
redistributing them is not ours to grant. Until that clearance arrives the demo
|
|
91
|
+
supports estimation but not the audit, `load_demo()` says so, and
|
|
92
|
+
`has_reported_values()` reports it:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from bayesian_pv_census import has_reported_values
|
|
96
|
+
has_reported_values() # False in this release
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Nothing is substituted in the meantime. A column of plausible-looking
|
|
100
|
+
placeholders would be indistinguishable from data at a glance, and reproducing a
|
|
101
|
+
published audit against invented references is worse than not reproducing it. The
|
|
102
|
+
four regression tests that need the column are skipped rather than weakened, so
|
|
103
|
+
the skip count in `pytest` is the honest signal. The national estimate above
|
|
104
|
+
needs no reference value and is checked in every release.
|
|
105
|
+
|
|
106
|
+
Once the column ships, the full battery reproduces the paper: 25 units
|
|
107
|
+
under-reported and 8 over-reported under specification A, an 18-unit hard core
|
|
108
|
+
and a 7-unit negative control.
|
|
109
|
+
|
|
110
|
+
Installation counts are deliberately absent for a different reason. The audit in
|
|
111
|
+
the paper is about capacity; shipping a count column would imply a quantity that
|
|
112
|
+
was never audited.
|
|
113
|
+
|
|
114
|
+
## What the correction assumes
|
|
115
|
+
|
|
116
|
+
The estimator is `raw × P/R`. Four assumptions stand behind it, and the package
|
|
117
|
+
can only speak to two of them.
|
|
118
|
+
|
|
119
|
+
| | Assumption | Can the package check it? |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| **H1** | Detection status is independent of object size: true positives, false positives and false negatives have the same mean size. | Partly. The gap between specifications A and B measures the violation, and B does not require H1. |
|
|
122
|
+
| **H2** | The detector is run over the entire unit; no sub-region is excluded. | No. Upstream of anything the package sees. |
|
|
123
|
+
| **H3** | The validation samples are drawn representatively from, respectively, the raw detections and the true population. | No. This is a property of how you annotated, and nothing in the counts reveals it. |
|
|
124
|
+
| **H4** | For a correctly detected object, its estimated size is unbiased for its true size. | Only its sensitivity, via the `scale` key of `run_battery`. |
|
|
125
|
+
|
|
126
|
+
H4 binds only when the quantity is a size. **If you correct a count of objects
|
|
127
|
+
rather than a capacity, H4 drops out entirely** — which is why the field is named
|
|
128
|
+
`raw` and not `raw_capacity`.
|
|
129
|
+
|
|
130
|
+
Two of the four are therefore assumptions you carry, not results the package
|
|
131
|
+
delivers. Reporting a credible interval without saying which of H2 and H3 you
|
|
132
|
+
believe, and why, states less than it appears to.
|
|
133
|
+
|
|
134
|
+
## Before you annotate
|
|
135
|
+
|
|
136
|
+
The interval's half-width has a closed form, so the annotation effort can be
|
|
137
|
+
budgeted in advance rather than discovered afterwards:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from bayesian_pv_census import required_sample_size
|
|
141
|
+
|
|
142
|
+
b = required_sample_size(target_half_width=0.15, expected_precision=0.85,
|
|
143
|
+
expected_recall=0.65, level=0.99)
|
|
144
|
+
print(b.n_precision, b.n_recall) # annotations needed per unit
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Specifications and the hard core
|
|
148
|
+
|
|
149
|
+
A verdict that only holds under one way of computing precision and recall is not
|
|
150
|
+
a finding. `run_battery` runs the audit under several and keeps what survives all
|
|
151
|
+
of them.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from bayesian_pv_census import run_battery
|
|
155
|
+
|
|
156
|
+
battery = run_battery(units, prior="empirical_bayes")
|
|
157
|
+
battery.hard_core("below") # flagged under-reported by every specification
|
|
158
|
+
battery.negative_control() # flagged the other way, unanimously — the control group
|
|
159
|
+
battery.concordance_table() # crosstab; empty off-diagonal corners mean no sign flips
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Specification A counts annotated objects. Specification B weights them by size.
|
|
163
|
+
**B is implemented exactly as in the paper**, which computes the weighted rates as
|
|
164
|
+
point estimates and applies them as a deterministic rescaling of A's posterior —
|
|
165
|
+
so B's interval is A's interval, shifted. That is a known limitation of the
|
|
166
|
+
published method; the package reproduces it rather than improving on it, because
|
|
167
|
+
reproducing the paper is the point. See the docstring of `correct_unit` for what
|
|
168
|
+
a properly weighted posterior would require, and for why the `min_weighted_n`
|
|
169
|
+
fallback matters more than its name suggests.
|
|
170
|
+
|
|
171
|
+
A third axis needs no specification of its own. Rescaling every raw quantity by a
|
|
172
|
+
constant — a different surface-to-power coefficient, a different filtering
|
|
173
|
+
threshold — multiplies the posterior and both its bounds while the reported value
|
|
174
|
+
stays put, so it is passed generically:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
run_battery(units, specs={"A": {}, "B": {"weighted": True},
|
|
178
|
+
"C_low": {"scale": 5.5 / 5.0}})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The status is monotone in that constant and flips once, so the flipping point has
|
|
182
|
+
a closed form and no sweep is needed:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from bayesian_pv_census import conversion_threshold
|
|
186
|
+
conversion_threshold(result, unit.reported_value, "below")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The coefficient itself stays outside the engine. `raw` arrives already converted,
|
|
190
|
+
and a `conversion_coefficient` argument would import photovoltaics into a core
|
|
191
|
+
that knows nothing about it.
|
|
192
|
+
|
|
193
|
+
## What forming the factor at a coarser scale costs
|
|
194
|
+
|
|
195
|
+
Correcting each unit and summing is not the same as pooling the units' rates and
|
|
196
|
+
correcting once. `1/R` is convex, so pooling always yields a smaller factor and a
|
|
197
|
+
smaller total. The cost is exactly zero when recall is homogeneous across the
|
|
198
|
+
pooled units, whatever the dispersion of precision, and second order in the
|
|
199
|
+
coefficient of variation of recall otherwise:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from bayesian_pv_census import compare_aggregation_scales, pooling_penalty
|
|
203
|
+
|
|
204
|
+
compare_aggregation_scales(units, groups={"north": [...], "south": [...]}).totals
|
|
205
|
+
pooling_penalty(units) # CV(R)^2 - rho * CV(P) * CV(R), and its two terms
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The practical consequence is that there is no optimal grid to search for. Forming
|
|
209
|
+
the factor at the finest level your annotation budget supports is weakly better
|
|
210
|
+
in every case.
|
|
211
|
+
|
|
212
|
+
## Scope
|
|
213
|
+
|
|
214
|
+
Out of scope by design, and unlikely to change: geospatial sampling, annotation
|
|
215
|
+
tooling, temporal alignment between imagery and register, and detector-specific
|
|
216
|
+
parsers. Those are format- and project-specific; this package starts once you can
|
|
217
|
+
write down a raw total and a validation sample.
|
|
218
|
+
|
|
219
|
+
## Tests
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
pip install -e '.[dev]' && pytest
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The suite validates the mathematics on synthetic data — the closed form against
|
|
226
|
+
the bootstrap, the monotonicity of the budgeting rule, the structural invariants
|
|
227
|
+
of the hard core, the exact vanishing of the pooling penalty under homogeneous
|
|
228
|
+
recall — and then checks the published French numbers against the shipped demo
|
|
229
|
+
data, so a regression in the engine cannot pass silently.
|
|
230
|
+
|
|
231
|
+
## Citation
|
|
232
|
+
|
|
233
|
+
See `CITATION.cff`. Licence MIT.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# bayesian-pv-census
|
|
2
|
+
|
|
3
|
+
**Turn a detector's raw count into a census with credible intervals, then audit a register against it.**
|
|
4
|
+
|
|
5
|
+
A detector that finds objects in imagery misses some and invents others, at rates
|
|
6
|
+
that vary from place to place. Its raw total is therefore not a measurement, and
|
|
7
|
+
comparing it directly to an official register says as much about the detector as
|
|
8
|
+
about the register. This package closes that gap: given a validation sample, it
|
|
9
|
+
turns the raw total into a posterior over the true quantity, and turns that
|
|
10
|
+
posterior into a verdict on whatever the register reports.
|
|
11
|
+
|
|
12
|
+
Nothing here knows about photovoltaics, France, or geometry. A *unit* is anything
|
|
13
|
+
with a raw total and a validation sample — a department, a grid cell, a utility
|
|
14
|
+
service area. `raw` is whatever you chose to count: installed capacity, number of
|
|
15
|
+
installations, roof area.
|
|
16
|
+
|
|
17
|
+
This is the statistical core of *Nationally Consistent, Locally Incomplete: A Bayesian Remote-Sensing Audit of Rooftop Photovoltaic Registries* (Kasmi et al., 2026, pending peer review) , extracted as a
|
|
18
|
+
library. Every public function corresponds to a named component of the paper.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install bayesian-pv-census # numpy, scipy, pandas
|
|
24
|
+
pip install 'bayesian-pv-census[figures]' # adds matplotlib
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## One minute
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from bayesian_pv_census import UnitRecord, correct_unit, evaluate
|
|
31
|
+
|
|
32
|
+
unit = UnitRecord(
|
|
33
|
+
unit_id="dept_86",
|
|
34
|
+
raw=26_800, # what the detector found, any consistent unit
|
|
35
|
+
precision_tp=116, precision_fp=4, # 120 detections checked by hand
|
|
36
|
+
recall_tp=68, recall_fn=33, # 101 real objects located independently
|
|
37
|
+
reported_value=26_820, # what the register reports
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
result = correct_unit(unit)
|
|
41
|
+
print(f"{result.mean:,.0f} 99% CI {result.ci(0.99)}")
|
|
42
|
+
print(evaluate(result, unit.reported_value).status) # below | within | above
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The verdict is decided by the interval, not the gap. A large discrepancy inside a
|
|
46
|
+
wide interval is not a finding; a small one outside a tight interval is.
|
|
47
|
+
|
|
48
|
+
## The paper's data
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from bayesian_pv_census import correct_batch, load_demo
|
|
52
|
+
|
|
53
|
+
units = load_demo() # 93 French reporting units, kWp
|
|
54
|
+
correct_batch(units, prior="empirical_bayes")["corrected_mean"].sum()
|
|
55
|
+
# 4_033_003 kWp, the paper's national estimate
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`load_demo()` returns the 93 continental French reporting units behind the
|
|
59
|
+
published audit: detected rooftop capacity below 36 kWp, the manual validation
|
|
60
|
+
counts (31,853 annotations), and the size-weighted rates of specification B.
|
|
61
|
+
|
|
62
|
+
**One column is not yet included.** The reference values under audit are the
|
|
63
|
+
French transmission system operator's grid-connection registry, and
|
|
64
|
+
redistributing them is not ours to grant. Until that clearance arrives the demo
|
|
65
|
+
supports estimation but not the audit, `load_demo()` says so, and
|
|
66
|
+
`has_reported_values()` reports it:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from bayesian_pv_census import has_reported_values
|
|
70
|
+
has_reported_values() # False in this release
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Nothing is substituted in the meantime. A column of plausible-looking
|
|
74
|
+
placeholders would be indistinguishable from data at a glance, and reproducing a
|
|
75
|
+
published audit against invented references is worse than not reproducing it. The
|
|
76
|
+
four regression tests that need the column are skipped rather than weakened, so
|
|
77
|
+
the skip count in `pytest` is the honest signal. The national estimate above
|
|
78
|
+
needs no reference value and is checked in every release.
|
|
79
|
+
|
|
80
|
+
Once the column ships, the full battery reproduces the paper: 25 units
|
|
81
|
+
under-reported and 8 over-reported under specification A, an 18-unit hard core
|
|
82
|
+
and a 7-unit negative control.
|
|
83
|
+
|
|
84
|
+
Installation counts are deliberately absent for a different reason. The audit in
|
|
85
|
+
the paper is about capacity; shipping a count column would imply a quantity that
|
|
86
|
+
was never audited.
|
|
87
|
+
|
|
88
|
+
## What the correction assumes
|
|
89
|
+
|
|
90
|
+
The estimator is `raw × P/R`. Four assumptions stand behind it, and the package
|
|
91
|
+
can only speak to two of them.
|
|
92
|
+
|
|
93
|
+
| | Assumption | Can the package check it? |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| **H1** | Detection status is independent of object size: true positives, false positives and false negatives have the same mean size. | Partly. The gap between specifications A and B measures the violation, and B does not require H1. |
|
|
96
|
+
| **H2** | The detector is run over the entire unit; no sub-region is excluded. | No. Upstream of anything the package sees. |
|
|
97
|
+
| **H3** | The validation samples are drawn representatively from, respectively, the raw detections and the true population. | No. This is a property of how you annotated, and nothing in the counts reveals it. |
|
|
98
|
+
| **H4** | For a correctly detected object, its estimated size is unbiased for its true size. | Only its sensitivity, via the `scale` key of `run_battery`. |
|
|
99
|
+
|
|
100
|
+
H4 binds only when the quantity is a size. **If you correct a count of objects
|
|
101
|
+
rather than a capacity, H4 drops out entirely** — which is why the field is named
|
|
102
|
+
`raw` and not `raw_capacity`.
|
|
103
|
+
|
|
104
|
+
Two of the four are therefore assumptions you carry, not results the package
|
|
105
|
+
delivers. Reporting a credible interval without saying which of H2 and H3 you
|
|
106
|
+
believe, and why, states less than it appears to.
|
|
107
|
+
|
|
108
|
+
## Before you annotate
|
|
109
|
+
|
|
110
|
+
The interval's half-width has a closed form, so the annotation effort can be
|
|
111
|
+
budgeted in advance rather than discovered afterwards:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from bayesian_pv_census import required_sample_size
|
|
115
|
+
|
|
116
|
+
b = required_sample_size(target_half_width=0.15, expected_precision=0.85,
|
|
117
|
+
expected_recall=0.65, level=0.99)
|
|
118
|
+
print(b.n_precision, b.n_recall) # annotations needed per unit
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Specifications and the hard core
|
|
122
|
+
|
|
123
|
+
A verdict that only holds under one way of computing precision and recall is not
|
|
124
|
+
a finding. `run_battery` runs the audit under several and keeps what survives all
|
|
125
|
+
of them.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from bayesian_pv_census import run_battery
|
|
129
|
+
|
|
130
|
+
battery = run_battery(units, prior="empirical_bayes")
|
|
131
|
+
battery.hard_core("below") # flagged under-reported by every specification
|
|
132
|
+
battery.negative_control() # flagged the other way, unanimously — the control group
|
|
133
|
+
battery.concordance_table() # crosstab; empty off-diagonal corners mean no sign flips
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Specification A counts annotated objects. Specification B weights them by size.
|
|
137
|
+
**B is implemented exactly as in the paper**, which computes the weighted rates as
|
|
138
|
+
point estimates and applies them as a deterministic rescaling of A's posterior —
|
|
139
|
+
so B's interval is A's interval, shifted. That is a known limitation of the
|
|
140
|
+
published method; the package reproduces it rather than improving on it, because
|
|
141
|
+
reproducing the paper is the point. See the docstring of `correct_unit` for what
|
|
142
|
+
a properly weighted posterior would require, and for why the `min_weighted_n`
|
|
143
|
+
fallback matters more than its name suggests.
|
|
144
|
+
|
|
145
|
+
A third axis needs no specification of its own. Rescaling every raw quantity by a
|
|
146
|
+
constant — a different surface-to-power coefficient, a different filtering
|
|
147
|
+
threshold — multiplies the posterior and both its bounds while the reported value
|
|
148
|
+
stays put, so it is passed generically:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
run_battery(units, specs={"A": {}, "B": {"weighted": True},
|
|
152
|
+
"C_low": {"scale": 5.5 / 5.0}})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The status is monotone in that constant and flips once, so the flipping point has
|
|
156
|
+
a closed form and no sweep is needed:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from bayesian_pv_census import conversion_threshold
|
|
160
|
+
conversion_threshold(result, unit.reported_value, "below")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The coefficient itself stays outside the engine. `raw` arrives already converted,
|
|
164
|
+
and a `conversion_coefficient` argument would import photovoltaics into a core
|
|
165
|
+
that knows nothing about it.
|
|
166
|
+
|
|
167
|
+
## What forming the factor at a coarser scale costs
|
|
168
|
+
|
|
169
|
+
Correcting each unit and summing is not the same as pooling the units' rates and
|
|
170
|
+
correcting once. `1/R` is convex, so pooling always yields a smaller factor and a
|
|
171
|
+
smaller total. The cost is exactly zero when recall is homogeneous across the
|
|
172
|
+
pooled units, whatever the dispersion of precision, and second order in the
|
|
173
|
+
coefficient of variation of recall otherwise:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from bayesian_pv_census import compare_aggregation_scales, pooling_penalty
|
|
177
|
+
|
|
178
|
+
compare_aggregation_scales(units, groups={"north": [...], "south": [...]}).totals
|
|
179
|
+
pooling_penalty(units) # CV(R)^2 - rho * CV(P) * CV(R), and its two terms
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The practical consequence is that there is no optimal grid to search for. Forming
|
|
183
|
+
the factor at the finest level your annotation budget supports is weakly better
|
|
184
|
+
in every case.
|
|
185
|
+
|
|
186
|
+
## Scope
|
|
187
|
+
|
|
188
|
+
Out of scope by design, and unlikely to change: geospatial sampling, annotation
|
|
189
|
+
tooling, temporal alignment between imagery and register, and detector-specific
|
|
190
|
+
parsers. Those are format- and project-specific; this package starts once you can
|
|
191
|
+
write down a raw total and a validation sample.
|
|
192
|
+
|
|
193
|
+
## Tests
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install -e '.[dev]' && pytest
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The suite validates the mathematics on synthetic data — the closed form against
|
|
200
|
+
the bootstrap, the monotonicity of the budgeting rule, the structural invariants
|
|
201
|
+
of the hard core, the exact vanishing of the pooling penalty under homogeneous
|
|
202
|
+
recall — and then checks the published French numbers against the shipped demo
|
|
203
|
+
data, so a regression in the engine cannot pass silently.
|
|
204
|
+
|
|
205
|
+
## Citation
|
|
206
|
+
|
|
207
|
+
See `CITATION.cff`. Licence MIT.
|