Changelog
Changelog
All notable changes to Ictonyx will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Planned
VariabilityStudyResults.report()for self-contained HTML/markdown summariesVariabilityStudyResults.bootstrap_ci()convenience methodInfrastructure sweep
PyTorchDataHandlerPaired/blocked experimental designs for model comparison
plot_run_independence_diagnostics,plot_paired_deltas,plot_epoch_run_heatmap,plot_sequential_ci,plot_stability_pareto,plot_run_metric_correlationsMigration Guide (prerequisite for v0.5.0 deprecated API removal)
v0.4.8 — 2026-04-26
Critical:
Fix PyTorch regression
val_lossappended twice per epoch, crashingpd.DataFrame()in the runner when validation data is provided.Fix
_build_from_class()not injectingrandom_statefor classes using**kwargs(e.g. XGBoost >=2.0). All runs received the default seed, producing identical models.BaseModelWrappersubclasses are excluded, as they handle seeding internally viarun_seedinfit().
Moderate:
Wilcoxon effect size now uses tie-corrected variance. The uncorrected formula overestimated sigma_W when tied ranks existed, systematically underestimating effect size r. P-values were unaffected.
compare_models(metric=None)auto-resolves from available metrics instead of hardcodingval_accuracy, which causedKeyErrorfor regression models.Keras
clear_session()now runs beforeset_random_seed(). In TF 2.16+, the previous ordering partially reset the random state.anova_testtreatsNonenormality (untestable, n < 3) as untestable rather than as a violation.
Other:
_make_dataloader()usestorch.floatlabels forBCEWithLogitsLoss/BCELoss, fixing test evaluation dtype mismatch.np.stdddof=0 changed to ddof=1 in deterministic-difference check.Dead code removed in
explainers.py(3 locations) andcheck_independence().Duplicate docstring heading removed in
check_independence().
Tests:
New integration test suite:
variability_study()end-to-end for sklearn classification, regression, and**kwargsclasses; PyTorch classification and regression.Existing tests updated to match corrected effect size formula,
random_stateinjection behavior, and metric auto-resolution.
[0.4.7] — 2026-04-20
Fixed
KerasModelWrapper.fit()now acceptsrun_seedandverbosefromfit_kwargs(previously crashed all Keras-based variability studies on v0.4.7.dev0).HuggingFaceModelWrapperper-run seeding actually works (previously fell back to seed=42 on every run, producing identical metrics across runs).HuggingFaceModelWrapperrespectsverbose=False(no more 50+ lines of Trainer chatter per run).BaseModelWrapper.__del__no longer raisesImportErrorduring interpreter shutdown.GridStudyResults.summarize()and.to_dataframe()no longer crash on dry-run results.plot_shap_waterfallhandles SHAP 0.45+’s 3-D ndarray return for multiclass models.VariabilityStudyResults.preferred_metric()raises a helpfulKeyErrorwhen the requested base metric isn’t tracked, instead of silently returning a phantom key.test_against_nullno longer emits aDeprecationWarningpointing at itself.compare_two_modelsbootstrap CI no longer silently skipped for paired and parametric test branches.kruskal_wallis_testeffect size correctly labelledeta-squared-H(was mislabelledepsilon-squared); also reportsepsilon-squared-Ras secondary.
Added
VariabilityStudyResults.test_above_chance()— one-sided Wilcoxon for “is this model above chance” question.VariabilityStudyResults.from_json()— symmetric with existingto_json()for round-trip persistence.compare_two_models(ci_target=...)— Hodges-Lehmann bootstrap CI for MW-matched inference (deprecation cycle; default flips tomedian_differencein v0.5.0).compare_two_models(test_method=...)— explicit test selection replaces data-driven pretest-then-choose (deprecation cycle; default flips tomann_whitneyin v0.5.0).friedman_test— Friedman chi-squared for 3+ paired groups with Kendall’s W effect size.bootstrap_hodges_lehmann_ci— new primitive for location-shift CI matching Mann-Whitney’s null.required_runs_paired— power analysis for paired comparisons.utils.train_val_test_split(split_basis=...)— unified semantics with data handlers (deprecation cycle; default flips tooriginalin v0.5.0).
Infrastructure
Dockerfile updated: TensorFlow 2.16+, mlflow 2.11+, Hugging Face stack (transformers, datasets, accelerate) installed at build time.
Module-scope
import sysincore.py(was inline in__del__, unreachable during shutdown).
Deprecations
wilcoxon_signed_rank_test(public) removed fromictonyx.__all__; still importable fromictonyx.analysis. Hard removal in v0.5.0.paired_wilcoxon_testwarning text now references the existingtest_against_nullinstead of the not-yet-extanttest_above_chance.
[0.4.6] — 2026-04-19
Added
plot_paired_deltas(results_a, results_b, metric)— per-run paired differences between two variability studies with a zero reference line, mean line, and optional bootstrap CI band. Shows “winner reverses” cases where the mean difference is positive but individual runs can reverse.plot_run_independence_diagnostics(results, metric)— visualizes per-lag autocorrelation of the run-indexed final-metric series with Bonferroni-corrected confidence bands. Wraps the existingcheck_independence()so the plot reflects exactly what the statistical test uses. Replaces the deprecatedplot_averaged_pacf.plot_epoch_run_heatmap(results, metric)— epoch × run heatmap of a per-epoch metric, with NaN padding for ragged run lengths. Reveals which runs train fast, plateau early, or diverge.plot_sequential_ci(results, metric)— bootstrap CI width as a function of number of runs. Supports the “how many runs do I need?” methodology argument by showing where additional runs stop meaningfully improving precision.VariabilityStudyResults.preferred_metric()now accepts acontext="scalar"|"epoch"keyword. Epoch context always resolves toval_*(matching what per-epoch plotters can actually use); scalar context preserves the priortest_* when available, else val_*behavior.
Fixed
plot_training_stability()now accepts aVariabilityStudyResultsdirectly viaresults=, with the stability analysis computed internally. The existing dict form (precomputed stability results) stays as a valid API for users who want multiple views of the same analysis.Per-epoch plotters (
plot_run_trajectories,plot_rank_correlation_over_epoch) now request epoch-context metrics explicitly, which correctly resolves toval_*regardless of whether test data is present. Previously, the presence of test metrics could cause these plotters to attempt to resolvetest_*keys that don’t exist in per-epoch history DataFrames.ExperimentRunnerno longer passesepochs,batch_size, orverbosetoScikitLearnModelWrapper.fit(), which had deprecated them. Eliminates threeDeprecationWarningemissions per sklearn run from library-internal code.
[0.4.5] — 2026-04-18
Fixed
compare_two_models()was attaching a Cohen’s d bootstrap confidence interval to paired Wilcoxon results. The two estimators have incompatible scales: Wilcoxon’sr = |z|/sqrt(n)is bounded by 1.0, Cohen’s d is unbounded. On large-effect paired data the CI could exclude the point estimate. Paired Wilcoxon now returnsci_effect_size=None; a Wilcoxon-r bootstrap CI is planned for v0.4.9.compare_models()now routes pairwise comparisons throughcompare_two_models(), surfacing the bootstrap confidence intervals that the analytic core was already computing. Previously, the public API silently droppedci_effect_sizeandconfidence_interval. Multiple-comparison corrections continue to apply to p-values only._build_from_class()now forwards kwargs from ModelConfig to wrapper constructors. Previously, user-supplied constructor arguments (including required ones likemodel_name_or_pathforHuggingFaceModelWrapper) were silently dropped, causing every run to fail.paired_wilcoxon_testemitsUserWarningand returnsconclusion="inconclusive"whenstd(differences) < 1e-10. Previously, deterministic model pairs produced spurious p-values. The tolerance is exposed via thedeterministic_tolparameter.
Added
VariabilityStudyResults.save()now includes a_schema_versionfield.load()warns viaUserWarningon missing or mismatched schema versions.VariabilityStudyResults.run_seeds: List[int]captures the per-run child seeds spawned viaSeedSequence.spawn(), enabling exact per-run reproduction of a study.
Tests
Several pre-existing paired-comparison tests used constant-offset data, which the new deterministic-pair guardrail correctly rejects. Test inputs updated to use genuine paired variation. One integration test (iris + sklearn) was rewritten to verify the integration path rather than assert significance on a mathematically undefined comparison.
Documentation
New README section What variability does Ictonyx measure? clarifies that the library characterizes training-stochastic variance under a fixed data split, not sampling variance.
[0.4.4] — 2026-04-18
Added
HuggingFaceModelWrapper— text classification viatransformers.Trainer. Accepts(List[str], List[int])ordatasets.Dataset. Seed-controlled viatransformers.set_seed()andTrainingArguments(seed=..., data_seed=...). Temporary checkpoint directory cleaned up after each run to prevent disk accumulation.pip install ictonyx[huggingface]plot_rank_correlation_over_epoch(results, metric, threshold, window, ax, show)— Spearman correlation between epoch-k and final-epoch run rankings over training. Annotates the first epoch at which the smoothed correlation exceedsthreshold. Requiresn_runs >= 15.Sphinx documentation site with ReadTheDocs integration at
https://ictonyx.readthedocs.io. API reference pages for all 13 modules.
Fixed
PyTorchModelWrapperregression history now recordsval_mse,val_r2,val_mae, andval_rmseper epoch via a new_compute_epoch_regression_metrics()helper. Previously onlyval_losswas recorded for regression tasks._standardize_history_df()rename map extended to coverr2 → train_r2,mse → train_mse,rmse → train_rmse,mae → train_mae.failed_runscount is now persisted to and restored from checkpoint. Previously reset to zero on resume, under-counting failures.Failure rate denominator now uses attempted runs (successful + failed). A study losing 5 of 20 runs previously reported 0% failure rate.
assertguards in public paths replaced with explicitRuntimeErrororValueError, and# type: ignore[return-value]where the assert was suppressing a mypy false positive.assertis silently stripped bypython -O.KerasModelWrapper.predict_proba():rtolchanged toatolin probability sum check. Relative tolerance is semantically wrong for checking whether probabilities sum to 1.HuggingFaceModelWrapper.fit(): history buffer lengths now aligned before constructingTrainingResult. Mismatched lengths (val metrics captured per epoch; train loss captured once) caused silent run failures invariability_study().KerasModelWrappergainsclear_session: bool = Trueconstructor parameter. WhenTrue, callstf.keras.backend.clear_session()at the start of eachfit()to release GPU memory from previous runs.summarize()output now includes N (run count) and SE (standard error) for each metric block.stability_weightnow raisesValueErrorat construction time for values outside[0, 1].osmoved to top-level import incore.py; redundant local imports removed.
Deprecated
wilcoxon_signed_rank_test()now emitsDeprecationWarning. Usecompare_results()ortest_against_null()instead. Will be removed in v0.5.0.
Compatibility notes
TrainingArguments:no_cudareplaced withuse_cpu(transformers ≥ 4.45).Trainer.__init__():tokenizerparameter removed (transformers ≥ 4.46). Padding is handled entirely byDataCollatorWithPadding.Test model for CI:
prajjwal/bert-tinyreplaced withgoogle/bert_uncased_L-2_H-128_A-2which includes a completetokenizer.json.
[0.4.3] — 2026-04-17
Fixed — Critical correctness
VariabilityStudyResults.preferred_metric(): key-prefix mismatch caused the method to always return"val_accuracy"even when a held-out test set was provided.final_test_metricsstores bare keys ("accuracy") but the method was checking for"test_accuracy", silently routingtest_against_null()andcompare_results()to validation metrics for every user who provides a test set.get_test_metric_values()now accepts both bare ("accuracy") and prefixed ("test_accuracy") forms transparently.KerasModelWrapper.evaluate(): no longer crashes withTypeErroron loss-only Keras models. TF 2.x returns a plainfloatwhen no additional metrics are compiled;dict(zip(metric_names, float))raised because a float is not iterable. A scalar guard wraps the result before zipping.PyTorchModelWrapper.assess(): inlinefrom sklearn.metrics import accuracy_scoreremoved. The module-level symbol (which has a numpy fallback for sklearn-free environments) is used instead.PyTorchModelWrapper.predict_proba(): last-layer softmax detection changed fromlist(self.model.modules())[-1]tolist(self.model.children())[-1]. The prior implementation performed a depth-first traversal returning the deepest leaf of the last sub-branch for non-Sequential architectures (ResNets, attention blocks), not the output layer.compare_models(): metric routing reverted to always default to"val_accuracy"whenmetric=None. The BUG-39 fix caused auto-routing to test metrics wheneverhas_test_datawas true, including cases where the DataHandler created an internal test split not intended for cross-model comparison. Users who want test-set comparison passmetric="test_accuracy"explicitly.check_independence()docstring:Note:corrected — the function applies Bonferroni correction; the stale text claiming it did not was removed.
Fixed — Plotting correctness
plot_pairwise_comparison_matrix(): no longer crashes onModelComparisonResultsdataclass;hasattr/isinstanceguards replace dict key access.plot_roc_curve()andplot_precision_recall_curve():_check_tensorflow_utils()removed; sklearn users no longer blocked by a TF import check when a numpy fallback was already present.plot_comparison_forest(): CI now uses the pre-computedconfidence_intervalfromStatisticalTestResultwhen available (paired, correct). Falls back to Welch unpaired approximation. Guard added to prevent unpacking non-sequenceconfidence_intervalvalues (e.g. mocks).ax=parameter added.plot_pacf_vs_lag(): CI band no longer subtracted from PACF values; prior implementation centred the band at zero instead of at the PACF values.plot_averaged_pacf(): fabricated CI formula1.96/sqrt(len(lags)*10)replaced with correct1.96/sqrt(n_series). Newn_seriesparameter required for CI reference lines. Deprecated; will be removed in v0.5.0._finalize_plot(): now returnsNonewhen showing, preventing Jupyter double-render where the returned figure object caused a second inline display.
Changed — Style and infrastructure
Added 8 new keys to
settings.THEMEand all three theme variants (default,dark,publication):neutral,better,worse,point,positive,negative,sequential,diverging.set_figsize_scale(factor)andget_figsize(base)added tosettings.pyfor global figure size scaling._apply_style(ax)helper added toplotting.py— single source of truth for grid, spines, and tick parameters across all plot functions.import seaborn as sncorrected toimport seaborn as snsthroughoutplotting.py(10 call sites).Seven functions converted from
plt.figure()+ state-machineplt.*calls tofig, ax = plt.subplots()+ax.*methods:plot_confusion_matrix,plot_roc_curve,plot_precision_recall_curve,plot_autocorr_vs_lag,plot_averaged_autocorr,plot_pacf_vs_lag,plot_averaged_pacf. All now usesettings.get_figsize()anddpi=150.Hardcoded colour strings in
plot_training_stabilityreplaced withsettings.THEMEreferences.ax: Optional[Axes] = Noneadded toplot_comparison_boxplots,plot_autocorr_vs_lag,plot_averaged_autocorr,plot_pacf_vs_lag, and all three new variability functions.
Added
plot_run_trajectories(results, metric, ax, show)— overlaid per-run epoch trajectories with mean curve.plot_run_distribution(results, metric, ax, show)— violin plot of final metric values with individual run dots overlaid.plot_run_strip(results, metric, ax, show)— strip plot with mean and ±1 SD markers. Best for small n where a violin oversmooths.plot_variability_summary(): newkind=parameter dispatches to the three new functions when used withresults=. Old positional-argument form deprecated and will be removed in v0.5.0.plot_comparison_boxplots(): significance bracket annotations (*) drawn above boxes for pairs insignificant_comparisons.
[0.4.2] — 2026-04-15
Fixed
pyproject.toml: relaxed numpy pin from^1.24.0(>=1.24.0, <2.0.0) to>=1.24.0, restoring compatibility with numpy 2.x. The prior cap causedpip install ictonyxto silently downgrade numpy, breaking shap 0.46+, rasterio, and any other package requiring numpy 2. Also relaxed pandas, scipy, matplotlib, and seaborn to floor-only pins for the same reason. The shap optional dependency was similarly relaxed from^0.43.0to>=0.43.0(Poetry’s caret notation caps at<0.44.0when the major version is 0).
[0.4.1] — 2026-04-15
Fixed — Statistical correctness
check_convergence(): replaced OR-of-heuristics with slope-based linear regression test. Prior implementation declared convergence on virtually every decreasing loss curve; new test requires the recent window to be statistically flat by magnitude or by evidence.rank_biserial_correlation(): returns(nan, "undefined")withRuntimeWarningon exception instead of silently returning(0.0, "negligible").compare_results(): now uses paired Wilcoxon signed-rank test by default (paired=True) when both inputs have equal run counts, consistent withcompare_models(). Falls back to Mann-Whitney U withUserWarningwhen run counts differ. Passpaired=Falseto use Mann-Whitney U explicitly.assumptions_met.update()incompare_two_models(): replaced withsetdefaultso keys already set by the chosen test are not overwritten.Noneassumption values increate_results_dataframe(): no longer collapsed toFalse. A newassumptions_untestablecolumn counts untestable assumptions.ScikitLearnModelWrapper.evaluate(): now returnsrmsealongsider2,mse, andmaefor regression tasks, consistent with_regression_metrics().PyTorchModelWrapper.assess(): rewritten. RaisesValueErroron NaN predictions and ontask=Noneinstead of silently falling through to the regression branch. Classification path now usesaccuracy_scorefor consistency with Keras and sklearn wrappers. Full docstring added.
Fixed — Checkpoint reliability
Checkpoint writes are now atomic: data is written to a
.tmpfile and renamed into place withos.replace(), preventing corruption on interrupted runs.Checkpoints now include a
_schema_versionfield sourced fromictonyx/_version.py. Loading a checkpoint from a different version emitsUserWarningwith guidance to delete and restart.
Fixed — API consistency
run_grid_study(),ExperimentRunner.run_study(),run_variability_study(): defaultnum_runsraised to 20, consistent withvariability_study()andcompare_models().run_grid_study()now emitsUserWarningwhennum_runs < 20.for_variability_study()inModelConfig: defaultnum_runsraised from 10 to 20.compare_results(): newseedparameter threads random state to bootstrap CI computation for reproducible results.compare_models(): now applies_INFRA_KWARGSseparation before callingauto_resolve_handler(), preventing model hyperparameters from reaching the DataHandler and raisingTypeError.compare_models()two-model shortcut: aliasedStatisticalTestResultreplaced withdataclasses.replace(), preventing mutation ofoverall_testfrom affecting thepairwise_comparisonsentry.ModelConfig.merge()and.has(): changed fromDeprecationWarningtoUserWarningso the notice is visible in Jupyter notebooks. Python silencesDeprecationWarningby default in all non-__main__contexts; the identical fix was applied toget_final_metrics()in v0.3.15 but not here.test_against_null()docstring: correctednum_runs >= 10recommendation tonum_runs >= 20.ScikitLearnModelWrapper.load_model(): now emitslogger.warning()on pickle fallback, matching the security notice already present insave_model().
Fixed — Core wrappers
PyTorchModelWrapper.predict(): raisesValueErroron NaN outputs, checked on the raw tensor beforetorch.maxdiscards NaN values into integer indices.PyTorchModelWrapper.predict_proba(): emitsUserWarningwhen the model’s final layer isnn.Softmaxornn.LogSoftmax.ImageDataHandler: per-class file listing now usessorted()for consistent cross-platform ordering.ImageDataHandler.load(): emitsUserWarningabout fixed training shuffle across runs. Full per-run shuffle fix deferred to v0.5.0.All three
assess()docstrings and_regression_metrics()docstring:rmseadded to documented regression return keys.get_epoch_statistics()docstring: corrected “percentile CI band” to “t-based confidence interval on the per-epoch mean across runs”.
Fixed — MLflow and SHAP
MLflowLogger.log_model(): replaced fragilehasattrduck-typing withisinstancechecks for Keras, PyTorch, and sklearn model flavour selection.MLflowLogger.get_run_url()file://branch: now respects theMLFLOW_UI_URLenvironment variable instead of always returninghttp://localhost:5000.SHAP background dataset: now sampled randomly via
np.random.default_rng(42)instead of always selecting the first N rows.shap.DeepExplainer: emitsDeprecationWarningwhen SHAP >= 0.45.
Deprecated
HyperparameterTuner.tune()Hyperopt fallback: now emitsDeprecationWarningwhen Optuna is not installed. Hyperopt backend will be removed in v0.5.0. Install Optuna:pip install ictonyx[tuning].ScikitLearnModelWrapper.fit()epochs,batch_size,verbosekwargs: now emitDeprecationWarning. These parameters will raiseTypeErrorin v0.5.0. sklearn models have always silently ignored them.
Changed
ictonyx/_version.pyadded as the single source of truth for the package version.__init__.pyandrunners.pyboth import from it directly, replacing the prior inline__version__string and avoiding stale metadata fromimportlib.metadataduring development.compare_multiple_models()removed fromictonyx.*public namespace. Still callable asictonyx.analysis.compare_multiple_models. Useix.compare_models()instead.hyperoptremoved fromallandmlextras inpyproject.toml._SKLEARN_FIT_KWARGSand_KNOWN_IGNORED_KWARGSpromoted to class-levelfrozensetconstants inScikitLearnModelWrapper. Addedeval_setandcat_featuresahead of v0.5.0 XGBoost/LightGBM wrappers.
Internal
Redundant
mannwhitneyucall eliminated inmann_whitney_test()by passing the already-computed U statistic torank_biserial_correlation().BCa two-sample bootstrap: added
UserWarningwhen group size ratio exceeds 3:1, complementing the existingmin(n1, n2) < 15warning.calculate_averaged_autocorr(): parameter renamedhistories→series_list.kruskal_wallis_test(): epsilon-squared formula clarified in a comment.get_epoch_statistics(): emitsUserWarningwhen run counts vary across epoch positions.Redundant local
accuracy_scoreimport removed fromPyTorchModelWrapper.assess().run_grid_study()PEP 604 annotation:list[int | None]replaced withList[Optional[int]]for Python 3.10 runtime compatibility.Checkpoint docstring corrected:
checkpoint.joblib→checkpoint.pkl.Test coverage for
explainers.pyexpanded: SHAP availability guards, KernelExplainer path for non-tree models,DeepExplainerdeprecation warning, and out-of-bounds sample index handling.
[0.4.0] — 2026-04-10
Fixed — Statistical correctness
_hedges_g(): called_interpret_variance_explained()instead of_interpret_cohens_d()when labelling the effect size magnitude. g = 0.20 was labelled “large” (eta-squared scale) instead of “small” (Cohen’s d scale). Numeric value was correct; verbal interpretation was wrong for every call site.compare_two_models()Welch path: the bootstrap confidence interval was computed viabootstrap_effect_size_ci(), which uses the Cohen’s d (unpooled variance) formula internally. The point estimate used Hedges’ g (bias-corrected, pooled). CI and point estimate were for different estimators of the same quantity. Fixed by addingbootstrap_hedges_g_ci()tobootstrap.py, which applies the Hedges’ J correction factor1 - 3/(4·df - 1)inside each bootstrap resample. The Welch branch incompare_two_models()now calls this function.paired_wilcoxon_test(): effect size r was computed asnorm.ppf(p/2) / sqrt(n)— deriving Z from the p-value via the normal quantile. This is invalid when scipy uses exact distribution tables at small n, which is the common case. Replaced with the W-statistic asymptotic formula (z = (W - E[W]) / sqrt(Var[W]),r = |z| / sqrt(n)), consistent withwilcoxon_signed_rank_test().
Fixed — API
VariabilityStudyResults.summarize(): removed a spurious t-interval 95% CI on the mean that appeared below each metric’s SD. The interval was computed under an unvalidated normality assumption. Removing it avoids misleading users who would reasonably interpret it as a distribution interval rather than a CI on the population mean estimate. SD, Min, and Max remain.compare_models(): now defaults topaired=Truefor two-model comparisons. Both models receive identical per-run seeds by construction viaSeedSequence.spawn(), making runs genuinely paired at the RNG level. The priorpaired=Falsedefault discarded this pairing information and used a less powerful unpaired test. Behavioural change: two-model comparisons that previously used Mann-Whitney U will now use paired Wilcoxon signed-rank. Passpaired=Falseexplicitly to restore prior behaviour.
Fixed — Packaging
optunaadded to[tool.poetry.dependencies]as an optional dependency under thetuningextra. Previously declared in[tool.poetry.extras]only, causingpip install ictonyx[tuning]to install without optuna and fail at import time.
Internal
Regression tests added for all three statistical fixes in
test_analysis.pyandtest_bootstrap.py. The BUG-2 test verifies that the Welch-path CI and point estimate are for the same estimator. The BUG-3 test verifies that effect size r is consistent betweenpaired_wilcoxon_test()andwilcoxon_signed_rank_test()on the same paired data.
[0.3.15] — 2026-03-31
Fixed — Critical correctness
KerasModelWrapper.predict(): regression branch now returnsself.predictionsinstead of implicitly returningNone. Any caller capturing the return value ofpredict()on a Keras regression model receivedNonesilently;assess()then raisedValueError: Model has not generated predictions yeteven though predictions were computed and stored correctly.PyTorchModelWrapper.predict(): classification branch now returnsself.predictionsinstead of implicitly returningNone. Mirror of the Keras regression bug; affected all PyTorch classifiers. The regression branch was unaffected.
Fixed — Statistical correctness
check_independence(): short series (n < max_lag + 2) now returnsNoneinstead ofFalseas the independence flag.Falsesignals detected autocorrelation;Nonesignals untestable.mann_whitney_test()updated to treatNoneas untestable rather than emitting a spurious autocorrelation warning on studies with fewer than 7 runs. Return type annotation updated toOptional[bool].check_independence(): SE formula corrected from1 / sqrt(n)to1 / sqrt(n - lag). The prior formula made the significance threshold too tight at higher lags, inflating false-positive autocorrelation detection.calculate_averaged_autocorr(): standard deviation now usesddof=1(Bessel-corrected sample SD). Priorddof=0produced systematically narrow error bands inplot_averaged_autocorr().kruskal_wallis_test(): now performs a Levene equal-variance check and records the result inassumptions_met["equal_variances"], mirroring the existing check inanova_test(). A warning is added when groups have unequal variances, since KW can reject H₀ for variance differences rather than location differences.
Fixed — API and usability
variability_study()andcompare_models(): defaultrunsraised from 10 to 20. The prior default of 10 triggered the library’s ownruns < 20warning on every invocation that accepted the default — a self-contradictory design. Behavioural change: existing code that relies on the default will now run twice as many experiments. Passruns=10explicitly to restore prior behaviour.compare_models(): docstring forrunsparameter corrected from stale “Default 5” to “Default 20.”get_final_metrics(): deprecation warning changed fromDeprecationWarningtoUserWarning. Python silencesDeprecationWarningin all non-__main__contexts by default, making the warning invisible in notebooks and imported modules. Consistent with the identical fix applied toTextDataHandlerandTimeSeriesDataHandlerin v0.3.14.
Fixed — Dead code removal
analysis.py,bootstrap.py: removedHAS_SCIPY = Truedead flag, the_check_scipy()guard function, and all 8 call sites inanalysis.py. scipy is a mandatory hard dependency; the flag was alwaysTrueand the guard could never raise. Removed the two deadif method == "bca" and not HAS_SCIPYbranches inbootstrap.py._validate_process_isolation(): removed a verbatim-duplicate data-size serialisation check that ran twice back-to-back. For large datasets this serialised training data twice unnecessarily; on failure it emitted the same warning twice.
Fixed — Data handlers
TextDataHandlerandTimeSeriesDataHandler: now raiseImportErrorat construction when the requiredtf.keras.preprocessingAPIs are absent (Keras 3 / TF 2.16+), rather than emitting a warning and proceeding to crash inload(). Fail-fast behaviour at object construction.TimeSeriesDataHandler: corrected a copy-paste duplicate in theImportErrormessage body where the first line of the error string appeared twice.
Fixed — Documentation
check_normality(): removed verbatim-duplicaterequire_all_testsparameter entry from the docstring Args section.README.md: all quickstart examples updated fromruns=10/num_runs=10toruns=20to match the library’s own statistical recommendation. Allsummarize()output blocks updated fromStd:toSD (sample, N-1):to match the v0.3.14 label change. All output blocks re-executed.
Internal
core.py: addedassert self.predictions is not Nonebefore all fourpredict()return sites to satisfy mypyOptionalnarrowing. The assertion is true by construction — the preceding assignment guarantees it — and is checked at runtime.runners.py: hoistedimport pickle as _serializerabove thetry/exceptblock in_validate_process_isolation()to resolve a flake8 F821 false positive caused by assignment inside atryblock.
[0.3.14] — 2026-03-29
Fixed — Silent correctness
ScikitLearnModelWrapper.evaluate(): precision, recall, and F1 were computed on regression models due to atryblock outsideif is_classifier:. On regression models this produced nonsensical metrics or swallowed exceptions silently. The block is now correctly gated behindis_classifier._build_instance_cloner(): unknown instance types now raiseValueErrorinstead of silently reusing the same instance across all runs, which caused trained weights to leak between runs and invalidated study independence._run_single_fit_standard():run_seedis now injected intomodel_configbeforemodel_builder(), matching isolated mode. Previously, class-based sklearn builders receivedrandom_state=Nonein standard mode.PyTorchModelWrapper.predict_proba(): single-output sigmoid binary classifiers (output shape(n, 1)) now return valid probability pairs viasigmoid.softmaxon(n, 1)produces all-ones, not probabilities.
Fixed — PyTorch / GPU
ExperimentRunner:cudnn.deterministicandcudnn.benchmarkare now restored to their prior values after each run via a_deterministic_cudnn()context manager. Previously these were set permanently, silently degrading GPU throughput in any PyTorch code running in the same process afterwards.
Fixed — Statistical correctness
paired_wilcoxon_test(): addedmethod='auto', matching the fix applied towilcoxon_signed_rank_test()in v0.3.13. Ensures exact computation at small n rather than the normal approximation.compare_models(): defaultrunscorrected to 10, matchingvariability_study()and the v0.3.13 CHANGELOG.Warning threshold raised from
runs < 10toruns < 20in bothvariability_study()andcompare_models(), matching the>= 20recommendation stated in the warning message.
Fixed — Config and process isolation
ModelConfig.copy(): frozen state is now propagated to the copy. Previously a frozen config became mutable after copying._validate_process_isolation(): now usescloudpicklewhen available, matching the serialiser used during execution. Previously, lambdas and notebook-defined functions were incorrectly rejected by validation.
Fixed — Packaging and output
shap,hyperopt, andjoblibadded to pip-installable extras:ictonyx[explain],ictonyx[tuning], andictonyx[sklearn]respectively.TextDataHandlerandTimeSeriesDataHandler: deprecation warnings changed fromDeprecationWarning(silenced by Python by default outside__main__) toUserWarning, ensuring visibility in notebooks and imported modules.VariabilityStudyResults.summarize(): SD label clarified toSD (sample, N-1)to distinguish Bessel-corrected sample SD from population SD.
Internal
Removed unreachable
if self.predictions is Noneguards frompredict()in all three model wrappers. The branch was dead by construction and the# pragma: no branchannotation was suppressing coverage tooling.
[0.3.13] — 2026-03-28
Fixed — Critical (statistical correctness)
wilcoxon_signed_rank_test(): replacedmethod='approx'withmethod='auto'. At n=5 the normal approximation produced false positives (p=0.043 vs exact p=0.063), reversing significance decisions at α=0.05. Effect size is now derived from the p-value via inverse normal CDF, since scipy’sautomode does not exposezstatisticdirectly.check_convergence(): correctedandtoorin the final return statement. The docstring specified OR semantics; the code used AND, causing plateaued training runs to be reported as not converged and deflatingconvergence_rateinassess_training_stability()output.check_convergence(): return values now wrapped inbool()to preventnumpy.bool_identity-check failures under NumPy 2.0.
Fixed — Misleading output
StatisticalTestResult.get_summary(): the corrected p-value is now displayed and labelledp_corrwhen a multiple-comparison correction has been applied. Previously the raw p-value was shown alongside a marker derived from the corrected value, producing contradictory output such asp=0.0300 ns.
Fixed — Adoption
pyproject.toml: ML framework extras are now installable via pip.pip install ictonyx[tensorflow],ictonyx[torch],ictonyx[mlflow], andictonyx[all]now install their respective dependencies. Previously all ML frameworks were in a Poetry dev group, inaccessible through pip extras.variability_study()andcompare_models(): defaultrunsraised from 5 to 10. The previous default triggered the library’s ownruns < 10warning on every first call.compare_models(): raisesConfigurationErrorwith an actionable message when the requested metric is absent from a model’s results, instead of propagating a bareKeyError. Includes a targeted hint whenval_accuracyis requested but onlyaccuracyis available.
Fixed — Stale claims from v0.3.12
GridStudyResults.to_dataframe(): values now stored at full float precision.round(..., 4)was still applied to all five summary columns.variability_study()docstring: corrected seed derivation from the staleseed + idescription toSeedSequence.spawn().exceptions.py:import datetimemoved to module top; timestamps now use UTC viadatetime.timezone.utc.
Fixed — Code quality
check_normality(): misplaced comment moved out of unreachable position (after an earlyreturn) to before the line it documents.check_normality(): added missingrequire_all_testsparameter to the docstring Args section.check_independence(): removed redundantn = len(data.dropna())recomputation._ensure_wrapper(): explanatory comments moved to before theifblocks they describe, rather than after the precedingreturnstatements.ScikitLearnModelWrapper.fit(): removed inlineimport warnings as _warnings; uses the module-top import directly.
Deprecated
TextDataHandlerandTimeSeriesDataHandlernow emitDeprecationWarningat construction. Both handlers are inoperable under TF 2.16+ (the required version) and will be replaced with framework-agnostic implementations in v0.4.0.
Changed
README installation section rewritten with per-framework install commands.
[0.3.12] — 2026-03-27
Fixed — critical
variability_study():verbose=Falsenow correctly suppresses output. The parameter was stored inModelConfigbut never forwarded to the runner — full output was always produced regardless.PyTorchModelWrapper.fit(): regression validation history no longer doubles in length per epoch.val_losswas appended twice — once unconditionally and once in the regression branch — corrupting every PyTorch regression variability study silently.TextDataHandlerandTimeSeriesDataHandler: now raiseImportErrorwith clear guidance whentf.keras.preprocessingAPIs are unavailable (removed in Keras 3 / TF 2.16+). Full framework-agnostic rewrites of both handlers are planned for v0.4.0.
Fixed — reproducibility
_set_seeds():torch.backends.cudnn.deterministic = Trueandbenchmark = Falsenow set when CUDA is available. GPU PyTorch studies were not reproducible across invocations without this.run_grid_study(): child seeds now derived viaSeedSequence.spawn()for statistical independence, consistent withrun_study(). Previously usedseed + i, which can produce correlated RNG states.sklearn
random_statenow injected at wrapper construction when the estimator accepts it, making sklearn studies reproducible with the stored seed.compare_models(paired=True): raisesValueErroron unequal run counts instead of warning and proceeding into a scipy error.MemoryManager: warns whencloudpickleis absent and process isolation falls back to standard pickle, which cannot serialize notebook-defined functions or lambdas.allow_memory_growth: wrapped in try/except with a clear warning when TF was already initialized beforeMemoryManagerconstruction.
Fixed — statistical correctness
anova_test(): emitsUserWarningwhen any group has fewer than 30 samples, noting that normality cannot be assumed at small n and suggestingkruskal_wallis_test()instead.
Fixed — API and wrappers
_ensure_wrapper(): dead comments moved out of unreachable block; refactored to cleanif/elifstructure.KerasModelWrapper._cleanup_implementation():clear_session()now called beforedel self.model, preventing dangling session references.ScikitLearnModelWrapper.fit(): warningstacklevelcorrected from 3 to 2.ScikitLearnModelWrapper.save_model(): usesjoblib.dump()instead of raw pickle; significantly faster for ensemble models.ImageDataHandler: now acceptscolor_mode(default'rgb'),val_split(default0.2), andtest_split(default0.1) constructor parameters, consistent with other data handlers. Invalidcolor_modevalues raiseValueErrorat construction time.ModelConfig.__hash__ = Nonemade explicit.ModelConfig.for_cnn(): docstring clarifies that the"loss"key is advisory only and is not read by any runner or wrapper.
Fixed — plotting
All plot functions now unconditionally return
plt.Figure. Previously_finalize_plot()returnedNonewhen display was enabled, making plots untestable and uncomposable programmatically.plot_autocorr_vs_lag(): emitsUserWarningwhen series is too short to compute autocorrelation at the requested lag, instead of returningNonesilently._find_metric_columns():Noneremoved from training column candidate list, preventing spurious column matches.
Added
variability_study()andcompare_models(): emitUserWarningwhenruns < 10, noting low statistical power at small n.paired_wilcoxon_testexported fromictonyxtop-level namespace.plot_variability_summary(): accepts aVariabilityStudyResultsobject directly viaresults=parameter, eliminating manual extraction ofall_runs_metricsandfinal_metrics_series.plot_grid_study_heatmap(): heatmap visualization for 2D parameter sweeps fromGridStudyResults, showing mean or SD across two swept parameters with annotated cells.ModelConfig.freeze(): makes a config instance read-only after construction. Attempting to set or update parameters on a frozen config raisesRuntimeError.
Deprecated
get_final_metrics()now emitsDeprecationWarning. Useget_metric_values()instead. Will be removed in v0.5.0.
Internal
scipy,matplotlib, andseaborncommitted as unconditional core dependencies; dead try/except fallback paths removed fromanalysis.pyandbootstrap.py.loggers.py: allprint()calls replaced withlogger.info()so output respects the global verbose setting.verbose=Falsenow propagates through the full stack including data loading, viaset_verbose()in the high-level API.GridStudyResults.to_dataframe(): values no longer rounded at storage time; full float precision preserved for downstream analysis.HyperparameterTuner:eval_timenow stores wall-clock seconds;_should_minimize()helper extracted; deprecatedmerge()replaced withupdate().Dead code removed from
bootstrap_mean_difference_ci.import warningsandimport datetimemoved to module top incore.pyandexceptions.pyrespectively;exceptions.pytimestamps now use UTC.joblibremoved from required dependencies inpyproject.toml.ExperimentRunner,run_grid_study(), and_set_seeds()docstrings corrected to describeSeedSequence.spawn()accurately._INFRA_KWARGSdocumented invariability_study()docstring.README
summarize()output block updated to match current format.
[0.3.11] — 2026-03-25
Added
VariabilityStudyResults.test_against_null(null_value, metric, alpha): one-sample Wilcoxon signed-rank test against a user-specified null value. The v0.3.10 removal stub forcompare_models_statistically()directed users to this method; it did not exist. Requiresnum_runs >= 6for reliable results.variability_study()andcompare_models():verboseparameter (defaultTrue).compare_models():pairedparameter (defaultFalse). WhenTrue, uses Wilcoxon signed-rank — correct when models share the same seeds. Warns when series have unequal run counts.run_grid_study():seedparameter; each configuration receivesseed + ifor reproducible grid studies.
Fixed — critical
KerasModelWrapper.predict(): regression branch now returns raw float values. Previously applied>= 0.5threshold and cast to integer, silently corrupting every Keras regression prediction.KerasModelWrapper.predict(): binary classification threshold corrected from> 0.5to>= 0.5. A sigmoid output of exactly 0.5 was assigned to class 0; correct behaviour is class 1, consistent with sklearn convention. The v0.3.10 CHANGELOG claimed this fix was shipped — it was not.
Fixed — statistical correctness
summarize(),get_summary_stats(), andassess_training_stability(): standard deviation now usesddof=1(sample std). Reported std was systematically too small — approximately 5% at n=10, 11% at n=5.get_epoch_statistics():ci_lower/ci_uppernow contain a t-distribution CI on the epoch mean. Previously empirical percentiles, which at n=5 are essentially min and max.get_epoch_statistics(): raisesValueErrorifconfidenceis not in (0, 1). Passingconfidence=95previously produced garbage silently.plot_comparison_forest(): CI now uses Welch t-multiplier per comparison instead of hardcoded z=1.96. CIs at small n were too narrow.compare_two_models(): paired path refactored to dedicatedpaired_wilcoxon_test()with natural conclusion text. Previously passed pre-computed differences to a single-sample test, producing misleading metadata.compare_two_models(): effect size CI no longer computed for the Mann-Whitney path, where Cohen’s d CI is the wrong pairing.check_convergence(): secondary criterion now requires both criteria to agree. Previously fired on any decreasing curve. Falls back to variance criterion alone when autocorrelation cannot be computed.check_independence(): false-positive “independent” on short series corrected — returns early whenn < max_lag + 2. Loop bound corrected fromn // 4tomin(max_lag, n - 2); SE uses post-dropna count.check_normality():require_all_testsparameter added (defaultFalse); n<3 return structure standardized; n<20 comment corrected.cohens_d(): returnsNaNwithRuntimeWarningwhen pooled std is undefined. Previously silently returned 0.0.assess_training_stability(): correct results for DataFrame histories; all std/variance computations now useddof=1.compare_multiple_models(): warns when multi-value Series are passed, indicating accidental per-epoch data rather than per-run data.ModelConfig.for_variability_study():epochs_per_runkey now read byrun_study()before falling back toepochs.R² returns
NaNinstead of0.0whenss_tot == 0in all three model wrappers.
Fixed — API and runners
compare_models(): two instances of the same class with different parameters no longer silently overwrite each other in results.compare_models(): usesget_metric_values()instead of deprecatedget_final_metrics().variability_study(): model kwargs no longer bleed intoModelConfigand experiment logs.ScikitLearnModelWrapper.assess(): readsself.taskset duringfit(); usesaccuracy_scorefor consistency with other wrappers.run_study(): raisesValueErrorfornum_runs < 1.plot_pairwise_comparison_matrix(): no longer crashes on model names containing underscores; returnsNonewith a warning on empty input.plot_roc_curve()/plot_pr_curve(): TensorFlow dependency removed from label binarization; falls back tonp.eye()when TF is absent.BaseLogger: debug-only messages removed from stdout.
[0.3.10] — 2026-03-24
Removed
VariabilityStudyResults.compare_models_statistically()removed. The method applied Kruskal-Wallis to groups of one observation each (one per run), producing statistically incoherent results. A stub raisesAttributeErrorwith migration guidance. Useix.compare_models()for cross-model comparison.
Added
ArraysDataHandlernow accepts optionalX_testandy_testparameters. When provided,load()bypasses internal test splitting and returns the supplied arrays astest_datadirectly, performing only a train/val split on the training arrays. Ensures deterministic, consistent test evaluation across variability study runs for users with existing held-out test sets. Fully backward compatible.ArraysDataHandler.__init__()now acceptsval_splitandtest_splitas constructor parameters, storing them as defaults forload(). Explicit arguments toload()still take precedence; existing call sites are unaffected.
Fixed — data integrity (re-run may be required)
ScikitLearnModelWrapper.fit():val_accuracyandval_r2are no longer written to the training history when no validation data is provided. Previouslyval_scorewas silently set equal totrain_score, fabricating a generalisation metric that propagated intofinal_metricsand any downstream comparison or summary. Users who ran sklearn variability studies without an explicit validation split should re-run their experiments.
Fixed — statistical correctness
analysis.py—rank_biserial_correlation()returned the wrong sign on every call. The formula1 - 2U/(n₁n₂)has been corrected to2U/(n₁n₂) - 1, consistent with the Kerby (2014) definition and scipy’s convention. Previously, a result where model A outperformed model B would report a negative rank-biserial correlation. Effect size magnitude and all p-values were unaffected.ScikitLearnModelWrapper.evaluate(): precision, recall, and F1 averaging strategy ('binary'vs'weighted') is now determined from the training label count rather thannp.unique(y_test). A test batch missing any class would silently apply the wrong averaging.HyperparameterTuner.tune(): now appliesspace_eval()before returning, decoding hyperopt-encoded indices back to actual hyperparameter values. Previously a search overhp.choice("batch_size", [16, 32, 64])could return{"batch_size": 2}(the index) instead of{"batch_size": 64}(the value).KerasModelWrapper.predict(): binary classification threshold corrected from> 0.5to>= 0.5. A sigmoid output of exactly 0.5 was previously assigned to class 0 instead of class 1, inconsistent with sklearn convention andpredict_proba.
Fixed — crashes and silent failures
_ensure_wrapper(): Keras models are now correctly wrapped asKerasModelWrapper. Previously the duck-typing check (hasattr(obj, "fit") and hasattr(obj, "predict")) fired before the Keras check, silently mis-wrapping any Keras model asScikitLearnModelWrapper.ArraysDataHandler.__init__(): now raisesValueErrorif exactly one ofX_test/y_testis provided. Previously the asymmetric pair was silently accepted;load()then returnedtest_data=(array, None), causing a confusing crash insideevaluate().ImageDataHandler._preprocess_image(): replaced dead nestedtry/exceptblocks with a direct call totf.image.decode_image(). Python exception handlers cannot intercept TF op errors insidetf.data.Dataset.map()in graph execution mode — the JPEG→PNG fallback chain was never triggered.ImageDataHandler.load(): added_validate_image_files()pre-flight scan using Pillow before the dataset is built. Previously, any unreadable image silently injected an all-zero tensor with its original class label into training data. Now raisesDataValidationErrorlisting the offending files. RequiresPillow; skips validation with a warning if not installed.TabularDataHandler.load(): NaN values in feature columns now emit alogger.warningidentifying the affected columns and counts. Previously the check computednull_countsand discarded it — a commented-outprintwas the only remnant. Target-column NaN already warned; feature columns now do too.predict()in all three wrappers:assert self.predictions is not Nonereplaced with an explicitModelError. Python stripsassertstatements when running with-O, silently turning the invariant check into dead code.PyTorchModelWrapper.load_model():weights_onlyparameter is now passed through totorch.load(). Previously hardcoded asTrue, silently overriding the caller’s value and causingUnpicklingErrorfor any legacy checkpoint loaded withweights_only=False.BaseModelWrapper.__del__(): cleanup is now skipped during interpreter shutdown. Previously, callingtf.keras.backend.clear_session()during teardown could trigger TF’s shutdown sequence out of order, causingAttributeErroror segfaults in some TF versions.ModelConfigproperty setters (epochs,batch_size,num_runs,epochs_per_run,learning_rate,cleanup_threshold): now acceptnumpy.integervalues. Previouslyisinstance(value, int)rejectednp.int64and similar types with aValueError, silently breaking any grid search that iterated over a numpy parameter array.
Fixed — data layer
data.py: TF preprocessing import guard now catchesAttributeErrorin addition toImportError. In TF 2.16+ / Keras 3,tensorflow.keras.preprocessingexists as a module stub but its sub-attributes have moved, raisingAttributeErrorinstead ofImportError. Without this fix, importingArraysDataHandlerorTabularDataHandlerwould fail in any Keras 3 environment.TabularDataHandler.get_data_info(): no longer returnsdata_path: "in_memory_dataframe"andpath_exists: Falsewhen the handler was initialised from a DataFrame. Now returnssource: "dataframe"alongside the actual shape and column metadata.
Internal
Stale comment removed from
BaseModelWrapper.check_memory_and_cleanup_if_needed().conftest.pyfixtureTypeErrorresolved; shared test fixtures now work as intended.
[0.3.9] - 2026-03-18
Fixed
apply_multiple_comparison_correction(): Holm step-down algorithm usednp.minimum.accumulateinstead ofnp.maximum.accumulateto enforce monotonicity. This collapsed all corrected p-values down to the smallest scaled value, making the correction more permissive than intended. With input[0.001, 0.01, 0.03]the old code produced[0.003, 0.003, 0.003]; the correct output is[0.003, 0.02, 0.03].kruskal_wallis_test(): effect size interpretation now calls_interpret_epsilon_squared()rather than_interpret_eta_squared(). Docstring Returns section corrected to say “epsilon-squared”.wilcoxon_signed_rank_test(): Z-score for effect size now usesscipy.stats.wilcoxon(method='approx').zstatistic, which applies scipy’s internal tie-corrected variance. The previous manual formula ignored ties and could produce an incorrect effect sizer.VariabilityStudyResults.compare_models_statistically(): minimum run guard raised from 2 to 3, matching the actual requirement ofkruskal_wallis_test().VariabilityStudyResults.to_dataframe(): test metrics now stored withrun_idand joined by identity rather than list position. Previously, a failed test evaluation for one run silently assigned wrong metrics to all subsequent runs.SHAP tree detection replaced string-based class name matching with
isinstance()checks against all sklearn tree types, plus name-fragment fallback for XGBoost, LightGBM, and CatBoost. Fixes silent fallback toKernelExplainerforGradientBoostingClassifier,HistGradientBoostingClassifier,ExtraTreesClassifier,AdaBoostClassifier, and others.DataHandlerrefactored: path validation moved to newFileDataHandlerintermediate class.ArraysDataHandlerno longer carries a dummy"in_memory_arrays"path or a no-op_validate_data_path()override. Minor breaking change:ArraysDataHandler.data_pathno longer exists.run_grid_study(): replacedprint()calls withlogger.info/warning. Addedverboseparameter (defaultTrue).set_verbose(False)now correctly suppresses grid study output.ScikitLearnModelWrapper.fit(): unrecognized keyword arguments now produce aUserWarninginstead of being silently discarded.
Deprecated
ModelConfig.merge(): useupdate()instead. Removal in v0.5.0.ModelConfig.has(): use'key' in configinstead. Removal in v0.5.0.
Added
VariabilityStudyResults.get_epoch_statistics(): computes per-epoch mean, SD, SE, and percentile confidence band across all runs. Returns a DataFrame suitable for use withplt.fill_between().ModelConfig.__iter__(),__len__(),__eq__(),to_dict():dict(config),len(config), and equality comparison now work as expected.Coverage floor set at 60% via
--cov-fail-underin pytest config. Total coverage: 65%.
[0.3.8] - 2026-03-18
Fixed
_isolated_training_function()no longer catches all exceptions internally. Exceptions now propagate to the subprocess worker, which returns{"success": False}. Training crashes inside subprocesses were previously silently treated as empty history with no error message surfaced to the caller.cloudpickleandpsutildeclared asoptional = truein[tool.poetry.dependencies].pip install ictonyx[isolation]now actually installs these packages.check_independence(): removedfrom scipy.stats import norminside the function body; replaced withstats.normfrom the module-level import. The naked import re-executed on every call.apply_multiple_comparison_correction(): removed phantomalphaparameter from docstring (the parameter does not exist in the signature).settings.should_verbose()added as a public accessor for_VERBOSE;api.pyno longer accesses the private attribute directly.ScikitLearnModelWrapper.evaluate(): replaced two bareexcept Exception: passblocks withlogger.warning()calls. Failures computing precision/recall/f1 or R²/MSE/MAE were previously silent.check_independence()returned dict now includesthresholdandnkeys.max_autocorrnow usesabs()so large negative autocorrelations are correctly captured.Returns:docstring completed with full key inventory.protobuf = "<4.0.0"constraint removed from ml dependency group; conflicted with TensorFlow 2.16+ and MLflow 2.x._standardize_history_df()extracted as a shared static method onExperimentRunner; removes duplicated column rename logic from both_run_single_fit_isolated()and_run_single_fit_standard().__version__moved to the top of__init__.py, before imports.PyTorchModelWrapper.load_model()now defaults toweights_only=True, preventing arbitrary code execution when loading checkpoints from untrusted sources. Aweights_onlyparameter is exposed for callers who need to load legacy checkpoints.Security warnings added to
save_object(),load_object(), andScikitLearnModelWrapper.load_model()docstrings.load_object()now raisesFileNotFoundErrorwith a clean message before attempting to open the file, matching what the docstring already promised.MemoryManagerno longer callsmp.set_start_method("spawn", force=True), which permanently mutated global process state and could break PyTorchDataLoaderon Linux. Now usesmp.get_context("spawn")throughout.
Added
SECURITY.mddocumenting pickle risks, theweights_onlydefault for PyTorch checkpoints, and a vulnerability reporting path.mypyadded to CI lint job and to dev dependencies. All 74 type errors resolved;py.typedmarker is now backed by actual type checking.
[0.3.7] - 2026-03-17
Fixed
ScikitLearnModelWrapper.assess()raisedAttributeErroron any regressionpredict → assesscall becauseself.taskwas never set in__init__. Fixed by addingtask: Optional[str] = Noneto the constructor and assigningself.taskat the end offit()using theis_classifierflag already computed there.HyperparameterTunersilently returned the worst model when optimising onr2,f1, oraucbecause those metrics were absent from the negation condition (Hyperopt minimises). Fixed in all three locations: theobjectivefunction, the best-loss conversion intune(), andget_best_params().logger.addHandler()was called unconditionally at import time insettings.py, causing every ictonyx message to be emitted twice in any environment with an already-configured root logger (Jupyter, applications usinglogging.basicConfig()). Handler addition is now guarded byif not logger.handlers;logger.propagateset toFalse.VariabilityStudyResults.compare_models_statistically()passed a full per-epochpd.Seriesas each observation tocompare_multiple_models(). Withn_epochs > 1, statistical tests receivedn_runs × n_epochsobservations instead ofn_runs, inflating the effective sample size and producing incorrect p-values. Fixed by reading directly fromself.final_metrics, which stores exactly one scalar per run.ExperimentRunnerseeded each run withself.seed + run_id, producing consecutive integers (e.g. 42, 43, 44). Many RNG implementations exhibit correlation between adjacent seeds, introducing systematic bias across runs. Replaced withnp.random.SeedSequence, which uses a hash-based spawning algorithm to produce independent, uncorrelated child states. Child seeds are generated once at the start ofrun_study()and used in both the standard and process-isolated execution paths.ScikitLearnModelWrapper.fit()included fabricated'loss'and'val_loss'keys (1 - accuracy) in classifier training history.1 - accuracyis not a valid loss value, cannot be meaningfully compared to Kerasval_loss, and produces values outside[0, 1]for regressors wherescore()returns R². Both keys removed. Classifier history now contains onlyaccuracyandval_accuracy; regressor history unchanged (r2,val_r2).assess_training_stability()silently reportedconverged=Falsefor every sklearn run after the loss key removal, because the convergence check looked only for'loss'and'val_loss'. The fallback now checks'train_loss','val_loss','val_accuracy', and'r2'in order before giving up.
Changed
ModelConfig.for_variability_study()defaultnum_runscorrected from5to10to match the documented behaviour. This is a behavioural change: studies created withfor_variability_study(base_config)without an explicitnum_runsargument will now run 10 times instead of 5.
Added
tests/test_tuning.py— first direct test coverage forHyperparameterTuner, including a regression test confirmingr2is negated correctly (bug fixed above). Skipped automatically whenhyperoptis not installed.tests/test_integration.pyexpanded from 3 tests to cover the full sklearn classifier and regressor pipelines end-to-end, two-model comparison viacompare_models(),results.to_dataframe(), save/load roundtrip, and statistical stability analysis on real runner output.
[0.3.6] - 2026-03-17
Added
KerasModelWrappernow accepts an explicittaskparameter ('classification','regression', orNone). When set, task detection skips loss-function inspection entirely. Required for custom loss functions, uncompiled models, and any loss name not in the built-in recognition list.load_model()updated to accept and forwardtask=to the loaded wrapper.tqdmdeclared as an optional dependency.pip install ictonyx[progress]now installs it. Previously tqdm was used byrunners.pyfor progress bars but was undeclared, sopip install ictonyx[all]silently omitted it.[progress]extra added;tqdmadded to[all]extra.set_theme()exported from the public API —ictonyx.set_theme()now works. It was present insettings.pybut missing from__init__.pyimports and__all__, causingAttributeErroron any direct use.tests/conftest.py— shared fixtures available to all test files without explicit import:small_classification_arrays,small_multiclass_arrays,small_regression_arrays,minimal_config,tabular_classification_handler,tabular_regression_handler.New regression tests for previously fixed bugs:
TestScikitLearnWrapperExtended::test_assess_regression— verifiesScikitLearnModelWrapper.assess()returns{'r2','mse','mae'}for regressors and never returns'accuracy'.TestPyTorchRegression::test_regression_assess_returns_full_metrics— same contract forPyTorchModelWrapper.TestPyTorchUtilities::test_load_model_without_architecture_raises— verifiesPyTorchModelWrapper.load_model(path)raisesValueErrorwith a descriptive message whenmodel=is omitted.
Fixed
ScikitLearnModelWrapper.assess()returned wrong metrics for regressors. Always calledaccuracy_score()regardless of task type, producing 0.0 for regression models with no error or warning. Now uses the same classifier/regressor heuristic asfit()andevaluate(): classifiers get{'accuracy'}, regressors get{'r2', 'mse', 'mae'}via pure NumPy.KerasModelWrapper.assess()had the same regression bug. Fixed with identical approach, routing through_is_classification_model().PyTorchModelWrapper.assess()returned only{'mse'}for regression. Inconsistent withScikitLearnModelWrapper(which returns{'r2','mse','mae'}), silently causingKeyErrorwhen callers readresult['r2']orresult['mae']on a PyTorch regressor. Now returns{'r2', 'mse', 'mae'}using the same pure-NumPy formula across all three wrappers.KerasModelWrapper._is_classification_model()silently returnedTruefor unknown losses. Uncompiled models, custom loss functions, and inspection exceptions all fell through toreturn True, treating regression models as classifiers and producing wrong predictions and metrics with no indication of the problem. Now raisesValueErrorwith actionable guidance in all three cases, directing the user to settask=explicitly.PyTorchModelWrapper.load_model()violated the Liskov Substitution Principle. The base class signature isload_model(cls, path: str). The PyTorch override required a non-defaultedmodel: nn.Moduleargument, making it impossible to call through the base class interface.modelis nowOptional[nn.Module] = None; passingNoneraisesValueErrorexplaining the PyTorch state-dict constraint with a usage example.set_theme()silently ignored unknown theme names. A typo likeset_theme("pubication")was a no-op with no error or warning. Now raisesValueErrornaming the invalid theme and listing all valid options ('default','dark','publication'). A'default'branch added to restore the original palette, backed by a_DEFAULT_THEMEconstant.HyperparameterTuner.tune()accessed raw Keras history API. The objective function assigned the return value ofwrapped_model.fit()tohistoryand then readhistory.history— treating it as a KerasHistoryobject.fit()returnsNonefor all three wrapper types. Thehasattrguard caught this and raised a genericValueErroron every non-Keras trial, makingHyperparameterTunereffectively Keras-only. Now callsfit()for its side effect, then readswrapped_model.training_result.history, which works for all wrappers.PyTorch model detection in
api.pyused a fragile string heuristic. Both_build_instance_cloner()and_ensure_wrapper()detected PyTorch models with"torch" in str(type(obj).__mro__)— a substring match on a stringified list of type objects. Could false-positive on any class whose module path contains “torch”. Replaced withPYTORCH_AVAILABLE and isinstance(obj, _torch_nn.Module), the same pattern used for sklearn.torch.nnimported once at module level as_torch_nn.test_unknown_theme_no_changeasserted old broken behavior and caused five consecutive CI failures. The test calledset_theme("nonexistent_theme")with nopytest.raisesguard and asserted THEME was unchanged — correct for the old silent no-op, wrong after theValueErrorfix. Replaced withtest_unknown_theme_raises_value_error. FullTestSetThemeclass rewritten:teardown_methodnow callsset_theme("default")instead of hardcoding palette values; three new tests added covering the'default'reset branch, THEME immutability on error, and error message content.check_normality()had a misleading inline comment. Comment read “Consider normal if any test fails to reject normality” but the code usedall(), which requires every test to agree. The code is correct. Comment updated to accurately describe the conservativeall()semantics and explain why that choice is appropriate for the small-sample ML regime.
Changed
KerasModelWrapper._is_classification_model()restructured for clarity. Thetryblock now covers only the attribute access steps that can raiseAttributeErrororTypeError. Indicator list matching moved outside thetryblock; the finalraise ValueErrorfor unknown losses is now plainly outside any exception handler, making the control flow unambiguous.settings.pygains two module-level constants:_DEFAULT_THEME(the original colour palette, used byset_theme('default')to restore defaults) and_VALID_THEMES(used inValueErrormessages).
Removed
“Regression task support (MSE, MAE, R² as first-class metrics)” removed from
[Unreleased]planned list — fully delivered in this release across all three wrappers (assess()) and inScikitLearnModelWrapper.evaluate().
[0.3.5] - 2026-03-15
Added
ModelComparisonResultsdataclass inanalysis.py— typed return object forcompare_models(), replacing the previous untypedDict[str, Any]. Includesis_significant()andget_summary()convenience methods.seedparameter tocompare_models()— all models in a comparison now use the same base seed, making comparisons reproducible. IfNone, a seed is generated and used consistently across all models.cloudpickleandpsutildeclared as optional[isolation]extra inpyproject.toml. Both were silently used inmemory.pybut undeclared, causing degraded process isolation behavior on clean installs.Python 3.13 classifier added to
pyproject.toml(already tested in CI).
Fixed
compare_models()now raisesValueError(with a descriptive message) when fewer than 2 models produce valid results, replacing a silent error-dict return.compare_models()raw_datafield was documented in the return dict but silently absent; now populated correctly inModelComparisonResults.stop_on_failure_ratedefault inExperimentRunner.run_study()corrected from0.5to0.8to match the documented value in the class docstring.check_independence()accepted analphaparameter but ignored it, hardcoding1.96as the critical value. Now correctly derives the critical value vianorm.ppf(1 - alpha / 2).check_independence()docstring listed a phantomthresholdparameter that did not exist in the function signature; removed.ScikitLearnModelWrapper.fit()kwarg allowlist containedX_idx_sortedandcheck_input, both removed from scikit-learn in v1.0 (2021). Onlysample_weightremains.utils.pyimportedsklearn.model_selection.train_test_splitat module level, causingimport ictonyxto fail on installs without scikit-learn despite sklearn being optional. Import moved insidetrain_val_test_split().test_compare_models_insufficient_dataupdated to match newValueErrorbehavior (previously asserted on a silent error-dict return).Dead
gcre-imports removed fromKerasModelWrapperandPyTorchModelWrappercleanup methods (gcalready imported at module level).Dead
_cleanup_model_references()and_cleanup_tensorflow_session()methods removed fromKerasModelWrapper(defined but never called; logic already present in_cleanup_implementation()).Dead
_check_matplotlib()and_check_seaborn()alias functions removed fromplotting.py; all call sites now use_check_plotting()directly. Removed associated# FIX THIScomment.Unnecessary
f-string prefix removed from logger calls with no interpolation inrunners.py,explainers.py, andtuning.py.
Changed
requirements.txtremoved. It incorrectly listed all optional ML frameworks (TensorFlow, PyTorch, MLflow, SHAP) as mandatory dependencies, causing 1.5–3GB of unwanted installs for users who only need sklearn support. Dev environment setup instructions moved toCONTRIBUTING.md.CONTRIBUTING.mdexpanded with tiered dev setup: core-only install vs full ML environment, with explicit install commands for each.README corrected: version badge showed 0.3.3 (was 0.3.4).
README
compare_models()example updated to useresults.get_summary()reflecting the newModelComparisonResultsreturn type.
[0.3.4] - 2026-03-13
Added
run_grid_study()— runs a full variability study across a Cartesian product of parameter configurations. Each configuration executes viarun_variability_study()with process isolation by default, preventing CUDA memory accumulation across configurations. Supportsdry_run=Truefor execution planning before committing to long runs.GridStudyResultsdataclass — holds oneVariabilityStudyResultsper configuration with summary methods:to_dataframe(),summarize(),get_results_for_config(),list_configurations().Both names exported from
ictonyxpublic API.16 new tests in
test_runners.py. runners.py coverage 58% → 72%.
[0.3.3] - 2026-02-17
Fixed
ScikitLearnModelWrapper.fit()now reportsr2/val_r2for regressors instead of mislabeling R² asaccuracy/val_accuracy. Classifier history keys are unchanged.
[0.3.2] - 2026-02-15
Added
tqdm progress bars for variability studies (optional dependency, graceful fallback)
Pre-commit hooks for black and isort
Changed
scikit-learn is now an optional dependency; install with
pip install ictonyx[sklearn]Linting (black, isort, flake8) now enforced in CI — previously ran but did not fail builds
Rewrote CONTRIBUTING.md with development setup, style guide, and project map
Fixed
auto_resolve_handlercrashed when passing (X, y) tuples due to extra kwargsPyTorchModelWrapper.load_model()returned a raw dict instead of a wrapper_get_model_builderleaked fitted state between runs when given sklearn instancesOff-by-one in failure rate calculation in
ExperimentRunner.run_study()Undefined name
pdintuning.pyUnused
global THEMEdeclaration insettings.pySeaborn
FutureWarningin comparison boxplots (compatible with v0.14)Pinned black version in CI to prevent formatting drift
[0.3.1] - 2026-02-14
Fixed
auto_resolve_handlercrashed when extra kwargs (e.g.batch_size) were passed with tuple data, due to**kwargsforwarded toArraysDataHandlerPyTorchModelWrapper.load_model()now returns a fully reconstructed wrapper instead of a raw checkpoint dict, matching theBaseModelWrappercontract_get_model_buildernow clones sklearn instances per run viasklearn.base.clone(), preventing fitted weights from leaking between runs and silently corrupting variability studiesOff-by-one in
ExperimentRunner.run_study()failure rate calculationDocumented
compare_modelsdata handler reuse for identical splits
[0.3.0] - 2026-02-13
Added
PyTorchModelWrapperfor PyTorchnn.Modulemodels with built-in training loop, device management, and metric tracking for classification and regressionPYTORCH_AVAILABLEflag for conditional import detectionAuto-wrapping support for
nn.Modulein the high-levelvariability_study()APIPyTorch CPU added to CI test matrix (all platforms)
Two example notebooks demonstraing basic PyTorch use
Changed
core.pynow conditionally imports PyTorch alongside TensorFlow and sklearnvalidate_imports.pychecksPyTorchModelWrapperwhen torch is available
[0.2.0] - 2026-02-12
Breaking Changes
Metric-agnostic pipeline:
ExperimentRunnernow tracks all metrics per run infinal_metrics: Dict[str, List[float]], replacing the hardcodedfinal_val_accuracies: List[float]. Code that accessedfinal_val_accuraciesdirectly must switch tofinal_metrics['val_accuracy']or useVariabilityStudyResults.get_metric_values('val_accuracy').TrainingResultdataclass: All model wrappers now setself.training_result(aTrainingResultwith a.historydict) instead ofself.history.MockHistoryis deleted.run_study()return type: ReturnsVariabilityStudyResultsdirectly instead of an unnamed(List, List, List)tuple.
Added
TrainingResultdataclass as the universal training output for all wrappersVariabilityStudyResults.get_metric_values()for direct metric accessScikitLearnModelWrapper.evaluate()now returns precision, recall, and F1 in addition to accuracy; regression models get R², MSE, and MAEPython 3.12 added to CI test matrix and PyPI classifiers
Changed
Replaced all
print()calls with structuredloggeroutput acrossdata.py,explainers.py,memory.py, andtuning.pyReplaced bare
except:clauses withexcept Exception:inmemory.pyBumped seaborn dependency to
^0.13.0
Fixed
ExperimentRunner.run_study()now resets all accumulators at the start of each call; previously, callingrun_study()twice on the same runner silently appended to existing results
[0.1.0] - 2026-02-12
First release to PyPI.
Added
Bootstrap confidence intervals — new
ictonyx.bootstrapmodule with:bootstrap_ci(): general-purpose engine for arbitrary statisticsbootstrap_mean_difference_ci(): CI for difference in means between groupsbootstrap_effect_size_ci(): CI for Cohen’s dbootstrap_paired_difference_ci(): CI for paired comparisonsBoth BCa (bias-corrected and accelerated) and percentile methods
compare_two_models()now automatically computes 95% BCa confidence intervals for both the mean difference and Cohen’s d effect sizeNew fields on
StatisticalTestResult:confidence_interval,ci_confidence_level,ci_method,ci_effect_sizeCI data included in
get_summary(),generate_statistical_summary(), andcreate_results_dataframe()output52 new bootstrap tests, 7 CI integration tests
Expanded
analysis.pytest coverage from ~42% to 84%
Fixed
mann_whitney_testandwilcoxon_signed_rank_testsilently overwrotesample_sizesandassumptions_metwhen constructing resultsanova_testandshapiro_wilk_testcrashed on every call due to missing requiredstatisticandp_valuearguments inStatisticalTestResultconstructorIndentation bug in
wilcoxon_signed_rank_testthat misaligned warning logicMisleading inline comment in
mann_whitney_testthat described wrong test directionScikitLearnModelWrappermock loss history produced constant values instead of realistic decaying loss curves
[0.0.5] - 2026-01-29
Added
High-level API (
variability_study,compare_models) for single-function usageVariabilityStudyResultsobject with.summarize()and.get_final_metrics()methodsProcess isolation for GPU memory management (
use_process_isolation=True)Statistical comparison functions with automatic test selection
Effect size calculations (Cohen’s d, rank-biserial correlation, eta-squared)
Multiple comparison corrections (Bonferroni, Holm, Benjamini-Hochberg)
Forest plot visualization for effect sizes with confidence intervals
Comparison boxplot visualization
Docker GPU environment (
build-gpu.sh,run-gpu.sh,test-gpu.sh)
Changed
Refactored model wrappers with abstract base class (
BaseModelWrapper)Improved scikit-learn compatibility with mock history objects
Enhanced error messages with custom exception hierarchy
Fixed
GPU memory leaks in repeated Keras training runs
Matplotlib backend issues in CI/CD environments (headless mode)
[0.0.4] - 2025-12
Added
CI/CD pipeline with GitHub Actions
Cross-platform testing (Ubuntu, macOS, Windows)
Coverage reporting with Codecov integration
pytest configuration with coverage thresholds
[0.0.3] - 2025-11
Added
TabularDataHandlerfor CSV and DataFrame inputsImageDataHandlerfor image classification tasksTextDataHandlerfor NLP datasetsTimeSeriesDataHandlerfor sequential dataauto_resolve_handler()for automatic data format detection
[0.0.2] - 2025-10
Added
KerasModelWrapperfor TensorFlow/Keras modelsScikitLearnModelWrapperfor sklearn estimatorsBasic plotting functions:
plot_training_history()plot_confusion_matrix()plot_roc_curve()plot_precision_recall_curve()
[0.0.1] - 2025-09
Added
Initial release
Core
ExperimentRunnerfunctionalityBasic variability study capability
ModelConfigfor hyperparameter managementBaseLoggerfor experiment trackingMLflow integration (
MLflowLogger)