SIENTIAPDE-1430: Implement advanced model training capabilities and enhanced data preprocessing. This includes support for Polynomial Regression with configurable degree and interaction terms, flexible per-variable lag configurations, and new data filtering options by date range and removed intervals. Comprehensive business validations are now enforced for all parameters, and MLflow logging has been extended to capture these detailed configurations. Additionally, Reduced Coulomb Energy (RCE) metrics are added for drift detection, with a new changelog documenting all pipeline parameter updates.

This commit is contained in:
2025-12-17 21:39:43 -03:00
parent 4d6674758c
commit 6e8f87b2a3
10 changed files with 796 additions and 81 deletions

View File

@@ -81,13 +81,15 @@ def test_linear_regression_model_fit():
def test_linear_regression_model_fit_without_variable_columns():
"""Test LinearRegressionModel fit raises AssertionError without variable_columns."""
"""Test LinearRegressionModel fit infers variable_columns when not set."""
model = LinearRegressionModel(target_variable='target')
data = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
with raises(AssertionError, match='variable_columns must be set before fitting'):
model.fit(data)
# Model should infer variable_columns from data (all columns except target)
result = model.fit(data)
assert result is model
assert model.variable_columns == ['var1']
def test_linear_regression_model_predict_without_clipping():
@@ -196,7 +198,7 @@ def test_data_preprocessor_init_with_custom_steps_order():
assert 'Normalization' in preprocessor.steps_order
assert 'Feature Creation' in preprocessor.steps_order
assert len(preprocessor.steps_order) == 7
assert len(preprocessor.steps_order) == 8 # Now includes RANGE_SELECTION step
def test_data_preprocessor_get_scaler():

View File

@@ -8,8 +8,8 @@ def valid_train_params_dict():
"""Create a valid dictionary for TrainModelParams."""
return {
'variable_columns': ['var1', 'var2'],
'lag_train': 5,
'lag_val': 3,
'lag_train': {'var1': 5, 'var2': 5},
'lag_val': {'var1': 3, 'var2': 3},
'target_variable': 'target',
'rem_static_win': True,
'low_lim': {'var1': 0.0, 'var2': 1.0},
@@ -26,6 +26,14 @@ def valid_train_params_dict():
'experiment_run_id': 1,
'experiment_name': 'test_experiment',
'removed_intervals': [],
'model_name': 'Linear Regression',
'degree': 1,
'interaction_only': False,
'nan_treatment': 'drop',
'start_date': None,
'end_date': None,
'scaler_name': 'Standard Scaler',
'support_filters': {},
}
@@ -36,8 +44,8 @@ def test_train_model_params_from_dict_success(valid_train_params_dict):
params = TrainModelParams.from_dict(valid_train_params_dict)
assert params.variable_columns == ['var1', 'var2']
assert params.lag_train == 5
assert params.lag_val == 3
assert params.lag_train == {'var1': 5, 'var2': 5}
assert params.lag_val == {'var1': 3, 'var2': 3}
assert params.target_variable == 'target'
assert params.rem_static_win is True
assert params.low_lim == {'var1': 0.0, 'var2': 1.0}
@@ -118,9 +126,9 @@ def test_train_model_params_from_dict_wrong_type(valid_train_params_dict):
"""Test from_dict raises TypeError when field has wrong type."""
from model_manager.utils.models.train_model_params import TrainModelParams
valid_train_params_dict['lag_train'] = 'not_an_int'
valid_train_params_dict['lag_train'] = 'not_a_dict'
with pytest.raises(TypeError, match='lag_train must be of type int, but got str'):
with pytest.raises(TypeError, match='lag_train must be of type dict, but got str'):
TrainModelParams.from_dict(valid_train_params_dict)
@@ -179,10 +187,10 @@ def test_validate_business_rules_negative_lag_train(valid_train_params_dict):
"""Test validate_business_rules raises error when lag_train is negative."""
from model_manager.utils.models.train_model_params import TrainModelParams
valid_train_params_dict['lag_train'] = -1
valid_train_params_dict['lag_train'] = {'var1': -1, 'var2': 5}
params = TrainModelParams.from_dict(valid_train_params_dict)
with pytest.raises(ValueError, match='lag_train must be positive, got -1'):
with pytest.raises(ValueError, match='lag_train for var1 must be non-negative, got -1'):
params.validate_business_rules()
@@ -190,10 +198,10 @@ def test_validate_business_rules_negative_lag_val(valid_train_params_dict):
"""Test validate_business_rules raises error when lag_val is negative."""
from model_manager.utils.models.train_model_params import TrainModelParams
valid_train_params_dict['lag_val'] = -2
valid_train_params_dict['lag_val'] = {'var1': 3, 'var2': -2}
params = TrainModelParams.from_dict(valid_train_params_dict)
with pytest.raises(ValueError, match='lag_val must be positive, got -2'):
with pytest.raises(ValueError, match='lag_val for var2 must be non-negative, got -2'):
params.validate_business_rules()
@@ -204,7 +212,7 @@ def test_validate_business_rules_negative_window(valid_train_params_dict):
valid_train_params_dict['window'] = -5
params = TrainModelParams.from_dict(valid_train_params_dict)
with pytest.raises(ValueError, match='window must be positive, got -5'):
with pytest.raises(ValueError, match='window must be non-negative, got -5'):
params.validate_business_rules()
@@ -334,7 +342,7 @@ def test_validate_business_rules_zero_lag_train(valid_train_params_dict):
"""Test validate_business_rules accepts lag_train = 0."""
from model_manager.utils.models.train_model_params import TrainModelParams
valid_train_params_dict['lag_train'] = 0
valid_train_params_dict['lag_train'] = {'var1': 0, 'var2': 0}
params = TrainModelParams.from_dict(valid_train_params_dict)
params.validate_business_rules() # Should not raise
@@ -344,7 +352,7 @@ def test_validate_business_rules_zero_lag_val(valid_train_params_dict):
"""Test validate_business_rules accepts lag_val = 0."""
from model_manager.utils.models.train_model_params import TrainModelParams
valid_train_params_dict['lag_val'] = 0
valid_train_params_dict['lag_val'] = {'var1': 0, 'var2': 0}
params = TrainModelParams.from_dict(valid_train_params_dict)
params.validate_business_rules() # Should not raise

View File

@@ -14,8 +14,8 @@ def sample_params():
"""Create sample TrainModelParams for testing."""
return TrainModelParams(
variable_columns=['var1', 'var2'],
lag_train=5,
lag_val=3,
lag_train={'var1': 5, 'var2': 5},
lag_val={'var1': 3, 'var2': 3},
target_variable='target',
rem_static_win=True,
low_lim={'var1': 0.0, 'var2': 0.0},
@@ -32,6 +32,14 @@ def sample_params():
experiment_run_id=123,
experiment_name='test-experiment',
removed_intervals=[],
model_name='Linear Regression',
degree=1,
interaction_only=False,
nan_treatment='drop',
start_date=None,
end_date=None,
scaler_name='Standard Scaler',
support_filters={},
)

View File

@@ -33,8 +33,8 @@ def sample_params():
experiment_name='test_experiment',
target_variable='target',
variable_columns=['var1', 'var2', 'var3'],
lag_train=0,
lag_val=0,
lag_train={'var1': 0, 'var2': 0, 'var3': 0},
lag_val={'var1': 0, 'var2': 0, 'var3': 0},
rem_static_win=False,
low_lim={},
upp_lim={},
@@ -48,6 +48,14 @@ def sample_params():
line_separator=',',
decimal_separator='.',
removed_intervals=[],
model_name='Linear Regression',
degree=1,
interaction_only=False,
nan_treatment='drop',
start_date=None,
end_date=None,
scaler_name='None',
support_filters={},
)
@@ -106,8 +114,8 @@ class TestExtractModelEquation:
experiment_name='test',
target_variable='y',
variable_columns=['x'],
lag_train=0,
lag_val=0,
lag_train={'x': 0},
lag_val={'x': 0},
rem_static_win=False,
low_lim={},
upp_lim={},
@@ -121,6 +129,14 @@ class TestExtractModelEquation:
line_separator=',',
decimal_separator='.',
removed_intervals=[],
model_name='Linear Regression',
degree=1,
interaction_only=False,
nan_treatment='drop',
start_date=None,
end_date=None,
scaler_name='None',
support_filters={},
)
# Mock model with single coefficient
@@ -177,6 +193,7 @@ class TestInitDataPreprocessor:
def test_init_preprocessor_with_scaler(self, training_repo, sample_params):
"""Test preprocessor initialization with scaler enabled."""
sample_params.use_scaler = True
sample_params.scaler_name = 'Standard Scaler'
preprocessor = training_repo._init_data_preprocessor(sample_params)
assert preprocessor.scaler_name == 'Standard Scaler'
@@ -184,6 +201,7 @@ class TestInitDataPreprocessor:
def test_init_preprocessor_without_scaler(self, training_repo, sample_params):
"""Test preprocessor initialization without scaler."""
sample_params.use_scaler = False
sample_params.scaler_name = 'None'
preprocessor = training_repo._init_data_preprocessor(sample_params)
assert preprocessor.scaler_name == 'None'
@@ -211,14 +229,13 @@ class TestInitDataPreprocessor:
def test_init_preprocessor_lag_configuration(self, training_repo, sample_params):
"""Test preprocessor lag configuration."""
sample_params.lag_train = 5
sample_params.lag_val = 3
sample_params.lag_train = {'var1': 5, 'var2': 5, 'var3': 5}
sample_params.lag_val = {'var1': 3, 'var2': 3, 'var3': 3}
preprocessor = training_repo._init_data_preprocessor(sample_params)
# Check that lag dictionaries are created correctly
for col in sample_params.variable_columns:
assert preprocessor.lag_train[col] == 5
assert preprocessor.lag_transform[col] == 3
# Check that lag dictionaries are passed correctly
assert preprocessor.lag_train == {'var1': 5, 'var2': 5, 'var3': 5}
assert preprocessor.lag_transform == {'var1': 3, 'var2': 3, 'var3': 3}
class TestInitScalerDict: