mlsurv 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mlsurv/__init__.py +215 -0
- mlsurv/_calibration_thresholds.py +25 -0
- mlsurv/_iter_warn.py +396 -0
- mlsurv/_loky_patch.py +34 -0
- mlsurv/_patient_select.py +109 -0
- mlsurv/_shared.py +585 -0
- mlsurv/_warnings.py +280 -0
- mlsurv/cli/__init__.py +173 -0
- mlsurv/cli/__main__.py +4 -0
- mlsurv/cli/_artifacts.py +471 -0
- mlsurv/cli/_commands.py +1891 -0
- mlsurv/cli/_data.py +450 -0
- mlsurv/cli/_exit_codes.py +16 -0
- mlsurv/cli/_helpers.py +143 -0
- mlsurv/cli/_output.py +69 -0
- mlsurv/cli/_parsers.py +1368 -0
- mlsurv/cli/_plot.py +879 -0
- mlsurv/constants.py +317 -0
- mlsurv/evaluation/__init__.py +92 -0
- mlsurv/evaluation/_bootstrap_iteration.py +971 -0
- mlsurv/evaluation/_bootstrap_workers.py +240 -0
- mlsurv/evaluation/_calibrators.py +482 -0
- mlsurv/evaluation/_coefficients.py +257 -0
- mlsurv/evaluation/_concordance.py +533 -0
- mlsurv/evaluation/_config.py +113 -0
- mlsurv/evaluation/_ctd.py +113 -0
- mlsurv/evaluation/_diagnostics.py +335 -0
- mlsurv/evaluation/_explain.py +168 -0
- mlsurv/evaluation/_feature_summary.py +388 -0
- mlsurv/evaluation/_incremental.py +437 -0
- mlsurv/evaluation/_interactions.py +1501 -0
- mlsurv/evaluation/_paired_comparison.py +706 -0
- mlsurv/evaluation/_prediction_bootstrap.py +660 -0
- mlsurv/evaluation/_prediction_helpers.py +482 -0
- mlsurv/evaluation/_recalibration.py +499 -0
- mlsurv/evaluation/_shap.py +389 -0
- mlsurv/evaluation/_subpopulation.py +807 -0
- mlsurv/evaluation/_univariate_cox.py +101 -0
- mlsurv/evaluation/bootstrap.py +1266 -0
- mlsurv/evaluation/evaluate.py +2177 -0
- mlsurv/evaluation/helpers.py +108 -0
- mlsurv/evaluation/importance.py +871 -0
- mlsurv/evaluation/metrics.py +2882 -0
- mlsurv/exceptions.py +54 -0
- mlsurv/guidance.py +865 -0
- mlsurv/learner/__init__.py +19 -0
- mlsurv/learner/_benchmarking.py +970 -0
- mlsurv/learner/_config.py +76 -0
- mlsurv/learner/_core.py +1384 -0
- mlsurv/learner/_cv_training.py +706 -0
- mlsurv/learner/_eval_context.py +64 -0
- mlsurv/learner/_eval_times.py +119 -0
- mlsurv/learner/_evaluation.py +71 -0
- mlsurv/learner/_evaluation_bootstrap.py +424 -0
- mlsurv/learner/_evaluation_core.py +1143 -0
- mlsurv/learner/_evaluation_helpers.py +932 -0
- mlsurv/learner/_evaluation_interactions.py +1642 -0
- mlsurv/learner/_explain.py +144 -0
- mlsurv/learner/_export.py +399 -0
- mlsurv/learner/_format.py +275 -0
- mlsurv/learner/_patient_report.py +510 -0
- mlsurv/learner/_patient_report_html.py +587 -0
- mlsurv/learner/_patient_report_template.py +385 -0
- mlsurv/learner/_patient_resolve.py +91 -0
- mlsurv/learner/_persistence.py +457 -0
- mlsurv/learner/_prediction.py +590 -0
- mlsurv/learner/_recalibration.py +290 -0
- mlsurv/learner/_references.py +193 -0
- mlsurv/learner/_reports.py +237 -0
- mlsurv/learner/_setup.py +2133 -0
- mlsurv/learner/_state.py +253 -0
- mlsurv/learner/_studies.py +235 -0
- mlsurv/learner/_sweep.py +568 -0
- mlsurv/learner/_training.py +1453 -0
- mlsurv/learner/_tuning.py +1134 -0
- mlsurv/learner/_views.py +453 -0
- mlsurv/learner/_visualization.py +1337 -0
- mlsurv/logging_utils.py +254 -0
- mlsurv/models/__init__.py +445 -0
- mlsurv/models/_auto_defaults.py +369 -0
- mlsurv/models/_metadata.py +35 -0
- mlsurv/models/_overflow.py +250 -0
- mlsurv/models/_reference_adapter.py +127 -0
- mlsurv/models/base.py +763 -0
- mlsurv/models/coxnet.py +470 -0
- mlsurv/models/coxph.py +442 -0
- mlsurv/models/deephit.py +459 -0
- mlsurv/models/deepsurv.py +624 -0
- mlsurv/models/fssvm.py +412 -0
- mlsurv/models/gbsa.py +181 -0
- mlsurv/models/rsf.py +170 -0
- mlsurv/models/weibull_aft.py +342 -0
- mlsurv/models/xgbaft.py +770 -0
- mlsurv/models/xgbcox.py +339 -0
- mlsurv/preprocessing/__init__.py +46 -0
- mlsurv/preprocessing/_cox_fit.py +285 -0
- mlsurv/preprocessing/indicators.py +64 -0
- mlsurv/preprocessing/pipelines.py +281 -0
- mlsurv/preprocessing/selection.py +1201 -0
- mlsurv/preprocessing/utils.py +221 -0
- mlsurv/py.typed +0 -0
- mlsurv/reports/__init__.py +7 -0
- mlsurv/reports/_constants.py +90 -0
- mlsurv/reports/_figure_export.py +103 -0
- mlsurv/reports/_helpers.py +213 -0
- mlsurv/reports/_text.py +38 -0
- mlsurv/reports/generator.py +697 -0
- mlsurv/reports/renderers/__init__.py +11 -0
- mlsurv/reports/renderers/_figures.py +415 -0
- mlsurv/reports/renderers/_scripts.py +488 -0
- mlsurv/reports/renderers/_styles.py +800 -0
- mlsurv/reports/renderers/html.py +537 -0
- mlsurv/reports/renderers/manuscript.py +345 -0
- mlsurv/reports/renderers/markdown.py +388 -0
- mlsurv/reports/sections/__init__.py +50 -0
- mlsurv/reports/sections/_metric_glossary.py +70 -0
- mlsurv/reports/sections/_subsection_base.py +48 -0
- mlsurv/reports/sections/_trained_details.py +314 -0
- mlsurv/reports/sections/base.py +565 -0
- mlsurv/reports/sections/benchmarking.py +987 -0
- mlsurv/reports/sections/data_preprocessing.py +1918 -0
- mlsurv/reports/sections/environment.py +469 -0
- mlsurv/reports/sections/evaluation/__init__.py +10 -0
- mlsurv/reports/sections/evaluation/_base.py +35 -0
- mlsurv/reports/sections/evaluation/_calibration.py +413 -0
- mlsurv/reports/sections/evaluation/_discrimination.py +162 -0
- mlsurv/reports/sections/evaluation/_helpers.py +99 -0
- mlsurv/reports/sections/evaluation/_methodology.py +64 -0
- mlsurv/reports/sections/evaluation/_paired_comparison.py +196 -0
- mlsurv/reports/sections/evaluation/_performance.py +468 -0
- mlsurv/reports/sections/evaluation/_roc.py +224 -0
- mlsurv/reports/sections/evaluation/_section.py +233 -0
- mlsurv/reports/sections/evaluation/_stratification.py +678 -0
- mlsurv/reports/sections/feature_analysis/__init__.py +20 -0
- mlsurv/reports/sections/feature_analysis/_audit.py +255 -0
- mlsurv/reports/sections/feature_analysis/_coefficient_analysis.py +100 -0
- mlsurv/reports/sections/feature_analysis/_coefficients.py +140 -0
- mlsurv/reports/sections/feature_analysis/_correlation.py +251 -0
- mlsurv/reports/sections/feature_analysis/_hazard_ratios.py +198 -0
- mlsurv/reports/sections/feature_analysis/_helpers.py +128 -0
- mlsurv/reports/sections/feature_analysis/_importance.py +445 -0
- mlsurv/reports/sections/feature_analysis/_interactions.py +253 -0
- mlsurv/reports/sections/feature_analysis/_interactive_table.py +353 -0
- mlsurv/reports/sections/feature_analysis/_section.py +858 -0
- mlsurv/reports/sections/feature_analysis/_shap.py +102 -0
- mlsurv/reports/sections/limitations.py +1027 -0
- mlsurv/reports/sections/model_development.py +851 -0
- mlsurv/reports/sections/references.py +863 -0
- mlsurv/reports/sections/subpopulation_analysis.py +288 -0
- mlsurv/reports/sections/summary.py +1114 -0
- mlsurv/reports/sections/tripod_checklist.py +474 -0
- mlsurv/reports/sections/validation/__init__.py +10 -0
- mlsurv/reports/sections/validation/_base.py +41 -0
- mlsurv/reports/sections/validation/_bootstrap.py +200 -0
- mlsurv/reports/sections/validation/_calibration.py +323 -0
- mlsurv/reports/sections/validation/_discrimination.py +120 -0
- mlsurv/reports/sections/validation/_helpers.py +179 -0
- mlsurv/reports/sections/validation/_performance_drop.py +150 -0
- mlsurv/reports/sections/validation/_section.py +496 -0
- mlsurv/reports/sections/validation/_stratification.py +377 -0
- mlsurv/reports/sections/validation/_table1.py +351 -0
- mlsurv/results/__init__.py +65 -0
- mlsurv/results/_containers_extraction.py +321 -0
- mlsurv/results/_containers_helpers.py +410 -0
- mlsurv/results/_containers_printing.py +343 -0
- mlsurv/results/_explanation.py +46 -0
- mlsurv/results/_prediction.py +733 -0
- mlsurv/results/containers.py +1662 -0
- mlsurv/results/printing.py +1599 -0
- mlsurv/results/tables.py +396 -0
- mlsurv/tuning/__init__.py +72 -0
- mlsurv/tuning/base.py +1528 -0
- mlsurv/tuning/exceptions.py +24 -0
- mlsurv/tuning/optuna_tuner.py +89 -0
- mlsurv/tuning/search_spaces.py +592 -0
- mlsurv/tuning/storage.py +108 -0
- mlsurv/tuning/studies.py +389 -0
- mlsurv/tuning/tuners/__init__.py +72 -0
- mlsurv/tuning/tuners/coxnet.py +50 -0
- mlsurv/tuning/tuners/coxph.py +46 -0
- mlsurv/tuning/tuners/custom.py +55 -0
- mlsurv/tuning/tuners/deephit.py +57 -0
- mlsurv/tuning/tuners/deepsurv.py +56 -0
- mlsurv/tuning/tuners/fssvm.py +78 -0
- mlsurv/tuning/tuners/gbsa.py +29 -0
- mlsurv/tuning/tuners/rsf.py +30 -0
- mlsurv/tuning/tuners/weibull_aft.py +27 -0
- mlsurv/tuning/tuners/xgbaft.py +44 -0
- mlsurv/tuning/tuners/xgbcox.py +35 -0
- mlsurv/utils.py +1730 -0
- mlsurv/viz/__init__.py +179 -0
- mlsurv/viz/_context.py +348 -0
- mlsurv/viz/_data.py +1323 -0
- mlsurv/viz/_data_metrics.py +1295 -0
- mlsurv/viz/_features_coefficients.py +795 -0
- mlsurv/viz/_features_correlation.py +286 -0
- mlsurv/viz/_features_hazard_ratios.py +966 -0
- mlsurv/viz/_features_helpers.py +144 -0
- mlsurv/viz/_features_importance.py +623 -0
- mlsurv/viz/_features_importance_multi.py +840 -0
- mlsurv/viz/_metrics_helpers.py +846 -0
- mlsurv/viz/_metrics_multi.py +527 -0
- mlsurv/viz/_metrics_renderers.py +603 -0
- mlsurv/viz/_metrics_roc.py +884 -0
- mlsurv/viz/_metrics_time.py +1214 -0
- mlsurv/viz/_palettes.py +1055 -0
- mlsurv/viz/_plot_impl.py +84 -0
- mlsurv/viz/_plot_impl_calibration.py +726 -0
- mlsurv/viz/_plot_impl_importance.py +69 -0
- mlsurv/viz/_plot_impl_metric.py +513 -0
- mlsurv/viz/_plot_impl_metric_time.py +821 -0
- mlsurv/viz/_plot_impl_roc.py +804 -0
- mlsurv/viz/_plot_impl_secondary.py +840 -0
- mlsurv/viz/_plot_impl_shared.py +494 -0
- mlsurv/viz/_plot_impl_survival.py +400 -0
- mlsurv/viz/_plotly_helpers.py +106 -0
- mlsurv/viz/_resolve.py +489 -0
- mlsurv/viz/_survival_figure.py +203 -0
- mlsurv/viz/_survival_helpers.py +376 -0
- mlsurv/viz/_survival_logrank.py +192 -0
- mlsurv/viz/_survival_maxstat.py +458 -0
- mlsurv/viz/_survival_renderers.py +692 -0
- mlsurv/viz/_theme.py +401 -0
- mlsurv/viz/_time_format.py +50 -0
- mlsurv/viz/_validation.py +45 -0
- mlsurv/viz/benchmarking_scatter.py +274 -0
- mlsurv/viz/calibration.py +1333 -0
- mlsurv/viz/diagnostics.py +713 -0
- mlsurv/viz/distributions.py +372 -0
- mlsurv/viz/explain.py +278 -0
- mlsurv/viz/export.py +337 -0
- mlsurv/viz/features.py +420 -0
- mlsurv/viz/flowchart.py +252 -0
- mlsurv/viz/interactions.py +1080 -0
- mlsurv/viz/metrics.py +911 -0
- mlsurv/viz/missing.py +145 -0
- mlsurv/viz/shap.py +637 -0
- mlsurv/viz/survival.py +509 -0
- mlsurv/viz/sweep.py +160 -0
- mlsurv-0.1.0.dist-info/METADATA +297 -0
- mlsurv-0.1.0.dist-info/RECORD +245 -0
- mlsurv-0.1.0.dist-info/WHEEL +5 -0
- mlsurv-0.1.0.dist-info/entry_points.txt +2 -0
- mlsurv-0.1.0.dist-info/licenses/LICENSE +9 -0
- mlsurv-0.1.0.dist-info/top_level.txt +1 -0
mlsurv/__init__.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mlsurv: Low-code survival machine learning package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import warnings
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Single source of truth: read the version from the installed package metadata
|
|
12
|
+
# (pyproject.toml) so it can never drift from the distribution version.
|
|
13
|
+
try:
|
|
14
|
+
__version__ = _pkg_version("mlsurv")
|
|
15
|
+
except PackageNotFoundError: # running from an uninstalled source checkout
|
|
16
|
+
__version__ = "0.0.0+unknown"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _configure_matplotlib_cache_dir() -> None:
|
|
20
|
+
"""Set a writable MPLCONFIGDIR when HOME is not writable."""
|
|
21
|
+
if os.environ.get("MPLCONFIGDIR"):
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
home_dir = Path.home()
|
|
25
|
+
mpl_dir = home_dir / ".matplotlib"
|
|
26
|
+
|
|
27
|
+
# If an existing cache dir is writable, keep matplotlib defaults.
|
|
28
|
+
if mpl_dir.exists() and os.access(mpl_dir, os.W_OK):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# If HOME itself is writable, matplotlib can create ~/.matplotlib.
|
|
32
|
+
if os.access(home_dir, os.W_OK):
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
fallback = Path(tempfile.gettempdir()) / "mlsurv-matplotlib"
|
|
37
|
+
fallback.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
os.environ["MPLCONFIGDIR"] = str(fallback)
|
|
39
|
+
except OSError:
|
|
40
|
+
from mlsurv._warnings import InternalWarning
|
|
41
|
+
warnings.warn(
|
|
42
|
+
"mlsurv could not create a writable matplotlib cache directory. "
|
|
43
|
+
"Set the MPLCONFIGDIR environment variable to a writable path "
|
|
44
|
+
"to avoid potential plotting errors.",
|
|
45
|
+
InternalWarning,
|
|
46
|
+
stacklevel=1,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Register mlsurv's tiered warning filters — must be before any code
|
|
51
|
+
# that may emit warnings (e.g., matplotlib cache setup).
|
|
52
|
+
from mlsurv._warnings import configure_default_filters
|
|
53
|
+
configure_default_filters()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _check_stdout_encoding() -> None:
|
|
57
|
+
"""Warn if stdout encoding cannot represent Unicode used by mlsurv."""
|
|
58
|
+
import codecs
|
|
59
|
+
import sys
|
|
60
|
+
encoding = getattr(sys.stdout, "encoding", None)
|
|
61
|
+
if not encoding:
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
canonical = codecs.lookup(encoding).name
|
|
65
|
+
except LookupError:
|
|
66
|
+
return
|
|
67
|
+
if canonical in ("cp1252", "ascii", "iso8859-1", "latin-1"):
|
|
68
|
+
from mlsurv._warnings import SuggestionWarning
|
|
69
|
+
warnings.warn(
|
|
70
|
+
f"stdout encoding is '{encoding}', which cannot represent all "
|
|
71
|
+
"characters used by mlsurv. Set the environment variable "
|
|
72
|
+
"PYTHONIOENCODING=utf-8 to avoid UnicodeEncodeError crashes.",
|
|
73
|
+
SuggestionWarning,
|
|
74
|
+
stacklevel=1,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_check_stdout_encoding()
|
|
79
|
+
_configure_matplotlib_cache_dir()
|
|
80
|
+
|
|
81
|
+
# Suppress loky resource_tracker KeyError tracebacks on Windows.
|
|
82
|
+
# Loky's tracker subprocess has a race condition where multiple workers
|
|
83
|
+
# UNREGISTER the same temp folder. We swap the module-level `main`
|
|
84
|
+
# reference so the subprocess imports our wrapper (mlsurv._loky_patch),
|
|
85
|
+
# which installs a selective sys.excepthook before delegating to the
|
|
86
|
+
# real main. Also disable memmapping globally since it provides
|
|
87
|
+
# negligible benefit for typical survival analysis datasets.
|
|
88
|
+
import joblib
|
|
89
|
+
joblib.parallel_config(max_nbytes=None)
|
|
90
|
+
from joblib.externals.loky.backend import resource_tracker as _rt_mod
|
|
91
|
+
from mlsurv._loky_patch import main as _patched_rt_main
|
|
92
|
+
_rt_mod.main = _patched_rt_main
|
|
93
|
+
|
|
94
|
+
# Suppress NumPy 2.x deprecation warning from sksurv calling np.trapz.
|
|
95
|
+
# Cannot fix upstream; narrow filter matches only sksurv.metrics subpackage.
|
|
96
|
+
warnings.filterwarnings(
|
|
97
|
+
"ignore",
|
|
98
|
+
message=r"`trapz` is deprecated",
|
|
99
|
+
category=DeprecationWarning,
|
|
100
|
+
module=r"sksurv\.metrics",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Suppress lifelines emitting pandas-4.0 keyword-only deprecation warnings
|
|
104
|
+
# from internal Series.var/mean/std calls during fit. On lifelines
|
|
105
|
+
# v0.30.3 + pandas 3.0.2 this is ``pandas.errors.Pandas4Warning`` (a
|
|
106
|
+
# ``DeprecationWarning`` subclass), but earlier versions of lifelines have
|
|
107
|
+
# emitted equivalent messages as ``UserWarning``. Use ``category=Warning``
|
|
108
|
+
# (the common base) so the filter is robust across versions; the message +
|
|
109
|
+
# module narrowing keeps legitimate lifelines warnings (convergence,
|
|
110
|
+
# ill-conditioned design, etc.) flowing through.
|
|
111
|
+
warnings.filterwarnings(
|
|
112
|
+
"ignore",
|
|
113
|
+
message=r"Starting with pandas version 4\.0",
|
|
114
|
+
category=Warning,
|
|
115
|
+
module=r"lifelines\..*",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
from mlsurv.utils import (
|
|
119
|
+
to_structured_array, load_survival_data,
|
|
120
|
+
compute_survival_time, SurvivalArray,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Model wrappers (imported after definition)
|
|
124
|
+
from mlsurv.models import (
|
|
125
|
+
BaseSurvivalModel,
|
|
126
|
+
CoxPHModel,
|
|
127
|
+
CoxNetModel,
|
|
128
|
+
RSFModel,
|
|
129
|
+
GBSAModel,
|
|
130
|
+
FSSVMModel,
|
|
131
|
+
XGBCoxModel,
|
|
132
|
+
XGBAFTModel,
|
|
133
|
+
WeibullAFTModel,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Result containers
|
|
137
|
+
from mlsurv.results import (
|
|
138
|
+
EvaluationResult,
|
|
139
|
+
BootstrapResult,
|
|
140
|
+
ModelResult,
|
|
141
|
+
ValidationResult,
|
|
142
|
+
BenchmarkResult,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Preprocessing
|
|
146
|
+
from mlsurv.preprocessing import (
|
|
147
|
+
TopKCoxSelector,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Guidance
|
|
151
|
+
from mlsurv.guidance import recommend_models, RecommendedModels
|
|
152
|
+
|
|
153
|
+
# Main API
|
|
154
|
+
from mlsurv.learner import SurvivalLearner
|
|
155
|
+
load_learner = SurvivalLearner.load_learner
|
|
156
|
+
|
|
157
|
+
# Reports
|
|
158
|
+
from mlsurv.reports import ReportGenerator
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
# Version
|
|
162
|
+
"__version__",
|
|
163
|
+
# Main API
|
|
164
|
+
"SurvivalLearner",
|
|
165
|
+
"load_learner",
|
|
166
|
+
# Reports
|
|
167
|
+
"ReportGenerator",
|
|
168
|
+
# Utilities
|
|
169
|
+
"recommend_models",
|
|
170
|
+
"RecommendedModels",
|
|
171
|
+
"to_structured_array",
|
|
172
|
+
"load_survival_data",
|
|
173
|
+
"compute_survival_time",
|
|
174
|
+
"SurvivalArray",
|
|
175
|
+
# Models
|
|
176
|
+
"BaseSurvivalModel",
|
|
177
|
+
"CoxPHModel",
|
|
178
|
+
"CoxNetModel",
|
|
179
|
+
"RSFModel",
|
|
180
|
+
"GBSAModel",
|
|
181
|
+
"FSSVMModel",
|
|
182
|
+
"XGBCoxModel",
|
|
183
|
+
"XGBAFTModel",
|
|
184
|
+
"WeibullAFTModel",
|
|
185
|
+
# Results
|
|
186
|
+
"EvaluationResult",
|
|
187
|
+
"BootstrapResult",
|
|
188
|
+
"ModelResult",
|
|
189
|
+
"ValidationResult",
|
|
190
|
+
"BenchmarkResult",
|
|
191
|
+
# Preprocessing
|
|
192
|
+
"TopKCoxSelector",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
# Lazy import for DeepSurv (requires torch)
|
|
196
|
+
def __getattr__(name):
|
|
197
|
+
if name == "DeepSurvModel":
|
|
198
|
+
try:
|
|
199
|
+
from mlsurv.models.deepsurv import DeepSurvModel
|
|
200
|
+
return DeepSurvModel
|
|
201
|
+
except ImportError:
|
|
202
|
+
raise ImportError(
|
|
203
|
+
"DeepSurvModel requires torch and pycox. "
|
|
204
|
+
"Install with: pip install mlsurv[deep]"
|
|
205
|
+
)
|
|
206
|
+
if name == "DeepHitModel":
|
|
207
|
+
try:
|
|
208
|
+
from mlsurv.models.deephit import DeepHitModel
|
|
209
|
+
return DeepHitModel
|
|
210
|
+
except ImportError:
|
|
211
|
+
raise ImportError(
|
|
212
|
+
"DeepHitModel requires torch and pycox. "
|
|
213
|
+
"Install with: pip install mlsurv[deep]"
|
|
214
|
+
)
|
|
215
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared calibration thresholds.
|
|
2
|
+
|
|
3
|
+
Single source of truth for the slope/p-value cutoffs used by both the
|
|
4
|
+
runtime ``MethodologicalWarning`` (``mlsurv.evaluation.evaluate``) and the
|
|
5
|
+
HTML report's poor-calibration flag (``mlsurv.reports.sections.limitations``).
|
|
6
|
+
Lives at the package root to avoid an upward dependency from ``evaluation``
|
|
7
|
+
into ``reports``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Calibration slope is the sole drift trigger. The band is deliberately wide:
|
|
13
|
+
# a slope inside [0.5, 1.5] (|slope-1| <= 0.5) is treated as adequately
|
|
14
|
+
# calibrated for deployment, so the flag fires only on genuinely large
|
|
15
|
+
# miscalibration that calibrating S(t|x) would meaningfully fix. Both
|
|
16
|
+
# CAL_SLOPE_LOW/HIGH and SEVERE_DRIFT_THRESHOLD describe the same |slope-1| > 0.5
|
|
17
|
+
# cutoff (kept as separate names for the two call sites that read them).
|
|
18
|
+
CAL_SLOPE_LOW = 0.5
|
|
19
|
+
CAL_SLOPE_HIGH = 1.5
|
|
20
|
+
SEVERE_DRIFT_THRESHOLD = 0.5
|
|
21
|
+
|
|
22
|
+
# GND (Greenwood-Nam-D'Agostino) p-value: reported as context in the drift
|
|
23
|
+
# message text only. It is NOT a standalone trigger -- it over-rejects on
|
|
24
|
+
# well-powered cohorts, so the slope band alone decides whether to flag.
|
|
25
|
+
P_THRESHOLD_SIGNIFICANT = 0.05
|
mlsurv/_iter_warn.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Coalescer for repeated IterativeWarnings.
|
|
2
|
+
|
|
3
|
+
Per-iteration loops emit warnings whose text varies per iteration (e.g.,
|
|
4
|
+
``seed=...`` for failed bootstrap iterations, or ``label='X'`` for
|
|
5
|
+
small-subpopulation notices). Python's ``simplefilter("once", ...)`` cannot
|
|
6
|
+
deduplicate when message text differs, so stderr fills with hundreds of
|
|
7
|
+
near-identical lines.
|
|
8
|
+
|
|
9
|
+
Two modes are supported:
|
|
10
|
+
|
|
11
|
+
* ``mode="failures"`` (default): each record carries a caught
|
|
12
|
+
exception. Examples are formatted as
|
|
13
|
+
``"{summary_label} iteration failed{prefix}: {message}"``; the aggregate
|
|
14
|
+
summary describes count + error-type diversity.
|
|
15
|
+
|
|
16
|
+
* ``mode="notices"``: each record carries a pre-rendered message
|
|
17
|
+
(no exception). Examples are emitted verbatim; the aggregate summary
|
|
18
|
+
reads ``"{n}/{n_total} ({pct}%) {summary_label}s {notice_phrase}: {keys}.{tail}"``,
|
|
19
|
+
where each ``key`` is the notice's ``display`` text (or ``group_key`` as
|
|
20
|
+
a fallback). ``group_key`` is reserved as a true identifier for future
|
|
21
|
+
grouping/dedup logic; ``display`` is the short summary-list text.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import warnings
|
|
27
|
+
from collections import OrderedDict
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Iterable, Literal, Optional, Type
|
|
30
|
+
|
|
31
|
+
from ._warnings import IterativeWarning
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class IterativeFailure:
|
|
36
|
+
"""A single per-iteration failure to be reported by the coalescer."""
|
|
37
|
+
|
|
38
|
+
exc_type_name: str
|
|
39
|
+
message: str
|
|
40
|
+
context_prefix: str = ""
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_exception(
|
|
44
|
+
cls, exc: BaseException, *, prefix: str = ""
|
|
45
|
+
) -> "IterativeFailure":
|
|
46
|
+
"""Build a failure record from a caught exception."""
|
|
47
|
+
return cls(
|
|
48
|
+
exc_type_name=type(exc).__name__,
|
|
49
|
+
message=str(exc),
|
|
50
|
+
context_prefix=prefix,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class IterativeNotice:
|
|
56
|
+
"""A single per-iteration notice (non-failure) to be coalesced.
|
|
57
|
+
|
|
58
|
+
Fields:
|
|
59
|
+
|
|
60
|
+
* ``message`` — full per-iteration text, emitted verbatim when
|
|
61
|
+
``max_emit > 0``.
|
|
62
|
+
* ``group_key`` — true identifier for the iteration (e.g. a label,
|
|
63
|
+
seed, or bucket name). Reserved for future grouping/dedup logic
|
|
64
|
+
inside the coalescer.
|
|
65
|
+
* ``display`` — short text used in the aggregate summary's
|
|
66
|
+
"affected items" list. Falls back to ``group_key`` when empty,
|
|
67
|
+
so callers that only need an identifier can keep passing
|
|
68
|
+
``group_key=`` alone.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
message: str
|
|
72
|
+
group_key: str = ""
|
|
73
|
+
display: str = ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def coalesce_failures(
|
|
77
|
+
failures: Iterable[IterativeFailure],
|
|
78
|
+
*,
|
|
79
|
+
n_total: int,
|
|
80
|
+
summary_label: str,
|
|
81
|
+
max_emit: int = 3,
|
|
82
|
+
category: Type[Warning] = IterativeWarning,
|
|
83
|
+
stacklevel: int = 3,
|
|
84
|
+
tail_msg: str = "",
|
|
85
|
+
) -> int:
|
|
86
|
+
"""Emit warnings for ``failures`` according to the coalescer policy.
|
|
87
|
+
|
|
88
|
+
Policy:
|
|
89
|
+
1. Group by ``exc_type_name`` (encounter order).
|
|
90
|
+
2. Emit up to ``max_emit`` distinct error-type examples verbatim.
|
|
91
|
+
3. Emit one aggregate summary describing total count and type diversity.
|
|
92
|
+
|
|
93
|
+
Empty ``failures`` emits nothing.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
failures : iterable of IterativeFailure
|
|
98
|
+
Per-iteration failure records to coalesce.
|
|
99
|
+
n_total : int
|
|
100
|
+
Total number of iterations attempted (denominator for the summary).
|
|
101
|
+
summary_label : str
|
|
102
|
+
Human-readable label for the iteration kind (e.g. ``"bootstrap"``).
|
|
103
|
+
max_emit : int, default=3
|
|
104
|
+
Maximum number of distinct error-type examples emitted verbatim
|
|
105
|
+
before the aggregate summary.
|
|
106
|
+
category : type of Warning, default=IterativeWarning
|
|
107
|
+
Warning class to emit.
|
|
108
|
+
stacklevel : int, default=3
|
|
109
|
+
``stacklevel`` forwarded to :func:`warnings.warn`.
|
|
110
|
+
tail_msg : str, default=""
|
|
111
|
+
Optional text appended to the aggregate summary message.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
int
|
|
116
|
+
Number of failures, for caller convenience.
|
|
117
|
+
|
|
118
|
+
Raises
|
|
119
|
+
------
|
|
120
|
+
ValueError
|
|
121
|
+
If ``n_total`` is less than 1, if ``max_emit`` is negative, or if
|
|
122
|
+
the number of failures exceeds ``n_total``.
|
|
123
|
+
"""
|
|
124
|
+
if n_total < 1:
|
|
125
|
+
raise ValueError(f"n_total must be >= 1; got {n_total}")
|
|
126
|
+
if max_emit < 0:
|
|
127
|
+
raise ValueError(f"max_emit must be >= 0; got {max_emit}")
|
|
128
|
+
failures = list(failures)
|
|
129
|
+
n_failed = len(failures)
|
|
130
|
+
if n_failed == 0:
|
|
131
|
+
return 0
|
|
132
|
+
if n_failed > n_total:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"len(failures)={n_failed} exceeds n_total={n_total}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
grouped: "OrderedDict[str, list[IterativeFailure]]" = OrderedDict()
|
|
138
|
+
for f in failures:
|
|
139
|
+
grouped.setdefault(f.exc_type_name, []).append(f)
|
|
140
|
+
|
|
141
|
+
for exc_type_name, group in list(grouped.items())[:max_emit]:
|
|
142
|
+
example = group[0]
|
|
143
|
+
prefix = f" ({example.context_prefix})" if example.context_prefix else ""
|
|
144
|
+
warnings.warn(
|
|
145
|
+
f"{summary_label} iteration failed{prefix}: {example.message}",
|
|
146
|
+
category,
|
|
147
|
+
stacklevel=stacklevel,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
pct = n_failed / n_total * 100
|
|
151
|
+
tail = f" {tail_msg}" if tail_msg else ""
|
|
152
|
+
if len(grouped) == 1:
|
|
153
|
+
only_type = next(iter(grouped))
|
|
154
|
+
summary = (
|
|
155
|
+
f"{n_failed}/{n_total} ({pct:.0f}%) {summary_label} iterations failed; "
|
|
156
|
+
f"all shared the same error type ({only_type}).{tail}"
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
breakdown = ", ".join(
|
|
160
|
+
f"{name} (n={len(group)})" for name, group in grouped.items()
|
|
161
|
+
)
|
|
162
|
+
summary = (
|
|
163
|
+
f"{n_failed}/{n_total} ({pct:.0f}%) {summary_label} iterations failed "
|
|
164
|
+
f"across {len(grouped)} distinct error types: {breakdown}.{tail}"
|
|
165
|
+
)
|
|
166
|
+
warnings.warn(summary, category, stacklevel=stacklevel)
|
|
167
|
+
return n_failed
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def coalesce_notices(
|
|
171
|
+
notices: Iterable[IterativeNotice],
|
|
172
|
+
*,
|
|
173
|
+
n_total: int,
|
|
174
|
+
summary_label: str,
|
|
175
|
+
notice_phrase: str,
|
|
176
|
+
max_emit: int = 3,
|
|
177
|
+
category: Type[Warning] = IterativeWarning,
|
|
178
|
+
stacklevel: int = 3,
|
|
179
|
+
tail_msg: str = "",
|
|
180
|
+
) -> int:
|
|
181
|
+
"""Emit warnings for ``notices`` (non-failure repeated warnings).
|
|
182
|
+
|
|
183
|
+
Policy:
|
|
184
|
+
1. Emit up to ``max_emit`` notice messages verbatim (encounter order).
|
|
185
|
+
2. Emit one aggregate summary listing the affected ``group_key`` values.
|
|
186
|
+
|
|
187
|
+
Empty ``notices`` emits nothing.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
notices : iterable of IterativeNotice
|
|
192
|
+
Per-iteration notice records to coalesce.
|
|
193
|
+
n_total : int
|
|
194
|
+
Total number of iterations attempted (denominator for the summary).
|
|
195
|
+
summary_label : str
|
|
196
|
+
Human-readable label for the iteration kind (e.g. ``"bootstrap"``).
|
|
197
|
+
notice_phrase : str
|
|
198
|
+
Phrase describing the notice condition, used in the aggregate summary.
|
|
199
|
+
max_emit : int, default=3
|
|
200
|
+
Maximum number of notice messages emitted verbatim before the
|
|
201
|
+
aggregate summary.
|
|
202
|
+
category : type of Warning, default=IterativeWarning
|
|
203
|
+
Warning class to emit.
|
|
204
|
+
stacklevel : int, default=3
|
|
205
|
+
``stacklevel`` forwarded to :func:`warnings.warn`.
|
|
206
|
+
tail_msg : str, default=""
|
|
207
|
+
Optional text appended to the aggregate summary message.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
int
|
|
212
|
+
Number of notices, for caller convenience.
|
|
213
|
+
|
|
214
|
+
Raises
|
|
215
|
+
------
|
|
216
|
+
ValueError
|
|
217
|
+
If ``n_total`` is less than 1, if ``max_emit`` is negative, or if
|
|
218
|
+
the number of notices exceeds ``n_total``.
|
|
219
|
+
"""
|
|
220
|
+
if n_total < 1:
|
|
221
|
+
raise ValueError(f"n_total must be >= 1; got {n_total}")
|
|
222
|
+
if max_emit < 0:
|
|
223
|
+
raise ValueError(f"max_emit must be >= 0; got {max_emit}")
|
|
224
|
+
notices = list(notices)
|
|
225
|
+
n = len(notices)
|
|
226
|
+
if n == 0:
|
|
227
|
+
return 0
|
|
228
|
+
if n > n_total:
|
|
229
|
+
raise ValueError(f"len(notices)={n} exceeds n_total={n_total}")
|
|
230
|
+
|
|
231
|
+
for notice in notices[:max_emit]:
|
|
232
|
+
warnings.warn(notice.message, category, stacklevel=stacklevel)
|
|
233
|
+
|
|
234
|
+
pct = n / n_total * 100
|
|
235
|
+
tail = f" {tail_msg}" if tail_msg else ""
|
|
236
|
+
keys = ", ".join(
|
|
237
|
+
(nt.display or nt.group_key)
|
|
238
|
+
for nt in notices
|
|
239
|
+
if (nt.display or nt.group_key)
|
|
240
|
+
)
|
|
241
|
+
keys_part = f": {keys}" if keys else ""
|
|
242
|
+
summary = (
|
|
243
|
+
f"{n}/{n_total} ({pct:.0f}%) {summary_label}s {notice_phrase}"
|
|
244
|
+
f"{keys_part}.{tail}"
|
|
245
|
+
)
|
|
246
|
+
warnings.warn(summary, category, stacklevel=stacklevel)
|
|
247
|
+
return n
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class IterativeWarningCoalescer:
|
|
251
|
+
"""Context-manager form of the coalescer functions.
|
|
252
|
+
|
|
253
|
+
Two modes:
|
|
254
|
+
|
|
255
|
+
* ``mode="failures"`` (default): call ``.record(exc, prefix=...)`` for
|
|
256
|
+
each caught exception. Dispatches to :func:`coalesce_failures`.
|
|
257
|
+
* ``mode="notices"``: call ``.record_notice(message, group_key=...)``
|
|
258
|
+
for each pre-rendered notice. Requires ``notice_phrase``. Dispatches
|
|
259
|
+
to :func:`coalesce_notices`.
|
|
260
|
+
|
|
261
|
+
Calling the wrong ``record*`` method for the active mode raises
|
|
262
|
+
``RuntimeError``.
|
|
263
|
+
|
|
264
|
+
Example (failures)::
|
|
265
|
+
|
|
266
|
+
with IterativeWarningCoalescer(summary_label="bootstrap",
|
|
267
|
+
n_total=n_bootstrap) as cz:
|
|
268
|
+
for seed in seeds:
|
|
269
|
+
try:
|
|
270
|
+
...
|
|
271
|
+
except (ValueError, RuntimeError) as exc:
|
|
272
|
+
cz.record(exc, prefix=f"seed={seed}")
|
|
273
|
+
|
|
274
|
+
Example (notices)::
|
|
275
|
+
|
|
276
|
+
with IterativeWarningCoalescer(
|
|
277
|
+
mode="notices",
|
|
278
|
+
summary_label="'stage' subpopulation",
|
|
279
|
+
notice_phrase="have insufficient events (< 30)",
|
|
280
|
+
n_total=len(labels),
|
|
281
|
+
category=SmallSubpopulationWarning,
|
|
282
|
+
) as cz:
|
|
283
|
+
for label in labels:
|
|
284
|
+
if n_events < min_events:
|
|
285
|
+
cz.record_notice(msg, group_key=str(label))
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def __init__(
|
|
289
|
+
self,
|
|
290
|
+
*,
|
|
291
|
+
summary_label: str,
|
|
292
|
+
n_total: int,
|
|
293
|
+
max_emit: int = 3,
|
|
294
|
+
category: Type[Warning] = IterativeWarning,
|
|
295
|
+
stacklevel: int = 3,
|
|
296
|
+
tail_msg: str = "",
|
|
297
|
+
mode: Literal["failures", "notices"] = "failures",
|
|
298
|
+
notice_phrase: str = "",
|
|
299
|
+
):
|
|
300
|
+
if n_total < 1:
|
|
301
|
+
raise ValueError(f"n_total must be >= 1; got {n_total}")
|
|
302
|
+
if max_emit < 0:
|
|
303
|
+
raise ValueError(f"max_emit must be >= 0; got {max_emit}")
|
|
304
|
+
if mode not in ("failures", "notices"):
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"mode must be 'failures' or 'notices'; got {mode!r}"
|
|
307
|
+
)
|
|
308
|
+
if mode == "notices" and not notice_phrase:
|
|
309
|
+
raise ValueError("notice_phrase is required when mode='notices'")
|
|
310
|
+
self.summary_label = summary_label
|
|
311
|
+
self.n_total = n_total
|
|
312
|
+
self.max_emit = max_emit
|
|
313
|
+
self.category = category
|
|
314
|
+
self.stacklevel = stacklevel
|
|
315
|
+
self.tail_msg = tail_msg
|
|
316
|
+
self.mode = mode
|
|
317
|
+
self.notice_phrase = notice_phrase
|
|
318
|
+
self._failures: list[IterativeFailure] = []
|
|
319
|
+
self._notices: list[IterativeNotice] = []
|
|
320
|
+
self._summarized = False
|
|
321
|
+
|
|
322
|
+
def record(self, exc: Exception, *, prefix: str = "") -> None:
|
|
323
|
+
if self.mode != "failures":
|
|
324
|
+
raise RuntimeError(
|
|
325
|
+
f"record() requires mode='failures'; coalescer is in "
|
|
326
|
+
f"mode={self.mode!r}. Use record_notice() instead."
|
|
327
|
+
)
|
|
328
|
+
self._failures.append(IterativeFailure.from_exception(exc, prefix=prefix))
|
|
329
|
+
|
|
330
|
+
def record_failure(self, failure: IterativeFailure) -> None:
|
|
331
|
+
if self.mode != "failures":
|
|
332
|
+
raise RuntimeError(
|
|
333
|
+
f"record_failure() requires mode='failures'; coalescer is in "
|
|
334
|
+
f"mode={self.mode!r}. Use record_notice() instead."
|
|
335
|
+
)
|
|
336
|
+
self._failures.append(failure)
|
|
337
|
+
|
|
338
|
+
def record_notice(
|
|
339
|
+
self, message: str, *, group_key: str = "", display: str = ""
|
|
340
|
+
) -> None:
|
|
341
|
+
if self.mode != "notices":
|
|
342
|
+
raise RuntimeError(
|
|
343
|
+
f"record_notice() requires mode='notices'; coalescer is in "
|
|
344
|
+
f"mode={self.mode!r}. Use record() instead."
|
|
345
|
+
)
|
|
346
|
+
self._notices.append(
|
|
347
|
+
IterativeNotice(message=message, group_key=group_key, display=display)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def summarize(self) -> int:
|
|
351
|
+
if self._summarized:
|
|
352
|
+
return len(self._failures) + len(self._notices)
|
|
353
|
+
self._summarized = True
|
|
354
|
+
# Bump stacklevel by +2 to account for the summarize() and __exit__()
|
|
355
|
+
# frames the coalesce_* function will see above its own warnings.warn
|
|
356
|
+
# call. This way callers pass the same stacklevel they would use for a
|
|
357
|
+
# direct coalesce_*() call, and the warning still attributes to the
|
|
358
|
+
# caller of compute_subpopulation_metrics() (or whichever site uses
|
|
359
|
+
# the context-manager form).
|
|
360
|
+
bumped_stacklevel = self.stacklevel + 2
|
|
361
|
+
if self.mode == "failures":
|
|
362
|
+
return coalesce_failures(
|
|
363
|
+
self._failures,
|
|
364
|
+
n_total=self.n_total,
|
|
365
|
+
summary_label=self.summary_label,
|
|
366
|
+
max_emit=self.max_emit,
|
|
367
|
+
category=self.category,
|
|
368
|
+
stacklevel=bumped_stacklevel,
|
|
369
|
+
tail_msg=self.tail_msg,
|
|
370
|
+
)
|
|
371
|
+
return coalesce_notices(
|
|
372
|
+
self._notices,
|
|
373
|
+
n_total=self.n_total,
|
|
374
|
+
summary_label=self.summary_label,
|
|
375
|
+
notice_phrase=self.notice_phrase,
|
|
376
|
+
max_emit=self.max_emit,
|
|
377
|
+
category=self.category,
|
|
378
|
+
stacklevel=bumped_stacklevel,
|
|
379
|
+
tail_msg=self.tail_msg,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def __enter__(self) -> "IterativeWarningCoalescer":
|
|
383
|
+
return self
|
|
384
|
+
|
|
385
|
+
def __exit__(self, exc_type, exc, tb) -> Optional[bool]:
|
|
386
|
+
# Always attempt to summarize so a propagating exception in the
|
|
387
|
+
# with-block does not silently discard previously recorded
|
|
388
|
+
# records. If summarize() itself raises while a user exception
|
|
389
|
+
# is propagating, prefer the user exception — masking it with
|
|
390
|
+
# our own bookkeeping failure is the worse outcome.
|
|
391
|
+
try:
|
|
392
|
+
self.summarize()
|
|
393
|
+
except Exception:
|
|
394
|
+
if exc_type is None:
|
|
395
|
+
raise
|
|
396
|
+
return None
|
mlsurv/_loky_patch.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Wrapper for loky resource_tracker.main that suppresses KeyError tracebacks.
|
|
2
|
+
|
|
3
|
+
The loky resource tracker has a race condition on Windows where multiple
|
|
4
|
+
workers try to UNREGISTER the same temp folder, causing noisy KeyError
|
|
5
|
+
tracebacks on stderr. This wrapper installs a selective sys.excepthook
|
|
6
|
+
that suppresses only those specific errors.
|
|
7
|
+
|
|
8
|
+
Used by monkeypatching loky's resource_tracker.main reference at mlsurv
|
|
9
|
+
import time, so the tracker subprocess imports this wrapper instead of
|
|
10
|
+
the original.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# Save the ORIGINAL main before mlsurv.__init__ replaces it.
|
|
16
|
+
# This module is imported by __init__.py *before* the replacement happens,
|
|
17
|
+
# so _original_main always points to loky's real main function.
|
|
18
|
+
from joblib.externals.loky.backend.resource_tracker import (
|
|
19
|
+
main as _original_main,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(fd, verbose=False):
|
|
24
|
+
_orig_excepthook = sys.excepthook
|
|
25
|
+
|
|
26
|
+
def _quiet_excepthook(exc_type, exc_value, exc_tb):
|
|
27
|
+
if exc_type is KeyError and exc_tb is not None:
|
|
28
|
+
filename = getattr(exc_tb.tb_frame.f_code, "co_filename", "")
|
|
29
|
+
if "resource_tracker" in filename:
|
|
30
|
+
return
|
|
31
|
+
_orig_excepthook(exc_type, exc_value, exc_tb)
|
|
32
|
+
|
|
33
|
+
sys.excepthook = _quiet_excepthook
|
|
34
|
+
_original_main(fd, verbose)
|