Model Cards › Code: Building a Model Card

Michael BrenndoerferMarch 13, 202613 min read
Language AI HandbookAll chapters
Part 66 of 67Frontier Methods of 2025
Chapter 506Model Cards › Code: Building a Model Card
Reading section 9 of 13 · Code: Building a Model Card · 13 minSection 9 of 13 · 13 min

Book contents

Section 9 of 13 · 13 min read

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.

In[3]:
Code
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]
Out[4]:
Console
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.

In[5]:
Code
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)
Out[6]:
Console
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       400

The 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.

In[7]:
Code
# 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(),
    }
Out[8]:
Console
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.

In[9]:
Code
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)
Out[10]:
Console
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.

In[11]:
Code
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)
Out[12]:
Console
# 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.

Out[13]:
Visualization
Three-panel model card visualization: bar chart of overall metrics, grouped bar chart by category, and calibration curve.
Model card evaluation summary showing overall performance metrics (left), disaggregated accuracy and F1 by product category (center), and the confidence calibration curve (right). The dashed diagonal in the calibration panel represents perfect calibration; deviations from it indicate systematic over- or under-confidence at specific probability ranges.

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.

Out[14]:
Visualization
Overlapping histogram of confidence scores for positive and negative sentiment samples with annotated review threshold region.
Distribution of model confidence scores on the test set, split by true label. The green distribution (true positives) concentrates near 1.0 and the red distribution (true negatives) concentrates near 0.0, indicating clear separation. The shaded uncertain region between the dashed review thresholds marks predictions where model confidence is insufficient for autonomous action and human review is recommended.

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.

Out[15]:
Visualization
Horizontal bar chart of performance metric gaps between electronics and apparel categories with concern threshold lines.
Subgroup performance gap analysis showing the difference in accuracy, F1, and AUC between electronics and apparel product categories. Positive values indicate higher performance for electronics; negative values indicate higher performance for apparel. The dashed vertical lines mark a 5% concern threshold: gaps exceeding this level typically warrant additional mitigation before deployment.

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.