Evaluation Challenges › Code Implementation

Michael BrenndoerferJanuary 8, 202611 min read
Language AI HandbookAll chapters
Part 53 of 67Evaluation Fundamentals
Chapter 414Evaluation Challenges › Code Implementation
Reading section 7 of 10 · Code Implementation · 11 minSection 7 of 10 · 11 min

Book contents

Section 7 of 10 · 11 min read

Evaluation Challenges › Code ImplementationOne section · Sections in this chapter7 / 10 ↓
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 implement a practical evaluation framework that addresses several of the challenges discussed above, including measurement of prompt sensitivity and contamination detection.

Prompt Sensitivity Measurement

We will compare two models: one clean and one with contamination inflating its scores. By evaluating both across multiple prompt formats, we can see how single-format evaluation can produce misleading rankings.

In[7]:
Code
from collections import defaultdict

# Five evaluation questions with known correct answers
questions = [
    {
        "id": "q1",
        "question": "What is the capital of France?",
        "correct": "Paris",
    },
    {
        "id": "q2",
        "question": "What element has atomic number 8?",
        "correct": "Oxygen",
    },
    {
        "id": "q3",
        "question": "In what year did World War II end?",
        "correct": "1945",
    },
    {
        "id": "q4",
        "question": "What is the speed of light in m/s?",
        "correct": "3e8",
    },
    {"id": "q5", "question": "Who wrote Hamlet?", "correct": "Shakespeare"},
]

# Three prompt formats for the same questions
prompt_formats = {
    "direct": "Question: {q}\nAnswer:",
    "polite": "Please answer the following question: {q}\nYour answer:",
    "cot": "Think step by step and answer the following: {q}\nAnswer:",
}


def simulate_model_response(
    prompt_format, model_capability, contamination_bias=0.0
):
    """Simulate a model response with capability, contamination, and format effects."""
    effective_prob = min(model_capability + contamination_bias, 1.0)
    # Different prompt formats produce slightly different accuracies
    format_effects = {"direct": 0.0, "polite": -0.05, "cot": 0.08}
    effective_prob += format_effects.get(prompt_format, 0.0)
    effective_prob = np.clip(effective_prob, 0.0, 1.0)
    return np.random.random() < effective_prob


# Two models: clean (true capability 0.70) and contaminated (true 0.65, bias +0.18)
models = {
    "Model_A_clean": {"capability": 0.70, "contamination": 0.0},
    "Model_B_contaminated": {"capability": 0.65, "contamination": 0.18},
}

n_trials = 200  # Repeat each question many times for stable estimates

results = defaultdict(lambda: defaultdict(list))
for model_name, model_params in models.items():
    for fmt_name in prompt_formats:
        for _ in range(n_trials):
            for q in questions:
                correct = simulate_model_response(
                    fmt_name,
                    model_params["capability"],
                    model_params["contamination"],
                )
                results[model_name][fmt_name].append(correct)

# Summarize accuracy and standard error per model per format
accuracy_table = {}
for model_name in models:
    accuracy_table[model_name] = {}
    for fmt_name in prompt_formats:
        scores = results[model_name][fmt_name]
        accuracy_table[model_name][fmt_name] = {
            "mean": np.mean(scores),
            "std": np.std(scores) / np.sqrt(len(scores)),
        }
Out[8]:
Console
Evaluation Results by Model and Prompt Format
==============================================================
Format          Model A (Clean)          Model B (Contam.)       
--------------------------------------------------------------
direct          0.711 ± 0.014           0.846 ± 0.011
polite          0.632 ± 0.015           0.787 ± 0.013
cot             0.771 ± 0.013           0.897 ± 0.010

Model_A_clean: avg=0.705,  prompt sensitivity (range)=0.139
Model_B_contaminated: avg=0.843,  prompt sensitivity (range)=0.110

The output shows that Model B's contamination gives it a higher score on the direct prompt, making it look superior. But when you average across prompt formats, Model A's higher true capability gives it a better mean score. Model B also shows greater prompt sensitivity (a larger gap between its best and worst prompt format scores), which is consistent with its performance being partly driven by matching memorized patterns that work better with one format than another.

This simulation compresses into a few lines a dynamic that plays out across the whole field. A researcher who evaluates Model B only with the direct prompt format will conclude it is the better model. A more careful evaluator who tests multiple formats will reach the opposite conclusion. The gap between these two conclusions is not about which evaluator worked harder; it is about whether evaluation was designed as a scientific measurement or as a score retrieval exercise.

Contamination Detection via N-gram Overlap

A common contamination detection strategy is measuring how many n-grams from a test item appear in the training corpus. Items with high overlap are flagged as potentially contaminated. This approach scales to large corpora and can be applied without running the model, making it a useful preprocessing step before any evaluation.

In[9]:
Code
def get_ngrams(text, n):
    """Extract n-grams from a text string."""
    words = text.lower().split()
    return [tuple(words[i : i + n]) for i in range(len(words) - n + 1)]


def contamination_score(test_item, training_corpus, n=3):
    """
    Compute the fraction of n-grams in test_item that appear in training_corpus.
    Higher scores indicate potential contamination.
    """
    test_ngrams = set(get_ngrams(test_item, n))
    if not test_ngrams:
        return 0.0
    training_ngrams = set()
    for doc in training_corpus:
        training_ngrams.update(get_ngrams(doc, n))
    overlap = test_ngrams & training_ngrams
    return len(overlap) / len(test_ngrams)


# Simulated training corpus: documents covering various factual topics
training_docs = [
    "The capital of France is Paris a city on the Seine river",
    "Paris is the capital and most populous city of France",
    "Oxygen has atomic number 8 and is essential for combustion",
    "Shakespeare was an English playwright who wrote many famous works",
    "The speed of light in vacuum is approximately three times ten to the eighth meters per second",
    "World War II ended in nineteen forty five with the surrender of Germany and Japan",
    "Some unrelated document about cooking recipes and kitchen utensils",
    "Another document about programming languages and software development practices",
]

# Test items formed from our question set
test_items = [
    "What is the capital of France Paris is the correct answer",
    "What element has atomic number 8 it is oxygen",
    "In what year did World War II end 1945 was the final year",
    "What programming language was invented in the early 1990s",
    "Who wrote the play Hamlet it was William Shakespeare",
]

contam_threshold = 0.30
contam_scores = [
    (item[:42] + "...", contamination_score(item, training_docs, n=3))
    for item in test_items
]
Out[10]:
Console
Contamination Detection Results (3-gram overlap with training corpus)
====================================================================
Test Item (truncated)                            Score   Status
--------------------------------------------------------------------
What is the capital of France Paris is the...    0.444  FLAGGED
What element has atomic number 8 it is oxy...    0.286    clean
In what year did World War II end 1945 was...    0.091    clean
What programming language was invented in ...    0.000    clean
Who wrote the play Hamlet it was William S...    0.000    clean

Flagged: 1/5 items (20%) at threshold 30%

Items about well-documented facts (Paris as capital, World War II) are flagged because the training corpus contains text about those topics. The programming language question scores low because nothing similar appears in training. In production contamination detection, this logic is applied at much larger scale, often using suffix lengths of 50 or more tokens and corpora of billions of documents.

The key insight from this detection exercise is that flagging does not tell you whether a model's correct answers on flagged items came from memorization or from learned knowledge. A model that knows the capital of France because it was trained on a document mentioning Paris is not in the same situation as a model that memorized a specific benchmark question and its answer. But from the perspective of evaluation, both situations create the same problem: the model may have an unfair advantage on those items that inflates its measured accuracy relative to its performance on truly novel questions.

Now let's visualize how prompt brittleness affects model rankings when evaluated across multiple prompt variants:

In[11]:
Code
n_models = 5
n_prompt_variants = 10

true_caps = np.array([0.74, 0.70, 0.71, 0.68, 0.66])
# Brittleness: std of performance across prompt variants
brittleness = np.array([0.04, 0.14, 0.08, 0.16, 0.06])

model_names_short = [f"M{i + 1}" for i in range(n_models)]

# Structured prompt effects preserve each model's mean while making brittle models
# more likely to look unusually strong under one favorable formulation.
prompt_effects = np.array(
    [
        [-1.2, -0.8, -0.5, -0.3, -0.1, 0.1, 0.3, 0.5, 0.8, 1.2],
        [-1.6, -1.2, -0.8, -0.5, -0.2, 0.1, 0.3, 0.6, 1.2, 2.1],
        [-1.5, -1.0, -0.6, -0.3, -0.1, 0.1, 0.3, 0.5, 0.9, 1.7],
        [-1.8, -1.2, -0.8, -0.4, -0.1, 0.1, 0.4, 0.8, 1.2, 1.8],
        [-1.3, -0.9, -0.5, -0.2, -0.1, 0.1, 0.2, 0.5, 0.9, 1.3],
    ]
)
scores_by_prompt = np.clip(
    true_caps[:, None] + brittleness[:, None] * prompt_effects, 0, 1
)

mean_scores = scores_by_prompt.mean(axis=1)

# Rankings: by best single-prompt score vs. mean across all prompts
rank_by_best = np.argsort(-scores_by_prompt.max(axis=1))
rank_by_mean = np.argsort(-mean_scores)
Out[12]:
Visualization
Box plot showing performance variability across prompt variants for five models, with single-prompt and mean rankings annotated.
Performance distribution across ten prompt variants for five models. Each box shows the spread from the 25th to 75th percentile; whiskers extend to the full range. Models M2 and M4 have high brittleness, achieving top single-prompt ranks but lower mean ranks. A single-prompt evaluation of M2 could misrank it as the best model, while multi-prompt evaluation reveals it is middle of the pack.

The box plots make prompt brittleness tangible. Models M2 and M4 have wide boxes and long whiskers, indicating high variance across prompt variants. Under a single favorable prompt, M2 ranks first. But its mean rank is third. A researcher who evaluated M2 on one lucky prompt would overestimate its capability. A more careful evaluator using 10 prompt variants would correctly identify M1 as the most reliable model.

The annotation showing both best-prompt rank and mean rank captures what multi-prompt evaluation adds beyond single-prompt evaluation. Selecting a model for deployment based on its best single-prompt score would lead you to choose a model that performs inconsistently in the field. Selecting based on mean rank across prompts would lead you to a more reliable choice, even though the peak score is lower.

Computing Confidence Intervals for Benchmark Scores

Let's implement the statistical uncertainty calculation described earlier in the best practices section, making it concrete with both the analytical formula and a bootstrap approach.

In[13]:
Code
from scipy import stats


def analytical_ci(correct, n, confidence=0.95):
    """
    Compute a confidence interval for accuracy using the Wilson score method.
    Wilson score is more accurate than the normal approximation for extreme probabilities.
    """
    p_hat = correct / n
    z = stats.norm.ppf((1 + confidence) / 2)
    denominator = 1 + z**2 / n
    center = (p_hat + z**2 / (2 * n)) / denominator
    margin = (
        z * np.sqrt(p_hat * (1 - p_hat) / n + z**2 / (4 * n**2))
    ) / denominator
    return center - margin, center + margin


def bootstrap_ci(correct_vector, n_boot=2000, confidence=0.95):
    """
    Compute a bootstrap confidence interval for accuracy.
    Resamples the binary correct/incorrect vector with replacement.
    """
    boot_means = [
        np.mean(
            np.random.choice(
                correct_vector, size=len(correct_vector), replace=True
            )
        )
        for _ in range(n_boot)
    ]
    alpha = (1 - confidence) / 2
    return np.percentile(boot_means, [alpha * 100, (1 - alpha) * 100])


# Simulate benchmark results for two models on a 500-item benchmark
n_items = 500
model_x_correct = np.random.binomial(1, 0.72, n_items)  # true p=0.72
model_y_correct = np.random.binomial(1, 0.75, n_items)  # true p=0.75

model_x_acc = model_x_correct.mean()
model_y_acc = model_y_correct.mean()

# Confidence intervals using both methods
x_analytical = analytical_ci(model_x_correct.sum(), n_items)
y_analytical = analytical_ci(model_y_correct.sum(), n_items)
x_bootstrap = bootstrap_ci(model_x_correct)
y_bootstrap = bootstrap_ci(model_y_correct)
Out[14]:
Console
Model X: accuracy = 0.730
  Analytical 95% CI: (0.689, 0.767)
  Bootstrap  95% CI: (0.692, 0.766)

Model Y: accuracy = 0.732
  Analytical 95% CI: (0.692, 0.769)
  Bootstrap  95% CI: (0.692, 0.772)

CI overlap: 0.076
The confidence intervals overlap -- the difference may not be statistically significant.
Out[15]:
Visualization
Horizontal error bar chart showing accuracy estimates and 95% confidence intervals for two models.
Accuracy estimates with 95% confidence intervals for two models evaluated on a 500-item benchmark. The horizontal error bars represent the Wilson score confidence intervals. When intervals overlap, the apparent difference in accuracy may lie within sampling noise and should not be interpreted as a meaningful performance gap.

The confidence interval chart makes explicit what a single reported number hides. Model Y's accuracy appears higher than Model X's, but if the intervals overlap, that difference is consistent with sampling variation alone. Reporting only point estimates without error bars creates an illusion of precision that the data do not support and leads readers to draw conclusions that are not warranted.