API reference¶
Generated from the source, so it cannot drift from the code.
Core¶
bdp_model_gate.core.context.StructuredGateContext
dataclass
¶
StructuredGateContext(
model=None,
X=None,
y_true=None,
y_pred=None,
protected_df=None,
latencies_ms=None,
cost_per_inference=None,
model_card=None,
generate_fn=None,
expected_loss=None,
predict_fn=None,
predict_proba_fn=None,
gradient_fn=None,
class_order=None,
favourable_classes=None,
modality="structured",
task="auto",
)
Everything a structured-data governance check needs to run.
Only model, X, y_true, and y_pred are required. Every other
field is optional — omitting one simply causes the checks that depend
on it to report NOT_APPLICABLE rather than raise or fail the gate.
All inputs are validated eagerly by ModelGate.run() before any check
executes; see bdp_model_gate.core.validation.
Attributes:
| Name | Type | Description |
|---|---|---|
model |
Any
|
A fitted model exposing |
X |
DataFrame
|
Feature dataframe used for validation/inference. |
y_true |
Sequence[Any] | None
|
Ground-truth labels for the validation set. |
y_pred |
Sequence[Any] | None
|
Model predictions on X (probabilities or hard labels, per your metric). |
protected_df |
DataFrame | None
|
Dataframe of protected attributes (gender, region, etc.), row-aligned to X. Needed for the fairness checks. |
latencies_ms |
Sequence[float] | None
|
Per-request inference latencies from a benchmark run, for the performance gate. |
cost_per_inference |
float | None
|
Estimated or measured cost per inference, for the performance gate. |
model_card |
dict | None
|
Dict describing the model (legal_basis, use_case, etc.), for the compliance gate. |
generate_fn |
Callable[[str], str] | None
|
callable(str) -> str, the entry point of any generative component sitting alongside the structured model, for prompt injection testing. |
expected_loss |
Sequence[float] | None
|
Per-row expected loss (or technical/pure premium),
row-aligned to X. Enables |
predict_fn |
Callable[[DataFrame], Any] | None
|
|
predict_proba_fn |
Callable[[DataFrame], Any] | None
|
|
gradient_fn |
Callable[[DataFrame], Any] | None
|
|
class_order |
Sequence[Any] | None
|
For multiclass problems, the class labels in ascending order of favourability, e.g. ["decline", "refer", "accept"]. Supplying it marks the problem as ordinal, unlocking metrics that know a decline-vs-accept error costs more than a refer-vs-accept one. Omit it for a nominal problem where no ordering exists. |
favourable_classes |
Sequence[Any] | None
|
Which outcomes count as a positive result for
demographic parity. Defaults to the most favourable class when
|
task |
str
|
"auto" (default), "binary", "multiclass" or "regression".
"auto" infers from y_true and logs what it inferred. Set it
explicitly for anything you gate on: a claims-frequency target of
0/1/2/3 is indistinguishable from a four-class problem by shape.
See |
bdp_model_gate.core.gate.ModelGate
¶
Runs a list of governance checks against a context and aggregates a GateReport.
If no checks are supplied, defaults to the full structured-data check
suite (built-in checks plus any registered via the plugin entry-point
group — see bdp_model_gate.registry). Pass a custom checks list to
run a subset, add your own checks (subclass BaseCheck), or reorder
blocking behavior.
Source code in bdp_model_gate/core/gate.py
bdp_model_gate.core.report.GateReport
dataclass
¶
Aggregated results of a gate run.
model_metric/model_score are the headline score lifted from the
performance check — whichever metric was configured, not always AUC.
Both are None if no performance check ran or the score was unavailable.
model_auc
property
¶
Deprecated. Returns model_score only when the configured
metric really was ROC AUC, and None otherwise — reading it as
"the AUC" was only ever correct by coincidence.
gate_status
property
¶
BLOCKED if any blocking check failed, NEEDS_REVIEW if only non-blocking checks failed, PASS otherwise.
to_html
¶
Renders the report as one self-contained HTML page.
A NEEDS_REVIEW verdict asks a person to decide, and to_json()
hands that person a blob. This hands them a page: the verdict, every
finding, and each check's plot beside the number it explains — no
network, no JavaScript, nothing to install to open it.
Plots need the checks and the data they scored, which ModelGate.run
attaches to the report for you. Pass checks and context explicitly
to override, or include_plots=False for text only. Without the
[plots] extra the page renders text-only by itself.
Source code in bdp_model_gate/core/report.py
bdp_model_gate.core.base.CheckResult
dataclass
¶
CheckResult(
check_name,
category,
flag,
detail="",
blocking=True,
metadata=dict(),
duration_ms=None,
)
The outcome of a single governance check.
flag is "OK", "NOT_APPLICABLE" (check skipped — e.g. optional input
missing or optional dependency not installed), "CHECK_ERROR" (the check
raised an exception), or a check-specific risk string such as
"PROXY_RISK" or "PII_LEAKAGE_RISK".
bdp_model_gate.core.base.BaseCheck
¶
Interface every governance check implements.
Subclasses set name, category, blocking and optionally
supported_tasks as class attributes, and implement run(context). blocking=True means a failing flag from
this check should block promotion outright; blocking=False routes a
failure to human review instead (used for checks that need judgment,
like fairness flags that may be false positives).
plot
¶
Draw this check's finding, or return None if it has none to draw.
Optional. The report renderer calls plot() on every check and uses
whatever comes back, so there is nothing to declare or register — an
override is the whole opt-in.
Implementations take an optional matplotlib Axes and return it,
which is what lets a caller compose these into their own figure and
restyle the result. Draw only where the shape matters: a check whose
finding is genuinely one number should leave this alone rather than
chart it.
Anything a plot needs beyond CheckResult.metadata is recomputed here
rather than stored on the result, so the archival JSON does not carry
presentation data most consumers never read. A check that scored a
subsample must redraw the same rows the finding came from — use
bdp_model_gate._sampling.stable_sample, which is content-addressed
and so returns the same rows by construction.
Source code in bdp_model_gate/core/base.py
Configuration¶
bdp_model_gate.config
¶
Configuration dataclasses for every gate category. Override any field to tune thresholds per model/use case; defaults are reasonable starting points, not regulatory guidance.
FairnessConfig
dataclass
¶
FairnessConfig(
disparity_threshold=0.1,
decision_threshold=0.5,
equalised_odds_threshold=0.1,
subgroup_calibration_threshold=0.05,
n_calibration_bins=10,
intersectional=False,
mean_gap_threshold=0.1,
error_parity_threshold=0.2,
calibration_threshold=0.1,
loss_ratio_threshold=0.1,
min_group_size=30,
proxy_corr_threshold=0.3,
shap_gap_threshold=0.5,
counterfactual_shift_threshold=0.05,
)
Thresholds for the fairness checks.
decision_threshold turns continuous predictions into class labels for
DisparateImpactCheck, which measures selection rates and so needs
hard classes. Predictions already in {0, 1} are used as-is.
Every gap threshold here is relative, not absolute: each is measured
as a fraction of the corresponding overall figure — the overall mean,
the overall error, or the mean absolute SHAP contribution. That is what
lets one default be meaningful for a premium in naira, a claim count and
a probability alike. An absolute threshold in the units of the model
output flags nothing on one scale and everything on another.
min_group_size guards against a three-policy segment producing a wild
ratio that reads as a fairness finding.
PerformanceConfig
dataclass
¶
PerformanceConfig(
metric=AUTO,
min_score=0.8,
max_error=None,
max_ece=0.1,
n_calibration_bins=10,
calibration_strategy="uniform",
decision_threshold=0.5,
average="macro",
max_latency_ms_p95=200.0,
max_cost_per_inference=0.002,
)
Thresholds for the performance gate.
metric selects how the model is scored — a name from
bdp_model_gate.metrics.BUILTIN_METRICS ("roc_auc", "accuracy", "f1",
"precision", "recall", "balanced_accuracy", "average_precision"), a
fn(y_true, y_pred) -> float callable of your own, or "auto" to use
whichever of roc_auc/accuracy the installed dependencies support. Under
"auto" a fallback is logged and named in the report, never silent.
Metrics point in different directions. Higher-is-better metrics
(roc_auc, f1, r2, ...) are gated with min_score; error metrics where
lower is better (rmse, mae, mape, poisson_deviance) are gated with
max_error, which has no default because a sensible ceiling depends
entirely on the scale of your target. Configuring an error metric
without max_error is a GateConfigurationError rather than a guess.
min_score/max_error are interpreted against whichever metric ran, so
set the metric and its threshold together.
average is the multiclass averaging strategy for f1, precision and
recall (scikit-learn's average=). It defaults to "macro", which
weights every class equally — so a rarely predicted "decline" counts as
much as a common "accept". Use "weighted" to weight by support instead.
Ignored for binary and regression. decision_threshold is used to binarize continuous
predictions for metrics that need hard class labels; it's ignored for
ranking metrics like roc_auc and for custom callables.
min_accuracy
property
writable
¶
Deprecated alias for min_score.
The old name was misleading: the threshold was compared against
ROC AUC whenever scikit-learn was installed, and against accuracy
otherwise. Kept working so existing configs and CLI --config
files don't break.
Tasks and classes¶
bdp_model_gate.task
¶
Prediction-task identification.
Nearly half the check suite is task-agnostic — PII scanning, prompt injection, model-card compliance and proxy correlation care about features and documentation, not about what the model predicts. The rest is not: demographic parity needs a favourable class, a "prediction flip" means nothing for a continuous output, and ROC AUC cannot score a premium.
Every check therefore declares the tasks it supports via
BaseCheck.supported_tasks, and reports NOT_APPLICABLE for the others
rather than producing a confident, meaningless number.
StructuredGateContext.task names the task. It defaults to "auto", which
infers from y_true and logs what it inferred — inference is genuinely
ambiguous (a claims-frequency target of 0/1/2/3 is indistinguishable from a
four-class problem by shape alone), so the guess is never silent. State
task explicitly for anything you intend to gate on.
resolve_task
¶
Returns the concrete task for a context, inferring it if set to "auto".
Raises GateConfigurationError for an unknown setting. When "auto" is
asked to infer from a context with no y_true, it warns and falls back
to "binary" — the assumption every release before 0.3.0 made implicitly.
Source code in bdp_model_gate/task.py
infer_task
¶
Best-effort task inference from the ground-truth labels.
Deliberately conservative and explainable rather than clever: strings and booleans are classification, two distinct values are binary, integral targets with few distinct values are multiclass, everything else is regression.
Source code in bdp_model_gate/task.py
validate_task
¶
Rejects an unusable task setting before any check runs.
Source code in bdp_model_gate/task.py
supports
¶
Whether check declares support for task. Checks predating
supported_tasks are treated as task-agnostic.
bdp_model_gate.classes
¶
Class ordering and favourable outcomes for multiclass problems.
Binary classification carries two conventions for free: class 1 is the positive class, and there is nothing to order. Multiclass has neither, and guessing at them is how a governance tool produces a confident wrong answer.
Two pieces of problem structure make the multiclass checks meaningful:
class_order The classes in ascending order of favourability,
e.g. ["decline", "refer", "accept"]. Supplying it
marks the problem as **ordinal**, which unlocks
metrics that know a decline-vs-accept error costs
more than a refer-vs-accept one. Omit it for a
genuinely nominal problem (product category, say),
where no ordering exists.
favourable_classes Which outcomes count as a positive result for
demographic parity. Defaults to the single most
favourable class when `class_order` is given, and
is logged when inferred.
Both live on the context rather than in GateConfig, because they describe
the problem rather than a threshold you might tune per run.
resolve_favourable
¶
Determines which classes count as a favourable outcome.
Returns None when it cannot be determined, which callers report as NOT_APPLICABLE rather than guessing — for a nominal multiclass problem there is no basis for picking one.
Source code in bdp_model_gate/classes.py
to_ranks
¶
Converts class labels into ordinal ranks.
Rank distance is what makes an ordinal metric ordinal: predicting "decline" for an "accept" case is two steps wrong, "refer" is one.
Source code in bdp_model_gate/classes.py
validate_class_order
¶
Rejects an unusable class_order before any check runs.
Source code in bdp_model_gate/classes.py
favourable_mask
¶
Boolean mask of rows whose label is a favourable outcome.
Source code in bdp_model_gate/classes.py
Metrics¶
bdp_model_gate.metrics
¶
Metric resolution for the performance gate.
The performance gate scores a model with whichever metric the caller
configured (PerformanceConfig.metric). This module owns the mapping from
that config value to a callable, and — importantly — makes the choice
explicit in the report rather than silently depending on which optional
dependencies happen to be installed.
Three kinds of value are accepted:
"auto" try the metrics in AUTO_PREFERENCE in order, using the
first one whose dependencies are available. A fallback is
logged at WARNING level and named in the check's output,
so it never happens invisibly.
"<name>" any key of BUILTIN_METRICS. If its dependencies are
missing, that's a GateConfigurationError — an explicit
request is never silently substituted.
callable any `fn(y_true, y_pred) -> float`. Called with y_pred
exactly as supplied (no thresholding), since only the
caller knows what their metric expects.
Metrics differ in what they want from y_pred: ranking metrics like
roc_auc need continuous scores, while accuracy/f1/precision/
recall need hard class labels. needs_hard_labels records which, and
the check binarizes at PerformanceConfig.decision_threshold when needed.
MetricSpec
dataclass
¶
MetricSpec(
name,
sklearn_fn,
needs_hard_labels,
fallback=None,
greater_is_better=True,
tasks=CLASSIFICATION_TASKS,
needs_average=False,
needs_class_order=False,
)
How to obtain one named metric, and what it expects from y_pred.
ResolvedMetric
dataclass
¶
ResolvedMetric(
name,
fn,
needs_hard_labels,
greater_is_better=True,
is_fallback=False,
used_fallback_impl=False,
)
A metric ready to call, plus how it was arrived at.
resolve_metric
¶
Turns a config value into a callable metric.
Raises GateConfigurationError if an explicitly named metric can't be satisfied — the gate reports that as a blocking CHECK_ERROR rather than scoring the model with something the caller didn't ask for.
Source code in bdp_model_gate/metrics.py
validate_metric
¶
Cheap, import-free check that metric is a usable setting.
Called when the check is constructed so a typo'd metric name fails at
configuration time rather than midway through a gate run. Whether the
metric's dependencies are actually installed is deliberately not
checked here — see resolve_metric.
Source code in bdp_model_gate/metrics.py
to_hard_labels
¶
Binarizes continuous scores for a metric that needs class labels.
Values already restricted to {0, 1} are passed through untouched, so
callers who supply hard labels aren't affected by decision_threshold.
Source code in bdp_model_gate/metrics.py
to_class_labels
¶
Reduces multiclass predictions to one label per row.
Accepts predicted labels as-is. An (n, n_classes) probability matrix is
reduced by argmax, mapped back through class_order when it is known so
the result is comparable with y_true rather than a bare column index.
Source code in bdp_model_gate/metrics.py
ordinal_mae
¶
Mean absolute error in rank space.
The point of an ordinal metric: for accept/refer/decline, predicting "decline" on an "accept" case is two steps wrong while "refer" is one. Plain accuracy scores both as a single mistake, which is exactly the distinction an underwriting gate needs to make.
Source code in bdp_model_gate/metrics.py
quadratic_kappa
¶
Cohen's kappa with quadratic weights — the standard ordinal agreement measure. 1.0 is perfect, 0.0 is chance, and negative is worse than chance. Disagreements are penalised by the square of their rank distance, so a two-step error costs four times a one-step one.
Source code in bdp_model_gate/metrics.py
Model adapter¶
bdp_model_gate.model.ModelAdapter
¶
Normalises any supported model into predict / predict_proba / gradients.
Source code in bdp_model_gate/model.py
describe
¶
How predictions are obtained, for logs and result metadata.
Source code in bdp_model_gate/model.py
predict
¶
Point predictions as a 1-D array.
Source code in bdp_model_gate/model.py
predict_positive_proba
¶
Probability of the positive class, as a 1-D array.
Normalises the shapes different frameworks emit for binary
classification: scikit-learn's (n, 2), Keras's (n, 1) from a
sigmoid, and a bare (n,) vector are all reduced to one column.
Source code in bdp_model_gate/model.py
predict_proba_matrix
¶
Full (n_rows, n_classes) probability matrix, for multiclass.
Unlike predict_positive_proba this keeps every column, because a
multiclass check needs to pick out one or more favourable classes.
Source code in bdp_model_gate/model.py
gradients
¶
Per-row, per-feature gradients of the output, if available.
Returns an (n_rows, n_features) array aligned to X.columns, or None
when no gradient_fn was supplied. A mis-shaped result is refused
rather than broadcast into a meaningless perturbation.
Source code in bdp_model_gate/model.py
Checks¶
Fairness¶
bdp_model_gate.structured.fairness
¶
Per-feature and outcome-level fairness checks for structured data models.
ProxyCorrelationCheck
¶
Bases: BaseCheck
Flags numeric input features that correlate strongly with a protected attribute — even when that attribute itself is excluded from the model.
Source code in bdp_model_gate/structured/fairness.py
plot
¶
Heatmap of eta^2, feature by protected attribute.
Replaces a table that runs to one row per pair — forty on a modest model. The eye finds the hot cell in a grid immediately and cannot scan forty rows for it, and the cool cells matter too: they are the evidence that the flagged feature is the exception rather than the whole feature set leaking.
Source code in bdp_model_gate/structured/fairness.py
DisparateImpactCheck
¶
Bases: BaseCheck
Outcome-level disparity check per protected attribute (demographic parity).
For multiclass, "predicted positive" means predicted into
context.favourable_classes — for underwriting, typically ["accept"].
That set defaults to the most favourable entry of context.class_order
when one is given; with neither, the check reports NOT_APPLICABLE rather
than picking a class arbitrarily, because which outcome counts as
favourable is a judgement the data cannot supply.
Demographic parity compares selection rates — the share of each group
predicted positive — so it needs hard class labels. Continuous
predictions are binarised at config.decision_threshold before being
handed to fairlearn; predictions already in {0, 1} pass through
untouched. Without that step a probability y_pred yields a selection
rate of 0 in every group and a parity difference of exactly 0.0, which
reads as "perfectly fair" no matter how skewed the model is.
Source code in bdp_model_gate/structured/fairness.py
plot
¶
Parity difference swept across every decision threshold.
A single cutoff produces a single number, and the number is a cliff-edge: 0.49 and 0.51 can sit on opposite sides of the verdict. The sweep answers the question a reviewer actually has — does this verdict survive a small change of cutoff, or was it an artefact of where the cutoff happened to land?
Returns None for multiclass, where the prediction is a class rather than a score and there is no threshold to move.
Source code in bdp_model_gate/structured/fairness.py
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | |
ShapSubgroupCheck
¶
Bases: BaseCheck
For each feature, checks whether its SHAP contribution differs meaningfully across protected-attribute groups — catches features that look fair on average but drive outcomes differently for a subgroup.
The gap is measured relative to the mean absolute SHAP contribution, not in the raw units of the model output. SHAP values inherit the target's scale, so an absolute threshold that is sensible for a probability (contributions around 0.5) flags every feature on a premium model whose contributions run to thousands of naira. Relative, one threshold works on both: a value of 0.5 means "this feature's cross-group gap is worth half of a typical contribution".
Source code in bdp_model_gate/structured/fairness.py
Fairness — regression¶
bdp_model_gate.structured.regression_fairness
¶
Fairness checks for continuous-output models.
Demographic parity has no regression analogue — there is no "selected" class to count — so this module asks four different questions, each answering something the others cannot:
GroupMeanGapCheck Does one group receive systematically higher
predictions? Raw level difference.
ErrorParityCheck Is the model materially *worse* for one group?
Quality of service, independent of level.
CalibrationParityCheck Does one group's prediction systematically
over- or under-shoot its realised outcome?
LossRatioParityCheck Is one group charged a higher margin over its
own expected loss? The actuarial question.
The distinction matters most in insurance. A pricing model should charge
more in a higher-loss segment — that is risk-based pricing, not
discrimination — so a raw mean gap flags legitimate rating differences and
will be noisy on its own. Loss-ratio parity is the one that isolates
unfairness from actuarially justified variation, which is why it is worth
supplying context.expected_loss when you have it.
Every gap is measured relative to the overall figure, so a single
threshold works whether the target is a naira premium or a claim count.
Groups smaller than FairnessConfig.min_group_size are reported but not
scored: a three-policy segment produces wild ratios that read as findings.
GroupMeanGapCheck
¶
Bases: _RegressionFairnessCheck
Relative spread in mean prediction across protected groups.
The bluntest of the four. On a risk-priced model a gap here is expected and not by itself evidence of unfairness — read it alongside LossRatioParityCheck, which says whether the gap is justified by cost.
Source code in bdp_model_gate/structured/regression_fairness.py
ErrorParityCheck
¶
Bases: _RegressionFairnessCheck
Relative spread in per-group prediction error (MAE).
Answers a quality-of-service question rather than a pricing one: a group the model simply predicts worse for is being under-served, however fair the average price looks.
Source code in bdp_model_gate/structured/regression_fairness.py
CalibrationParityCheck
¶
Bases: _RegressionFairnessCheck
Per-group bias: does one group's prediction systematically over- or under-shoot its realised outcome?
Distinct from error parity, which is scale-free about direction. A group can have perfectly typical error magnitude while being consistently over-predicted — systematically overcharged, in a pricing model.
Source code in bdp_model_gate/structured/regression_fairness.py
plot
¶
Actual over expected, by predicted band, per group.
A mean residual is one number for the whole book, and a book is not uniform. RMSE says "wrong by 25,000"; this says "under-priced in the top decile, and only for one group" — which is the difference between a model that needs recalibrating and a model that needs withdrawing.
Bands are quantiles of the prediction, shared across groups, so the lines are comparable. A ratio above 1 means the realised outcome exceeded the prediction: under-priced.
Source code in bdp_model_gate/structured/regression_fairness.py
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | |
LossRatioParityCheck
¶
Bases: _RegressionFairnessCheck
Margin parity: is one group charged more relative to its own expected loss than another?
This is the actuarially meaningful fairness test for a pricing model. Charging a higher premium in a higher-loss segment is risk-based pricing; charging a higher margin over expected loss is not justified by cost, and is what this check isolates.
Requires context.expected_loss — a per-row expected loss, technical
premium or pure premium, row-aligned to X. Without it the check reports
NOT_APPLICABLE rather than falling back to a raw price comparison, which
would answer a different question under the same name.
Source code in bdp_model_gate/structured/regression_fairness.py
plot
¶
Charged premium against expected loss, one point per policy.
The scalar says the margin gap is 18%. It cannot say where. A uniform vertical offset between two groups is a flat loading — argue about it, but it is one decision. A fan that opens at the top of the book is a gap concentrated in high-value risks, which is a different finding with a different remedy.
The 45° line is break-even: on it, premium equals expected loss.
Source code in bdp_model_gate/structured/regression_fairness.py
Performance¶
bdp_model_gate.structured.performance
¶
Performance/cost thresholds that must pass before promotion.
PerformanceThresholdCheck
¶
Bases: BaseCheck
Hard gate on model score, p95 latency, and cost-per-inference.
The score metric is whatever PerformanceConfig.metric names — see
bdp_model_gate.metrics. Which metric actually ran is recorded in the
result's detail string and metadata, so a report always states what
min_score was compared against.
latencies_ms and cost_per_inference are optional on the context — if neither is supplied, only the score is checked; if the score inputs are also unavailable the check reports NOT_APPLICABLE rather than failing.
Source code in bdp_model_gate/structured/performance.py
plot
¶
Confusion matrix in the caller's own class order.
Only drawn where the classes are ordered — context.class_order
set, three or more classes. quadratic_kappa penalises rank distance
squared and then reports one number, which hides direction entirely:
a model that sends accepts to decline and one that sends them to refer
can score alike, and only one of those is a scandal. Keeping the
caller's ordering on both axes is what makes distance from the
diagonal readable as severity.
A binary matrix is four numbers the detail line already carries, so this returns None there rather than charting a table.
Source code in bdp_model_gate/structured/performance.py
Compliance¶
bdp_model_gate.structured.compliance
¶
NDPA/NDPR-style compliance mapping tied to the model card.
ComplianceMappingCheck
¶
Bases: BaseCheck
Validates model card completeness, DPIA trigger, and explainability
requirement. Expects context.model_card — a dict that can include:
legal_basis (str)
data_minimization_justification (str)
training_data_source (str)
use_case (str) — matched against config.high_risk_use_cases
dpia_completed (bool)
influences_decision_about_person (bool) — defaults to True if use_case
matches a high-risk use case
explainability_method (str)
Source code in bdp_model_gate/structured/compliance.py
Security¶
bdp_model_gate.structured.security
¶
Adversarial robustness, PII leakage, and prompt-injection checks.
AdversarialRobustnessCheck
¶
Bases: BaseCheck
Black-box robustness check: perturbs numeric features by a small relative amount and measures how much the prediction moves.
For classification that is the class flip rate — how often the predicted label changes — and a high rate means a fragile decision boundary.
For ordinal multiclass — where context.class_order is set — the
flip rate is reported alongside the mean rank distance moved, because
accept -> decline is a two-step error while accept -> refer is one. A
model that only ever slips by one rank is materially safer than one that
swings across the scale, and a bare flip rate cannot tell them apart.
For regression there is no such thing as a flip: every perturbation moves
a continuous output, so a flip rate would be ~1.0 and every model would
be permanently BLOCKED. Sensitivity is measured instead as the mean
relative change in prediction, gated with
SecurityConfig.adversarial_max_relative_shift. A model whose output
moves 30% when an input moves 2% is over-sensitive regardless of task.
Source code in bdp_model_gate/structured/security.py
plot
¶
Prediction movement as the perturbation budget grows.
One epsilon gives one number, and the shape of the approach to it is the risk. Linear decay is a model degrading predictably; flat-then- collapse is a cliff sitting just outside the budget that happened to be configured, and it will be found by whoever looks hardest.
Opt in with AdversarialRobustnessCheck(plot_sweep=True) — each point
re-scores the subsample, so this is the one plot in the suite that
costs an inference bill.
Source code in bdp_model_gate/structured/security.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | |
PIILeakageCheck
¶
Bases: BaseCheck
Scans string/object columns of structured data for PII patterns that shouldn't be present as raw features (should've been hashed/tokenized upstream).
Source code in bdp_model_gate/structured/security.py
PromptInjectionCheck
¶
Bases: BaseCheck
Only relevant if the model has a generative component (e.g. an
LLM-based feature explainer, chatbot, or report generator sitting
alongside the structured model). Requires context.generate_fn.
Flags any response that appears to comply with an injection attempt
rather than refuse.
Source code in bdp_model_gate/structured/security.py
Plotting and reports¶
See Plots and Reports for the guides.
bdp_model_gate.reporting
¶
A gate report as a page a reviewer can read and sign.
NEEDS_REVIEW is a verdict that delegates to a human. Until now that human
received a JSON blob: correct, archival, and close to unreadable at the moment
a decision has to be made. This renders the same report as one self-contained
HTML file — no network, no JavaScript, nothing to install to open it — with
each check's plot inlined beside the number it explains.
Three properties are deliberate:
- Self-contained. No
<script>, no external stylesheet, no remote font. A governance record is emailed, filed and reopened years later, and every external reference is a way for it to stop rendering. - Plots inlined as SVG, not
<img src="data:...">. Inline SVG inherits the page's CSS, which is what makes one render read correctly in light and dark. It also stays sharp when printed. - Degrades rather than fails. Without the
[plots]extra the page renders text-only. A plot that raises is reported in place as a note, because a broken chart must never cost a reviewer the findings around it.
render_html
¶
render_html(
report,
checks=None,
context=None,
title="Model gate report",
include_plots=True,
generated_at=None,
)
Renders a GateReport as one self-contained HTML document.
checks and context are what plotting needs — a plot recomputes from
the data rather than reading presentation arrays out of the archived
JSON. ModelGate.run attaches both to the report it returns, so
report.to_html() normally supplies them for you; pass them explicitly
when rendering a report reconstructed from elsewhere.
Without them, or without the [plots] extra, the page renders text-only.
Source code in bdp_model_gate/reporting.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | |
bdp_model_gate.plots
¶
Optional plotting for the checks that produce a shape, not just a number.
Plots are drawn only where a check collapses a distribution to a scalar and the shape is what a reviewer needs to judge. Latency, cost and model-card completeness are genuinely scalars; charting them would be decoration.
The contract with your own plotting code is deliberately narrow: every
plot() takes an optional matplotlib Axes and returns it. We draw onto your
canvas and hand it back, so these compose into your figures and can be
restyled. This package does not replace your plotting library.
matplotlib and seaborn live in the [plots] extra. Without them the
plotting calls raise GateConfigurationError naming the extra, and the HTML
report renders text-only — the same degradation shap and fairlearn already
follow.
require_plotting
¶
Returns (pyplot, seaborn), or explains how to get them.
Imported lazily rather than at module load so that importing
bdp_model_gate never costs a matplotlib import, which is slow and pulls
a font cache on first use.
Source code in bdp_model_gate/plots/__init__.py
plotting_available
¶
Whether the [plots] extra is installed. Used by the report renderer
to degrade to text rather than fail.
worst_result
¶
The result carrying the largest key, or None.
A plot() is handed one Axes, and a check may have scored six protected
attributes. Where only one can be drawn, draw the one the reader is being
asked to judge. Taking the first attribute instead would quietly hide the
finding on any report whose verdict came from the last one.
Source code in bdp_model_gate/plots/__init__.py
bdp_model_gate.groups
¶
Iterating protected groups, including intersections.
Checking each protected attribute on its own is the common approach and it misses the thing that matters most. A model can show acceptable disparity on gender and acceptable disparity on region while failing badly for women in one region — harm concentrates at intersections, and marginal checks are blind to it by construction.
iter_protected yields the marginal attributes and, when asked, their
pairwise intersections, so every group-based check gets intersectional
coverage from one place rather than each reimplementing it.
iter_protected
¶
Yields (label, group series) for each protected attribute.
With intersectional=True, also yields the pairwise combinations. Only
pairs are generated: three-way intersections fragment a validation set
faster than any realistic min_group_size tolerates, and reporting a
disparity computed over four rows is worse than not reporting one.
An intersection whose every group falls below min_group_size is skipped
with a log line rather than yielded, since the downstream check would
only discard it anyway.
Source code in bdp_model_gate/groups.py
group_series
¶
The group series a check reported under label, or None.
A CheckResult names its groups by label — "region", or
"gender × region" for an intersection — but carries only the summary
statistics, not the split they came from. A plot() that wants to draw
the underlying distribution has to recover it, and recovering it by
re-deriving the intersection by hand is how a chart ends up illustrating
a different split from the one that was scored.
Intersections are always searched, whatever the caller's intersectional
setting: the label is evidence the check produced it.
Source code in bdp_model_gate/groups.py
bdp_model_gate.calibration
¶
Calibration measurement.
A model is calibrated when its stated probabilities match observed frequencies: of the cases it scores 0.7, about 70% should be positive. Discrimination and calibration are independent properties — a model can rank perfectly (AUC 1.0) while every probability it emits is twice too high.
That distinction is why this module exists. For credit scoring and insurance pricing, calibration is often the property that matters: a well-ranked but badly calibrated model misprices every policy while scoring well on every metric the gate measured before 0.5.0.
Everything here is numpy-native, so it works on a core install.
CalibrationCurve
dataclass
¶
Observed frequency against predicted probability, per bin.
count is included because a bin holding four observations says almost
nothing, and any renderer or reader needs to weight accordingly.
calibration_curve
¶
Bins predictions and compares mean prediction to observed frequency.
strategy="uniform" splits [0, 1] into equal-width bins — the classic
reliability diagram. strategy="quantile" splits by equal count, which
matters for a skewed score distribution where uniform bins leave the
interesting region nearly empty. Fraud and default scores are usually
skewed, so quantile is often the honest choice.
Source code in bdp_model_gate/calibration.py
expected_calibration_error
¶
Mean gap between predicted and observed frequency, weighted by bin size.
0.0 is perfect. A value of 0.05 means predictions are off by five percentage points on average.
ECE is a summary and hides shape: two models with the same ECE can be
miscalibrated in opposite directions, one over-confident only at the top
and another wrong throughout. Read it alongside the curve, which is why
calibration_curve is public.
Source code in bdp_model_gate/calibration.py
brier_score
¶
Mean squared error of the probabilities. Lower is better; 0.0 perfect.
Source code in bdp_model_gate/calibration.py
brier_decomposition
¶
Murphy's decomposition: Brier = reliability - resolution + uncertainty.
Worth the extra numbers because they separate two different failures:
- reliability — how far predictions sit from observed frequency. Lower is better, and this is the part recalibration can fix.
- resolution — how much predictions vary from the base rate. Higher is better; a model predicting the base rate for everyone is perfectly reliable and completely useless.
- uncertainty — the base rate's own variance. A property of the problem, not the model, and a floor nothing can improve.
A model with excellent reliability and near-zero resolution has learned nothing, and neither the Brier score nor ECE says so on its own.
The identity holds exactly for the binned forecast, so both are
returned: binned_brier is what reliability - resolution + uncertainty
reconstructs, and brier is the score on the raw probabilities. They
differ by the information binning discards, which is small but not zero —
reporting only the raw score would leave an identity that almost adds up,
and a number that almost adds up is worse than two that are labelled.
Source code in bdp_model_gate/calibration.py
Registry and errors¶
bdp_model_gate.registry
¶
Plugin discovery for third-party checks.
A downstream package can register additional checks without forking this
library by declaring an entry point in the bdp_model_gate.checks group:
# in the plugin's pyproject.toml
[project.entry-points."bdp_model_gate.checks"]
my_check = "my_package.checks:MyCustomCheck"
Each entry point must resolve to a BaseCheck subclass (not an instance).
discover_plugin_checks() instantiates each with no arguments — plugin
checks that need configuration should read it from their own defaults or
from environment/config files, since the shared GateConfig only has
sections for the built-in categories.
discover_plugin_checks
¶
Finds and instantiates every check registered under the
bdp_model_gate.checks entry-point group. A plugin that fails to load
is logged and skipped rather than crashing the whole gate — a
misbehaving third-party check shouldn't block your own checks from running.
Source code in bdp_model_gate/registry.py
bdp_model_gate.exceptions
¶
Exceptions raised by BDP Model Gate.
GateConfigurationError is raised for problems with how the gate itself was set up (bad config values, unregistered checks). GateValidationError is raised for problems with the inputs handed to a run (shape mismatches, wrong types) — both are raised eagerly, before any check executes, so a misconfigured run fails fast with a clear message instead of an opaque traceback from deep inside a check.
BDPModelGateError
¶
Bases: Exception
Base class for all BDP Model Gate errors.
GateConfigurationError
¶
Bases: BDPModelGateError
Raised when the gate, its config, or a check is set up incorrectly.
GateValidationError
¶
Bases: BDPModelGateError
Raised when the context passed to a gate run is invalid.