Skip to content

Commit 5250a11

Browse files
rsasaki0109claude
andcommitted
feat(gsdc2023): add TripWhitelistDDGate (default-deny DD-carrier gate)
Phase 80 showed the broad DDAnchorGate (anchor coverage >= 0.6) is net-negative on Kaggle: ~84% of DD rows fell in p95_delta>=3m trips, with a 105.72m spike on ebf-y/pixel5. Broad DD gating cannot be rescued by a single floor. TripWhitelistDDGate keeps fgo_dd_carrier only for trips that are explicitly whitelisted, or that clear conservative internal-consistency thresholds (anchor>=0.8, dd_epochs>=1, dd_pairs_mean>=4.0, no_tdcp absent) -- stricter than the 0.6 floor. A deny-list always wins. It reads only internal signals (never truth), so an offline A/B audit can emit the whitelist and production replays it deterministically. It is a duck-typed drop-in for CombinedGate(dd=...). Adds 8 unit tests (incl. the marginal trip the broad gate kept but this denies, allow/deny precedence, DD-density floor, and CombinedGate substitution). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 240e2e2 commit 5250a11

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

experiments/gsdc2023_ab_gates.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
``passthrough``: gates never block them, but they also do not constitute a
1414
reason to keep the submission's row delta.
1515
16+
``TripWhitelistDDGate`` is a stricter, default-deny replacement for
17+
``DDAnchorGate`` (Phase 80 follow-up) that drops into ``CombinedGate(dd=...)``.
18+
1619
Inputs are the dataclasses from :mod:`gsdc2023_ab_source_mix` and
1720
:mod:`gsdc2023_ab_dd_signals`; gates do not touch JSON.
1821
"""
@@ -87,6 +90,51 @@ def decide(self, signals: DDSignals, counts: SourceCounts) -> Disposition:
8790
return Disposition.REVERTED
8891

8992

93+
@dataclass(frozen=True)
94+
class TripWhitelistDDGate:
95+
"""Default-deny DD-carrier gate (Phase 80 follow-up).
96+
97+
The broad :class:`DDAnchorGate` (anchor coverage >= 0.6) was net-negative on
98+
Kaggle: ~84% of DD rows landed in ``p95_delta >= 3 m`` trips, with a 105.72 m
99+
spike on ``ebf-y/pixel5``. Rather than relax/tighten a single floor, this gate
100+
keeps ``fgo_dd_carrier`` only for trips that are **explicitly whitelisted**,
101+
or that clear **conservative internal-consistency thresholds** (stricter than
102+
0.6). A deny-list always wins. It reads only internal signals (anchor
103+
coverage, DD density) -- never truth -- so an offline audit can emit the
104+
whitelist and production can replay it deterministically.
105+
106+
Drop-in for :class:`CombinedGate` (``CombinedGate(dd=TripWhitelistDDGate(...))``):
107+
same ``decide(signals, counts) -> bool`` shape as :class:`DDAnchorGate`.
108+
``allow`` / ``deny`` accept any container supporting ``in`` (set, frozenset,
109+
list); trip ids must already be normalized (no ``train/`` / ``test/`` prefix).
110+
"""
111+
112+
allow: frozenset = frozenset()
113+
deny: frozenset = frozenset()
114+
min_anchor_coverage: float = 0.8
115+
min_dd_epochs: int = 1
116+
min_dd_pairs_mean: float = 4.0
117+
require_no_tdcp_absent: bool = True
118+
119+
def decide(self, signals: DDSignals, counts: SourceCounts) -> bool:
120+
if counts.fgo_dd_carrier <= 0:
121+
return False
122+
if signals.trip_id in self.deny:
123+
return False
124+
if signals.trip_id in self.allow:
125+
return True
126+
# Default-deny: only trips with strong internal evidence survive.
127+
if self.require_no_tdcp_absent and counts.fgo_no_tdcp > 0:
128+
return False
129+
if signals.anchor_coverage < self.min_anchor_coverage:
130+
return False
131+
if signals.dd_dd_epochs < self.min_dd_epochs:
132+
return False
133+
if signals.dd_pairs_mean < self.min_dd_pairs_mean:
134+
return False
135+
return True
136+
137+
90138
@dataclass(frozen=True)
91139
class TripDisposition:
92140
"""Result of applying ``CombinedGate`` to a single trip."""
@@ -144,6 +192,7 @@ def disposition_counts(dispositions: Iterable[TripDisposition]) -> dict[str, int
144192
"Disposition",
145193
"NoTdcpCoexistGate",
146194
"TripDisposition",
195+
"TripWhitelistDDGate",
147196
"apply_gate",
148197
"disposition_counts",
149198
]

tests/test_gsdc2023_ab_gates.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
DDAnchorGate,
77
Disposition,
88
NoTdcpCoexistGate,
9+
TripWhitelistDDGate,
910
apply_gate,
1011
disposition_counts,
1112
)
@@ -85,6 +86,62 @@ def test_ntdc_gate_blocks_when_anchor_below_floor():
8586
assert gate.decide(_signals("a", anchor_cov=0.5), _counts("a", no_tdcp=100)) is False
8687

8788

89+
# --- TripWhitelistDDGate ---------------------------------------------------
90+
91+
92+
def test_whitelist_gate_requires_dd_rows_present():
93+
gate = TripWhitelistDDGate()
94+
assert gate.decide(_signals("a", anchor_cov=0.95), _counts("a", dd=0)) is False
95+
96+
97+
def test_whitelist_gate_default_denies_marginal_trip_that_broad_gate_kept():
98+
# anchor 0.65 clears the old 0.6 floor but not the conservative 0.8 default.
99+
broad = DDAnchorGate(min_anchor_coverage=0.6)
100+
strict = TripWhitelistDDGate()
101+
sig = _signals("a", anchor_cov=0.65)
102+
cnt = _counts("a", dd=200)
103+
assert broad.decide(sig, cnt) is True
104+
assert strict.decide(sig, cnt) is False
105+
106+
107+
def test_whitelist_gate_keeps_high_confidence_trip_by_internal_signals():
108+
gate = TripWhitelistDDGate()
109+
assert gate.decide(_signals("a", anchor_cov=0.9), _counts("a", dd=200)) is True
110+
111+
112+
def test_whitelist_gate_allow_overrides_failing_internal_signals():
113+
gate = TripWhitelistDDGate(allow={"a"})
114+
# Would fail the conservative floor, but the explicit allow keeps it.
115+
assert gate.decide(_signals("a", anchor_cov=0.3), _counts("a", dd=200)) is True
116+
117+
118+
def test_whitelist_gate_deny_overrides_everything():
119+
gate = TripWhitelistDDGate(allow={"a"}, deny={"a"})
120+
# Deny wins even over allow and strong internal evidence.
121+
assert gate.decide(_signals("a", anchor_cov=0.99), _counts("a", dd=200)) is False
122+
123+
124+
def test_whitelist_gate_blocks_low_dd_density():
125+
gate = TripWhitelistDDGate(min_dd_pairs_mean=4.0)
126+
sig = DDSignals(
127+
trip_id="a", n_epochs=1000, dd_anchor_epochs=900,
128+
dd_dd_epochs=900, dd_base_snapped_epochs=900, dd_pairs_mean=2.0,
129+
)
130+
assert gate.decide(sig, _counts("a", dd=200)) is False
131+
132+
133+
def test_whitelist_gate_blocks_no_tdcp_coexistence_by_default():
134+
gate = TripWhitelistDDGate()
135+
assert gate.decide(_signals("a", anchor_cov=0.9), _counts("a", dd=200, no_tdcp=100)) is False
136+
137+
138+
def test_whitelist_gate_drops_into_combined_gate():
139+
# Duck-typed substitution for the DD leg of CombinedGate.
140+
gate = CombinedGate(dd=TripWhitelistDDGate(allow={"a"}))
141+
assert gate.decide(_signals("a", anchor_cov=0.2), _counts("a", dd=200)) is Disposition.KEPT
142+
assert gate.decide(_signals("b", anchor_cov=0.65), _counts("b", dd=200)) is Disposition.REVERTED
143+
144+
88145
# --- CombinedGate ----------------------------------------------------------
89146

90147

0 commit comments

Comments
 (0)