Model Cards › Code: Building a Model Card
Language AI HandbookAll chapters Model Cards › Code: Building a Model CardOne section · Sections in this chapter9 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's build a practical model card for a text classification model. We'll create structured documentation, perform the kinds of evaluations a model card requires, and generate the disaggregated performance data needed to support deployment decisions.
We start by training a simple sentiment classifier to use as our example model. The code creates a synthetic dataset with demographic metadata attached, which allows us to demonstrate disaggregated evaluation. In a real workflow, this metadata would come from the data collection process.
import warnings
import numpy as np
warnings.filterwarnings("ignore")
# Simulate a labeled dataset with demographic metadata
np.random.seed(42)
# Create synthetic reviews with sentiment labels and demographic metadata
# In a real scenario, this would be actual labeled data
n_samples = 2000
# Simulate text features as TF-IDF-like vectors (20 features)
n_features = 20
# Generate base feature distributions for positive and negative classes
pos_mean = np.random.randn(n_features) * 0.5
neg_mean = -pos_mean * 0.8
# Create samples
half = n_samples // 2
X_pos = np.random.randn(half, n_features) * 0.8 + pos_mean
X_neg = np.random.randn(half, n_features) * 0.8 + neg_mean
X = np.vstack([X_pos, X_neg])
y = np.array([1] * half + [0] * half)
# Add demographic metadata for disaggregated evaluation. Each category contains
# both sentiment classes, while apparel reviews receive additional feature noise
# to create a modest, measurable performance difference.
categories = np.tile(["electronics", "apparel"], n_samples // 2)
apparel_mask = categories == "apparel"
X[apparel_mask] += np.random.randn(apparel_mask.sum(), n_features) * 0.45
# Shuffle consistently
shuffle_idx = np.random.permutation(n_samples)
X = X[shuffle_idx]
y = y[shuffle_idx]
categories = categories[shuffle_idx]Dataset: 2000 samples, 20 features Class balance: 50.0% positive Categories: (array(['apparel', 'electronics'], dtype='<U11'), array([1000, 1000]))
We have a balanced dataset with demographic metadata attached. The categories array plays the role that demographic or domain annotations play in real model card workflows. Now we train a logistic regression classifier and hold out a test set for evaluation.
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# Split into train/test while preserving every class-category combination
strata = np.array(
[f"{label}-{category}" for label, category in zip(y, categories)]
)
X_train, X_test, y_train, y_test, cat_train, cat_test = train_test_split(
X, y, categories, test_size=0.2, random_state=42, stratify=strata
)
# Train the model
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_train, y_train)Overall evaluation results:
Accuracy: 0.9900
F1 Score: 0.9900
ROC-AUC: 0.9987
precision recall f1-score support
Negative 0.99 0.99 0.99 200
Positive 0.99 0.99 0.99 200
accuracy 0.99 400
macro avg 0.99 0.99 0.99 400
weighted avg 0.99 0.99 0.99 400The overall metrics look solid. But overall metrics hide subgroup differences. This is the central problem that disaggregated evaluation addresses: a model can appear to perform well in aggregate while performing substantially worse for specific subpopulations. Let's compute disaggregated results by product category.
# Disaggregated evaluation by product category
subgroup_results = {}
for category in np.unique(cat_test):
mask = cat_test == category
y_sub = y_test[mask]
y_pred_sub = model.predict(X_test[mask])
y_prob_sub = model.predict_proba(X_test[mask])[:, 1]
subgroup_results[category] = {
"n": mask.sum(),
"accuracy": accuracy_score(y_sub, y_pred_sub),
"f1": f1_score(y_sub, y_pred_sub),
"auc": roc_auc_score(y_sub, y_prob_sub),
"pos_rate": y_sub.mean(),
}Disaggregated evaluation results: Category N Accuracy F1 AUC -------------------------------------------------- apparel 200 0.9850 0.9849 0.9992 electronics 200 0.9950 0.9950 0.9983 Performance gap (accuracy): 0.0100 Performance gap (F1): 0.0101
The disaggregated evaluation surfaces whether performance differences exist between subgroups. This is the information a deployer needs to assess whether the model performs acceptably across all the populations their deployment will serve. Let's also compute confidence calibration, which tells us how reliable the model's probability outputs are.
from sklearn.calibration import calibration_curve
# Compute calibration data
fraction_pos, mean_pred_prob = calibration_curve(
y_test, y_prob, n_bins=6, strategy="quantile"
)
# Also compute calibration error
# Expected Calibration Error (ECE)
bin_edges = np.linspace(0, 1, 11)
ece = 0.0
for i in range(len(bin_edges) - 1):
mask = (y_prob >= bin_edges[i]) & (y_prob < bin_edges[i + 1])
if mask.sum() > 0:
bin_acc = y_test[mask].mean()
bin_conf = y_prob[mask].mean()
bin_size = mask.sum() / len(y_test)
ece += bin_size * abs(bin_acc - bin_conf)Expected Calibration Error (ECE): 0.0147
Calibration curve (mean predicted probability vs fraction positive):
Pred Prob Fraction Pos
0.000 0.000
0.000 0.000
0.057 0.030
0.946 0.970 ===================
0.999 1.000 ====================
1.000 1.000 ====================The Expected Calibration Error measures the average discrepancy between predicted probabilities and observed outcome rates. A perfectly calibrated model has ECE of 0: when it says 80% confidence, 80% of such cases are positive. Calibration matters for model card documentation because confidence thresholds used to trigger human review only work as intended if the confidence scores are calibrated. Now let's assemble this into a structured model card document.
from datetime import datetime
def generate_model_card(
model_name,
version,
model_type,
training_data_description,
intended_uses,
out_of_scope_uses,
overall_metrics,
subgroup_metrics,
known_limitations,
ethical_considerations,
recommendations,
):
"""Generate a structured model card as a formatted string."""
lines = []
lines.append(f"# Model Card: {model_name}")
lines.append(f"\n**Version:** {version}")
lines.append(f"**Type:** {model_type}")
lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d')}\n")
lines.append("## Intended Use\n")
lines.append("**Primary uses:**")
for use in intended_uses:
lines.append(f"- {use}")
lines.append("\n**Out-of-scope uses:**")
for use in out_of_scope_uses:
lines.append(f"- {use}")
lines.append("\n## Training Data\n")
lines.append(training_data_description)
lines.append("\n## Evaluation Results\n")
lines.append("**Overall performance:**")
for metric, value in overall_metrics.items():
lines.append(f"- {metric}: {value:.4f}")
lines.append("\n**Disaggregated performance:**")
for subgroup, metrics in subgroup_metrics.items():
lines.append(f"\n*{subgroup}* (n={metrics['n']}):")
lines.append(f"- Accuracy: {metrics['accuracy']:.4f}")
lines.append(f"- F1 Score: {metrics['f1']:.4f}")
lines.append(f"- ROC-AUC: {metrics['auc']:.4f}")
lines.append("\n## Known Limitations\n")
for limitation in known_limitations:
lines.append(f"- {limitation}")
lines.append("\n## Ethical Considerations\n")
for consideration in ethical_considerations:
lines.append(f"- {consideration}")
lines.append("\n## Recommendations\n")
for rec in recommendations:
lines.append(f"- {rec}")
return "\n".join(lines)# Model Card: SentimentClassifier-v1 **Version:** 1.0.0 **Type:** Binary text classification (sentiment) **Generated:** 2026-08-15 ## Intended Use **Primary uses:** - Sentiment classification of English product reviews (electronics, apparel) - Batch processing of customer feedback in e-commerce contexts - Triage of customer service tickets by sentiment polarity **Out-of-scope uses:** - Non-English text (model not evaluated on any non-English inputs) - Service reviews (only product reviews in evaluation set) - Social media text (informal writing style not in training distribution) - Autonomous content moderation without human review - Medical, legal, or financial sentiment analysis ## Training Data Trained on 1,600 synthetic product reviews from electronics and apparel categories. Data collected 2023-2024. English only. Balanced 50/50 positive/negative split. ## Evaluation Results **Overall performance:** - Accuracy: 0.9900 - F1 Score: 0.9900 - ROC-AUC: 0.9987 - ECE: 0.0147 **Disaggregated performance:** *apparel* (n=200): - Accuracy: 0.9850 - F1 Score: 0.9849 - ROC-AUC: 0.9992 *electronics* (n=200): - Accuracy: 0.9950 - F1 Score: 0.9950 - ROC-AUC: 0.9983 ## Known Limitations - Performance gap of 1.0% between electronics and apparel categories - Not evaluated on reviews shorter than 20 words or longer than 500 words - Knowledge cutoff: no data after 2024; may miss emerging product terminology - Calibrated on balanced evaluation set; confidence scores may be miscalibrated on imbalanced real-world distributions ## Ethical Considerations - Model trained on synthetic data; real-world demographic disparities not evaluated - Sentiment labels reflect majority-annotator perspective; minority viewpoints may be systematically misclassified - Do not use confidence scores as a proxy for review authenticity ## Recommendations - Run disaggregated evaluation on your specific product categories before deployment - Apply human review to predictions with confidence scores below 0.75 - Re-evaluate after any fine-tuning or data updates - Monitor for distribution shift if deploying on new product categories
This model card is now a structured document that travels with the model. Notice how each section serves a different reader need: the intended use section tells deployers whether the model fits their context, the disaggregated evaluation tells them how it performs across the subgroups they care about, and the recommendations section tells them what operational precautions to implement. Let's visualize the key metrics to make the card more readable.

The three panels encode the core quantitative content of a model card: aggregate performance, disaggregated performance, and calibration quality. A deployer reading this visualization can immediately see how performance varies across categories and whether confidence scores are reliable. The calibration panel is often overlooked in model card visualizations but is critical for anyone using confidence thresholds to route predictions for human review.
Let's also visualize the confidence score distribution, which is essential for setting review thresholds. A well-designed deployment workflow uses confidence scores to distinguish predictions the model is confident about (which can be acted on automatically) from predictions where confidence is low (which benefit from human review). The model card should document where this threshold is and how it was chosen.

The confidence score distribution is part of operational model card documentation. The uncertain region between 0.25 and 0.75 marks predictions where model confidence is too low for autonomous action; a responsible deployment workflow routes these cases to human reviewers. The separation between positive and negative distributions shows how well the model's probability estimates track actual outcomes. Poor separation would indicate a model that assigns similar probabilities regardless of the true label, making confidence-based routing unreliable.
Finally, let's look at how subgroup performance gaps can be quantified and tracked over time. The gap visualization makes disparities legible to deployers who may not easily parse tabular numbers.

The gap analysis visualization makes subgroup disparities immediately legible. A deployer reviewing this chart can see at a glance whether any metric exceeds a concern threshold, and use that information to decide whether additional evaluation or mitigation is required before deployment. The choice of a 5% concern threshold is a policy decision, not a mathematical one. Different deployment contexts may set this threshold higher or lower depending on the consequences of differential performance.