+1 (726) 227-3241

Prove It: Bias Reports, Model Cards, and Approval Gates on SageMaker AI

Nobody schedules a governance project. What happens instead is that an enterprise client's procurement team sends over a 40-question AI assurance questionnaire two weeks before go-live, and the data science team discovers it cannot answer "what data was this trained on, who approved it, and how do you know it is not biased against a protected group?" without a week of archaeology in Slack.

This tutorial builds the answer ahead of time. We generate bias and explainability reports with SageMaker Clarify, capture the results plus the human context in a SageMaker Model Card, wire both into a SageMaker Pipeline so every registered model version carries its own evidence, and finish with a governance gate that blocks approval when a fairness metric drifts out of tolerance.

This is the piece our other tutorials deliberately skipped. Evaluation gates with fmeval answer "is the model good?". Model Monitor answers "is it still good?". Governance answers a third question — "can you prove it to somebody who does not trust you?" — and that is the one with a regulatory deadline attached. The EU AI Act's high-risk obligations phase in through 2026-2027, and the practical requirements (technical documentation, data governance, logging, human oversight) map almost one-to-one onto artifacts SageMaker AI already produces. You just have to turn them on.

Everything below runs from a Studio space or any machine with AWS credentials and a SageMaker execution role, using sagemaker>=2.200 and boto3>=1.35.

The four artifacts that answer an audit

ArtifactProduced byAnswers
Pre-training bias reportClarify (data only)Was the training data itself skewed?
Post-training bias reportClarify (data + model)Does the model treat groups differently?
Explainability reportClarify (SHAP / partial dependence)Which features drive predictions?
Model cardModel Card APIIntent, owners, risk rating, approvals, caveats

The first three are machine-generated and cheap to regenerate. The fourth is where humans write down the things no job can infer: what the model is for, what it must never be used for, and who signed off. Auditors care most about the fourth and will not accept it without the first three.

Step 1: A Clarify processing job for pre-training bias

Clarify runs as a SageMaker Processing job. Start with data-only bias, because you can run it before a model exists and it frequently kills bad ideas early.

Assume a tabular credit-decision dataset in S3 with a binary label approved and a facet column age_bracket.

import sagemaker
from sagemaker import clarify

session = sagemaker.Session()
role = sagemaker.get_execution_role()
bucket = session.default_bucket()
prefix = "governance-demo"

clarify_processor = clarify.SageMakerClarifyProcessor(
    role=role,
    instance_count=1,
    instance_type="ml.m5.xlarge",
    sagemaker_session=session,
)

data_config = clarify.DataConfig(
    s3_data_input_path=f"s3://{bucket}/{prefix}/train.csv",
    s3_output_path=f"s3://{bucket}/{prefix}/clarify/pretraining",
    label="approved",
    headers=["approved", "age_bracket", "income", "utilization", "tenure_months"],
    dataset_type="text/csv",
)

bias_config = clarify.BiasConfig(
    label_values_or_threshold=[1],      # 1 == favourable outcome
    facet_name="age_bracket",
    facet_values_or_threshold=[0],      # 0 == the group under examination
    group_name="tenure_months",
)

clarify_processor.run_pre_training_bias(
    data_config=data_config,
    data_bias_config=bias_config,
    methods="all",
    wait=True,
)

methods="all" computes the full pre-training family: class imbalance (CI), difference in proportions of labels (DPL), KL/JS divergence, Kolmogorov-Smirnov, and conditional demographic disparity. You will read two of them in practice:

  • CI tells you whether the facet is even represented. A CI above ~0.8 means the minority group is so thin that every downstream metric is noise — fix sampling before you fix the model.
  • DPL tells you whether the favourable outcome was historically distributed unevenly. A non-zero DPL is not automatically illegal, but it is the number the model will happily learn and amplify.

Output lands in analysis.json plus a rendered PDF/HTML report in the S3 output path.

Step 2: Post-training bias and SHAP explanations

Once you have a registered model, point Clarify at a shadow endpoint it spins up for the duration of the job:

model_config = clarify.ModelConfig(
    model_name="credit-model-2026-03",
    instance_type="ml.m5.xlarge",
    instance_count=1,
    accept_type="text/csv",
    content_type="text/csv",
)

predictions_config = clarify.ModelPredictedLabelConfig(probability_threshold=0.5)

shap_config = clarify.SHAPConfig(
    baseline=[[0, 52000, 0.31, 24]],   # one representative baseline row, no label column
    num_samples=200,
    agg_method="mean_abs",
    save_local_shap_values=True,
)

clarify_processor.run_bias_and_explainability(
    data_config=clarify.DataConfig(
        s3_data_input_path=f"s3://{bucket}/{prefix}/validation.csv",
        s3_output_path=f"s3://{bucket}/{prefix}/clarify/posttraining",
        label="approved",
        headers=["approved", "age_bracket", "income", "utilization", "tenure_months"],
        dataset_type="text/csv",
    ),
    model_config=model_config,
    model_predicted_label_config=predictions_config,
    explainability_config=shap_config,
    bias_config=bias_config,
    pre_training_methods="all",
    post_training_methods="all",
)

Three practical notes that save a day of debugging:

  1. The baseline is not optional and not arbitrary. SHAP values are differences from a baseline. Use the median row of your training set (or a small K-means summary of it), never a row of zeros — zeros produce explanations for a customer who does not exist.
  2. num_samples is the cost dial. Clarify invokes the shadow endpoint roughly num_samples times per explained record. Explain a 500-row sample, not your whole validation set.
  3. Post-training metrics need the facet in the data but not in the features. Keep age_bracket as a column Clarify can read while excluding it from the model's feature list if you do not want the model consuming it directly. Dropping it entirely means you can no longer measure the disparity — the most common self-inflicted governance wound.

The post-training report gives you DPPL, disparate impact (DI), recall/precision difference, and treatment equality. DI below 0.8 is the conventional adverse-impact trip wire in US employment/lending contexts; pick your own threshold with counsel, but pick one and write it down.

Step 3: Create the model card

The model card is the human layer. Create it once per model, then update the status as it moves through review.

import boto3, json

sm = boto3.client("sagemaker")

card = {
    "model_overview": {
        "model_name": "credit-decision-assist",
        "model_version": 7,
        "problem_type": "Binary classification",
        "algorithm_type": "XGBoost 1.7",
        "model_artifact": [f"s3://{bucket}/{prefix}/models/v7/model.tar.gz"],
        "model_description": (
            "Scores consumer credit applications to prioritise analyst review. "
            "Decision support only: no application is declined without a human reviewer."
        ),
        "inference_environment": {"container_image": ["<account>.dkr.ecr.<region>.amazonaws.com/xgboost:1.7-1"]},
    },
    "intended_uses": {
        "purpose_of_model": "Rank applications by likelihood of approval to route analyst attention.",
        "intended_uses": "Internal analyst queue prioritisation in the EU retail portfolio.",
        "factors_affecting_model_efficiency": "Degrades on thin-file applicants under 6 months tenure.",
        "risk_rating": "High",
        "explanations_for_risk_rating": "Consumer credit scoring; in scope for EU AI Act Annex III.",
    },
    "training_details": {
        "objective_function": {"function": {"function": "Minimize", "facet": "logloss"}},
        "training_observations": (
            "2019-2025 applications, EU only. Records before 2019 excluded (policy change). "
            "age_bracket retained for fairness measurement, excluded from features."
        ),
    },
    "evaluation_details": [{
        "name": "validation-2026-03",
        "evaluation_observation": "AUC 0.871; DI 0.93 on age_bracket; SHAP top drivers: utilization, income, tenure.",
        "metric_groups": [{
            "name": "fairness",
            "metric_data": [
                {"name": "disparate_impact", "type": "number", "value": 0.93},
                {"name": "dppl", "type": "number", "value": 0.04},
            ],
        }],
    }],
    "additional_information": {
        "ethical_considerations": "Adverse action reasons must be derived from SHAP values, not from the score alone.",
        "caveats_and_recommendations": "Retrain quarterly. Re-run Clarify on every retrain. Do not reuse outside the EU portfolio.",
    },
}

sm.create_model_card(
    ModelCardName="credit-decision-assist",
    Content=json.dumps(card),
    ModelCardStatus="Draft",
)

Statuses move Draft → PendingReview → Approved (or Archived). Each update_model_card call creates an immutable version, so the card doubles as the approval log: who changed the risk rating, when, and to what. Export a point-in-time PDF with create_model_card_export_job and hand that to the auditor rather than granting console access.

Attach the Clarify reports to the model package so the evidence travels with the artifact:

sm.create_model_package(
    ModelPackageGroupName="credit-decision-assist",
    ModelApprovalStatus="PendingManualApproval",
    DriftCheckBaselines={
        "Bias": {
            "PostTrainingConstraints": {
                "ContentType": "application/json",
                "S3Uri": f"s3://{bucket}/{prefix}/clarify/posttraining/analysis.json",
            }
        },
        "Explainability": {
            "Constraints": {
                "ContentType": "application/json",
                "S3Uri": f"s3://{bucket}/{prefix}/clarify/posttraining/analysis.json",
            }
        },
    },
    # ... InferenceSpecification omitted for brevity
)

Registering with PendingManualApproval is the whole point: the pipeline can build and document a model, but a named human flips it to Approved, and CloudTrail records who.

Step 4: Make the pipeline enforce it

Governance that depends on someone remembering to run a notebook is not governance. Put the Clarify job in the pipeline between training and registration, and add a condition step that refuses to register a regressed model.

from sagemaker.workflow.clarify_check_step import ClarifyCheckStep, ModelBiasCheckConfig
from sagemaker.workflow.check_job_config import CheckJobConfig
from sagemaker.workflow.conditions import ConditionGreaterThanOrEqualTo
from sagemaker.workflow.condition_step import ConditionStep
from sagemaker.workflow.functions import JsonGet

bias_check = ClarifyCheckStep(
    name="PostTrainingBiasCheck",
    clarify_check_config=ModelBiasCheckConfig(
        data_config=data_config,
        data_bias_config=bias_config,
        model_config=model_config,
        model_predicted_label_config=predictions_config,
    ),
    check_job_config=CheckJobConfig(role=role, instance_type="ml.m5.xlarge"),
    skip_check=False,           # compare against the registered baseline
    register_new_baseline=False,
    model_package_group_name="credit-decision-assist",
)

di_ok = ConditionGreaterThanOrEqualTo(
    left=JsonGet(
        step_name=bias_check.name,
        property_file=bias_report,   # PropertyFile over analysis.json
        json_path="post_training_bias_metrics.facets.age_bracket[0].metrics[?(@.name=='DI')].value",
    ),
    right=0.8,
)

gate = ConditionStep(name="FairnessGate", conditions=[di_ok],
                     if_steps=[register_step], else_steps=[fail_step])

skip_check=False with register_new_baseline=False is the combination that makes ClarifyCheckStep compare this run against the baseline stored on the model package group — a drift check on fairness itself. Set register_new_baseline=True only on a deliberate, reviewed re-baselining run; leaving it on permanently means every run quietly redefines "normal", which is how fairness drift becomes invisible.

Step 5: Keep it alive after launch

  • Schedule bias monitoring. ModelBiasMonitor in SageMaker Model Monitor re-runs post-training metrics on captured live traffic against the Clarify baseline and raises a CloudWatch alarm on breach. Requires data capture and a ground-truth upload path in S3 — labels arrive late for credit decisions, so run it monthly rather than hourly.
  • Schedule explainability monitoring. ModelExplainabilityMonitor catches the case where accuracy holds but the drivers change — a strong early signal that an upstream feature pipeline broke.
  • Review the card on a calendar, not on incident. Quarterly, re-read intended uses against what the model is actually being called for. Scope creep — the fraud team quietly reusing the credit model — is the single most common finding in real audits, and no automated check will catch it.
  • Log the decisions, not just the predictions. Data capture plus the request ID in your application log gives you the "what did the model say and what did the human do about it" trail that human-oversight requirements ask for.

The short version

Run Clarify pre-training bias before you train, post-training bias and SHAP after. Write a model card with intended uses, a risk rating and explicit out-of-scope statements. Register with PendingManualApproval and attach the Clarify baselines to the model package. Put a ClarifyCheckStep and a condition step in the pipeline so a regressed model cannot reach the registry. Then schedule bias and explainability monitors so the evidence stays current.

That is two or three days of work on an existing pipeline, and it converts the assurance questionnaire from a fire drill into an export job.

Facing an AI governance review, an EU AI Act readiness assessment, or an enterprise client's assurance questionnaire on a SageMaker workload? Our senior SageMaker consultants build Clarify, model card, and approval-gate pipelines as a matter of routine — get in touch and we will help you get the evidence in place before somebody asks for it.