1075 lines
37 KiB
Python
1075 lines
37 KiB
Python
"""Unit tests for sientia models module."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from pytest import raises
|
|
|
|
from model_manager.sientia.models import (
|
|
DataPreprocessor,
|
|
LinearRegressionModel,
|
|
_frontend_date_format_to_strftime,
|
|
)
|
|
|
|
|
|
class TestFrontendDateFormatToStrftime:
|
|
"""Tests for _frontend_date_format_to_strftime (models module)."""
|
|
|
|
def test_none_or_empty_returns_none_or_empty(self):
|
|
"""None or empty string returns None or falsy."""
|
|
assert _frontend_date_format_to_strftime(None) is None
|
|
assert _frontend_date_format_to_strftime('') is None
|
|
|
|
def test_dd_mm_yyyy_hh_mm_ss(self):
|
|
"""Converts dd/MM/yyyy HH:mm:ss to strftime."""
|
|
result = _frontend_date_format_to_strftime('dd/MM/yyyy HH:mm:ss')
|
|
assert result == '%d/%m/%Y %H:%M:%S'
|
|
|
|
def test_yyyy_mm_dd(self):
|
|
"""Converts yyyy-MM-dd to strftime."""
|
|
result = _frontend_date_format_to_strftime('yyyy-MM-dd')
|
|
assert result == '%Y-%m-%d'
|
|
|
|
def test_iso_datetime(self):
|
|
"""Converts yyyy-MM-dd HH:mm:ss to strftime."""
|
|
result = _frontend_date_format_to_strftime('yyyy-MM-dd HH:mm:ss')
|
|
assert result == '%Y-%m-%d %H:%M:%S'
|
|
|
|
|
|
class _IterableWithContains:
|
|
def __init__(self, iterable, contains_values):
|
|
self._iterable = iterable
|
|
self._contains = set(contains_values)
|
|
|
|
def __iter__(self):
|
|
return iter(self._iterable)
|
|
|
|
def __contains__(self, item):
|
|
return item in self._contains
|
|
|
|
|
|
# LinearRegressionModel Tests
|
|
|
|
|
|
def test_linear_regression_model_init_default():
|
|
"""Test LinearRegressionModel initialization with default parameters."""
|
|
model = LinearRegressionModel()
|
|
|
|
assert model.target_variable == ''
|
|
assert model.variable_columns is None
|
|
assert model.model_params is None
|
|
assert model.clipping is None
|
|
assert model.weights is None
|
|
assert model.q1_target is None
|
|
assert model.q3_target is None
|
|
|
|
|
|
def test_linear_regression_model_init_with_params():
|
|
"""Test LinearRegressionModel initialization with parameters."""
|
|
target = 'target'
|
|
variables = ['var1', 'var2']
|
|
params = {'fit_intercept': True}
|
|
clipping = {'min': 0, 'max': 100}
|
|
weights = {'var1': 0.5, 'var2': 0.3}
|
|
|
|
model = LinearRegressionModel(
|
|
target_variable=target,
|
|
variable_columns=variables,
|
|
model_params=params,
|
|
clipping=clipping,
|
|
weights=weights,
|
|
)
|
|
|
|
assert model.target_variable == target
|
|
assert model.variable_columns == variables
|
|
assert model.model_params == params
|
|
assert model.clipping == clipping
|
|
assert model.weights == weights
|
|
|
|
|
|
def test_linear_regression_model_fit():
|
|
"""Test LinearRegressionModel fit method."""
|
|
model = LinearRegressionModel(target_variable='target', variable_columns=['var1', 'var2'])
|
|
|
|
data = pd.DataFrame(
|
|
{'var1': [1, 2, 3, 4, 5], 'var2': [2, 3, 4, 5, 6], 'target': [3, 5, 7, 9, 11]}
|
|
)
|
|
|
|
result = model.fit(data)
|
|
|
|
assert result is model
|
|
assert model.q1_target is not None
|
|
assert model.q3_target is not None
|
|
assert model.weights is not None
|
|
assert 'Bias' in model.weights
|
|
|
|
|
|
def test_linear_regression_model_fit_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]})
|
|
|
|
# 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():
|
|
"""Test LinearRegressionModel predict without clipping."""
|
|
model = LinearRegressionModel(target_variable='target', variable_columns=['var1', 'var2'])
|
|
|
|
train_data = pd.DataFrame(
|
|
{'var1': [1, 2, 3, 4, 5], 'var2': [2, 3, 4, 5, 6], 'target': [3, 5, 7, 9, 11]}
|
|
)
|
|
model.fit(train_data)
|
|
|
|
test_data = pd.DataFrame({'var1': [6, 7], 'var2': [7, 8]})
|
|
predictions = model.predict(test_data)
|
|
|
|
assert isinstance(predictions, np.ndarray)
|
|
assert len(predictions) == 2
|
|
|
|
|
|
def test_linear_regression_model_predict_with_clipping_max():
|
|
"""Test LinearRegressionModel predict with clipping max."""
|
|
model = LinearRegressionModel(
|
|
target_variable='target', variable_columns=['var1'], clipping={'min': 0, 'max': 5}
|
|
)
|
|
|
|
train_data = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [1, 2, 3, 4, 5]})
|
|
model.fit(train_data)
|
|
|
|
test_data = pd.DataFrame({'var1': [10]})
|
|
predictions = model.predict(test_data)
|
|
|
|
assert predictions[0] == model.q3_target
|
|
|
|
|
|
def test_linear_regression_model_predict_with_clipping_min():
|
|
"""Test LinearRegressionModel predict with clipping min."""
|
|
model = LinearRegressionModel(
|
|
target_variable='target', variable_columns=['var1'], clipping={'min': 0, 'max': 10}
|
|
)
|
|
|
|
train_data = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [1, 2, 3, 4, 5]})
|
|
model.fit(train_data)
|
|
|
|
test_data = pd.DataFrame({'var1': [-10]})
|
|
predictions = model.predict(test_data)
|
|
|
|
assert predictions[0] == model.q1_target
|
|
|
|
|
|
def test_linear_regression_model_predict_with_clipping_within_range():
|
|
"""Test LinearRegressionModel predict with clipping but value within range."""
|
|
model = LinearRegressionModel(
|
|
target_variable='target', variable_columns=['var1'], clipping={'min': 0, 'max': 10}
|
|
)
|
|
|
|
train_data = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [1, 2, 3, 4, 5]})
|
|
model.fit(train_data)
|
|
|
|
test_data = pd.DataFrame({'var1': [3]})
|
|
predictions = model.predict(test_data)
|
|
|
|
# Prediction should be within range and not clipped
|
|
assert 0 <= predictions[0] <= 10
|
|
|
|
|
|
# DataPreprocessor Tests
|
|
|
|
|
|
def test_data_preprocessor_init_default():
|
|
"""Test DataPreprocessor initialization with default parameters."""
|
|
preprocessor = DataPreprocessor()
|
|
|
|
assert preprocessor.date_column == ''
|
|
assert preprocessor.target_variable == ''
|
|
assert preprocessor.input_columns is None
|
|
assert preprocessor.nan_treatment is None
|
|
assert preprocessor.lag_train == {}
|
|
assert preprocessor.lag_transform == {}
|
|
assert preprocessor.scaler is None
|
|
|
|
|
|
def test_data_preprocessor_init_with_standard_scaler():
|
|
"""Test DataPreprocessor initialization with Standard Scaler."""
|
|
preprocessor = DataPreprocessor(scaler_name='Standard Scaler')
|
|
|
|
assert preprocessor.scaler is not None
|
|
|
|
|
|
def test_data_preprocessor_init_with_none_scaler():
|
|
"""Test DataPreprocessor initialization with None scaler."""
|
|
preprocessor = DataPreprocessor(scaler_name='None')
|
|
|
|
assert preprocessor.scaler is None
|
|
|
|
|
|
def test_data_preprocessor_init_with_unknown_scaler():
|
|
"""Test DataPreprocessor initialization with unknown scaler."""
|
|
preprocessor = DataPreprocessor(scaler_name='Unknown')
|
|
|
|
assert preprocessor.scaler is None
|
|
|
|
|
|
def test_data_preprocessor_init_with_custom_steps_order():
|
|
"""Test DataPreprocessor initialization with custom steps order."""
|
|
custom_steps = ['Normalization', 'Feature Creation']
|
|
preprocessor = DataPreprocessor(steps_order=custom_steps)
|
|
|
|
assert 'Normalization' in preprocessor.steps_order
|
|
assert 'Feature Creation' in preprocessor.steps_order
|
|
assert len(preprocessor.steps_order) == 8 # Now includes RANGE_SELECTION step
|
|
|
|
|
|
def test_data_preprocessor_get_scaler():
|
|
"""Test DataPreprocessor get_scaler method."""
|
|
preprocessor = DataPreprocessor(scaler_name='Standard Scaler')
|
|
|
|
scaler = preprocessor.get_scaler()
|
|
|
|
assert scaler is not None
|
|
|
|
|
|
@patch('model_manager.sientia.models.treat_nan')
|
|
def test_data_preprocessor_treat_discontinuities_with_treatment(mock_treat_nan):
|
|
"""Test treat_discontinuities with nan_treatment."""
|
|
preprocessor = DataPreprocessor(nan_treatment='drop')
|
|
data = pd.DataFrame({'col1': [1, 2, np.nan]})
|
|
expected_data = pd.DataFrame({'col1': [1, 2]})
|
|
mock_treat_nan.return_value = expected_data
|
|
|
|
result = preprocessor.treat_discontinuities(data)
|
|
|
|
mock_treat_nan.assert_called_once_with(data, 'drop')
|
|
pd.testing.assert_frame_equal(result, expected_data)
|
|
|
|
|
|
def test_data_preprocessor_treat_discontinuities_without_treatment():
|
|
"""Test treat_discontinuities without nan_treatment."""
|
|
preprocessor = DataPreprocessor()
|
|
data = pd.DataFrame({'col1': [1, 2, 3]})
|
|
|
|
result = preprocessor.treat_discontinuities(data)
|
|
|
|
pd.testing.assert_frame_equal(result, data)
|
|
|
|
|
|
def test_data_preprocessor_lag_selection_with_lag():
|
|
"""Test lag_selection with lag."""
|
|
preprocessor = DataPreprocessor()
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5]})
|
|
lag_dict = {'var1': 1}
|
|
|
|
result = preprocessor.lag_selection(data, lag_dict)
|
|
|
|
assert len(result) == 4
|
|
assert result['var1'].iloc[0] == 1
|
|
|
|
|
|
def test_data_preprocessor_lag_selection_without_lag():
|
|
"""Test lag_selection without lag."""
|
|
preprocessor = DataPreprocessor()
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5]})
|
|
lag_dict = {}
|
|
|
|
result = preprocessor.lag_selection(data, lag_dict)
|
|
|
|
assert len(result) == 5
|
|
|
|
|
|
def test_data_preprocessor_lag_selection_with_zero_lag():
|
|
"""Test lag_selection with zero lag."""
|
|
preprocessor = DataPreprocessor()
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5]})
|
|
lag_dict = {'var1': 0}
|
|
|
|
result = preprocessor.lag_selection(data, lag_dict)
|
|
|
|
assert len(result) == 5
|
|
|
|
|
|
@patch('model_manager.sientia.models.TimeSeriesDiscontinuityAnalyzer')
|
|
def test_data_preprocessor_treat_static_windows(mock_analyzer_class):
|
|
"""Test treat_static_windows method."""
|
|
preprocessor = DataPreprocessor(static_threshold=3)
|
|
data = pd.DataFrame({'col1': [1, 1, 1, 2, 3]})
|
|
|
|
mock_analyzer = MagicMock()
|
|
mock_analyzer_class.return_value = mock_analyzer
|
|
mock_analyzer.get_treated_data.return_value = data
|
|
|
|
preprocessor.treat_static_windows(data)
|
|
|
|
mock_analyzer.infer_frequency.assert_called_once()
|
|
assert mock_analyzer.identify_static_windows.called
|
|
assert mock_analyzer.treat_static_windows.called
|
|
|
|
|
|
def test_data_preprocessor_treat_static_windows_without_threshold():
|
|
"""Test treat_static_windows without threshold."""
|
|
preprocessor = DataPreprocessor()
|
|
data = pd.DataFrame({'col1': [1, 2, 3]})
|
|
|
|
result = preprocessor.treat_static_windows(data)
|
|
|
|
pd.testing.assert_frame_equal(result, data)
|
|
|
|
|
|
@patch('model_manager.sientia.models.limit_dataset')
|
|
def test_data_preprocessor_adjust_limits(mock_limit_dataset):
|
|
"""Test adjust_limits method."""
|
|
preprocessor = DataPreprocessor(low_lim={'col1': 0}, upp_lim={'col1': 10})
|
|
data = pd.DataFrame({'col1': [1, 2, 3]})
|
|
expected_data = pd.DataFrame({'col1': [1, 2, 3]})
|
|
mock_limit_dataset.return_value = (expected_data, {'col1': 0}, {'col1': 10})
|
|
|
|
result = preprocessor.adjust_limits(data)
|
|
|
|
mock_limit_dataset.assert_called_once()
|
|
pd.testing.assert_frame_equal(result, expected_data)
|
|
|
|
|
|
@patch('model_manager.sientia.models.create_features')
|
|
def test_data_preprocessor_create_features(mock_create_features):
|
|
"""Test create_features method."""
|
|
preprocessor = DataPreprocessor(
|
|
self_operations=['{var1}_{pow}_{2}'], cross_operations=['{var1}_{*}_{var2}']
|
|
)
|
|
data = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4]})
|
|
expected_data = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'var1_pow_2': [1, 4, 9]})
|
|
mock_create_features.return_value = expected_data
|
|
|
|
result = preprocessor.create_features(data)
|
|
|
|
mock_create_features.assert_called_once()
|
|
pd.testing.assert_frame_equal(result, expected_data)
|
|
|
|
|
|
def test_data_preprocessor_create_ar():
|
|
"""Test create_ar method."""
|
|
preprocessor = DataPreprocessor(target_variable='target', ar_var='ar_target')
|
|
data = pd.DataFrame({'target': [1, 2, 3, 4, 5]})
|
|
|
|
result = preprocessor.create_ar(data)
|
|
|
|
assert 'ar_target' in result.columns
|
|
assert len(result) == 4
|
|
|
|
|
|
def test_data_preprocessor_create_ar_without_ar_var():
|
|
"""Test create_ar without ar_var."""
|
|
preprocessor = DataPreprocessor(target_variable='target')
|
|
data = pd.DataFrame({'target': [1, 2, 3, 4, 5]})
|
|
|
|
result = preprocessor.create_ar(data)
|
|
|
|
assert len(result) == 5
|
|
|
|
|
|
def test_data_preprocessor_create_lags():
|
|
"""Test create_lags method."""
|
|
preprocessor = DataPreprocessor(created_lags={'var1': 1})
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5]})
|
|
|
|
result = preprocessor.create_lags(data)
|
|
|
|
assert 'var1_lag1' in result.columns
|
|
assert len(result) == 4
|
|
|
|
|
|
def test_data_preprocessor_create_lags_with_zero_lag():
|
|
"""Test create_lags with zero lag."""
|
|
preprocessor = DataPreprocessor(created_lags={'var1': 0})
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5]})
|
|
|
|
result = preprocessor.create_lags(data)
|
|
|
|
assert 'var1_lag0' not in result.columns
|
|
assert len(result) == 5
|
|
|
|
|
|
def test_data_preprocessor_create_lags_without_created_lags():
|
|
"""Test create_lags without created_lags."""
|
|
preprocessor = DataPreprocessor()
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5]})
|
|
|
|
result = preprocessor.create_lags(data)
|
|
|
|
assert len(result) == 5
|
|
|
|
|
|
def test_data_preprocessor_create_lags_with_missing_column():
|
|
"""Test create_lags with missing column."""
|
|
preprocessor = DataPreprocessor(created_lags={'var2': 1})
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5]})
|
|
|
|
result = preprocessor.create_lags(data)
|
|
|
|
assert 'var2_lag1' not in result.columns
|
|
|
|
|
|
def test_data_preprocessor_fit_with_x_and_y():
|
|
"""Test fit method with x and y."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4]})
|
|
y = pd.Series([3, 5, 7], name='target')
|
|
|
|
result = preprocessor.fit(x, y)
|
|
|
|
assert result is preprocessor
|
|
|
|
|
|
def test_data_preprocessor_fit_with_only_x():
|
|
"""Test fit method with only x."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'target': [3, 5, 7]})
|
|
|
|
result = preprocessor.fit(x)
|
|
|
|
assert result is preprocessor
|
|
|
|
|
|
def test_data_preprocessor_fit_without_data():
|
|
"""Test fit method without data."""
|
|
preprocessor = DataPreprocessor(target_variable='target', input_columns=['var1'])
|
|
|
|
with raises(ValueError, match='No data was provided'):
|
|
preprocessor.fit(None, None)
|
|
|
|
|
|
def test_data_preprocessor_fit_without_input_columns():
|
|
"""Test fit method without input_columns."""
|
|
preprocessor = DataPreprocessor(target_variable='target')
|
|
x = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
|
|
with raises(AssertionError, match='input_columns must be set'):
|
|
preprocessor.fit(x)
|
|
|
|
|
|
def test_data_preprocessor_fit_with_normalization():
|
|
"""Test fit method with normalization."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
scaler_name='Standard Scaler',
|
|
scaler_params={},
|
|
steps_order=['Normalization'],
|
|
)
|
|
x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'target': [3, 5, 7]})
|
|
|
|
result = preprocessor.fit(x)
|
|
|
|
assert result is preprocessor
|
|
assert preprocessor.scaler_params is not None
|
|
|
|
|
|
def test_data_preprocessor_transform_with_timestamp():
|
|
"""Test transform method with timestamp column."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target', input_columns=['var1'], steps_order=['Discontinuity Treatment']
|
|
)
|
|
x = pd.DataFrame({'timestamp': [1, 2, 3], 'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
|
|
result = preprocessor.transform(x)
|
|
|
|
assert 'timestamp' not in result.columns
|
|
|
|
|
|
def test_data_preprocessor_transform_without_timestamp():
|
|
"""Test transform method without timestamp column."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target', input_columns=['var1'], steps_order=['Discontinuity Treatment']
|
|
)
|
|
x = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
|
|
result = preprocessor.transform(x)
|
|
|
|
assert 'var1' in result.columns
|
|
|
|
|
|
def test_data_preprocessor_transform_without_input_columns():
|
|
"""Test transform method without input_columns."""
|
|
preprocessor = DataPreprocessor(target_variable='target')
|
|
x = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
|
|
with raises(AssertionError, match='input_columns must be set'):
|
|
preprocessor.transform(x)
|
|
|
|
|
|
def test_data_preprocessor_transform_with_feature_creation():
|
|
"""Test transform method with feature creation."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1'],
|
|
self_operations=['{var1}_{pow}_{2}'],
|
|
steps_order=['Feature Creation'],
|
|
)
|
|
x = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
|
|
with patch('model_manager.sientia.models.create_features') as mock_create:
|
|
mock_create.return_value = x
|
|
preprocessor.transform(x)
|
|
mock_create.assert_called_once()
|
|
|
|
|
|
def test_data_preprocessor_transform_with_lag_creation():
|
|
"""Test transform method with lag creation."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'ar_target'],
|
|
ar_var='ar_target',
|
|
created_lags={'var1': 1},
|
|
steps_order=['Lag Creation'],
|
|
)
|
|
x = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [3, 5, 7, 9, 11]})
|
|
|
|
result = preprocessor.transform(x)
|
|
|
|
assert 'ar_target' in result.columns
|
|
assert 'var1_lag1' in result.columns
|
|
|
|
|
|
def test_data_preprocessor_transform_with_normalization():
|
|
"""Test transform method with normalization."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1'],
|
|
scaler_name='Standard Scaler',
|
|
scaler_params={},
|
|
steps_order=['Normalization'],
|
|
)
|
|
train_x = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
preprocessor.fit(train_x)
|
|
|
|
test_x = pd.DataFrame({'var1': [4, 5, 6], 'target': [9, 11, 13]})
|
|
result = preprocessor.transform(test_x)
|
|
|
|
assert 'var1' in result.columns
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_with_self_operations():
|
|
"""Test get_required_columns with self_operations."""
|
|
preprocessor = DataPreprocessor(self_operations=['{var1}_{pow}_{2}'])
|
|
existing_columns = ['var1', 'var2']
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
assert 'var1' in result
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_with_cross_operations():
|
|
"""Test get_required_columns with cross_operations."""
|
|
preprocessor = DataPreprocessor(cross_operations=['{var1}_{*}_{var2}'])
|
|
existing_columns = ['var1', 'var2']
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
assert 'var1' in result
|
|
assert 'var2' in result
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_with_created_lags():
|
|
"""Test get_required_columns with created_lags."""
|
|
preprocessor = DataPreprocessor(created_lags={'var1': 1})
|
|
existing_columns = ['var1', 'var2']
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
assert 'var1' in result
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_with_missing_columns():
|
|
"""Test get_required_columns with missing columns in existing_columns."""
|
|
preprocessor = DataPreprocessor(
|
|
self_operations=['{var3}_{pow}_{2}'], cross_operations=['{var4}_{*}_{var5}']
|
|
)
|
|
existing_columns = ['var1', 'var2']
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
assert 'var3' in result
|
|
assert 'var4' in result
|
|
assert 'var5' in result
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_removes_duplicates():
|
|
"""Test get_required_columns removes duplicates from self_operations."""
|
|
preprocessor = DataPreprocessor(self_operations=['{var1}_{pow}_{2}'], created_lags={'var1': 1})
|
|
existing_columns = ['var1', 'var2']
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
# var1 is in existing_columns, so it should not be in required_columns
|
|
assert 'var1' not in result or result.count('var1') <= 1
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_removes_self_operations_branch():
|
|
"""Ensure line 278 removes columns present in self_operations iterable."""
|
|
preprocessor = DataPreprocessor(
|
|
self_operations=_IterableWithContains(['{var1}_{pow}_{2}'], contains_values=['var1'])
|
|
)
|
|
existing_columns: list[str] = []
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
assert 'var1' not in result
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_removes_cross_operations_branch():
|
|
"""Ensure line 285 removes columns present in cross_operations iterable."""
|
|
preprocessor = DataPreprocessor(
|
|
cross_operations=_IterableWithContains(['{var1}_{*}_{var2}'], contains_values=['var1'])
|
|
)
|
|
existing_columns: list[str] = []
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
assert 'var1' not in result
|
|
assert 'var2' in result
|
|
|
|
|
|
def test_data_preprocessor_get_required_columns_removes_created_lags():
|
|
"""Test get_required_columns removes columns from created_lags when column is in created_lags dict - covers line 292."""
|
|
preprocessor = DataPreprocessor(created_lags={'var1': 1, 'var2': 1})
|
|
existing_columns = ['var3']
|
|
|
|
result = preprocessor.get_required_columns(existing_columns)
|
|
|
|
# var1 and var2 should be removed because they're in created_lags dict and not in existing_columns
|
|
assert 'var1' not in result
|
|
assert 'var2' not in result
|
|
|
|
|
|
def test_data_preprocessor_fit_all_steps():
|
|
"""Test fit method with all steps."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1'],
|
|
nan_treatment='drop',
|
|
lag_train={'var1': 1},
|
|
static_threshold=3,
|
|
low_lim={'var1': 0},
|
|
upp_lim={'var1': 10},
|
|
scaler_name='Standard Scaler',
|
|
scaler_params={},
|
|
)
|
|
x = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [3, 5, 7, 9, 11]})
|
|
|
|
with patch('model_manager.sientia.models.treat_nan') as mock_treat:
|
|
with patch('model_manager.sientia.models.TimeSeriesDiscontinuityAnalyzer'):
|
|
with patch('model_manager.sientia.models.limit_dataset') as mock_limit:
|
|
mock_treat.return_value = x
|
|
mock_limit.return_value = (x, {'var1': 0}, {'var1': 10})
|
|
result = preprocessor.fit(x)
|
|
|
|
assert result is preprocessor
|
|
|
|
|
|
def test_data_preprocessor_transform_all_steps():
|
|
"""Test transform method with all steps."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1'],
|
|
nan_treatment='drop',
|
|
lag_transform={'var1': 1},
|
|
static_threshold=3,
|
|
low_lim={'var1': 0},
|
|
upp_lim={'var1': 10},
|
|
scaler_name='Standard Scaler',
|
|
scaler_params={},
|
|
self_operations=['{var1}_{pow}_{2}'],
|
|
ar_var='ar_target',
|
|
created_lags={'var1': 1},
|
|
)
|
|
train_x = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [3, 5, 7, 9, 11]})
|
|
|
|
with patch('model_manager.sientia.models.treat_nan') as mock_treat:
|
|
with patch('model_manager.sientia.models.TimeSeriesDiscontinuityAnalyzer'):
|
|
with patch('model_manager.sientia.models.limit_dataset') as mock_limit:
|
|
with patch('model_manager.sientia.models.create_features') as mock_create:
|
|
mock_treat.return_value = train_x
|
|
mock_limit.return_value = (train_x, {'var1': 0}, {'var1': 10})
|
|
mock_create.return_value = train_x
|
|
preprocessor.fit(train_x)
|
|
|
|
test_x = pd.DataFrame({'var1': [6, 7, 8, 9, 10], 'target': [13, 15, 17, 19, 21]})
|
|
|
|
with patch('model_manager.sientia.models.treat_nan') as mock_treat:
|
|
with patch('model_manager.sientia.models.TimeSeriesDiscontinuityAnalyzer'):
|
|
with patch('model_manager.sientia.models.limit_dataset') as mock_limit:
|
|
with patch('model_manager.sientia.models.create_features') as mock_create:
|
|
mock_treat.return_value = test_x
|
|
mock_limit.return_value = (test_x, {'var1': 0}, {'var1': 10})
|
|
mock_create.return_value = test_x
|
|
result = preprocessor.transform(test_x)
|
|
|
|
assert isinstance(result, pd.DataFrame)
|
|
|
|
|
|
# ============================================================================
|
|
# Additional tests for coverage - LinearRegressionModel
|
|
# ============================================================================
|
|
|
|
|
|
def test_linear_regression_model_fit_without_target_variable():
|
|
"""Test fit raises error when target_variable is not set."""
|
|
model = LinearRegressionModel()
|
|
data = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
|
|
with raises(ValueError, match='target_variable must be set before fitting'):
|
|
model.fit(data)
|
|
|
|
|
|
def test_linear_regression_model_fit_with_missing_columns():
|
|
"""Test fit raises error when variable_columns are missing from data."""
|
|
model = LinearRegressionModel(
|
|
target_variable='target', variable_columns=['var1', 'var_missing']
|
|
)
|
|
data = pd.DataFrame({'var1': [1, 2, 3], 'target': [3, 5, 7]})
|
|
|
|
with raises(ValueError, match='Columns not found in input data'):
|
|
model.fit(data)
|
|
|
|
|
|
def test_linear_regression_model_fit_with_missing_target():
|
|
"""Test fit raises error when target_variable is not in data."""
|
|
model = LinearRegressionModel(target_variable='missing_target', variable_columns=['var1'])
|
|
data = pd.DataFrame({'var1': [1, 2, 3], 'other': [3, 5, 7]})
|
|
|
|
with raises(ValueError, match='Target variable missing_target not found in input data'):
|
|
model.fit(data)
|
|
|
|
|
|
def test_linear_regression_model_fit_with_inf_values():
|
|
"""Test fit handles infinite values by converting to NaN."""
|
|
model = LinearRegressionModel(target_variable='target', variable_columns=['var1', 'var2'])
|
|
# Include some inf values that will be converted to NaN and rows dropped
|
|
data = pd.DataFrame(
|
|
{
|
|
'var1': [1.0, 2.0, 3.0, np.inf, 5.0],
|
|
'var2': [2.0, 3.0, 4.0, 5.0, 6.0],
|
|
'target': [3.0, 5.0, 7.0, 9.0, 11.0],
|
|
}
|
|
)
|
|
|
|
model.fit(data)
|
|
|
|
# Model should fit successfully after removing row with inf
|
|
assert model.weights is not None
|
|
assert 'var1' in model.variable_columns
|
|
assert 'var2' in model.variable_columns
|
|
|
|
|
|
def test_linear_regression_model_fit_polynomial():
|
|
"""Test fit with polynomial features."""
|
|
model = LinearRegressionModel(target_variable='target', variable_columns=['var1'], degree=2)
|
|
data = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [3, 5, 7, 9, 11]})
|
|
|
|
model.fit(data)
|
|
|
|
assert model.poly_feature_names is not None
|
|
assert len(model.poly_feature_names) > 1
|
|
|
|
|
|
def test_linear_regression_model_predict_polynomial():
|
|
"""Test predict with polynomial features."""
|
|
model = LinearRegressionModel(target_variable='target', variable_columns=['var1'], degree=2)
|
|
train_data = pd.DataFrame({'var1': [1, 2, 3, 4, 5], 'target': [1, 4, 9, 16, 25]})
|
|
model.fit(train_data)
|
|
|
|
test_data = pd.DataFrame({'var1': [6, 7]})
|
|
predictions = model.predict(test_data)
|
|
|
|
assert isinstance(predictions, np.ndarray)
|
|
assert len(predictions) == 2
|
|
|
|
|
|
def test_linear_regression_model_create_poly_features_degree_1():
|
|
"""Test create_poly_features returns input unchanged when degree <= 1."""
|
|
model = LinearRegressionModel(degree=1)
|
|
data = pd.DataFrame({'var1': [1, 2, 3], 'var2': [4, 5, 6]})
|
|
|
|
result = model.create_poly_features(data, fit=True)
|
|
|
|
pd.testing.assert_frame_equal(result, data)
|
|
|
|
|
|
def test_linear_regression_model_create_poly_features_not_fitted():
|
|
"""Test create_poly_features raises error when not fitted and fit=False."""
|
|
model = LinearRegressionModel(degree=2)
|
|
data = pd.DataFrame({'var1': [1, 2, 3], 'var2': [4, 5, 6]})
|
|
|
|
with raises(ValueError, match='PolynomialFeatures not fitted'):
|
|
model.create_poly_features(data, fit=False)
|
|
|
|
|
|
def test_linear_regression_model_create_poly_features_transform():
|
|
"""Test create_poly_features with fit=False after fitting."""
|
|
model = LinearRegressionModel(degree=2)
|
|
train_data = pd.DataFrame({'var1': [1, 2, 3], 'var2': [4, 5, 6]})
|
|
model.create_poly_features(train_data, fit=True)
|
|
|
|
test_data = pd.DataFrame({'var1': [4, 5], 'var2': [7, 8]})
|
|
result = model.create_poly_features(test_data, fit=False)
|
|
|
|
assert isinstance(result, pd.DataFrame)
|
|
assert len(result.columns) > 2
|
|
|
|
|
|
def test_linear_regression_model_get_regressor():
|
|
"""Test get_regressor returns the underlying LinearRegression model."""
|
|
model = LinearRegressionModel()
|
|
|
|
regressor = model.get_regressor()
|
|
|
|
from sklearn.linear_model import LinearRegression
|
|
|
|
assert isinstance(regressor, LinearRegression)
|
|
|
|
|
|
# ============================================================================
|
|
# Additional tests for coverage - DataPreprocessor
|
|
# ============================================================================
|
|
|
|
|
|
@patch('model_manager.sientia.models.treat_nan')
|
|
def test_data_preprocessor_treat_discontinuities_linear_interpolation(mock_treat_nan):
|
|
"""Test treat_discontinuities with 'linear interpolation' treatment."""
|
|
preprocessor = DataPreprocessor(nan_treatment='linear interpolation')
|
|
data = pd.DataFrame({'col1': [1, np.nan, 3]})
|
|
expected_data = pd.DataFrame({'col1': [1.0, 2.0, 3.0]})
|
|
mock_treat_nan.return_value = expected_data
|
|
|
|
result = preprocessor.treat_discontinuities(data)
|
|
|
|
# Should map 'linear interpolation' to 'fill linear'
|
|
mock_treat_nan.assert_called_once_with(data, 'fill linear')
|
|
pd.testing.assert_frame_equal(result, expected_data)
|
|
|
|
|
|
def test_data_preprocessor_range_selection_with_start_date():
|
|
"""Test range_selection filters by start_date."""
|
|
preprocessor = DataPreprocessor(start_date='2023-01-02')
|
|
data = pd.DataFrame(
|
|
{'col1': [1, 2, 3]},
|
|
index=pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']),
|
|
)
|
|
|
|
result = preprocessor.range_selection(data)
|
|
|
|
assert len(result) == 2
|
|
assert result.index[0] == pd.Timestamp('2023-01-02')
|
|
|
|
|
|
def test_data_preprocessor_range_selection_with_end_date():
|
|
"""Test range_selection filters by end_date."""
|
|
preprocessor = DataPreprocessor(end_date='2023-01-02')
|
|
data = pd.DataFrame(
|
|
{'col1': [1, 2, 3]},
|
|
index=pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']),
|
|
)
|
|
|
|
result = preprocessor.range_selection(data)
|
|
|
|
assert len(result) == 2
|
|
assert result.index[-1] == pd.Timestamp('2023-01-02')
|
|
|
|
|
|
def test_data_preprocessor_range_selection_with_invalid_start_date():
|
|
"""Test range_selection handles invalid start_date gracefully."""
|
|
preprocessor = DataPreprocessor(start_date='invalid-date')
|
|
data = pd.DataFrame(
|
|
{'col1': [1, 2, 3]},
|
|
index=pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']),
|
|
)
|
|
|
|
result = preprocessor.range_selection(data)
|
|
|
|
# Should skip filtering and return original data
|
|
assert len(result) == 3
|
|
|
|
|
|
def test_data_preprocessor_range_selection_with_invalid_end_date():
|
|
"""Test range_selection handles invalid end_date gracefully."""
|
|
preprocessor = DataPreprocessor(end_date='invalid-date')
|
|
data = pd.DataFrame(
|
|
{'col1': [1, 2, 3]},
|
|
index=pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']),
|
|
)
|
|
|
|
result = preprocessor.range_selection(data)
|
|
|
|
# Should skip filtering and return original data
|
|
assert len(result) == 3
|
|
|
|
|
|
def test_data_preprocessor_range_selection_with_removed_intervals():
|
|
"""Test range_selection removes specified intervals."""
|
|
preprocessor = DataPreprocessor(removed_intervals=[['2023-01-02', '2023-01-03']])
|
|
data = pd.DataFrame(
|
|
{'col1': [1, 2, 3, 4, 5]},
|
|
index=pd.to_datetime(
|
|
['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04', '2023-01-05']
|
|
),
|
|
)
|
|
|
|
result = preprocessor.range_selection(data)
|
|
|
|
assert len(result) == 3
|
|
assert pd.Timestamp('2023-01-02') not in result.index
|
|
assert pd.Timestamp('2023-01-03') not in result.index
|
|
|
|
|
|
def test_data_preprocessor_range_selection_with_invalid_interval():
|
|
"""Test range_selection handles invalid interval dates gracefully."""
|
|
preprocessor = DataPreprocessor(removed_intervals=[['invalid', 'dates']])
|
|
data = pd.DataFrame(
|
|
{'col1': [1, 2, 3]},
|
|
index=pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']),
|
|
)
|
|
|
|
result = preprocessor.range_selection(data)
|
|
|
|
# Should skip invalid interval and return original data
|
|
assert len(result) == 3
|
|
|
|
|
|
def test_data_preprocessor_range_selection_with_short_interval():
|
|
"""Test range_selection skips intervals with less than 2 elements."""
|
|
preprocessor = DataPreprocessor(removed_intervals=[['2023-01-02']])
|
|
data = pd.DataFrame(
|
|
{'col1': [1, 2, 3]},
|
|
index=pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']),
|
|
)
|
|
|
|
result = preprocessor.range_selection(data)
|
|
|
|
# Should skip short interval and return original data
|
|
assert len(result) == 3
|
|
|
|
|
|
def test_data_preprocessor_predict():
|
|
"""Test predict method removes target and preserves feature order."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
train_x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'target': [3, 5, 7]})
|
|
preprocessor.fit(train_x)
|
|
|
|
test_x = pd.DataFrame({'var1': [4, 5], 'var2': [5, 6], 'target': [9, 11]})
|
|
result = preprocessor.predict(test_x)
|
|
|
|
assert 'target' not in result.columns
|
|
assert 'var1' in result.columns
|
|
assert 'var2' in result.columns
|
|
|
|
|
|
def test_data_preprocessor_predict_without_target():
|
|
"""Test predict when target is not in transformed data."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
train_x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'target': [3, 5, 7]})
|
|
preprocessor.fit(train_x)
|
|
|
|
# Include target in test data so transform works, predict will remove it
|
|
test_x = pd.DataFrame({'var1': [4, 5], 'var2': [5, 6], 'target': [9, 11]})
|
|
result = preprocessor.predict(test_x)
|
|
|
|
# Target should be removed by predict
|
|
assert 'target' not in result.columns
|
|
assert 'var1' in result.columns
|
|
assert 'var2' in result.columns
|
|
|
|
|
|
def test_data_preprocessor_predict_preserves_feature_order():
|
|
"""Test predict preserves feature order from fit."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
train_x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'target': [3, 5, 7]})
|
|
preprocessor.fit(train_x)
|
|
|
|
# Test data has columns in different order
|
|
test_x = pd.DataFrame({'var2': [5, 6], 'var1': [4, 5], 'target': [9, 11]})
|
|
result = preprocessor.predict(test_x)
|
|
|
|
# Should have columns in same order as during fit
|
|
assert list(result.columns) == ['var1', 'var2']
|
|
|
|
|
|
# ============================================================================
|
|
# Additional tests for 100% coverage
|
|
# ============================================================================
|
|
|
|
|
|
def test_data_preprocessor_predict_target_not_in_columns():
|
|
"""Test predict when target_variable is not in transformed data columns."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
train_x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'target': [3, 5, 7]})
|
|
preprocessor.fit(train_x)
|
|
|
|
# Test data without target column - predict should still work
|
|
test_x = pd.DataFrame({'var1': [4, 5], 'var2': [5, 6]})
|
|
# Need to add target for transform to work, then it gets removed
|
|
test_x['target'] = [9, 11]
|
|
|
|
# Manually remove target before calling predict to test the branch
|
|
preprocessor_copy = DataPreprocessor(
|
|
target_variable='nonexistent_target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
preprocessor_copy.fit(train_x.rename(columns={'target': 'nonexistent_target'}))
|
|
|
|
test_x_no_target = pd.DataFrame({'var1': [4, 5], 'var2': [5, 6], 'nonexistent_target': [9, 11]})
|
|
result = preprocessor_copy.predict(test_x_no_target)
|
|
|
|
assert 'var1' in result.columns
|
|
assert 'var2' in result.columns
|
|
|
|
|
|
def test_data_preprocessor_predict_without_fitted_feature_order():
|
|
"""Test predict when _fitted_feature_order is None."""
|
|
preprocessor = DataPreprocessor(
|
|
target_variable='target',
|
|
input_columns=['var1', 'var2'],
|
|
steps_order=['Discontinuity Treatment'],
|
|
)
|
|
train_x = pd.DataFrame({'var1': [1, 2, 3], 'var2': [2, 3, 4], 'target': [3, 5, 7]})
|
|
preprocessor.fit(train_x)
|
|
|
|
# Manually set _fitted_feature_order to None to test the branch
|
|
preprocessor._fitted_feature_order = None
|
|
|
|
test_x = pd.DataFrame({'var1': [4, 5], 'var2': [5, 6], 'target': [9, 11]})
|
|
result = preprocessor.predict(test_x)
|
|
|
|
# Should still work, just without reordering
|
|
assert 'target' not in result.columns
|
|
assert 'var1' in result.columns
|
|
assert 'var2' in result.columns
|