zekan 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.
- zekan-0.1.0/.gitignore +56 -0
- zekan-0.1.0/COVERAGE_MAP.md +241 -0
- zekan-0.1.0/FORBIDDEN_ZERO_COOCCURRENCE_CHECK.md +94 -0
- zekan-0.1.0/LICENSE +21 -0
- zekan-0.1.0/METHODOLOGY.md +344 -0
- zekan-0.1.0/PKG-INFO +297 -0
- zekan-0.1.0/PUBLISH_PREP_FINDINGS.md +441 -0
- zekan-0.1.0/README.md +258 -0
- zekan-0.1.0/examples/churn_instacart/README.md +6 -0
- zekan-0.1.0/examples/churn_instacart/fixture.csv +1501 -0
- zekan-0.1.0/examples/churn_instacart/zekan.yml +26 -0
- zekan-0.1.0/pyproject.toml +88 -0
- zekan-0.1.0/tests/__init__.py +1 -0
- zekan-0.1.0/tests/test_ablation_grouped.py +321 -0
- zekan-0.1.0/tests/test_ablation_parallel.py +209 -0
- zekan-0.1.0/tests/test_across_entity_null.py +372 -0
- zekan-0.1.0/tests/test_audit_integration.py +368 -0
- zekan-0.1.0/tests/test_benchmark.py +251 -0
- zekan-0.1.0/tests/test_bootstrap_ci.py +311 -0
- zekan-0.1.0/tests/test_ci_a_opt_in.py +240 -0
- zekan-0.1.0/tests/test_cli.py +1172 -0
- zekan-0.1.0/tests/test_config.py +81 -0
- zekan-0.1.0/tests/test_contract.py +268 -0
- zekan-0.1.0/tests/test_data_loading.py +284 -0
- zekan-0.1.0/tests/test_deployable_auc_labeling.py +161 -0
- zekan-0.1.0/tests/test_diff.py +635 -0
- zekan-0.1.0/tests/test_duplicate_probe.py +668 -0
- zekan-0.1.0/tests/test_entity_aggregate_probe.py +351 -0
- zekan-0.1.0/tests/test_entity_contamination_probe.py +503 -0
- zekan-0.1.0/tests/test_estimators.py +83 -0
- zekan-0.1.0/tests/test_explain.py +529 -0
- zekan-0.1.0/tests/test_f2a_parallel_null.py +348 -0
- zekan-0.1.0/tests/test_fast_mode.py +308 -0
- zekan-0.1.0/tests/test_fold_transparency.py +422 -0
- zekan-0.1.0/tests/test_forbidden_zero_cooccurrence_probe.py +358 -0
- zekan-0.1.0/tests/test_html_view.py +619 -0
- zekan-0.1.0/tests/test_init.py +431 -0
- zekan-0.1.0/tests/test_json_export.py +414 -0
- zekan-0.1.0/tests/test_leakage_verdict.py +236 -0
- zekan-0.1.0/tests/test_markers.py +152 -0
- zekan-0.1.0/tests/test_metrics.py +645 -0
- zekan-0.1.0/tests/test_module_invocation.py +38 -0
- zekan-0.1.0/tests/test_naive_auc_fold_seed.py +180 -0
- zekan-0.1.0/tests/test_near_bijection_probe.py +232 -0
- zekan-0.1.0/tests/test_near_bijection_view.py +383 -0
- zekan-0.1.0/tests/test_power.py +54 -0
- zekan-0.1.0/tests/test_preflight.py +371 -0
- zekan-0.1.0/tests/test_provenance.py +519 -0
- zekan-0.1.0/tests/test_robustness_gauntlet.py +439 -0
- zekan-0.1.0/tests/test_schema.py +338 -0
- zekan-0.1.0/tests/test_seed_stability.py +281 -0
- zekan-0.1.0/tests/test_severity.py +283 -0
- zekan-0.1.0/tests/test_splitter_contract_probe.py +556 -0
- zekan-0.1.0/tests/test_splitters.py +372 -0
- zekan-0.1.0/tests/test_structural_coverage.py +550 -0
- zekan-0.1.0/tests/test_structural_probe_wiring.py +489 -0
- zekan-0.1.0/tests/test_terminal_row_presence_probe.py +442 -0
- zekan-0.1.0/tests/test_terminal_row_view.py +488 -0
- zekan-0.1.0/tests/test_text_view.py +721 -0
- zekan-0.1.0/tests/test_tier2_sequential_null.py +545 -0
- zekan-0.1.0/tests/test_tier_system.py +376 -0
- zekan-0.1.0/tests/test_undeclared_feature_probe.py +458 -0
- zekan-0.1.0/tests/test_undeclared_screen_view.py +389 -0
- zekan-0.1.0/tests/test_verdict.py +525 -0
- zekan-0.1.0/zekan/__init__.py +3 -0
- zekan-0.1.0/zekan/__main__.py +9 -0
- zekan-0.1.0/zekan/cli.py +1292 -0
- zekan-0.1.0/zekan/config/__init__.py +1 -0
- zekan-0.1.0/zekan/config/schema.py +135 -0
- zekan-0.1.0/zekan/config/validator.py +1 -0
- zekan-0.1.0/zekan/contract/__init__.py +1 -0
- zekan-0.1.0/zekan/contract/contract_checks.py +465 -0
- zekan-0.1.0/zekan/contract/prediction_contract.py +69 -0
- zekan-0.1.0/zekan/data_loading.py +156 -0
- zekan-0.1.0/zekan/decision/__init__.py +1 -0
- zekan-0.1.0/zekan/decision/cost_model.py +1 -0
- zekan-0.1.0/zekan/decision/explanations.py +1 -0
- zekan-0.1.0/zekan/decision/ranking.py +1 -0
- zekan-0.1.0/zekan/decision/trust_score.py +1 -0
- zekan-0.1.0/zekan/detectors/__init__.py +1 -0
- zekan-0.1.0/zekan/detectors/base.py +1 -0
- zekan-0.1.0/zekan/detectors/contract_detector.py +1 -0
- zekan-0.1.0/zekan/detectors/deepchecks_adapter.py +1 -0
- zekan-0.1.0/zekan/detectors/duplicate_probe.py +337 -0
- zekan-0.1.0/zekan/detectors/entity_aggregate_probe.py +169 -0
- zekan-0.1.0/zekan/detectors/entity_contamination_risk.py +214 -0
- zekan-0.1.0/zekan/detectors/forbidden_zero_cooccurrence_probe.py +157 -0
- zekan-0.1.0/zekan/detectors/near_bijection_probe.py +210 -0
- zekan-0.1.0/zekan/detectors/schema.py +693 -0
- zekan-0.1.0/zekan/detectors/splitter_contract_probe.py +183 -0
- zekan-0.1.0/zekan/detectors/static_ast.py +1 -0
- zekan-0.1.0/zekan/detectors/terminal_row_presence_probe.py +197 -0
- zekan-0.1.0/zekan/detectors/undeclared_feature_probe.py +458 -0
- zekan-0.1.0/zekan/init_wizard.py +81 -0
- zekan-0.1.0/zekan/reports/__init__.py +1 -0
- zekan-0.1.0/zekan/reports/diff.py +219 -0
- zekan-0.1.0/zekan/reports/explain.py +373 -0
- zekan-0.1.0/zekan/reports/html.py +1 -0
- zekan-0.1.0/zekan/reports/html_view.py +693 -0
- zekan-0.1.0/zekan/reports/json_export.py +135 -0
- zekan-0.1.0/zekan/reports/markdown.py +1 -0
- zekan-0.1.0/zekan/reports/markers.py +83 -0
- zekan-0.1.0/zekan/reports/messages.py +323 -0
- zekan-0.1.0/zekan/reports/preflight.py +115 -0
- zekan-0.1.0/zekan/reports/provenance.py +219 -0
- zekan-0.1.0/zekan/reports/text_view.py +711 -0
- zekan-0.1.0/zekan/security/__init__.py +1 -0
- zekan-0.1.0/zekan/security/secrets_scan.py +1 -0
- zekan-0.1.0/zekan/severity/__init__.py +1 -0
- zekan-0.1.0/zekan/severity/ablation.py +443 -0
- zekan-0.1.0/zekan/severity/audit.py +759 -0
- zekan-0.1.0/zekan/severity/bootstrap_ci.py +221 -0
- zekan-0.1.0/zekan/severity/engine.py +908 -0
- zekan-0.1.0/zekan/severity/estimators.py +62 -0
- zekan-0.1.0/zekan/severity/metrics.py +728 -0
- zekan-0.1.0/zekan/severity/null_baseline.py +1339 -0
- zekan-0.1.0/zekan/severity/power.py +42 -0
- zekan-0.1.0/zekan/severity/splitters.py +287 -0
- zekan-0.1.0/zekan/severity/verdict.py +1350 -0
zekan-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
.installed.cfg
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.whl
|
|
20
|
+
|
|
21
|
+
# Virtual environments
|
|
22
|
+
venv/
|
|
23
|
+
.venv/
|
|
24
|
+
env/
|
|
25
|
+
.env/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# Zekan cache
|
|
29
|
+
.zekan/
|
|
30
|
+
|
|
31
|
+
# Testing
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
.coverage
|
|
34
|
+
htmlcov/
|
|
35
|
+
.tox/
|
|
36
|
+
|
|
37
|
+
# Type checking
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
.dmypy.json
|
|
40
|
+
dmypy.json
|
|
41
|
+
|
|
42
|
+
# Ruff
|
|
43
|
+
.ruff_cache/
|
|
44
|
+
|
|
45
|
+
# IDE
|
|
46
|
+
.vscode/
|
|
47
|
+
.idea/
|
|
48
|
+
*.swp
|
|
49
|
+
*.swo
|
|
50
|
+
|
|
51
|
+
# OS
|
|
52
|
+
.DS_Store
|
|
53
|
+
Thumbs.db
|
|
54
|
+
|
|
55
|
+
# Local scratch artifacts
|
|
56
|
+
scratch/
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Coverage Map
|
|
2
|
+
|
|
3
|
+
What Zekan checks, what it does not, and where the boundaries of what it
|
|
4
|
+
does check are known to sit. This is not a list of apologies — it is the
|
|
5
|
+
thing a tool that measures trust has no business hiding. Every claim below
|
|
6
|
+
is sourced to committed code or a committed calibration/validation
|
|
7
|
+
document.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. What runs in a real audit
|
|
12
|
+
|
|
13
|
+
Every `zekan audit` invocation runs two independent layers:
|
|
14
|
+
|
|
15
|
+
### The A/B/C severity engine
|
|
16
|
+
|
|
17
|
+
Measures `fixable_leakage` from columns you declared in
|
|
18
|
+
`forbidden_after_prediction`, with a permutation-null detection gate. See
|
|
19
|
+
[`METHODOLOGY.md`](METHODOLOGY.md) for the full definition. This is the
|
|
20
|
+
only layer that can change the verdict (`PASS`/`NOTE`/`WARN`/`FAIL`/
|
|
21
|
+
`UNCONFIRMED_HIGH_DAMAGE`).
|
|
22
|
+
|
|
23
|
+
### Nine structural probes (annotate-only — never change the verdict)
|
|
24
|
+
|
|
25
|
+
Registered in `zekan/severity/audit.py::_build_probe_registry`, run on
|
|
26
|
+
every audit alongside the engine:
|
|
27
|
+
|
|
28
|
+
| probe | catches | annotate-only |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `probe_forbidden_entity_level_aggregate` | a forbidden column that is constant *within* each entity — a value the model could learn as an entity ID in disguise | yes |
|
|
31
|
+
| `probe_raw_duplicates` | exact-duplicate rows anywhere in the dataset | yes |
|
|
32
|
+
| `probe_cross_fold_duplicates` | a test-fold row that is byte-identical to a training-fold row | yes |
|
|
33
|
+
| `probe_undeclared_feature_screen` (Upgrade 1) | non-forbidden features with a high standalone (univariate, temporal-fold) association with the target — a ranked candidate list, not a verdict | yes |
|
|
34
|
+
| `probe_near_bijection` (Upgrade H) | a non-forbidden column whose values determine the target almost exactly (Theil's U ≥ 0.99) — a near-perfect value-to-label partition, the shape of an ID leaking the answer | yes |
|
|
35
|
+
| `probe_terminal_row_presence` | a non-forbidden column populated *only* on an entity's last observed row — the shape of a post-outcome field (settlement amount, disposition code) nobody declared forbidden | yes |
|
|
36
|
+
| `probe_entity_contamination_risk` | whether the same entity recurs across more than one `prediction_time` period — the structural condition under which a *random* (non-grouped) split would leak that entity into both train and test | yes (`confirmed=False`, advisory) |
|
|
37
|
+
| `probe_splitter_contract_violation` | an internal self-check: verifies Zekan's own random-grouped splitter never placed the same entity in both train and test of a fold. If it fires, the bug is in Zekan, not your data | yes (internal integrity check, not a data finding) |
|
|
38
|
+
| `probe_forbidden_zero_cooccurrence` | a *declared forbidden* column with zero non-missing rows where the target is positive — the declared leak is structurally incapable of having contributed to the measured `fixable_leakage`, whatever number came back. See [`FORBIDDEN_ZERO_COOCCURRENCE_CHECK.md`](FORBIDDEN_ZERO_COOCCURRENCE_CHECK.md) | yes |
|
|
39
|
+
|
|
40
|
+
None of these nine can turn a clean `fl` into a WARN/FAIL. Six are
|
|
41
|
+
target-free by design (`probe_forbidden_entity_level_aggregate`,
|
|
42
|
+
`probe_raw_duplicates`, `probe_cross_fold_duplicates`,
|
|
43
|
+
`probe_terminal_row_presence`, `probe_entity_contamination_risk`,
|
|
44
|
+
`probe_splitter_contract_violation`); three read the target because that is
|
|
45
|
+
exactly what they measure — `probe_undeclared_feature_screen`'s univariate
|
|
46
|
+
association and `probe_near_bijection`'s value-to-label determinism are
|
|
47
|
+
both, by definition, feature-vs-target relationships;
|
|
48
|
+
`probe_forbidden_zero_cooccurrence` reads the target's own value (positive
|
|
49
|
+
vs. negative row) rather than any feature-vs-target statistical
|
|
50
|
+
relationship — a deductive non-missing-count fact, not a model score.
|
|
51
|
+
`probe_splitter_contract_violation`
|
|
52
|
+
reads neither the target nor forbidden-column content — it checks only
|
|
53
|
+
entity_id positions across the random-grouped fold indices Zekan itself
|
|
54
|
+
built. Every finding from these probes appears under
|
|
55
|
+
`structural_annotations` in the JSON export and in a dedicated section of
|
|
56
|
+
both human-readable formatters, never folded silently into the headline
|
|
57
|
+
verdict.
|
|
58
|
+
|
|
59
|
+
**A real, near-universal consequence worth stating plainly**:
|
|
60
|
+
`probe_entity_contamination_risk` will fire `WARN` on almost every dataset
|
|
61
|
+
Zekan is designed for — its trigger condition (an entity recurring across
|
|
62
|
+
more than one period) is the same "recurring over time" structure this
|
|
63
|
+
README's own fit criteria require. Measured directly against the Freddie
|
|
64
|
+
Mac Frame C data: 1,902 of 1,905 entities (99.8%) trigger it. This is not
|
|
65
|
+
noise or miscalibration; it is the advisory correctly describing the
|
|
66
|
+
structural risk of ever running a *non-grouped* split on this shape of
|
|
67
|
+
data, restated on every audit because the risk is present on every audit
|
|
68
|
+
of this shape of data.
|
|
69
|
+
|
|
70
|
+
**`structural_coverage_complete`** (a top-level JSON field, and a caveat
|
|
71
|
+
line in both formatters when false) tracks whether every probe that was
|
|
72
|
+
*supposed* to run actually did — a probe that crashed internally, or
|
|
73
|
+
that needed temporal folds the data couldn't supply, sets this false and
|
|
74
|
+
lists why, rather than letting a clean-looking PASS pass over an
|
|
75
|
+
unchecked category silently.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 2. A masking defect this document caused to be found, and its fix
|
|
80
|
+
|
|
81
|
+
Writing §1 above (first published `f3a7ffe`) surfaced a real defect:
|
|
82
|
+
`probe_entity_contamination_risk` and `probe_splitter_contract_violation`
|
|
83
|
+
were complete, individually-tested modules, **not** registered in
|
|
84
|
+
`_build_probe_registry()` — no real `zekan audit` run had ever called
|
|
85
|
+
either. `structural_coverage_complete` stayed `True` throughout, because
|
|
86
|
+
that field's computation only ever considers registered probes; an
|
|
87
|
+
unregistered probe is invisible to it by construction, not merely
|
|
88
|
+
untested. Full record, including why this is a worse defect than the
|
|
89
|
+
structural-coverage-caveat fix (§1's `structural_coverage_complete`
|
|
90
|
+
mechanism itself), how it was found (writing this document, not a test),
|
|
91
|
+
and why it does not change any conclusion in the Freddie Mac validation
|
|
92
|
+
study: `DATASET2_ADDENDUM_15_UNREGISTERED_STRUCTURAL_PROBES.md`.
|
|
93
|
+
|
|
94
|
+
**Fixed.** Both probes are now registered (§1's table). Wiring
|
|
95
|
+
`probe_splitter_contract_violation` required more than adding a registry
|
|
96
|
+
line: its own signature (`folds` first, ahead of `df`/`contract`) had
|
|
97
|
+
drifted from the registry's calling convention, and — found only by
|
|
98
|
+
measuring, not by reading the signature alone — the probe validates the
|
|
99
|
+
*random-grouped* splitter's contract specifically (no entity ever appears
|
|
100
|
+
in both train and test), a property the *temporal* splitter deliberately
|
|
101
|
+
does **not** uphold (`temporal_expanding_folds`'s own docstring: entity
|
|
102
|
+
recurrence across train/test "is allowed... this is not leakage"). Giving
|
|
103
|
+
it the temporal folds `_run_structural_probes` already had available
|
|
104
|
+
would have made it report `internal_fail` — "every evaluation result from
|
|
105
|
+
this run is unreliable" — on essentially every real audit of the
|
|
106
|
+
recurring-entity data Zekan is built for. The fix threads the
|
|
107
|
+
random-grouped folds through as a distinct, additive capability
|
|
108
|
+
(`SeverityResult.random_folds`, `_ProbeSpec.needs_random_folds`) rather
|
|
109
|
+
than reusing the temporal ones. Addendum 15 §2 and §4 have the full
|
|
110
|
+
before/after reasoning and the direct measurement
|
|
111
|
+
(`entity_overlap_count`: 100/100 entities under temporal folds, 0/0 under
|
|
112
|
+
random-grouped, on the same panel).
|
|
113
|
+
|
|
114
|
+
**Regression guard added**: a test walks `zekan/detectors/` dynamically
|
|
115
|
+
(`pkgutil`, not a hand-maintained module list — a hand-maintained list is
|
|
116
|
+
how this defect happened) and fails if any `probe_*` function exists
|
|
117
|
+
without a matching entry in `_build_probe_registry()`
|
|
118
|
+
(`tests/test_structural_probe_wiring.py::test_every_implemented_probe_module_is_registered`).
|
|
119
|
+
|
|
120
|
+
## 3. Declared in the schema, never implemented
|
|
121
|
+
|
|
122
|
+
`zekan/detectors/schema.py`'s `IssueType` enum reserves a few categories
|
|
123
|
+
that carry a full coverage-classification entry but no implementing probe
|
|
124
|
+
anywhere in the codebase: `ENTITY_CONTAMINATION` (the *confirmed*,
|
|
125
|
+
cross-fold-observed variant — distinct from the risk-advisory probe in
|
|
126
|
+
§2), `WRONG_SPLIT_STRATEGY`, `SUSPECTED_UNDECLARED_LEAK` (Upgrade 1's
|
|
127
|
+
retired lower-confidence tier — killed on calibration grounds, see
|
|
128
|
+
`UPGRADE1_CALIBRATION.md`), and `CODE_STRUCTURAL_LEAK`, which the schema
|
|
129
|
+
itself labels `SourceLayer.OUT_OF_SCOPE`. These are planned scope
|
|
130
|
+
markers, not silent gaps — the enum's own docstring calls it "the
|
|
131
|
+
scope-declaration forcing function," and `OUT_OF_SCOPE` is a real,
|
|
132
|
+
readable value a JSON consumer can check for, not an omission.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 4. Known boundaries of what the live probes actually catch
|
|
137
|
+
|
|
138
|
+
Findings below come from the Freddie Mac second-dataset validation study
|
|
139
|
+
(`zekan/benchmark/results/DATASET2_FREDDIEMAC_VALIDATION_STUDY.md`) —
|
|
140
|
+
every threshold Zekan ships was calibrated on one dataset
|
|
141
|
+
(Diabetes-130); this study is the record of what did and did not
|
|
142
|
+
transfer to an independently structured second one.
|
|
143
|
+
|
|
144
|
+
### The terminal-row check's miss set: event-stamp vs. persistent-state fields
|
|
145
|
+
|
|
146
|
+
`probe_terminal_row_presence` catches columns populated **exclusively**
|
|
147
|
+
on an entity's last row (`terminal_row_fraction == 1.0` exactly — a
|
|
148
|
+
deductive, not inductive, criterion by design,
|
|
149
|
+
`TERMINAL_ROW_PRESENCE_PREREGISTRATION.md` §9.3). Validated against a
|
|
150
|
+
hand-authored, independent ground-truth list of 24 known post-outcome
|
|
151
|
+
Freddie Mac columns: **16 of 24 hit exactly**, **0 false positives**
|
|
152
|
+
(study doc §6). The 8 misses are not random — every one is
|
|
153
|
+
outcome-adjacent for a reason *other than* row position:
|
|
154
|
+
|
|
155
|
+
| miss | why it's missed |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `MODIFICATION FLAG`, `PAYMENT DEFERRAL FLAG`, `INTEREST RATE STEP INDICATOR`, `CURRENT PERIOD MODIFICATION COSTS` | **persistent state, not an event stamp** — these recur across many rows of an affected entity's life once set (e.g. ~35 rows/loan for the modification-cost field), not a single terminal record |
|
|
158
|
+
| `BORROWER ASSISTANCE PLAN`, `DELINQUENCY DUE TO DISASTER` | current-period distress flags genuinely forward-associated with the target (93%/91% concentration) — real signal, not a row-position artifact |
|
|
159
|
+
| `MORTGAGE INSURANCE CANCELLATION INDICATOR` | dense on 1,893 of 1,905 entities — the opposite of terminal-only |
|
|
160
|
+
| `BANKRUPTCY CRAMDOWN COSTS` | the check's own named stress anchor: `terminal_row_fraction = 0.535`, a genuinely mixed pattern (a bankruptcy event that sometimes, not always, coincides with termination) — plausibly a real partial leak that no current structural check catches, recorded as an accepted limit, not discovered later as a surprise |
|
|
161
|
+
|
|
162
|
+
**The general shape of the miss set: a column whose leak-relevant state is
|
|
163
|
+
written once and persists across many subsequent rows (a flag, a status,
|
|
164
|
+
a running cost total) looks nothing like a terminal-only field to a
|
|
165
|
+
row-position check.** This is a real, principled boundary of the
|
|
166
|
+
detection mechanism, not a bug — the check is deductive precisely because
|
|
167
|
+
it only claims what row position alone can prove.
|
|
168
|
+
|
|
169
|
+
### One row per entity: the terminal-row check correctly recognizes it has nothing to check
|
|
170
|
+
|
|
171
|
+
On a frame where every entity contributes exactly one row (no lifecycle
|
|
172
|
+
for "terminal" to mean anything relative to), `probe_terminal_row_presence`
|
|
173
|
+
returns no findings — this frame shape was measured to make the check
|
|
174
|
+
fire on 40 of 40 candidates with zero discrimination before this guard
|
|
175
|
+
existed (`DATASET2_ADDENDUM_12_FRAME_T_FALSIFIED_INJECTED_ANCHOR.md` §3);
|
|
176
|
+
it is now recognized as **inapplicable**, not silently vacuous, and does
|
|
177
|
+
not count against `structural_coverage_complete`
|
|
178
|
+
(`TERMINAL_ROW_PRESENCE_PREREGISTRATION.md` §10). Zekan's core severity
|
|
179
|
+
engine likewise needs recurring per-entity observations over time — see
|
|
180
|
+
the README's "Does Zekan fit my project?" section — so this boundary
|
|
181
|
+
applies to the same class of one-row-per-item data the engine itself
|
|
182
|
+
already declines to fit.
|
|
183
|
+
|
|
184
|
+
### The screenability gate is anti-correlated with leak risk
|
|
185
|
+
|
|
186
|
+
`probe_undeclared_feature_screen` (Upgrade 1) requires a minimum
|
|
187
|
+
non-missing count and minority-class count before it will score a
|
|
188
|
+
candidate feature at all (`_MIN_NONMISSING_COUNT`,
|
|
189
|
+
`_MIN_MINORITY_CLASS_COUNT`). Measured on the Freddie Mac frame: this
|
|
190
|
+
gate excluded 16 of 63 candidate columns, and **15 of those 16 were
|
|
191
|
+
post-outcome/post-liquidation columns**
|
|
192
|
+
(`DATASET2_ADDENDUM_06_FRAME_D_PROBE_SEARCH.md` Finding 1) — the gate's
|
|
193
|
+
own sparsity floor excludes precisely the highest-risk class of column,
|
|
194
|
+
*because* outcome-derived fields are sparse for the same reason the
|
|
195
|
+
outcome itself is rare. The terminal-row check (above) closes part of
|
|
196
|
+
this specific gap for one leak shape (event-stamp fields); the gate's
|
|
197
|
+
general anti-correlation with risk remains, un-addressed, for any
|
|
198
|
+
candidate feature the terminal-row check's own narrower criterion
|
|
199
|
+
doesn't happen to also catch.
|
|
200
|
+
|
|
201
|
+
### Upgrade H's `U >= 0.99` remains untested on a second dataset
|
|
202
|
+
|
|
203
|
+
The near-bijection check has never fired on any real Freddie Mac feature,
|
|
204
|
+
across two independent runs
|
|
205
|
+
(`DATASET2_ADDENDUM_06_FRAME_D_PROBE_SEARCH.md`,
|
|
206
|
+
`DATASET2_ADDENDUM_07_FRAME_D_DROPPED.md`). This is recorded as
|
|
207
|
+
**untested, not passed** — the one pre-identified real candidate for a
|
|
208
|
+
near-bijective leak (DDLPI, a field the source data guide's own
|
|
209
|
+
definition says delinquency status is computed from) was ruled out on
|
|
210
|
+
data-quality grounds before it could reach the check: of the 27,524 rows
|
|
211
|
+
where the target actually fires, DDLPI is populated on only 47 (0.17%)
|
|
212
|
+
— too sparse for any screen to evaluate, not a clean negative result
|
|
213
|
+
(study doc §4a). The `0.99` threshold's separation margin
|
|
214
|
+
(`UPGRADE_H_CALIBRATION.md`) has only ever been measured against
|
|
215
|
+
Diabetes-130.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 5. What Zekan does not attempt, by design (v1 scope)
|
|
220
|
+
|
|
221
|
+
Stated in the audit's own "Scope note" (printed on every run) and in the
|
|
222
|
+
README's fit criteria:
|
|
223
|
+
|
|
224
|
+
- Combined or multi-feature leakage — the undeclared-feature screen is
|
|
225
|
+
strictly univariate.
|
|
226
|
+
- Notebooks, user split code, or external train/test artifacts — Zekan
|
|
227
|
+
evaluates under the contract you declare, not your actual pipeline code.
|
|
228
|
+
- One-row-per-item data with no repeated entity structure over time — the
|
|
229
|
+
core engine needs recurring per-entity observations to build temporal
|
|
230
|
+
folds at all.
|
|
231
|
+
- Regression targets and multi-class targets — v1 is yes/no prediction
|
|
232
|
+
only.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 6. See also
|
|
237
|
+
|
|
238
|
+
[`METHODOLOGY.md`](METHODOLOGY.md) — what `fixable_leakage` measures, why
|
|
239
|
+
detection and severity are separate questions, and why the calibrated
|
|
240
|
+
floors (`warn_floor`/`fail_floor`) were shown not to transfer as absolute
|
|
241
|
+
values to a second dataset.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Forbidden Zero-Cooccurrence Check
|
|
2
|
+
|
|
3
|
+
Dated 2026-08-20.
|
|
4
|
+
|
|
5
|
+
## Motivation
|
|
6
|
+
|
|
7
|
+
Frame P (`zekan/benchmark/results/DATASET2_ADDENDUM_10_FRAME_P_SEVERITY_DILUTION.md`)
|
|
8
|
+
declared 25 forbidden columns and Zekan returned `fixable_leakage=0.0085` --
|
|
9
|
+
a number that was **correct but meaningless**. That addendum's table A2
|
|
10
|
+
measured that 16 of those 25 columns (the arithmetic loss/disposition
|
|
11
|
+
family: `ACTUAL LOSS` and its components, `ZERO BALANCE CODE/EFFECTIVE
|
|
12
|
+
DATE/REMOVAL UPB`, `DELINQUENT ACCRUED INTEREST`, `DUE DATE OF LAST PAID
|
|
13
|
+
INSTALLMENT (DDLPI)`, `CUMULATIVE MODIFICATION COSTS`, `UNDERWRITING DEFECT
|
|
14
|
+
AND MAJOR SERVICING DEFECT SETTLEMENT DATE`) were **0.000000 populated on
|
|
15
|
+
every one of the frame's 4,567 positive rows**. A column that is never
|
|
16
|
+
non-missing on a positive row carries no information distinguishing
|
|
17
|
+
positives from negatives -- no leak declared through it could possibly have
|
|
18
|
+
contributed to the measured B-C gap on that frame, no matter what the
|
|
19
|
+
arithmetic landed on. Zekan already had every number needed to notice this
|
|
20
|
+
(non-missing counts, target values) and said nothing. This check closes
|
|
21
|
+
that gap.
|
|
22
|
+
|
|
23
|
+
## Criterion
|
|
24
|
+
|
|
25
|
+
No pre-registration measurement was needed before implementing this check
|
|
26
|
+
-- the claim is deductive, not statistical:
|
|
27
|
+
|
|
28
|
+
For each column in `forbidden_after_prediction`, count non-missing rows
|
|
29
|
+
where `target == 1` (the positive class; this project's targets are
|
|
30
|
+
0/1-encoded binary columns throughout, the same convention
|
|
31
|
+
`near_bijection_probe.py` already relies on). If that count is **zero**,
|
|
32
|
+
the declared leak cannot contribute to `B - C` on this frame: the column
|
|
33
|
+
carries no information distinguishing positives, so any severity number
|
|
34
|
+
computed with it among the forbidden set is structurally pinned toward
|
|
35
|
+
whatever the *other* forbidden columns alone would have produced.
|
|
36
|
+
|
|
37
|
+
Counting only -- no model, no folds, no target relationship beyond
|
|
38
|
+
non-missing presence at the row level.
|
|
39
|
+
|
|
40
|
+
## Behavior
|
|
41
|
+
|
|
42
|
+
- **Annotate-only.** Never changes `policy_decision.verdict`, same as every
|
|
43
|
+
other structural probe in `zekan/severity/audit.py::_build_probe_registry`.
|
|
44
|
+
- `confirmed=True`, scoped to the structural **fact** (zero co-occurrence)
|
|
45
|
+
-- never to severity. It says nothing about whether *other* declared
|
|
46
|
+
forbidden columns on the same frame carried a real, measurable leak.
|
|
47
|
+
- Message states meaning first: `"'{col}' is never populated on any row
|
|
48
|
+
where {target} is positive, so declaring it forbidden cannot change the
|
|
49
|
+
measured severity on this frame"`.
|
|
50
|
+
- Action line points at the contract or the target's own definition --
|
|
51
|
+
deliberately nothing about imputation, since imputing a value that was
|
|
52
|
+
never observed on a positive row would manufacture information that does
|
|
53
|
+
not exist, not recover it.
|
|
54
|
+
- Reports the count and the positive-row total as evidence
|
|
55
|
+
(`n_non_missing_on_positive_rows` / `n_positive_rows`).
|
|
56
|
+
- When **every** declared forbidden column meets the criterion, that is a
|
|
57
|
+
distinct, stronger statement: the entire declared forbidden set is
|
|
58
|
+
structurally incapable of contributing to `fixable_leakage` on this
|
|
59
|
+
frame, so the audit could not have found anything through it, whatever
|
|
60
|
+
number came back. Every per-column record carries `all_forbidden_zero`
|
|
61
|
+
in its evidence, and its `what` text states the stronger claim directly
|
|
62
|
+
rather than leaving the reader to infer it from N separate identical-
|
|
63
|
+
looking records.
|
|
64
|
+
|
|
65
|
+
See `zekan/detectors/forbidden_zero_cooccurrence_probe.py` for the
|
|
66
|
+
implementation and `zekan/detectors/schema.py`'s `FORBIDDEN_ZERO_COOCCURRENCE`
|
|
67
|
+
registry row / `ForbiddenZeroCooccurrenceDetail` for the typed evidence.
|
|
68
|
+
|
|
69
|
+
## Interaction with the structural coverage caveat (020a46a)
|
|
70
|
+
|
|
71
|
+
`zekan/severity/audit.py::_compute_structural_coverage` (introduced in
|
|
72
|
+
`020a46a`) marks a category as a coverage **gap** in exactly two cases: a
|
|
73
|
+
probe was silently skipped because its declared capability precondition
|
|
74
|
+
wasn't met (`needs_folds`/`needs_random_folds` and the fold set wasn't
|
|
75
|
+
available), or a probe raised and was isolated as `PROBE_FAILED`. Neither
|
|
76
|
+
condition applies here: `probe_forbidden_zero_cooccurrence` declares no
|
|
77
|
+
capability flags (same registration shape as
|
|
78
|
+
`probe_forbidden_entity_level_aggregate`) and runs unconditionally to
|
|
79
|
+
completion on every audit.
|
|
80
|
+
|
|
81
|
+
**Decision: this is NOT a coverage gap.** The check ran and returned a
|
|
82
|
+
record -- that record being "the declared forbidden set is structurally
|
|
83
|
+
incapable of measuring anything here" is the *informative* case
|
|
84
|
+
(`_compute_structural_coverage`'s own docstring: "a probe that ran and
|
|
85
|
+
returned real IssueRecords -- zero, one, or every candidate flagged --
|
|
86
|
+
counts as covered, full stop... never an informativeness check"), the same
|
|
87
|
+
bucket `probe_terminal_row_presence` already occupies when it fires on
|
|
88
|
+
every single candidate on a degenerate cross-sectional frame
|
|
89
|
+
(`DATASET2_ADDENDUM_12_FRAME_T_FALSIFIED_INJECTED_ANCHOR.md` section 3;
|
|
90
|
+
`tests/test_structural_coverage.py::test_vacuous_but_completed_probe_is_not_a_gap`).
|
|
91
|
+
The finding itself -- loud, in `structural_annotations`, `status="fail"`,
|
|
92
|
+
`confirmed=True` -- is the warning. `structural_coverage_complete` stays
|
|
93
|
+
`True` when this is the only thing that fired. Verified directly:
|
|
94
|
+
`tests/test_forbidden_zero_cooccurrence_probe.py::test_all_zero_is_not_a_coverage_gap`.
|
zekan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zekan 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.
|