Quick Reference · data, ML & LLM evaluation

evidently

Every evaluation is the same four moves: wrap a DataFrame in a Dataset that carries a DataDefinition schema, optionally score each row with Descriptors, summarise the whole thing with a Report of Metrics, and give any Metric a condition to turn it into a Test. Learn those four and the 100+ checks are just a catalogue.

setup & CLI Dataset & schema descriptors · row level Report & Metrics · dataset level Tests & conditions drift · output · UI gotcha / removed most common

Built against the official docs: docs.evidentlyai.com (library overview · data definition · descriptors · report · tests · output formats · metric generators · all metrics · all descriptors · all presets · drift explainer · drift customization · LLM judges · self-hosting · migration guide) · github.com/evidentlyai/evidently · PyPI · cross-checked against docs-old.evidentlyai.com to mark what died.

One evaluation end to end · and the two levels every check lives on
A · THE PIPELINE DataFrame plain pandas rows = inputs / outputs Dataset + DataDefinition + descriptors Report([...]) Metrics · Presets + tests=[...] .run(cur, ref) current FIRST reference SECOND snapshot HTML · JSON · dict DataFrame · UI Dataset.from_pandas(df, data_definition=...) reference is optional — pass None B · TWO LEVELS OF EVALUATION ROW LEVEL — Descriptors one score per row · adds columns to the Dataset answer Sentiment Length Denials Tokyo is the capital. 0.31 30 OK Sorry, I can't help. -0.44 19 DECLINE Sentiment("answer") · TextLength("answer") · DeclineLLMEval("answer") DATASET LEVEL — Metrics one number for the whole column or table MeanValue(column="Length") 24.5 MaxValue("Length", tests=[lte(150)]) PASS a Test is just a Metric with a condition — same Report, extra tab
The whole library in fourteen lines
import pandas as pd
from evidently import Dataset, DataDefinition, Report
from evidently.presets import DataDriftPreset
from evidently.metrics import MeanValue
from evidently.tests import lte

schema = DataDefinition(numerical_columns=["age", "fare"],
                        categorical_columns=["klass"])          # map types once

cur = Dataset.from_pandas(df_this_week, data_definition=schema)
ref = Dataset.from_pandas(df_last_week, data_definition=schema)

report = Report([DataDriftPreset(),                                # dataset-level metrics
                 MeanValue(column="fare", tests=[lte(100)])])         # + a condition = a Test

my_eval = report.run(cur, ref)   # CURRENT first, REFERENCE second. Not the other way round.
my_eval.save_html("report.html")      # or my_eval.json() / .dict() / just `my_eval` in Jupyter
Part I

Setup & the data layer

everything starts with a Dataset that knows its own schema
01Install & importPython 3.10+
02Dataset — wrap the DataFramethe entry point
03DataDefinition — column typesreplaces ColumnMapping
04Roles: id, timestamp, descriptorstype vs role
05Mapping ML taskstarget & prediction
06Current vs referenceorder matters
07The CLI & the local UIoptional dashboard
Part II

Scoring rows, summarising datasets

descriptors score each row · metrics score the whole table
08Report — the core objectconfig, then run
09The five Presetsthat is the whole list
10Column-level Metricscolumn= required
11Dataset-level Metricsno column needed
12Metric generatorsstop writing loops
13Descriptors — the row levelone score per row
14Deterministic descriptorsfree & instant
15ML-based descriptorslocal models, no API key
16LLM judges — built inneeds an API key
17LLM judges — your own criteriatemplates, not raw prompts
Part III

Tests, drift, output — and the legacy minefield

turning numbers into pass/fail, and what the internet still gets wrong
18Tests = Metric + conditionno separate object
19Condition operatorsevidently.tests
20Auto-generated conditionszero-config testing
21Row-level tests & TestSummarypass/fail per row
22Drift — how the test is chosensize & type decide
23Drift — picking your own20+ methods
24Getting results outfive destinations
25Tags, metadata & timestampsfor tracking runs
26The API the internet still teachesall dead on 0.7+
27When something looks wrongincluding in the docs

Four things worth seeing

The choices Evidently makes for you, and the one it makes you get right.

How the drift test gets chosen

You never pick one by default — the size of the reference set and the column type decide, and the meaning of the score flips with them.

rows in the reference set ≤ 1000 rows > 1000 rows HYPOTHESIS TESTS · drift if p < 0.05 numerical, >5 unique K–S categorical, or ≤5 unique chi-squared binary, ≤2 unique Z-test DISTANCES · drift if score ≥ 0.1 numerical, >5 unique Wasserstein categorical, or ≤5 unique Jensen–Shannon higher score = more drift TEXT COLUMNS — a domain classifier, either way Trains a model to tell current from reference. Drift score = its ROC AUC. ≤ 1000 rows: beat a random classifier at the 95th percentile (guards false positives) > 1000 rows: ROC AUC > 0.55

The argument order that bites

Old code named its arguments, so order never mattered. New code is positional — and the order is the reverse of how everybody says it aloud.

LEGACY ≤ 0.6.7 report.run(reference_data=ref, current_data=cur, column_mapping=cm) reference current they swap current reference CURRENT 0.7+ my_eval = report.run(cur, ref) Nothing errors if you swap them — drift is simply measured backwards. Use keywords until it sticks.

One Report, two tabs

Reports and Test Suites used to be separate objects producing separate files. Now Tests are an optional second tab on the same Report.

Report([DataSummaryPreset()], include_tests=True) Metrics Tests one .html file METRICS TAB — the numbers RowCount 4 812 MeanValue("fare") 32.20 DuplicatedRowCount 0 TESTS TAB — the verdicts rows > 10 PASS no missing values FAIL no duplicates WARNING drift on "notes" ERROR WARNING = is_critical=False — never alerts ERROR = the check itself failed to run Same computation feeds both tabs — a Test is only a Metric plus a condition, so nothing is calculated twice.

Which RAG descriptor watches which edge

A RAG row has up to four text columns. Each built-in judge inspects one relationship between them — pick by the edge you distrust.

question what was asked context what was retrieved response what was generated target ground truth, if any ContextRelevance ContextQualityLLMEval did retrieval find the answer at all? FaithfulnessLLMEval CompletenessLLMEval did it invent, or leave things out? CorrectnessLLMEval RESPONSE ALONE — no other column needed DeclineLLMEval · ToxicityLLMEval · PIILLMEval BiasLLMEval · Sentiment · TextLength · IsValidJSON Judges run on the first column; the other is passed as a named parameter.

Worth memorizing

run(cur, ref)current first, reference second — the opposite of how you say it
Report([...])positional list; metrics= is dead
ColumnMappingDataDefinition, inside a Dataset
metric_presetevidently.presets
a Testis just a Metric with tests=[...] attached
TestSuitegone — tests are a second tab on the Report
.show()gone — just name the object in Jupyter
.as_dict().dict()  ·  save_html() unchanged
five presetsDataSummary · DataDrift · TextEvals · Classification · Regression
descriptor vs metricone score per row vs one score per dataset
alias=set it on every descriptor — it names the column
text_columnsnever auto-mapped; required for text drift
1000 rowsdrift silently flips from p-values to distances
drift ≠ nullsdrift ignores missing values; test them separately
count vs sharetests= checks the count, share_tests= the proportion
include_tests=Trueauto conditions: from reference, or heuristics without one
Reference(relative=.1)thresholds relative to baseline instead of hard-coded
is_critical=FalseWarning instead of Fail — never fires an alert
TestSummaryput it last; it only sees tests declared before it
==0.6.7pin here if you need the pre-0.7 API