dataforge-playground / dataforge /calibration.py
Praneshrajan15's picture
Deploy DataForge playground API
13fe504 verified
Raw
History Blame Contribute Delete
7.54 kB
"""Calibration and abstention policy for DataForge repairs.
This is the product differentiator made measurable: DataForge auto-applies a
repair only when the proposing detector's calibrated confidence clears a
threshold fit to a target precision (default 0.95). Below that, the repair is
*proposed for review*, never silently applied. The tool is therefore broad and
safe at once - coverage rises while auto-apply precision stays high.
The policy is advisory to the detection/repair stack: a repairer marks a
proposed fix's confidence, and :meth:`AbstentionPolicy.action_for` decides
whether it is eligible for auto-apply. The existing SMT verifier and safety
constitution remain hard gates underneath - abstention only ever makes the
system *more* conservative, never less.
Thresholds are fit empirically (:func:`fit_thresholds`) against labeled
``(confidence, was_correct)`` samples from a benchmark run, so the auto-apply
boundary is grounded in measured precision, not guesswork.
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
__all__ = [
"AbstentionAction",
"AbstentionPolicy",
"corrector_default_policy",
"default_policy",
"fit_thresholds",
"policy_from_corrector_samples",
"severity_for_action",
]
AbstentionAction = Literal["auto_apply", "review"]
# Conservative defaults. Detector families whose deterministic proposals are
# provably exact (decimal_shift uses an arithmetic inverse; fd_violation a
# strict majority) auto-apply at lower confidence; fuzzier families require
# higher confidence before auto-apply.
_DEFAULT_THRESHOLDS: dict[str, float] = {
"decimal_shift": 0.70,
"fd_violation": 0.80,
"type_mismatch": 0.80,
"format_violation": 0.90,
"categorical_normalization": 0.90,
"missing_value": 1.01, # detection-only by default: never auto-apply
"outlier": 1.01, # detection-only: flag, do not auto-fix
"duplicate_row": 1.01, # detection-only: row deletes are constitution-blocked
}
class AbstentionPolicy(BaseModel):
"""Maps an issue's calibrated confidence to an auto-apply / review decision.
Args:
target_precision: The precision the auto-apply thresholds were fit for.
auto_apply_thresholds: Per-issue-type minimum confidence to auto-apply.
default_threshold: Threshold for issue types not listed.
"""
target_precision: float = Field(default=0.95, ge=0.0, le=1.0)
auto_apply_thresholds: dict[str, float] = Field(default_factory=dict)
default_threshold: float = Field(default=0.90, ge=0.0, le=1.01)
model_config = {"frozen": True}
def threshold_for(self, issue_type: str) -> float:
"""Return the auto-apply confidence threshold for an issue type."""
return self.auto_apply_thresholds.get(issue_type, self.default_threshold)
def action_for(self, issue_type: str, confidence: float) -> AbstentionAction:
"""Decide whether a proposed fix may auto-apply or must be reviewed.
Args:
issue_type: The detector issue type.
confidence: The proposing detector's calibrated confidence in [0, 1].
Returns:
``"auto_apply"`` if confidence clears the threshold, else ``"review"``.
"""
return "auto_apply" if confidence >= self.threshold_for(issue_type) else "review"
def default_policy() -> AbstentionPolicy:
"""Return the conservative default abstention policy."""
return AbstentionPolicy(
target_precision=0.95,
auto_apply_thresholds=dict(_DEFAULT_THRESHOLDS),
default_threshold=0.90,
)
def severity_for_action(action: AbstentionAction) -> str:
"""Map an abstention action to the detector severity label."""
return "safe" if action == "auto_apply" else "review"
def corrector_default_policy() -> AbstentionPolicy:
"""Return the honest default policy for the LLM corrector: propose-not-apply.
Until per-class thresholds are fit from measured corrector correctness (see
:func:`policy_from_corrector_samples`), every corrector proposal is surfaced
as a human-review suggestion and never auto-applied. The high target
precision is recorded so the intent of the boundary is explicit.
"""
return AbstentionPolicy(
target_precision=0.95,
auto_apply_thresholds={},
default_threshold=1.01,
)
def policy_from_corrector_samples(
samples_by_class: dict[str, list[tuple[float, bool]]],
*,
target_precision: float = 0.95,
min_support: int = 10,
) -> AbstentionPolicy:
"""Build a corrector abstention policy from labeled correctness samples.
Fits a per-class auto-apply threshold to the precision floor (reusing
:func:`fit_thresholds`) and keeps the propose-not-apply default for any
class that is unlisted, low-support, or cannot reach the floor. The result
auto-applies only where measured precision justifies it; everything else
becomes a review suggestion.
"""
thresholds = fit_thresholds(
samples_by_class,
target_precision=target_precision,
min_support=min_support,
)
return AbstentionPolicy(
target_precision=target_precision,
auto_apply_thresholds=thresholds,
default_threshold=1.01,
)
def fit_thresholds(
samples_by_class: dict[str, list[tuple[float, bool]]],
*,
target_precision: float = 0.95,
min_support: int = 10,
) -> dict[str, float]:
"""Fit per-class auto-apply confidence thresholds to a target precision.
For each class, finds the lowest confidence threshold ``t`` such that the
predictions with ``confidence >= t`` achieve at least ``target_precision``.
This maximizes recall subject to the precision floor. Classes with too few
samples, or that cannot reach the target at any threshold, get ``1.01``
(never auto-apply) - the honest, conservative default.
Args:
samples_by_class: ``{issue_type: [(confidence, was_correct), ...]}``.
target_precision: Minimum precision the threshold must guarantee.
min_support: Minimum labeled samples required to fit a class.
Returns:
``{issue_type: threshold}``. A threshold of 1.01 means detection-only.
"""
thresholds: dict[str, float] = {}
for issue_type, samples in samples_by_class.items():
if len(samples) < min_support:
thresholds[issue_type] = 1.01
continue
# Candidate thresholds are the observed confidences (descending): adding
# each next-lower confidence grows the auto-apply set. Precision is only
# evaluated at confidence-group boundaries so tied confidences (which a
# threshold includes together) are scored together.
ordered = sorted(samples, key=lambda s: s[0], reverse=True)
best_threshold = 1.01
applied = 0
correct = 0
index = 0
n = len(ordered)
while index < n:
confidence = ordered[index][0]
while index < n and ordered[index][0] == confidence:
applied += 1
correct += 1 if ordered[index][1] else 0
index += 1
if correct / applied >= target_precision:
best_threshold = confidence
thresholds[issue_type] = round(best_threshold, 4)
return thresholds