Skip to content

Introduce Reports for run evaluation - #868

Merged
cristian-tamblay merged 23 commits into
developfrom
feat/evaluation-reports
Sep 8, 2026
Merged

Introduce Reports for run evaluation#868
cristian-tamblay merged 23 commits into
developfrom
feat/evaluation-reports

Conversation

@Irozuku

@Irozuku Irozuku commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces Reports, artifact valued evaluation components computed once per evaluation partition of a run from (y_true, y_pred, class_names) and rendered in the run results. They are the structured, non rankable sibling of Metrics. A metric condenses a shape into a float; a report renders the shape, such as a confusion matrix, an ROC curve, residuals, or per segment scores.
The branch adds the abstract contract (BaseReport), thirteen concrete reports across classification, regression, forecasting and translation, the async ReportJob that computes them per partition, a Report table and REST endpoints, and a full frontend Reports tab with sidebar, cards, inline creator, partition selector, and persisted plot edits. A forward only fix keeps reports from ever asking a model to predict rows it was fitted on.

image image

Type of Change

Check all that apply like this [x]:

  • Backend change
  • Frontend change
  • CI / Workflow change
  • Build / Packaging change
  • Bug fix
  • Documentation

Changes (by file)

Backend

  • DashAI/back/reports/base_report.py: abstract BaseReport contract, compute(y_true, y_pred, class_names) returning a list of artifacts, the REQUIRES_PROBABILITIES gate, and the as_labels and resolve_class_names helpers.
  • DashAI/back/reports/{classification,regression,forecasting,translation}/: thirteen reports, Confusion Matrix, ROC Curve, Precision Recall Curve, Per Class Breakdown, Predicted vs Actual, Residual Plot, Residual Histogram, Forecast vs Actual, Residuals Over Time, Residual Autocorrelation, Per Segment Comparison, Segment Score Distribution, and Length Comparison.
  • DashAI/back/job/report_job.py: ReportJob computes a report over every partition the run exposes, skips partitions a forward only model cannot predict, and reports skipped partitions as text.
  • DashAI/back/api/api_v1/endpoints/reports.py and schemas/reports_params.py: create, list and delete reports, fetch artifacts, and persist and reset plot overrides.
  • DashAI/back/dependencies/database/models.py and DashAI/alembic/versions/e6c3b91a7d48 and b7e4d2a19c63: Report table and migrations.
  • DashAI/back/initial_components.py: registers every report in the registry.
  • DashAI/back/api/api_v1/api.py and core/enums/status.py: router mount and ReportStatus.

Frontend

  • DashAI/front/src/components/models/runResults/ReportResultsTab.jsx and ResultsTabsHeader.jsx: Reports tab in the run results.
  • DashAI/front/src/components/reports/: ReportsSidebar, ReportCard, InlineReportCreator, ConfigureReportStep, and createAndRunReport.js.
  • DashAI/front/src/components/shared/ArtifactList.jsx and ArtifactGroupSelector.jsx: shared artifact and grouped selector rendering extracted from the explainer plot flow.
  • DashAI/front/src/utils/artifactOverrides.js: persist and reset plot edits.
  • DashAI/front/src/components/notebooks/explorer/plotLayout/ColorscaleSelector.jsx: reverse toggle for heatmap colorscales.
  • DashAI/front/src/utils/i18n/locales/en,es,pt,de,zh/reports.json: new locale bundles.

Tests

  • tests/back/reports/: unit tests per report, inputs, normalization, and type registration.
  • tests/back/api/test_reports_api.py: end to end create, ReportJob, artifacts, and overrides on holdout and cross validated runs.
  • tests/back/api/test_reports_forecasting_api.py: end to end proof that a forward only run reports only on its forecastable partitions.

Testing (optional)

  • Train a run per task, classification, regression, forecasting, and translation, and add each report from the Reports sidebar. Confirm partitions appear as selector entries and plots render.
  • Edit a plot and reload the page. The edit should persist and reset correctly.

Notes (optional)

  • Reports are model agnostic by contract. They receive only predictions and truth, never model internals or inputs, so each report must work with every model of its task.
  • Forward only tasks, such as forecasting, can never be scored on the rows the model was fitted on. ReportJob resolves partitions through the same predictable_splits helper the prediction flow uses.

Irozuku added 22 commits August 3, 2026 11:41
ExplainersPlot held the only renderer for the backend artifact wire format:
leaf artifacts, grouped selectors, and the layout that pairs a selector with
its batch. Any second consumer had to import from the explainers tree.

Move that into a shared ArtifactList. The group picker becomes a render prop
so the shared component never reaches for the dataset API, which only the
explainers' instance table needs.
A report is the artifact-valued sibling of a metric: both compare a model's
predictions over one split against the truth, and they differ only in
codomain. A metric reduces to a rankable float; a report reduces to a shape
a float cannot hold, such as a K x K confusion grid or an ROC curve.

The hook takes no features and no model, so a report cannot probe how the
model responds to inputs; that stays the explainer's job.
Reports are added from the right sidebar on the run's Reports tab, mirroring
how explainers are added: click a compatible report, pick the split it should
describe, configure it when it takes parameters, and the creator both stores
the row and enqueues its job.

The parameter step is dropped for the reports that take none, since a one
step wizard is just a dialog with a Save button.
ModelListItem defaults its drag type to the model type, so report rows were
draggable but announced themselves as models. The central panel read the drop
as a model and opened the Add Model dialog instead of the report creator.

Give the rows the report drag type and a payload of just the name and display
name, and teach the panel's drag handlers the third type.
The colorscale selector always read and wrote layout.coloraxis.colorscale.
That only reaches the plot for figures built by px.imshow, which point their
trace at a shared colour axis. A figure built straight from go.Heatmap, like
the confusion matrix report, carries its colorscale on the trace, so every
edit landed on a layout key nothing renders from and the colours never moved.

Follow the same rule the colorbar fields already followed: write to the shared
axis only when the trace references one, and to the trace otherwise.
Plotly reverses a scale with a flag rather than by rewriting its stops, so one
switch covers a named preset and a hand built array alike. It stays visible in
both modes: hiding it in array mode would leave a flag silently reversing a
set of stops the user built by hand.
A report's edits were client side only: ArtifactViewer kept them in local
state, so a reload showed the computed figure again. Reports already had a
plot_overrides column; nothing read or wrote it.

Add the save and reset endpoints, apply the stored overrides when the
artifacts are read, and have the card seed its overridden set from the
response so a reload still offers a reset.

The override helper moves from the explainers endpoint module into
core.artifacts, since it is artifact logic that two callers now share.
ArtifactViewer held a saved edit only in local state, and cleared it whenever
the artifact prop changed. A local explainer's instance picker swaps which
artifact sits in each slot, so switching instance dropped the edit and
switching back showed the stale computed figure, since the fetched list still
carried it.

Fold the saved figure into that list instead, so the artifact itself carries
the edit for as long as the list lives.

Also remove the reset edits button and the plumbing that existed only to drive
it: the overridden index bookkeeping, the reset client calls and the label.
Mirrors the explainer card. ReportStatus uses the same codes as the run and
explainer statuses, so the shared RunStatusDot maps them without a second
colour table: grey not started, blue queued or running, green finished, red
failed.

The dot reads the polled status rather than the prop, so a card that is still
computing turns green in place when the job lands.
Deleting a report threw away a computed job and any plot edits saved against
it, with no way back, on a single click of a small icon. Route it through the
same confirmation modal the explainer cards use.
Matches how the explainer list is ordered, so adding one from the sidebar
appends it in both tabs rather than pushing the list down in one of them.
Opening the tab lands at the bottom, adding a report glides to it, and the new
card flashes a ring for four seconds.

New rows are detected by diffing ids against the previous fetch rather than by
a flag from the creator, so the three second poll that refreshes running
reports cannot replay the scroll or the ring.
Adding a report refreshed the list, and the refresh unmounted every card to
show a centred spinner before bringing them back. It happened twice per add,
once on create and again when the job landed, which read as lag rather than
progress. Only the first load shows the spinner now; later fetches swap the
rows in place, as the explainer list already did.

Also drop the per card artifact poll. A running report has no artifacts to
fetch, and the parent already polls the list while any report is running, so
the timer only doubled requests during the wait.
Three costs compounded on the reports tab, which unlike the explainer list
mounts every card at once.

ArtifactRenderer built a fresh layout object on each render, and
react-plotly.js diffs by reference, so every render sent Plotly through a full
relayout. Starting a drag sets state on SessionVisualization, which re-renders
the whole run detail tree, so the drag itself relayouted every plot on screen
before the pointer had moved.

ArtifactViewer also kept its fullscreen lightbox mounted, so each artifact held
two Plot instances forever, for a dialog that opens with no transition.

Memoize the layout and the artifact objects, memoize the renderer so an
ancestor re-render cannot reach Plotly, and drop keepMounted.
Reports no longer ask which split to describe. The job runs the report once
per partition and wraps each result in a group, so the card renders the
selector layout global explainers use: partitions on the left, that
partition's artifacts on the right.

Partitions are now data rather than part of the API contract. The split enum,
the split column and the split step are gone, and PARTITIONS in the job is the
single place that knows a holdout run has three of them. Teaching DashAI cross
validation means yielding k folds there; no report class changes, because none
of them know what a partition is.

compute() narrows to leaf artifacts for the same reason: the per partition
grouping belongs to the job, and a group cannot contain another group.

A partition that cannot be computed, such as a split holding a single class,
is skipped and named in the output rather than failing the whole report.
Most reports have nothing to configure now that the split is gone, so their
dialog was a single Create button standing between the click and the work.
Add them outright instead, and keep the dialog for the two that do take
parameters.

The component list already carries each schema, so the decision needs no
extra request. Dropping a report onto the central panel follows the same
rule, with the schema carried in the drag payload; the create and enqueue
path is shared so both entry points behave identically.

The decision deliberately sits in the click and drop handlers rather than in
an effect watching the selection: StrictMode double invokes effects, and this
one would POST.
Develop moved PlotOverrideBody into core.artifacts, so the reports only
duplicate goes.
Partitions come from the run's splitter now, the same set the predict and
local explainer flows offer, so a cross validated run reports on its
reserved test rows and the rest.
The shared artifact list built only the save handler, so the reset button
the viewer already had could never render.
The report job resolved its partitions with run_splits, which offers every
partition a splitter declares plus the whole dataset. A task that predicts
forward only, such as forecasting, refuses to return a value for dates inside
the training window, so asking it to report on the train partition failed the
whole job.

Resolve partitions through predictable_splits, the same helper the prediction
flow uses, passing the run's task and evaluation strategy. It drops the rows
the kept model was fitted on for forward-only tasks, so a forecasting report
covers only the partitions that lie after the fit.
Forecasting reports (work on any forecaster's numeric output):
- ForecastVsActual: truth and forecast as lines over the observation index.
- ResidualsOverTime: residual against observation index, revealing drift.
- ResidualAutocorrelation: residual autocorrelation by lag with a confidence
  band and a configurable max lag.

Translation reports (work on any translation model's text output):
- PerSegmentComparison: reference against translation per segment with
  sentence level BLEU, highlighting the worst rows.
- SegmentScoreDistribution: histogram of the per segment scores.
- LengthComparison: reference against translation length scatter, revealing
  truncation.

All six consume only y_true and y_pred, never model internals, and are
registered in get_initial_components under their task's COMPATIBLE_COMPONENTS.
@Irozuku Irozuku added front Frontend work back Backend work enhancement New feature or request labels Sep 7, 2026
@cristian-tamblay
cristian-tamblay merged commit 093b379 into develop Sep 8, 2026
21 checks passed
@cristian-tamblay
cristian-tamblay deleted the feat/evaluation-reports branch September 8, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

back Backend work enhancement New feature or request front Frontend work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants