Add explainability support for cross-validation - #834
Merged
Conversation
Irozuku
force-pushed
the
fix/explainers-cross-validation
branch
from
August 24, 2026 16:32
d0b2e3c to
b560dd4
Compare
cristian-tamblay
force-pushed
the
fix/explainers-cross-validation
branch
from
August 24, 2026 20:32
b560dd4 to
d22fb91
Compare
cristian-tamblay
approved these changes
Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cross-validation runs can now be explained. Until now they could not: the explainer job crashed on the fold shaped
split_indexespayload withKeyError: 'train_indexes', and even past that there was nothing honest to explain, because the model that gets saved is refit on every row the folds used.Cross-validation sessions now reserve a slice of the dataset (default 10%, configurable, 0 to disable) that the folds never touch. The final model never sees those rows, so they are what explanations are measured on. A run that reserved nothing is refused with a reason rather than explained on data the model already learned from.
Which partitions a run exposes is decided by the splitter that produced it, not by code that inspects the payload's shape, so a splitter with a different partitioning scheme, including one from a plugin, participates without changes to the explainability code or the frontend.
Type of Change
Changes (by file)
Backend
splitters/base_splitter.py: new contract,explainable_partitions()plusEVALUATION_PARTITION, naming the partition that holds rows the model has not seen.explainable_splits()is generic on top of it, so a splitter usually implements one method.splitters/fold_splitter.py: carves the reserved rows before folding, then maps fold positions back to original row numbers so no fold ever sees a reserved row. The carve respects the splitter through oneHOLDOUT_STRATEGYattribute:ShuffleSplit,StratifiedShuffleSplit, orGroupShuffleSplitso a group is never split across the carve. Declares its partitions astrainandholdout.splitters/holdout.py: declares itstrain/test/valpartitions.splitters/*.py(all seven fold splitters): newholdoutschema field, float 0 to 0.5, default 0.1, so the wizard renders it automatically.splitters/splits_payload.py:splitter_class_for()resolves a run's splitter from the registry;explainable_indexes()asks that class for the rows an explainer may use and refuses when the partition it evaluates on is empty. The previous shape sniffing is gone.job/explainer_job.py: resolves the run's splitter and asks it which partitions exist, instead of indexing fixed keys. Accepts whichever partition name the splitter offers and maps it onto the dataset dictionary. Refuses deriving a split of a dataset the session never saw.api/api_v1/endpoints/explainers.py: newGET /explainer/explainable-splits/{run_id}, returning the partitions and row counts a run can be explained on.Frontend
explainers/SelectDatasetStep.jsx: renders the partitions the backend returns, with real row counts, replacing the hardcoded["train","test","val","all"]and the fraction arithmetic that producedNaN / NaNfor cross-validated sessions. The split choice only appears when the selected dataset is the session's own. Unknown partition names render viat(..., { defaultValue: name }), so a future backend-added partition needs no frontend change.explainers/SplitSelector.jsx: deleted, unused and emitting a split name the backend never accepted.api/explainer.ts: client for the new endpoint.models/runResults/ResultsTabsHeader.jsx: disables the Explainability tab for a cross-validation run that reserved nothing, with the reason in the tooltip.Testing
Inside the app: set a cross-validation session with the default 10% reserved, confirming that an explainer runs on the reserved set; one with the reserved field set to 0, confirming that the tab is disabled and displays its tooltip; and a group-based splitter, confirming that it moves whole groups so the reserved count is approximate rather than exact.