The Problem
When a model is deployed in production, the people who use it, audit it, and are affected by it need to understand what it does, how well it works, and where it fails. Google researchers Margaret Mitchell et al. proposed model cards in 2019 as a structured documentation format — inspired by the nutrition labels on food packaging. Timnit Gebru et al. proposed datasheets for datasets in the same spirit.
The EU AI Act makes this not just good practice but law: Annex IV requires technical documentation covering the system's intended purpose, training data, design choices, performance metrics, and risk management measures. A model card satisfies most of these requirements in a single document.
Google's model card template
| Section | Content | EU AI Act Annex IV Mapping |
|---|
| Model Details | Name, version, owner, date, license | Item 1: System description |
| Intended Use | Primary use cases, out-of-scope uses | Item 2: Intended purpose |
| Training Data | Source, composition, preprocessing | Item 3: Data governance (Art. 10) |
| Evaluation Data | Test set description, demographics | Item 7: Testing & validation |
| Metrics | Accuracy, fairness metrics, thresholds | Item 7: Testing & validation |
| Ethical Considerations | Bias findings, privacy risks, societal impact | Item 5: Risk management (Art. 9) |
| Caveats & Recommendations | Known limitations, deployment warnings | Item 11: Log & monitoring |
Building a model card generator
"""
model_card.py
Programmatic model card generation from audit results.
"""
import json
from datetime import datetime
def generate_model_card(model_info, training_info, evaluation_info,
fairness_results, explainability_results, ethical_considerations):
"""
Generate a model card from structured inputs.
Args:
model_info: Dict with model name, version, owner, type, date
training_info: Dict with training data description, size, features
evaluation_info: Dict with test data description, accuracy, metrics
fairness_results: Dict from bias audit (Fairlearn/AIF360)
explainability_results: Dict from SHAP/LIME analysis
ethical_considerations: Dict with bias findings, privacy risks, recommendations
Returns:
Dict with structured model card data
"""
card = {
"model_details": {
"name": model_info.get("name", "Unknown"),
"version": model_info.get("version", "1.0"),
"owner": model_info.get("owner", "Unknown"),
"model_type": model_info.get("type", "Unknown"),
"date": model_info.get("date", datetime.now().strftime("%Y-%m-%d")),
"license": model_info.get("license", "Proprietary"),
},
"intended_use": {
"primary_use_cases": model_info.get("use_cases", []),
"out_of_scope_uses": model_info.get("out_of_scope", []),
"target_users": model_info.get("target_users", []),
},
"training_data": {
"description": training_info.get("description", "Not documented"),
"size": training_info.get("size", "Unknown"),
"features": training_info.get("features", []),
"preprocessing": training_info.get("preprocessing", "Not documented"),
"data_source": training_info.get("source", "Not documented"),
},
"evaluation_data": {
"description": evaluation_info.get("description", "Not documented"),
"size": evaluation_info.get("size", "Unknown"),
"demographics": evaluation_info.get("demographics", "Not documented"),
},
"metrics": {
"accuracy": evaluation_info.get("accuracy"),
"precision": evaluation_info.get("precision"),
"recall": evaluation_info.get("recall"),
"f1": evaluation_info.get("f1"),
"fairness": {},
},
"fairness_audit": {},
"explainability": {},
"ethical_considerations": ethical_considerations,
"caveats_and_recommendations": {
"known_limitations": ethical_considerations.get("limitations", []),
"deployment_warnings": ethical_considerations.get("warnings", []),
"monitoring_recommendations": ethical_considerations.get("monitoring", []),
},
"generated_date": datetime.now().isoformat(),
}
# Add fairness results
for attr, fm in fairness_results.get("fairness_metrics", {}).items():
card["metrics"]["fairness"][attr] = {
"demographic_parity_difference": fm.get("demographic_parity_difference"),
"equalized_odds_difference": fm.get("equalized_odds_difference"),
"disparate_impact_ratio": fm.get("disparate_impact_ratio"),
"passes_80_percent_rule": fm.get("passes_80_percent_rule"),
"passes_equalized_odds": fm.get("passes_equalized_odds"),
}
card["fairness_audit"][attr] = fm
# Add explainability results
if "global_feature_importance_ranked" in explainability_results:
card["explainability"]["global_feature_importance"] = (
explainability_results["global_feature_importance_ranked"][:10]
)
if "local_explanations" in explainability_results:
card["explainability"]["local_explanations_count"] = len(
explainability_results["local_explanations"]
)
return card
def render_model_card_markdown(card):
"""Render the model card as a formatted Markdown document."""
lines = []
md = card["model_details"]
lines.append(f"# Model Card: {md['name']}")
lines.append(f"\n**Version:** {md['version']} ")
lines.append(f"**Owner:** {md['owner']} ")
lines.append(f"**Type:** {md['model_type']} ")
lines.append(f"**Date:** {md['date']} ")
lines.append(f"**License:** {md['license']}\n")
iu = card["intended_use"]
lines.append("## Intended Use\n")
lines.append("**Primary use cases:**")
for uc in iu["primary_use_cases"]:
lines.append(f"- {uc}")
lines.append(f"\n**Out-of-scope uses:**")
for uc in iu["out_of_scope_uses"]:
lines.append(f"- {uc}")
lines.append("")
td = card["training_data"]
lines.append("## Training Data\n")
lines.append(f"- **Description:** {td['description']}")
lines.append(f"- **Size:** {td['size']}")
lines.append(f"- **Features:** {', '.join(td['features'])}")
lines.append(f"- **Source:** {td['data_source']}")
lines.append(f"- **Preprocessing:** {td['preprocessing']}\n")
lines.append("## Evaluation Metrics\n")
m = card["metrics"]
lines.append(f"- **Accuracy:** {m.get('accuracy', 'N/A')}")
lines.append(f"- **Precision:** {m.get('precision', 'N/A')}")
lines.append(f"- **Recall:** {m.get('recall', 'N/A')}")
lines.append(f"- **F1:** {m.get('f1', 'N/A')}\n")
if m.get("fairness"):
lines.append("### Fairness Metrics\n")
lines.append("| Protected Attribute | DI Ratio | EO Difference | 80% Rule | EO Pass |")
lines.append("|---|---|---|---|---|")
for attr, fm in m["fairness"].items():
di = fm.get("disparate_impact_ratio", "N/A")
eo = fm.get("equalized_odds_difference", "N/A")
di_pass = "✅" if fm.get("passes_80_percent_rule") else "❌"
eo_pass = "✅" if fm.get("passes_equalized_odds") else "❌"
lines.append(f"| {attr} | {di} | {eo} | {di_pass} | {eo_pass} |")
lines.append("")
if card.get("explainability", {}).get("global_feature_importance"):
lines.append("## Explainability — Global Feature Importance\n")
lines.append("| Rank | Feature | Mean |SHAP| |")
lines.append("|---|---|---|")
for i, (feat, val) in enumerate(card["explainability"]["global_feature_importance"], 1):
lines.append(f"| {i} | {feat} | {val} |")
lines.append("")
ec = card["ethical_considerations"]
lines.append("## Ethical Considerations\n")
if ec.get("bias_findings"):
lines.append("**Bias findings:**")
for bf in ec["bias_findings"]:
lines.append(f"- {bf}")
if ec.get("privacy_risks"):
lines.append("\n**Privacy risks:**")
for pr in ec["privacy_risks"]:
lines.append(f"- {pr}")
lines.append("")
cr = card["caveats_and_recommendations"]
lines.append("## Caveats & Recommendations\n")
if cr.get("known_limitations"):
lines.append("**Known limitations:**")
for lim in cr["known_limitations"]:
lines.append(f"- {lim}")
if cr.get("deployment_warnings"):
lines.append("\n**Deployment warnings:**")
for warn in cr["deployment_warnings"]:
lines.append(f"- {warn}")
if cr.get("monitoring_recommendations"):
lines.append("\n**Monitoring recommendations:**")
for mon in cr["monitoring_recommendations"]:
lines.append(f"- {mon}")
lines.append(f"\n---\n*Generated: {card['generated_date']}*")
return "\n".join(lines)
if __name__ == "__main__":
# Example: Generate a model card from audit results
model_info = {
"name": "LoanApproval-GBM-v1",
"version": "1.0",
"owner": "Risk Analytics Team",
"type": "Gradient Boosting Classifier",
"use_cases": ["Automated loan approval for consumer credit"],
"out_of_scope": ["Mortgage approval", "Commercial lending", "Credit card approval"],
"target_users": ["Loan officers (review tool)", "Risk committee (audit)"],
}
training_info = {
"description": "Historical loan repayment data from 2020-2024",
"size": "3,000 records",
"features": ["income", "credit_score", "debt_ratio", "employment_years"],
"source": "Internal loan database",
"preprocessing": "Missing values imputed with median, no feature scaling applied",
}
evaluation_info = {
"description": "30% held-out test set, stratified by repayment outcome",
"size": "900 records",
"accuracy": 0.87,
"precision": 0.89,
"recall": 0.92,
"f1": 0.90,
}
fairness_results = {
"fairness_metrics": {
"race": {
"demographic_parity_difference": 0.12,
"equalized_odds_difference": 0.04,
"disparate_impact_ratio": 0.72,
"passes_80_percent_rule": False,
"passes_equalized_odds": True,
},
},
}
explainability_results = {
"global_feature_importance_ranked": [
("credit_score", 0.45), ("income", 0.28),
("debt_ratio", 0.15), ("employment_years", 0.08),
],
"local_explanations": [{"sample_index": 0}, {"sample_index": 1}],
}
ethical_considerations = {
"bias_findings": [
"Disparate impact ratio of 0.72 for race (below 80% rule threshold) — Group_B is approved at a lower rate",
"Equalized odds passes (0.04 difference) — model makes errors at similar rates across groups",
],
"privacy_risks": [
"Training data contains income and credit score — PII handling per GDPR Art. 10 required",
],
"limitations": [
"Model trained on 2020-2024 data — may not generalize to economic conditions outside this period",
"Does not include macroeconomic features (interest rates, unemployment) that affect repayment",
],
"warnings": [
"Do not deploy without mitigation for disparate impact — legal exposure under EEOC 80% rule",
],
"monitoring": [
"Monitor disparate impact ratio weekly — alert if drops below 0.75",
"Retrain quarterly with updated data to prevent drift",
],
}
card = generate_model_card(
model_info, training_info, evaluation_info,
fairness_results, explainability_results, ethical_considerations,
)
md = render_model_card_markdown(card)
with open("model_card.md", "w") as f:
f.write(md)
print("Model card saved to model_card.md")
with open("model_card.json", "w") as f:
json.dump(card, f, indent=2)
print("Model card JSON saved to model_card.json")
Your model card shows disparate impact ratio of 0.72 (fails 80% rule) but equalized odds passes. The business team wants to deploy the model and "fix it later." What do you put in the model card's caveats section?
The model card is a legal and governance artifact, not a marketing document. If you document the disparate impact failure in the model card and deploy anyway, you've created evidence that you knew about the problem and chose to proceed — which is worse for legal liability than not knowing. But the right answer isn't to hide it — it's to fix it before deployment. The model card's caveats section should clearly state the failure, the recommended mitigation, and the monitoring plan. If the business overrides your recommendation, the model card documents that you raised the concern — which is your professional obligation as the RAI engineer. Never sanitize a model card to make deployment easier.