SIENTIAPDE-1430: Introduce comprehensive integration testing with JSON-based scenarios and detailed README documentation. Enhance training workflow to support advanced model configurations, including polynomial regression with mandatory scaler validation. Ensure robust prediction handling by calculating training predictions (y_train_pred) before denormalization and automatically configuring datetime indices for time-series operations.

This commit is contained in:
Bruno Domingues
2025-12-18 17:05:10 -03:00
parent 6e8f87b2a3
commit 06fd08dc70
18 changed files with 631 additions and 37 deletions

View File

@@ -183,11 +183,11 @@ def test_train_model_result_is_dataclass(sample_params, sample_dataframes):
def test_train_model_result_field_count():
"""Test that TrainModelResult has exactly 19 fields."""
"""Test that TrainModelResult has exactly 20 fields."""
from dataclasses import fields
result_fields = fields(TrainModelResult)
assert len(result_fields) == 19
assert len(result_fields) == 20
field_names = {f.name for f in result_fields}
expected_fields = {
@@ -200,6 +200,7 @@ def test_train_model_result_field_count():
'regr',
'scaler_dict',
'y_pred',
'y_train_pred',
'mse_val',
'mae_val',
'r2_val',

View File

@@ -325,8 +325,14 @@ class TestAfterTrainCalculation:
y_train = pd.Series([100, 200, 300], index=[0, 1, 2], name='target')
y_test = pd.Series([400, 500], index=[3, 4], name='target')
# Mock predict to return a simple array
sample_linear_model.predict = MagicMock(return_value=np.array([450.0, 550.0]))
# Mock predict to return arrays with correct length based on input
def mock_predict(data):
if len(data) == 3: # x_train
return np.array([150.0, 250.0, 350.0])
else: # x_test
return np.array([450.0, 550.0])
sample_linear_model.predict = MagicMock(side_effect=mock_predict)
return TrainModelResult(
params=sample_params,
@@ -409,13 +415,24 @@ class TestAfterTrainCalculation:
y_train = pd.Series([100, 200, 300], index=[0, 1, 2], name='target')
y_test = pd.Series([400, 500], index=[3, 4], name='target')
# Mock predict to return a simple array
sample_linear_model.predict = MagicMock(return_value=np.array([450.0, 550.0]))
# Mock predict to return arrays with correct length based on input
def mock_predict(data):
if len(data) == 3: # x_train
return np.array([150.0, 250.0, 350.0])
else: # x_test
return np.array([450.0, 550.0])
sample_linear_model.predict = MagicMock(side_effect=mock_predict)
# Create mock scaler with denormalize methods
mock_scaler = MagicMock()
mock_scaler.denormalize_single_input = MagicMock(side_effect=lambda x, col: x * 2)
mock_scaler.denormalize_predictions = MagicMock(return_value=np.array([900.0, 1100.0]))
# denormalize_predictions needs to return correct length based on input
def mock_denormalize_predictions(arr, col):
return arr * 2
mock_scaler.denormalize_predictions = MagicMock(side_effect=mock_denormalize_predictions)
# Create mock preprocessor
mock_process_data = MagicMock()
@@ -453,8 +470,14 @@ class TestAfterTrainCalculation:
y_train = pd.Series([100, 200, 300], index=[0, 1, 2], name='target')
y_test = pd.Series([400, 500], index=[3, 4], name='target')
# Mock predict
sample_linear_model.predict = MagicMock(return_value=np.array([450.0, 550.0]))
# Mock predict to return arrays with correct length based on input
def mock_predict(data):
if len(data) == 3: # x_train
return np.array([150.0, 250.0, 350.0])
else: # x_test
return np.array([450.0, 550.0])
sample_linear_model.predict = MagicMock(side_effect=mock_predict)
# Create mock sklearn scaler (without denormalize methods)
mock_scaler = MagicMock()
@@ -506,6 +529,7 @@ class TestTrain:
"""
return BytesIO(csv_content.encode('utf-8'))
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_basic_workflow(
@@ -551,6 +575,7 @@ class TestTrain:
# Verify split was called
assert mock_split_train_test.called
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_with_scaler(
@@ -580,6 +605,7 @@ class TestTrain:
assert result is not None
assert result.scaler_dict is not None
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_with_shuffle_enabled(
@@ -610,6 +636,7 @@ class TestTrain:
call_kwargs = mock_split_train_test.call_args[1]
assert call_kwargs['shuffle'] is True
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_with_different_train_size(
@@ -640,6 +667,7 @@ class TestTrain:
call_kwargs = mock_split_train_test.call_args[1]
assert call_kwargs['train_size'] == 0.7
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_raises_on_empty_data_after_transform(
@@ -660,6 +688,7 @@ class TestTrain:
with pytest.raises(ValueError, match='Data view is empty after transformation'):
training_repo.train(sample_csv_data, sample_params)
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_logs_success(
@@ -696,6 +725,7 @@ class TestTrain:
'Model trained successfully' in str(call) for call in mock_logger.info.call_args_list
)
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_with_custom_separators(
@@ -726,6 +756,7 @@ class TestTrain:
# Verify load_data was called with custom separators
mock_load_data.assert_called_once_with(sample_csv_data, ';', ',')
@patch.object(TrainingRepository, '_configure_datetime_index', lambda self, df: df)
@patch('model_manager.utils.repository.training_repository.split_train_test')
@patch('model_manager.utils.repository.training_repository.load_data')
def test_train_result_contains_all_fields(