Measuring Accuracy, Consistency, and Failure Rates

Ma Mahalakshmi V Updated 19 Sep 2026
7 min read ·Lesson 170 of 224

Three Different Signals, Three Different Questions

Once an evaluation dataset exists (Lesson 6), running it through a model produces raw results — but raw results are not yet insight. Three specific metrics turn results into something you can track over time, compare across versions, and use to decide whether a system is production-ready:

  • Accuracy answers: of the cases with a known correct answer, how many did the system get right?
  • Consistency answers: if I ask the exact same question multiple times, how often do I get the same answer?
  • Failure rate answers: how often does the system fail outright — an exception, a malformed response, a timeout — regardless of whether the content was correct?

These are distinct because a system can score well on one while failing badly on another. A system that is 95% accurate but only 60% consistent is unreliable in a way accuracy alone hides: it means the 95% figure came from a single run, and a different run on the same inputs could produce a meaningfully different number. A system that is both accurate and consistent but has a 10% failure rate (crashes or empty responses on one input in ten) still fails one in ten real user requests. Production readiness requires looking at all three together, not any single number in isolation.

Measuring Accuracy

Accuracy, for a classification-style task like the ticket-triage dataset from Lesson 6, is the simplest of the three: the fraction of examples where the system's output matches the expected label.

def compute_accuracy(results: list[dict]) -> float:
    """Each result dict has 'expected_category' and 'predicted_category'."""
    if not results:
        return 0.0
    correct = sum(
        1 for r in results if r["predicted_category"] == r["expected_category"]
    )
    return correct / len(results)


def test_compute_accuracy_basic():
    results = [
        {"expected_category": "billing", "predicted_category": "billing"},
        {"expected_category": "technical", "predicted_category": "account"},
        {"expected_category": "account", "predicted_category": "account"},
        {"expected_category": "other", "predicted_category": "other"},
    ]
    accuracy = compute_accuracy(results)
    assert accuracy == 0.75
    print("PASS: compute_accuracy returns the fraction of correct predictions")


def test_compute_accuracy_handles_empty_results():
    assert compute_accuracy([]) == 0.0
    print("PASS: compute_accuracy handles an empty results list without dividing by zero")

compute_accuracy is itself a piece of deterministic code — the same kind this whole unit has been building test coverage for — because a bug in the accuracy calculation (for example, an off-by-one in the denominator, or comparing the wrong two fields) would silently produce a misleading metric that no amount of dataset quality could fix. The empty-list guard matters in practice: a filtering step upstream that accidentally drops every record should surface as an obvious bug, not as a ZeroDivisionError or, worse, a wrong number.

Accuracy alone can be misleading when categories are imbalanced. If 80% of real tickets are technical, a system that always predicts technical scores 80% accuracy while being useless. A confusion matrix — a breakdown of predicted-versus-expected category counts — reveals this kind of failure that a single accuracy number hides.

from collections import defaultdict


def build_confusion_matrix(results: list[dict]) -> dict:
    matrix = defaultdict(lambda: defaultdict(int))
    for r in results:
        matrix[r["expected_category"]][r["predicted_category"]] += 1
    return {k: dict(v) for k, v in matrix.items()}


def test_build_confusion_matrix_tracks_misclassifications():
    results = [
        {"expected_category": "technical", "predicted_category": "billing"},
        {"expected_category": "technical", "predicted_category": "technical"},
        {"expected_category": "billing", "predicted_category": "billing"},
    ]
    matrix = build_confusion_matrix(results)
    assert matrix["technical"]["billing"] == 1
    assert matrix["technical"]["technical"] == 1
    assert matrix["billing"]["billing"] == 1
    print("PASS: build_confusion_matrix tracks which categories are confused with which")

The confusion matrix shows which categories get mixed up with which others — in this example, one technical ticket was misclassified as billing. Knowing this specific failure pattern is far more actionable than knowing an overall accuracy percentage, because it points directly at where prompt or schema improvements should focus.

Measuring Consistency

Consistency requires running the same input through the model multiple times and checking how often the outputs agree with each other — not with a ground-truth label, but with each other. This matters because temperature, sampling, and model updates all introduce variability that a single-run accuracy score cannot detect.

from collections import Counter


def compute_consistency(repeated_outputs: list[str]) -> float:
    """Given N outputs for the *same* input, return the fraction matching the mode."""
    if not repeated_outputs:
        return 0.0
    counts = Counter(repeated_outputs)
    most_common_count = counts.most_common(1)[0][1]
    return most_common_count / len(repeated_outputs)


def test_compute_consistency_fully_consistent():
    outputs = ["billing", "billing", "billing", "billing"]
    assert compute_consistency(outputs) == 1.0
    print("PASS: compute_consistency returns 1.0 when all outputs agree")


def test_compute_consistency_partial_agreement():
    outputs = ["billing", "billing", "account", "billing"]
    assert compute_consistency(outputs) == 0.75
    print("PASS: compute_consistency returns the fraction matching the majority answer")

compute_consistency treats the most frequent output as the "consensus" answer and reports what fraction of the repeated runs agreed with it — it deliberately does not need to know the correct answer, only whether the system agrees with itself. A consistency score run across an entire dataset (repeating each example some fixed number of times, for example 5) gives an average consistency figure that reveals how much of your accuracy number might shift on a re-run purely due to randomness.

def average_consistency_across_dataset(per_example_outputs: dict) -> float:
    """per_example_outputs maps example_id -> list of repeated outputs."""
    if not per_example_outputs:
        return 0.0
    scores = [compute_consistency(outputs) for outputs in per_example_outputs.values()]
    return sum(scores) / len(scores)


def test_average_consistency_across_dataset():
    per_example_outputs = {
        "triage-001": ["billing", "billing", "billing"],
        "triage-002": ["technical", "account", "technical"],
    }
    avg = average_consistency_across_dataset(per_example_outputs)
    assert round(avg, 4) == round((1.0 + (2 / 3)) / 2, 4)
    print("PASS: average_consistency_across_dataset averages per-example consistency scores")

Note: Lowering temperature toward 0 generally increases consistency for classification-style tasks, but does not guarantee perfect determinism — infrastructure-level nondeterminism can still produce different outputs for identical inputs on some models and configurations.

Measuring Failure Rate

Failure rate is distinct from inaccuracy: an inaccurate answer is a wrong-but-valid answer ("billing" when the truth was "technical"), while a failure is the absence of a usable answer at all — an exception, a timeout, an empty string, or output that does not parse as one of the valid categories.

def compute_failure_rate(raw_results: list[dict]) -> float:
    """Each raw_result has 'predicted_category', which may be None on failure."""
    if not raw_results:
        return 0.0
    failures = sum(
        1 for r in raw_results
        if r["predicted_category"] is None or not is_valid_category(r["predicted_category"])
    )
    return failures / len(raw_results)


def test_compute_failure_rate_counts_none_and_invalid_predictions():
    raw_results = [
        {"predicted_category": "billing"},
        {"predicted_category": None},          # exception during the call
        {"predicted_category": "not_a_label"},  # model returned something invalid
        {"predicted_category": "technical"},
    ]
    rate = compute_failure_rate(raw_results)
    assert rate == 0.5
    print("PASS: compute_failure_rate counts both missing and invalid predictions as failures")

This function relies on is_valid_category from Lesson 6, reinforcing why defining the valid output space explicitly, as its own testable function, pays off here: failure detection and accuracy scoring both depend on a shared, unambiguous notion of what counts as a legitimate answer at all. In a real pipeline, predicted_category becomes None specifically because the calling code caught an exception (a timeout, a malformed structured-output response, an API error) and recorded the failure rather than letting it crash the whole evaluation run — which is itself a piece of application logic worth unit testing with the mocking techniques from Lesson 3.

Tracking All Three Over Time

None of these three metrics is meaningful as a single snapshot; their value comes from tracking them across evaluation runs — after every meaningful prompt change, schema change, or model version bump — and watching for regressions. A simple record format makes this tracking straightforward to build on top of the functions above:

def summarize_evaluation_run(run_id: str, results: list[dict], raw_results: list[dict],
                              per_example_outputs: dict) -> dict:
    return {
        "run_id": run_id,
        "accuracy": compute_accuracy(results),
        "consistency": average_consistency_across_dataset(per_example_outputs),
        "failure_rate": compute_failure_rate(raw_results),
    }

Storing the output of summarize_evaluation_run for every run (appended to a log file, a database table, or a JSON Lines history) is what makes the next lesson's regression testing possible: comparing today's numbers against a known-good baseline requires that baseline to have been recorded somewhere in exactly this shape.

Common Mistakes

  • Reporting only accuracy and ignoring consistency. A high accuracy figure from a single run can be an artifact of favorable randomness; without a consistency measurement, you cannot tell whether a re-run would reproduce the same score.
  • Counting a wrong-but-valid answer the same as an outright failure. Conflating "the model was wrong" with "the system crashed" hides two very different engineering problems — one is a prompt or model quality issue, the other is often a bug or missing error handling in your code.
  • Computing metrics on filtered or partial result sets without noticing. A bug that silently drops failed examples before computing accuracy will report an artificially high accuracy, since the hardest, most-likely-to-fail cases were never counted at all.

Best Practices

  • Always report accuracy, consistency, and failure rate together, since each one can look good while another hides a real problem.
  • Repeat at least a subset of dataset examples multiple times to measure consistency directly, rather than assuming a single run's accuracy is representative.
  • Persist every evaluation run's metrics in a consistent, comparable format, tagged with what changed (prompt version, model version, schema version), so regressions can be detected automatically rather than noticed anecdotally.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Measuring Accuracy, Consistency, and Failure Rates and get answers drawn from it.

Signed-in readers only.