1- import os
2- import pickle
3- from abc import ABCMeta , abstractmethod
4- from typing import Callable , Final , List , Optional
1+ """What a run records, declared per strategy.
52
6- from kink import di
3+ These classes used to run the training too: ``execute`` took the run row and the
4+ database session and did the fitting, the search, the scoring, the aggregation
5+ and the persistence behind one method. That work is the units' now -- see
6+ ``FitModelUnit``, ``FitModelOverFoldsUnit`` and their siblings -- and what is
7+ left here is what was underneath it all along: a declaration of how a run is
8+ carved and which partitions it records a score for.
9+
10+ **They stay registered even though they no longer do anything.** The frontend
11+ reads them in four places, and only one is about metrics:
12+
13+ - the session wizard lists them so the user can choose one, and
14+ ``ModelSession.evaluation_strategy`` is NOT NULL, so without that listing a
15+ session cannot be created at all;
16+ - it starts on the first one whose ``kind`` is holdout;
17+ - ``kind`` decides the shape of the splits payload and which controls are shown;
18+ - ``scored_splits`` tells the metric charts which partitions exist to plot.
19+
20+ There is precedent for a class in this codebase that declares and does not
21+ execute: ``BaseSplitter.PARTITIONING`` and ``explainable_partitions`` are read
22+ the same way, by the backend and by the frontend, and nothing calls them to do
23+ work.
24+ """
25+
26+ from typing import Final
727
8- from DashAI .back .core .artifacts import normalize_artifacts
928from DashAI .back .core .enums .metrics import SplitEnum
10- from DashAI .back .dependencies .database .models import Run
11- from DashAI .back .models .base_model import BaseModel
12- from DashAI .back .models .model_factory import ModelFactory
13- from DashAI .back .optimizers .base_optimizer import BaseOptimizer
1429
1530
16- class BaseEvaluationStrategy ( metaclass = ABCMeta ) :
17- """Abstract base class defining the interface for model evaluation strategies .
31+ class BaseEvaluationStrategy :
32+ """How a run is carved, and what it records .
1833
19- Concrete implementations (e.g., CrossValidationEvaluationStrategy,
20- HoldoutEvaluationStrategy) inherit from this class and provide specific
21- strategies for model evaluation.
34+ Subclasses declare; none of them execute.
2235 """
2336
2437 TYPE : Final [str ] = "EvaluationStrategy"
2538
39+ #: Whether this strategy splits the dataset once or into folds. It decides
40+ #: which unit prepares the data, because the two publish different shapes,
41+ #: and which controls the session wizard offers.
2642 KIND : str = "holdout"
43+
44+ #: The partitions a run records a score for. Not the same as which
45+ #: partitions have metrics configured: a forecaster has training metrics
46+ #: and still must not be judged on the dates it was fitted on, because an
47+ #: in-sample fit statistic is not comparable with a forecast. A screen that
48+ #: offers one control per partition reads this instead of assuming all
49+ #: three exist.
2750 SCORED_SPLITS : tuple = (SplitEnum .TRAIN , SplitEnum .VALIDATION , SplitEnum .TEST )
2851
52+ #: The partition the kept model was fitted on, which is what decides which
53+ #: partitions of a finished run can still be predicted.
2954 FINAL_FIT_PARTITIONS : tuple = ("train" ,)
3055
3156 @classmethod
@@ -37,209 +62,9 @@ def get_metadata(cls) -> dict:
3762 dict
3863 Mapping with ``kind``, which says whether this strategy splits the
3964 dataset once or into folds, and ``scored_splits``, the partitions
40- it writes metrics for. A screen that offers one control per
41- partition reads the latter instead of assuming all three exist:
42- a forecasting strategy scores no training partition, so asking it
43- for train metrics finds nothing.
65+ it writes metrics for.
4466 """
4567 return {
4668 "kind" : cls .KIND ,
4769 "scored_splits" : [split .value for split in cls .SCORED_SPLITS ],
4870 }
49-
50- def __init__ (
51- self ,
52- factory : ModelFactory ,
53- optimizer : BaseOptimizer ,
54- goal_metric ,
55- ** kwargs ,
56- ):
57- """Initialize the evaluation strategy with model and optimization configuration.
58-
59- Parameters
60- ----------
61- factory : ModelFactory
62- Factory owning the model to be trained/evaluated and the
63- hyperparameters that are eligible for optimization.
64- optimizer : BaseOptimizer
65- The hyperparameter optimizer instance. Can be None if no HPO is needed.
66- goal_metric : dict (obtained from Metric component registry)
67- The target metric to optimize during hyperparameter search.
68- **kwargs
69- Additional keyword arguments passed from subclasses (ignored).
70- """
71- self .factory : ModelFactory = factory
72- self .model : BaseModel = factory .model
73- self .run_optimizable_parameters = factory .optimizable_parameters
74- self .optimizer : BaseOptimizer = optimizer
75- self .goal_metric = goal_metric
76- self ._progress_reporter : Optional [
77- Callable [[Optional [float ], Optional [str ]], None ]
78- ] = None
79-
80- def set_progress_reporter (
81- self ,
82- progress_reporter : Optional [Callable [[Optional [float ], Optional [str ]], None ]],
83- ) -> None :
84- """Register a callback that will receive progress updates."""
85- self ._progress_reporter = progress_reporter
86-
87- def _report_progress (
88- self , fraction : Optional [float ], message : Optional [str ] = None
89- ):
90- """Emit progress updates when a reporter has been registered."""
91- if self ._progress_reporter is not None :
92- self ._progress_reporter (fraction , message )
93-
94- @abstractmethod
95- def execute (self , x , y , run : Run , db ):
96- """Execute the evaluation strategy on the provided data.
97-
98- This is the main entry point for the evaluation process. Subclasses implement
99- strategy-specific logic for:
100- - Model training across folds/splits
101- - Metric computation and persistence
102- - HPO execution and result handling
103-
104- Parameters
105- ----------
106- x : DatasetDict or list of DastasetDict
107- Input features. Structure depends on the evaluation strategy:
108- - For holdout: DatasetDict with train/validation/test splits
109- - For CV: List of DatasetDicts, one per fold with train/test splits
110- y : dict or list
111- Target labels. Same structure as x.
112- run : Run
113- Database model representing the current experiment run.
114- db : Session
115- SQLAlchemy database session for persisting results.
116-
117- Returns
118- -------
119- tuple
120- (trained_model, plot_paths) where:
121- - trained_model : BaseModel - The trained model after evaluation
122- - plot_paths : list[str] - Paths to generated HPO visualization files
123- """
124- raise NotImplementedError ("Subclasses must implement this method" )
125-
126- @abstractmethod
127- def evaluate (self , model : BaseModel , x , y , metric ):
128- """Evaluate the model on the given data and return the score.
129-
130- This method is called during hyperparameter optimization to compute
131- the objective function value for a given set of hyperparameters.
132- Different strategies may compute metrics differently (e.g., across CV folds
133- or on a validation split).
134-
135- Parameters
136- ----------
137- model : BaseModel
138- The model instance to evaluate.
139- x : DatasetDict or list of DastasetDict
140- Input features for evaluation (structure depends on strategy).
141- y : DatasetDict or list of DastasetDict
142- Target labels for evaluation (structure depends on strategy).
143- metric : Metric
144- The metric instance to compute.
145-
146- Returns
147- -------
148- float
149- The computed metric value used as the optimization objective.
150- """
151- raise NotImplementedError ("Subclasses must implement this method" )
152-
153- def _do_hpo (self , model : BaseModel , x , y , run : Run , db ):
154- """Execute hyperparameter optimization using the configured optimizer.
155-
156- The optimizer uses the self.evaluate method as the objective function,
157- allowing each strategy to define its own evaluation logic.
158-
159- Parameters
160- ----------
161- model : BaseModel
162- The model instance to optimize.
163- x : DatasetDict or list of DatasetDict
164- Training input features (structure varies by strategy).
165- y : DatasetDict or list of DatasetDict
166- Training target labels (structure varies by strategy).
167- run : Run
168- Database run instance to update with optimized parameters.
169- db : Session
170- SQLAlchemy database session for transactions.
171-
172- Returns
173- -------
174- BaseModel
175- The model with the best hyperparameters found during optimization.
176- """
177- from sqlalchemy .orm .attributes import flag_modified
178-
179- # Execute hyperparameter optimization and get best model with parameters
180- self .optimizer .optimize (
181- model ,
182- x ,
183- y ,
184- self .run_optimizable_parameters ,
185- self .goal_metric ,
186- strategy = self .evaluate ,
187- )
188- model = self .optimizer .get_model ()
189- best_params = self .optimizer .get_best_params ()
190-
191- # Update the run's parameters with the optimized hyperparameters
192- old_parameters = run .parameters .copy ()
193- updated_parameters = self .factory .update_parameters (old_parameters , best_params )
194-
195- # Persist the updated parameters to the database
196- run .parameters = updated_parameters
197- flag_modified (run , "parameters" )
198- db .commit ()
199-
200- return model
201-
202- def _generate_hpo_plots (self , run : Run ) -> List [str ]:
203- """Generate and pickle the hyperparameter optimization plots to disk.
204-
205- Shared by every evaluation strategy that runs HPO, so the plot
206- generation logic only needs to be maintained in one place.
207-
208- Parameters
209- ----------
210- run : Run
211- The run the plots belong to (used for the plot filenames).
212-
213- Returns
214- -------
215- list[str]
216- Paths to the pickled plot files, in the order produced by the
217- optimizer.
218- """
219- config = di ["config" ]
220- plot_paths : List [str ] = []
221-
222- # Retrieve optimization trial data from the optimizer
223- trials = self .optimizer .get_trials_values ()
224-
225- # Generate plot visualizations from the trial data
226- # Plots typically show parameter importance, optimization history, etc.
227- plot_filenames , plots = self .optimizer .create_plots (
228- trials ,
229- run .id ,
230- n_params = len (self .run_optimizable_parameters ),
231- goal_metric = self .goal_metric ,
232- )
233-
234- # Convert plots to serializable format (handles special objects, arrays, etc.)
235- normalized_plots = normalize_artifacts (plots )
236-
237- # Serialize and persist each plot to disk
238- for filename , plot in zip (plot_filenames , normalized_plots , strict = False ):
239- plot_path = os .path .join (config ["RUNS_PATH" ], filename )
240- # Serialize the plot object using pickle and write to disk
241- with open (plot_path , "wb" ) as file :
242- pickle .dump (plot , file )
243- plot_paths .append (plot_path )
244-
245- return plot_paths
0 commit comments