robustmargin 0.1.1__py3-none-any.whl
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.
- robustmargin/__init__.py +90 -0
- robustmargin/censoring.py +652 -0
- robustmargin/empirical.py +348 -0
- robustmargin/intervals.py +785 -0
- robustmargin/models.py +2119 -0
- robustmargin/py.typed +0 -0
- robustmargin/ratio.py +941 -0
- robustmargin/report.py +650 -0
- robustmargin/schema/scorecard-v1.json +777 -0
- robustmargin/testing.py +465 -0
- robustmargin/threshold.py +631 -0
- robustmargin/types.py +541 -0
- robustmargin/validation.py +722 -0
- robustmargin-0.1.1.dist-info/METADATA +281 -0
- robustmargin-0.1.1.dist-info/RECORD +18 -0
- robustmargin-0.1.1.dist-info/WHEEL +5 -0
- robustmargin-0.1.1.dist-info/licenses/LICENSE +201 -0
- robustmargin-0.1.1.dist-info/top_level.txt +1 -0
robustmargin/testing.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Schema-valid fake results for **consumers' own tests**.
|
|
2
|
+
|
|
3
|
+
``fake_fit``, ``fake_threshold`` and ``fake_ratio`` build deterministic, schema-valid
|
|
4
|
+
:class:`~robustmargin.Fit`, :class:`~robustmargin.Threshold` and :class:`~robustmargin.Ratio`
|
|
5
|
+
objects so a consumer can write and run its own report-rendering and serialisation tests without
|
|
6
|
+
fitting any data. They are **not** statistical estimates of anything: every one of them carries
|
|
7
|
+
a warning saying so, and nothing here should be read as a claim about what a real fit would
|
|
8
|
+
produce for the same inputs.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import dataclasses
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
from scipy.stats import binomtest
|
|
17
|
+
|
|
18
|
+
from robustmargin.empirical import BaselineRate, GridBreakPoint
|
|
19
|
+
from robustmargin.models import Covariance, Fit, Parameter
|
|
20
|
+
from robustmargin.ratio import Ratio
|
|
21
|
+
from robustmargin.threshold import Threshold
|
|
22
|
+
from robustmargin.types import (
|
|
23
|
+
Axis,
|
|
24
|
+
Cell,
|
|
25
|
+
Censoring,
|
|
26
|
+
Definition,
|
|
27
|
+
IntervalShape,
|
|
28
|
+
JSONValue,
|
|
29
|
+
Observations,
|
|
30
|
+
Status,
|
|
31
|
+
)
|
|
32
|
+
from robustmargin.validation import Diagnostics, MonotonicityCheck, check_monotonicity, diagnose
|
|
33
|
+
|
|
34
|
+
__all__ = ["fake_fit", "fake_ratio", "fake_threshold"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _Unset:
|
|
38
|
+
"""Sentinel distinguishing "argument not passed" from an explicit ``None`` override for
|
|
39
|
+
``fake_fit``'s ``diagnostics``/``monotonicity`` parameters: an explicit argument --
|
|
40
|
+
``None`` included -- always overrides; only *omitting* the argument entirely means "compute
|
|
41
|
+
the real one"."""
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return "<unset>"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_UNSET = _Unset()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _observations_from_cells(fit: Fit) -> Observations:
|
|
51
|
+
"""Rebuild the :class:`~robustmargin.Observations` a fake ``fit`` "came from", so
|
|
52
|
+
:func:`~robustmargin.check_monotonicity` can run on it the same way it would on real data."""
|
|
53
|
+
return Observations.from_counts(
|
|
54
|
+
fit.axis,
|
|
55
|
+
severity=[c.severity for c in fit.cells],
|
|
56
|
+
successes=[c.successes for c in fit.cells],
|
|
57
|
+
trials=[c.trials for c in fit.cells],
|
|
58
|
+
outcome=fit.outcome,
|
|
59
|
+
direction=fit.direction,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_AXIS = Axis(name="severity", unit="unit", scale="log")
|
|
64
|
+
_FAKE_WARNING = (
|
|
65
|
+
"This is a fake object from robustmargin.testing, built for a consumer's own tests. It is "
|
|
66
|
+
"schema-valid, not a statistical estimate."
|
|
67
|
+
)
|
|
68
|
+
_CELLS: tuple[Cell, ...] = (
|
|
69
|
+
Cell(severity=0.0, successes=100, trials=100),
|
|
70
|
+
Cell(severity=1.0, successes=60, trials=100),
|
|
71
|
+
Cell(severity=2.0, successes=20, trials=100),
|
|
72
|
+
)
|
|
73
|
+
# fake_ratio builds its two thresholds from two separate fits on disjoint data (different
|
|
74
|
+
# severities entirely), not the same _CELLS reused twice, so a consumer's test never mistakes
|
|
75
|
+
# the pair for a ratio computed from one dataset against itself.
|
|
76
|
+
_CELLS_A: tuple[Cell, ...] = _CELLS
|
|
77
|
+
_CELLS_B: tuple[Cell, ...] = (
|
|
78
|
+
# severity=0.0 is an all-success control, like _CELLS_A's: 'upper' is fixed at exactly 1.0
|
|
79
|
+
# in _fake_ok_params_and_covariance, and a failing control combined with a fixed upper=1.0
|
|
80
|
+
# makes the likelihood zero on a log axis (Fit's own check).
|
|
81
|
+
Cell(severity=0.0, successes=100, trials=100),
|
|
82
|
+
Cell(severity=1.5, successes=50, trials=100),
|
|
83
|
+
Cell(severity=3.0, successes=10, trials=100),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# The 14 (status, censoring) combinations a Threshold allows, mirroring the invariants
|
|
87
|
+
# Threshold.__post_init__ enforces.
|
|
88
|
+
_ALLOWED_THRESHOLD_COMBINATIONS: frozenset[tuple[Status, Censoring]] = frozenset(
|
|
89
|
+
{(Status.OK, censoring) for censoring in Censoring}
|
|
90
|
+
| {
|
|
91
|
+
(status, censoring)
|
|
92
|
+
for status in (Status.SEPARATION, Status.NOT_CONVERGED)
|
|
93
|
+
for censoring in (Censoring.NONE, Censoring.RIGHT, Censoring.LEFT)
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
(status, Censoring.NONE)
|
|
97
|
+
for status in (Status.UNREACHABLE, Status.FAILS_AT_BASELINE, Status.CONTROL_INCOMPATIBLE)
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# UNREACHABLE and FAILS_AT_BASELINE are properties of a perfectly good fit (the target
|
|
102
|
+
# performance is out of the fitted curve's range, or the control already fails); SEPARATION,
|
|
103
|
+
# NOT_CONVERGED and CONTROL_INCOMPATIBLE are properties of the fit itself, so a threshold with
|
|
104
|
+
# one of those statuses is built on a fit carrying the same status.
|
|
105
|
+
_FIT_STATUS_FOR_THRESHOLD_STATUS: dict[Status, Status] = {
|
|
106
|
+
Status.OK: Status.OK,
|
|
107
|
+
Status.UNREACHABLE: Status.OK,
|
|
108
|
+
Status.FAILS_AT_BASELINE: Status.OK,
|
|
109
|
+
Status.SEPARATION: Status.SEPARATION,
|
|
110
|
+
Status.NOT_CONVERGED: Status.NOT_CONVERGED,
|
|
111
|
+
Status.CONTROL_INCOMPATIBLE: Status.CONTROL_INCOMPATIBLE,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fake_provenance(provenance: Mapping[str, JSONValue] | None) -> dict[str, JSONValue]:
|
|
116
|
+
return dict(provenance) if provenance is not None else {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _fake_ok_params_and_covariance() -> tuple[dict[str, Parameter], Covariance]:
|
|
120
|
+
params = {
|
|
121
|
+
"mu": Parameter(value=0.5, fixed=False),
|
|
122
|
+
"s": Parameter(value=0.3, fixed=False),
|
|
123
|
+
"upper": Parameter(value=1.0, fixed=True),
|
|
124
|
+
"lower": Parameter(value=0.0, fixed=True),
|
|
125
|
+
}
|
|
126
|
+
covariance = Covariance(names=("mu", "s"), matrix=((0.01, 0.0), (0.0, 0.02)))
|
|
127
|
+
return params, covariance
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _fake_ok_fit(
|
|
131
|
+
cells: tuple[Cell, ...],
|
|
132
|
+
*,
|
|
133
|
+
provenance: Mapping[str, JSONValue] | None = None,
|
|
134
|
+
diagnostics: Diagnostics | None | _Unset = _UNSET,
|
|
135
|
+
monotonicity: MonotonicityCheck | None | _Unset = _UNSET,
|
|
136
|
+
) -> Fit:
|
|
137
|
+
params, covariance = _fake_ok_params_and_covariance()
|
|
138
|
+
base = Fit(
|
|
139
|
+
axis=_AXIS,
|
|
140
|
+
outcome="success",
|
|
141
|
+
direction="decreasing",
|
|
142
|
+
model="binomial",
|
|
143
|
+
link="probit",
|
|
144
|
+
params=params,
|
|
145
|
+
covariance=covariance,
|
|
146
|
+
log_likelihood=-12.5,
|
|
147
|
+
cells=cells,
|
|
148
|
+
status=Status.OK,
|
|
149
|
+
cluster_ids=None,
|
|
150
|
+
warnings=(_FAKE_WARNING,),
|
|
151
|
+
provenance=_fake_provenance(provenance),
|
|
152
|
+
)
|
|
153
|
+
# Mirrors a real fit_dose_response result -- diagnostics through diagnose() (status is OK
|
|
154
|
+
# here, so this never raises), monotonicity through check_monotonicity() on the same cells --
|
|
155
|
+
# unless the caller explicitly overrode either.
|
|
156
|
+
resolved_diagnostics = diagnose(base) if isinstance(diagnostics, _Unset) else diagnostics
|
|
157
|
+
resolved_monotonicity = (
|
|
158
|
+
check_monotonicity(_observations_from_cells(base))
|
|
159
|
+
if isinstance(monotonicity, _Unset)
|
|
160
|
+
else monotonicity
|
|
161
|
+
)
|
|
162
|
+
return dataclasses.replace(
|
|
163
|
+
base, diagnostics=resolved_diagnostics, monotonicity=resolved_monotonicity
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def fake_fit(
|
|
168
|
+
*,
|
|
169
|
+
status: Status = Status.OK,
|
|
170
|
+
provenance: Mapping[str, JSONValue] | None = None,
|
|
171
|
+
diagnostics: Diagnostics | None | _Unset = _UNSET,
|
|
172
|
+
monotonicity: MonotonicityCheck | None | _Unset = _UNSET,
|
|
173
|
+
) -> Fit:
|
|
174
|
+
"""Build a deterministic, schema-valid :class:`~robustmargin.Fit`.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
status
|
|
179
|
+
The :class:`~robustmargin.Status` to build. Every member other than ``UNREACHABLE`` and
|
|
180
|
+
``FAILS_AT_BASELINE`` is accepted (those describe a threshold, not a fit, and
|
|
181
|
+
:class:`~robustmargin.Fit` itself rejects them). When ``status`` is not ``OK``, ``params``,
|
|
182
|
+
``covariance`` and ``log_likelihood`` are all ``None``, matching
|
|
183
|
+
:class:`~robustmargin.Fit`'s own invariant.
|
|
184
|
+
provenance
|
|
185
|
+
Stored on the result as given, or ``{}`` if not provided.
|
|
186
|
+
diagnostics
|
|
187
|
+
Mirrors a real fit: when omitted entirely, this is :func:`~robustmargin.diagnose` on the
|
|
188
|
+
built fit when ``status`` is ``OK``, or ``None`` otherwise (:func:`~robustmargin.diagnose`
|
|
189
|
+
itself requires ``status is Status.OK``). Passing an explicit value -- ``None`` included --
|
|
190
|
+
always overrides that and is stored as given; ``Fit`` itself still rejects a non-``None``
|
|
191
|
+
value when ``status`` is not ``OK`` (a failed fit never carries a number).
|
|
192
|
+
monotonicity
|
|
193
|
+
Mirrors a real fit, the same way ``diagnostics`` does: when omitted entirely, this is
|
|
194
|
+
:func:`~robustmargin.check_monotonicity` on the built fit's own cells, for *every*
|
|
195
|
+
``status`` (a real :func:`~robustmargin.fit_dose_response` result is never ``None`` here
|
|
196
|
+
regardless of status). An explicit value -- ``None`` included -- always overrides.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
Fit
|
|
201
|
+
A binomial-probit fit on a neutral ``severity`` axis, with ``cluster_ids=None`` and
|
|
202
|
+
``warnings`` naming it a fake.
|
|
203
|
+
"""
|
|
204
|
+
if status is Status.OK:
|
|
205
|
+
return _fake_ok_fit(
|
|
206
|
+
_CELLS, provenance=provenance, diagnostics=diagnostics, monotonicity=monotonicity
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
base = Fit(
|
|
210
|
+
axis=_AXIS,
|
|
211
|
+
outcome="success",
|
|
212
|
+
direction="decreasing",
|
|
213
|
+
model="binomial",
|
|
214
|
+
link="probit",
|
|
215
|
+
params=None,
|
|
216
|
+
covariance=None,
|
|
217
|
+
log_likelihood=None,
|
|
218
|
+
cells=_CELLS,
|
|
219
|
+
status=status,
|
|
220
|
+
cluster_ids=None,
|
|
221
|
+
warnings=(_FAKE_WARNING,),
|
|
222
|
+
provenance=_fake_provenance(provenance),
|
|
223
|
+
)
|
|
224
|
+
resolved_diagnostics = None if isinstance(diagnostics, _Unset) else diagnostics
|
|
225
|
+
resolved_monotonicity = (
|
|
226
|
+
check_monotonicity(_observations_from_cells(base))
|
|
227
|
+
if isinstance(monotonicity, _Unset)
|
|
228
|
+
else monotonicity
|
|
229
|
+
)
|
|
230
|
+
return dataclasses.replace(
|
|
231
|
+
base, diagnostics=resolved_diagnostics, monotonicity=resolved_monotonicity
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def fake_threshold(
|
|
236
|
+
*,
|
|
237
|
+
status: Status = Status.OK,
|
|
238
|
+
censoring: Censoring = Censoring.NONE,
|
|
239
|
+
provenance: Mapping[str, JSONValue] | None = None,
|
|
240
|
+
) -> Threshold:
|
|
241
|
+
"""Build a deterministic, schema-valid :class:`~robustmargin.Threshold`.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
status
|
|
246
|
+
The :class:`~robustmargin.Status` to build.
|
|
247
|
+
censoring
|
|
248
|
+
The :class:`~robustmargin.Censoring` to build.
|
|
249
|
+
provenance
|
|
250
|
+
Stored on the result as given, or ``{}`` if not provided.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
Threshold
|
|
255
|
+
A threshold on a neutral ``severity`` axis with ``dependence="independent"``, and
|
|
256
|
+
``warnings`` naming it a fake.
|
|
257
|
+
|
|
258
|
+
Raises
|
|
259
|
+
------
|
|
260
|
+
ValueError
|
|
261
|
+
If ``(status, censoring)`` is not one of the fourteen combinations a ``Threshold``
|
|
262
|
+
allows.
|
|
263
|
+
"""
|
|
264
|
+
if (status, censoring) not in _ALLOWED_THRESHOLD_COMBINATIONS:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"fake_threshold: status={status!r} with censoring={censoring!r} is not a "
|
|
267
|
+
"combination Threshold allows"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
fit = fake_fit(status=_FIT_STATUS_FOR_THRESHOLD_STATUS[status])
|
|
271
|
+
|
|
272
|
+
value: float | None
|
|
273
|
+
lo: float | None
|
|
274
|
+
hi: float | None
|
|
275
|
+
interval_method: str
|
|
276
|
+
|
|
277
|
+
if status in (Status.UNREACHABLE, Status.FAILS_AT_BASELINE, Status.CONTROL_INCOMPATIBLE):
|
|
278
|
+
value, lo, hi = None, None, None
|
|
279
|
+
interval_method = "exact_bound"
|
|
280
|
+
elif censoring is Censoring.RIGHT:
|
|
281
|
+
value, lo, hi = None, 10.0, None
|
|
282
|
+
interval_method = "exact_bound"
|
|
283
|
+
elif censoring is Censoring.LEFT:
|
|
284
|
+
value, lo, hi = None, None, 0.01
|
|
285
|
+
interval_method = "exact_bound"
|
|
286
|
+
elif censoring is Censoring.OPEN_UPPER:
|
|
287
|
+
value, lo, hi = 0.05, 0.03, None
|
|
288
|
+
interval_method = "profile"
|
|
289
|
+
elif censoring is Censoring.OPEN_LOWER:
|
|
290
|
+
value, lo, hi = 0.05, None, 0.08
|
|
291
|
+
interval_method = "profile"
|
|
292
|
+
elif status is Status.OK:
|
|
293
|
+
value, lo, hi = 0.05, 0.03, 0.08
|
|
294
|
+
interval_method = "profile"
|
|
295
|
+
else:
|
|
296
|
+
# SEPARATION / NOT_CONVERGED with censoring NONE (the exact-bound fallback, with both
|
|
297
|
+
# sides happening to be available here).
|
|
298
|
+
value, lo, hi = None, 0.02, 0.09
|
|
299
|
+
interval_method = "exact_bound"
|
|
300
|
+
|
|
301
|
+
# The baseline and grid fields are set exactly where the invariants allow them, so a
|
|
302
|
+
# consumer testing against a fake sees the same shape a real result has. A fake that left
|
|
303
|
+
# them None on FAILS_AT_BASELINE would quietly stop exercising the field that status exists
|
|
304
|
+
# to carry.
|
|
305
|
+
baseline: BaselineRate | None = None
|
|
306
|
+
grid: GridBreakPoint | None = None
|
|
307
|
+
if status is Status.FAILS_AT_BASELINE:
|
|
308
|
+
# The two-sided exact interval for 98/200 at level=0.95, which is what
|
|
309
|
+
# `censoring._baseline_rate` produces for the same counts. Computed rather than
|
|
310
|
+
# transcribed, so the fake cannot drift from the real path.
|
|
311
|
+
_lo, _hi = binomtest(98, 200).proportion_ci(confidence_level=0.95, method="exact")
|
|
312
|
+
baseline = BaselineRate(
|
|
313
|
+
severity=0.0,
|
|
314
|
+
successes=98,
|
|
315
|
+
trials=200,
|
|
316
|
+
rate=0.49,
|
|
317
|
+
lo=float(_lo),
|
|
318
|
+
hi=float(_hi),
|
|
319
|
+
level=0.95,
|
|
320
|
+
)
|
|
321
|
+
elif status in (Status.SEPARATION, Status.NOT_CONVERGED):
|
|
322
|
+
grid = GridBreakPoint(0.05, 0.1, Censoring.NONE)
|
|
323
|
+
|
|
324
|
+
return Threshold(
|
|
325
|
+
axis=_AXIS,
|
|
326
|
+
definition=Definition.absolute(0.95),
|
|
327
|
+
direction="decreasing",
|
|
328
|
+
value=value,
|
|
329
|
+
lo=lo,
|
|
330
|
+
hi=hi,
|
|
331
|
+
level=0.95,
|
|
332
|
+
interval_method=interval_method,
|
|
333
|
+
censoring=censoring,
|
|
334
|
+
status=status,
|
|
335
|
+
dependence="independent",
|
|
336
|
+
fit=fit,
|
|
337
|
+
warnings=(_FAKE_WARNING,),
|
|
338
|
+
baseline=baseline,
|
|
339
|
+
grid=grid,
|
|
340
|
+
provenance=_fake_provenance(provenance),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _fake_ok_threshold(*, fit: Fit, value: float, lo: float, hi: float) -> Threshold:
|
|
345
|
+
return Threshold(
|
|
346
|
+
axis=_AXIS,
|
|
347
|
+
definition=Definition.absolute(0.95),
|
|
348
|
+
direction="decreasing",
|
|
349
|
+
value=value,
|
|
350
|
+
lo=lo,
|
|
351
|
+
hi=hi,
|
|
352
|
+
level=0.95,
|
|
353
|
+
interval_method="profile",
|
|
354
|
+
censoring=Censoring.NONE,
|
|
355
|
+
status=Status.OK,
|
|
356
|
+
dependence="independent",
|
|
357
|
+
fit=fit,
|
|
358
|
+
warnings=(_FAKE_WARNING,),
|
|
359
|
+
provenance={},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def fake_ratio(
|
|
364
|
+
*,
|
|
365
|
+
shape: IntervalShape | None = None,
|
|
366
|
+
status: Status = Status.OK,
|
|
367
|
+
censoring: Censoring = Censoring.NONE,
|
|
368
|
+
provenance: Mapping[str, JSONValue] | None = None,
|
|
369
|
+
) -> Ratio:
|
|
370
|
+
"""Build a deterministic, schema-valid :class:`~robustmargin.Ratio`.
|
|
371
|
+
|
|
372
|
+
A failed or censored ratio carries no numbers: whenever ``status`` is not
|
|
373
|
+
``OK`` or ``censoring`` is not ``NONE``, ``shape``, ``estimate``, ``lo`` and ``hi`` are all
|
|
374
|
+
``None``, matching :class:`~robustmargin.Ratio`'s own invariant.
|
|
375
|
+
|
|
376
|
+
Parameters
|
|
377
|
+
----------
|
|
378
|
+
shape
|
|
379
|
+
The :class:`~robustmargin.IntervalShape` to build. ``None`` (the default) means
|
|
380
|
+
``BOUNDED`` when ``status`` is ``OK`` and ``censoring`` is ``NONE``, and means "no
|
|
381
|
+
shape" (forced ``None``) otherwise. Passing an explicit shape together with a non-``OK``
|
|
382
|
+
``status`` or a non-``NONE`` ``censoring`` raises ``ValueError``, since that combination
|
|
383
|
+
can never be a valid ``Ratio``. Every member is buildable, including
|
|
384
|
+
``IntervalShape.EXCLUSIVE``, which a real :func:`~robustmargin.ratio_interval` call cannot
|
|
385
|
+
produce while ``dependence="independent"`` but whose :class:`~robustmargin.Ratio` invariants
|
|
386
|
+
still need a consumer-testable fixture.
|
|
387
|
+
status
|
|
388
|
+
The :class:`~robustmargin.Status` to build.
|
|
389
|
+
censoring
|
|
390
|
+
The :class:`~robustmargin.Censoring` to build.
|
|
391
|
+
provenance
|
|
392
|
+
Stored on the result as given, or ``{}`` if not provided.
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
Ratio
|
|
397
|
+
A ratio built from two fits on disjoint data (different cells entirely), with
|
|
398
|
+
``dependence="independent"``, ``cluster_ids=None`` on both fits, and ``warnings``
|
|
399
|
+
naming it a fake. On the ``OK`` + ``NONE`` path, ``estimate`` is exactly
|
|
400
|
+
``threshold_a.value / threshold_b.value``, matching
|
|
401
|
+
:class:`~robustmargin.Ratio`'s own invariant; the point estimate always lies inside the
|
|
402
|
+
Fieller confidence set described by ``shape``, including for ``EXCLUSIVE``.
|
|
403
|
+
|
|
404
|
+
Raises
|
|
405
|
+
------
|
|
406
|
+
ValueError
|
|
407
|
+
If ``shape`` is given explicitly while ``status`` is not ``OK`` or ``censoring`` is not
|
|
408
|
+
``NONE``.
|
|
409
|
+
"""
|
|
410
|
+
is_ok_none = status is Status.OK and censoring is Censoring.NONE
|
|
411
|
+
if not is_ok_none and shape is not None:
|
|
412
|
+
raise ValueError(
|
|
413
|
+
f"fake_ratio: shape={shape!r} is not allowed when status={status!r} or "
|
|
414
|
+
f"censoring={censoring!r} -- a failed or censored ratio carries no shape"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
value_a, lo_a, hi_a = 0.05, 0.03, 0.08
|
|
418
|
+
value_b, lo_b, hi_b = 0.1, 0.06, 0.16
|
|
419
|
+
threshold_a = _fake_ok_threshold(fit=_fake_ok_fit(_CELLS_A), value=value_a, lo=lo_a, hi=hi_a)
|
|
420
|
+
threshold_b = _fake_ok_threshold(fit=_fake_ok_fit(_CELLS_B), value=value_b, lo=lo_b, hi=hi_b)
|
|
421
|
+
|
|
422
|
+
resolved_shape: IntervalShape | None
|
|
423
|
+
estimate: float | None
|
|
424
|
+
lo: float | None
|
|
425
|
+
hi: float | None
|
|
426
|
+
|
|
427
|
+
if not is_ok_none:
|
|
428
|
+
resolved_shape, estimate, lo, hi = None, None, None, None
|
|
429
|
+
else:
|
|
430
|
+
resolved_shape = IntervalShape.BOUNDED if shape is None else shape
|
|
431
|
+
ratio_value = value_a / value_b
|
|
432
|
+
estimate = ratio_value
|
|
433
|
+
if resolved_shape is IntervalShape.BOUNDED:
|
|
434
|
+
lo, hi = 0.3, 0.7
|
|
435
|
+
elif resolved_shape is IntervalShape.UNBOUNDED:
|
|
436
|
+
lo, hi = None, None
|
|
437
|
+
elif resolved_shape is IntervalShape.HALF_OPEN:
|
|
438
|
+
# [lo, inf): lo <= estimate is Ratio's own invariant. This is a shape a real
|
|
439
|
+
# ratio_interval(method="fieller") call does produce -- it covers an open region of
|
|
440
|
+
# the parameter space -- unlike EXCLUSIVE below.
|
|
441
|
+
lo, hi = ratio_value * 0.5, None
|
|
442
|
+
else:
|
|
443
|
+
# EXCLUSIVE: unreachable through ratio_interval() itself while
|
|
444
|
+
# dependence="independent" fixes the covariance term at 0, but Ratio's own
|
|
445
|
+
# invariants for it stay enforced against v0.2's dependence="paired", and this fake
|
|
446
|
+
# builds one directly so a consumer can still test against it. The point estimate
|
|
447
|
+
# sits on the near edge of the excluded gap, inside the (-inf, lo] ray of the
|
|
448
|
+
# Fieller confidence set.
|
|
449
|
+
lo, hi = ratio_value, ratio_value + 1.0
|
|
450
|
+
|
|
451
|
+
return Ratio(
|
|
452
|
+
threshold_a=threshold_a,
|
|
453
|
+
threshold_b=threshold_b,
|
|
454
|
+
estimate=estimate,
|
|
455
|
+
lo=lo,
|
|
456
|
+
hi=hi,
|
|
457
|
+
shape=resolved_shape,
|
|
458
|
+
method="fieller",
|
|
459
|
+
dependence="independent",
|
|
460
|
+
level=0.95,
|
|
461
|
+
censoring=censoring,
|
|
462
|
+
status=status,
|
|
463
|
+
warnings=(_FAKE_WARNING,),
|
|
464
|
+
provenance=_fake_provenance(provenance),
|
|
465
|
+
)
|