SIENTIAPDE-1430: Introduce static_threshold parameter for static window removal.
This parameter allows customizing the threshold (1-1000) used when rem_static_win is enabled, defaulting to 1 if null. Updates include parameter definition, business rule validation, repository logic for passing the threshold, documentation in README.md and PIPELINE_PARAMS_CHANGELOG.md, and new unit and integration tests.
This commit is contained in:
@@ -28,6 +28,7 @@ Este documento descreve as alterações nos parâmetros de entrada do pipeline T
|
||||
| `start_date` | `str \| null` | Data inicial para filtrar dados (formato: `"YYYY-MM-DD HH:MM:SS"`) | `null` |
|
||||
| `end_date` | `str \| null` | Data final para filtrar dados (formato: `"YYYY-MM-DD HH:MM:SS"`) | `null` |
|
||||
| `support_filters` | `dict \| null` | Filtros customizados por variável | `{}` |
|
||||
| `static_threshold` | `int \| null` | Threshold para remoção de janelas estáticas (1-1000). Só usado quando `rem_static_win` é `true`. | `1` |
|
||||
|
||||
---
|
||||
|
||||
@@ -79,6 +80,7 @@ Este documento descreve as alterações nos parâmetros de entrada do pipeline T
|
||||
},
|
||||
|
||||
"rem_static_win": true,
|
||||
"static_threshold": null,
|
||||
"low_lim": {"pressao": 0, "umidade": 0, "velocidade": 0},
|
||||
"upp_lim": {"pressao": 100, "umidade": 100, "velocidade": 50},
|
||||
"window": 0,
|
||||
@@ -212,6 +214,7 @@ O sistema valida automaticamente:
|
||||
11. **`degree` vs `model_name`**: Se `model_name` = "Polynomial Regression", `degree` deve ser >= 2; se "Linear Regression", `degree` deve ser = 1
|
||||
12. **`removed_intervals`**: Cada elemento deve ser lista/tupla com pelo menos 2 elementos (start, end)
|
||||
13. **`target_variable`**: Não pode estar vazio
|
||||
14. **`static_threshold`**: Se `rem_static_win` = `true` e `static_threshold` tiver valor, deve estar entre 1 e 1000 (inclusive). Se `null`, assume valor `1`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -296,6 +296,7 @@ The **TrainModel** workflow orchestrates the complete ML model training pipeline
|
||||
"lag_train": {"feature1": 0, "feature2": 2},
|
||||
"lag_val": {"feature1": 0, "feature2": 1},
|
||||
"rem_static_win": false,
|
||||
"static_threshold": null,
|
||||
"low_lim": {"feature1": 0.0, "feature2": 0.0},
|
||||
"upp_lim": {"feature1": 100.0, "feature2": 100.0},
|
||||
"window": 10,
|
||||
@@ -359,6 +360,7 @@ The workflow validates comprehensive business rules beyond type checking:
|
||||
11. **model_name**: Must be 'Linear Regression' or 'Polynomial Regression'
|
||||
12. **Polynomial Regression requires Scaler**: Models with degree > 1 must have a scaler to prevent numerical overflow
|
||||
13. **Linear Regression requires degree 1**: Linear models must have degree = 1
|
||||
14. **static_threshold**: When `rem_static_win` is true and `static_threshold` has a value, it must be between 1 and 1000 (inclusive). If null, defaults to 1.
|
||||
|
||||
### Cleanup Files Workflow (`cleanup_files.py`)
|
||||
|
||||
@@ -977,6 +979,7 @@ Test scenarios are defined as JSON files in `docs/test-scenarios/`. Each scenari
|
||||
| `08-linear-regression-with-limits` | Linear regression with variable limits | `lowLim`/`uppLim` configuration |
|
||||
| `09-polynomial-degree2-with-scaler-and-lags` | Complete polynomial scenario | Scaler + lags + degree 2 |
|
||||
| `10-linear-regression-with-ar` | Linear regression with autoregressive variable | `includeAr: true` |
|
||||
| `11-linear-regression-static-threshold-custom` | Linear regression with custom static threshold | `staticThreshold: 100` |
|
||||
|
||||
#### Scenario File Structure
|
||||
|
||||
@@ -991,6 +994,7 @@ Test scenarios are defined as JSON files in `docs/test-scenarios/`. Each scenari
|
||||
"lagTrain": {"feature1": 0, "feature2": 0},
|
||||
"lagVal": {"feature1": 0, "feature2": 0},
|
||||
"remStaticWin": false,
|
||||
"staticThreshold": null,
|
||||
"lowLim": {},
|
||||
"uppLim": {},
|
||||
"window": 0,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"_description": "Regressão linear com remoção de janelas estáticas e static_threshold customizado",
|
||||
"experimentName": "test-linear-static-threshold",
|
||||
"username": "bruno.domingues@aignosi.com.br",
|
||||
"modelName": "Linear Regression",
|
||||
"targetVariable": "03CV020/CORRENTE_N_M1_PV(Value)",
|
||||
"variableColumns": ["303-WIT-200(Value)"],
|
||||
"lagTrain": {"303-WIT-200(Value)": 0},
|
||||
"lagVal": {"303-WIT-200(Value)": 0},
|
||||
"remStaticWin": true,
|
||||
"staticThreshold": 100,
|
||||
"lowLim": {},
|
||||
"uppLim": {},
|
||||
"window": 10,
|
||||
"useScaler": false,
|
||||
"includeAr": false,
|
||||
"trainSize": 80,
|
||||
"shuffle": true,
|
||||
"lineSeparator": ",",
|
||||
"decimalSeparator": ".",
|
||||
"removedIntervals": [],
|
||||
"degree": 1,
|
||||
"interactionOnly": false,
|
||||
"nanTreatment": "drop",
|
||||
"startDate": null,
|
||||
"endDate": null,
|
||||
"scalerName": "None",
|
||||
"supportFilters": {}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ class TrainModelParams:
|
||||
end_date (str | None): End date for filtering data.
|
||||
scaler_name (str): Name of the scaler to use ('Standard Scaler' or 'None').
|
||||
support_filters (dict): Custom support filters per variable.
|
||||
static_threshold (int | None): Threshold for static window removal (1-1000). Only used when rem_static_win is True.
|
||||
"""
|
||||
|
||||
variable_columns: list[str]
|
||||
@@ -76,6 +77,7 @@ class TrainModelParams:
|
||||
end_date: str | None
|
||||
scaler_name: str
|
||||
support_filters: dict
|
||||
static_threshold: int | None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> 'TrainModelParams':
|
||||
@@ -138,6 +140,7 @@ class TrainModelParams:
|
||||
scaler_name=cls._check_none(data.get('scaler_name'), str, 'scaler_name'),
|
||||
support_filters=cls._check_type(data.get('support_filters'), dict, 'support_filters')
|
||||
or {},
|
||||
static_threshold=cls._check_type(data.get('static_threshold'), int, 'static_threshold'),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -228,6 +231,13 @@ class TrainModelParams:
|
||||
if self.window < 0:
|
||||
raise ValueError(f'window must be non-negative, got {self.window}')
|
||||
|
||||
# Validate static_threshold only when rem_static_win is True and value is provided
|
||||
if self.rem_static_win and self.static_threshold is not None:
|
||||
if not 1 <= self.static_threshold <= 1000:
|
||||
raise ValueError(
|
||||
f'static_threshold must be between 1 and 1000, got {self.static_threshold}'
|
||||
)
|
||||
|
||||
def _validate_model_params(self) -> None:
|
||||
"""Validate model-related parameters."""
|
||||
if self.degree < 1:
|
||||
|
||||
@@ -206,7 +206,10 @@ class ModelRepository:
|
||||
self.model_serving.log_param('lag_train', data.params.lag_train)
|
||||
self.model_serving.log_param('lag_transform', data.params.lag_val)
|
||||
self.model_serving.log_param(
|
||||
'static_threshold', 1 if data.params.rem_static_win else None
|
||||
'static_threshold',
|
||||
(data.params.static_threshold if data.params.static_threshold is not None else 1)
|
||||
if data.params.rem_static_win
|
||||
else None,
|
||||
)
|
||||
self.model_serving.log_param('lower_limits', data.params.low_lim)
|
||||
self.model_serving.log_param('upper_limits', data.params.upp_lim)
|
||||
|
||||
@@ -269,7 +269,9 @@ class TrainingRepository:
|
||||
start_date=params.start_date,
|
||||
end_date=params.end_date,
|
||||
removed_intervals=removed_intervals,
|
||||
static_threshold=1 if params.rem_static_win else None,
|
||||
static_threshold=(params.static_threshold if params.static_threshold is not None else 1)
|
||||
if params.rem_static_win
|
||||
else None,
|
||||
low_lim=params.low_lim,
|
||||
upp_lim=params.upp_lim,
|
||||
scaler_name=params.scaler_name,
|
||||
|
||||
@@ -34,6 +34,7 @@ def valid_train_params_dict():
|
||||
'end_date': None,
|
||||
'scaler_name': 'Standard Scaler',
|
||||
'support_filters': {},
|
||||
'static_threshold': None,
|
||||
}
|
||||
|
||||
|
||||
@@ -564,3 +565,96 @@ def test_validate_business_rules_polynomial_regression_valid(valid_train_params_
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
params.validate_business_rules() # Should not raise
|
||||
|
||||
|
||||
def test_validate_business_rules_static_threshold_valid(valid_train_params_dict):
|
||||
"""Test validate_business_rules accepts valid static_threshold when rem_static_win is True."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['rem_static_win'] = True
|
||||
valid_train_params_dict['static_threshold'] = 500
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
params.validate_business_rules() # Should not raise
|
||||
|
||||
|
||||
def test_validate_business_rules_static_threshold_min_valid(valid_train_params_dict):
|
||||
"""Test validate_business_rules accepts static_threshold = 1."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['rem_static_win'] = True
|
||||
valid_train_params_dict['static_threshold'] = 1
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
params.validate_business_rules() # Should not raise
|
||||
|
||||
|
||||
def test_validate_business_rules_static_threshold_max_valid(valid_train_params_dict):
|
||||
"""Test validate_business_rules accepts static_threshold = 1000."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['rem_static_win'] = True
|
||||
valid_train_params_dict['static_threshold'] = 1000
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
params.validate_business_rules() # Should not raise
|
||||
|
||||
|
||||
def test_validate_business_rules_static_threshold_below_min(valid_train_params_dict):
|
||||
"""Test validate_business_rules raises error when static_threshold < 1."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['rem_static_win'] = True
|
||||
valid_train_params_dict['static_threshold'] = 0
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
with pytest.raises(ValueError, match='static_threshold must be between 1 and 1000, got 0'):
|
||||
params.validate_business_rules()
|
||||
|
||||
|
||||
def test_validate_business_rules_static_threshold_above_max(valid_train_params_dict):
|
||||
"""Test validate_business_rules raises error when static_threshold > 1000."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['rem_static_win'] = True
|
||||
valid_train_params_dict['static_threshold'] = 1001
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
with pytest.raises(ValueError, match='static_threshold must be between 1 and 1000, got 1001'):
|
||||
params.validate_business_rules()
|
||||
|
||||
|
||||
def test_validate_business_rules_static_threshold_none_when_rem_static_win_true(
|
||||
valid_train_params_dict,
|
||||
):
|
||||
"""Test validate_business_rules accepts None static_threshold when rem_static_win is True."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['rem_static_win'] = True
|
||||
valid_train_params_dict['static_threshold'] = None
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
params.validate_business_rules() # Should not raise - None is allowed
|
||||
|
||||
|
||||
def test_validate_business_rules_static_threshold_ignored_when_rem_static_win_false(
|
||||
valid_train_params_dict,
|
||||
):
|
||||
"""Test validate_business_rules ignores static_threshold when rem_static_win is False."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['rem_static_win'] = False
|
||||
valid_train_params_dict['static_threshold'] = 5000 # Invalid value, but should be ignored
|
||||
params = TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
params.validate_business_rules() # Should not raise - validation skipped
|
||||
|
||||
|
||||
def test_from_dict_static_threshold_type_error(valid_train_params_dict):
|
||||
"""Test from_dict raises TypeError when static_threshold has wrong type."""
|
||||
from model_manager.utils.models.train_model_params import TrainModelParams
|
||||
|
||||
valid_train_params_dict['static_threshold'] = 'not_an_int'
|
||||
|
||||
with pytest.raises(TypeError, match='static_threshold must be of type int, but got str'):
|
||||
TrainModelParams.from_dict(valid_train_params_dict)
|
||||
|
||||
@@ -40,6 +40,7 @@ def sample_params():
|
||||
end_date=None,
|
||||
scaler_name='Standard Scaler',
|
||||
support_filters={},
|
||||
static_threshold=None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ def mock_train_result():
|
||||
result.params.include_ar = False
|
||||
result.params.train_size = 80
|
||||
result.params.removed_intervals = []
|
||||
result.params.rem_static_win = True
|
||||
result.params.static_threshold = None
|
||||
result.run_name = 'test_run'
|
||||
result.run_dir = '/tmp/test_run' # noqa: S108
|
||||
result.report_path = '/tmp/test_run/report.html' # noqa: S108
|
||||
|
||||
@@ -56,6 +56,7 @@ def sample_params():
|
||||
end_date=None,
|
||||
scaler_name='None',
|
||||
support_filters={},
|
||||
static_threshold=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -137,6 +138,7 @@ class TestExtractModelEquation:
|
||||
end_date=None,
|
||||
scaler_name='None',
|
||||
support_filters={},
|
||||
static_threshold=None,
|
||||
)
|
||||
|
||||
# Mock model with single coefficient
|
||||
@@ -221,12 +223,33 @@ class TestInitDataPreprocessor:
|
||||
assert preprocessor.ar_var is None
|
||||
|
||||
def test_init_preprocessor_with_static_removal(self, training_repo, sample_params):
|
||||
"""Test preprocessor with static window removal enabled."""
|
||||
"""Test preprocessor with static window removal enabled and no static_threshold."""
|
||||
sample_params.rem_static_win = True
|
||||
sample_params.static_threshold = None
|
||||
preprocessor = training_repo._init_data_preprocessor(sample_params)
|
||||
|
||||
assert preprocessor.static_threshold == 1
|
||||
|
||||
def test_init_preprocessor_with_static_removal_custom_threshold(
|
||||
self, training_repo, sample_params
|
||||
):
|
||||
"""Test preprocessor with static window removal and custom static_threshold."""
|
||||
sample_params.rem_static_win = True
|
||||
sample_params.static_threshold = 500
|
||||
preprocessor = training_repo._init_data_preprocessor(sample_params)
|
||||
|
||||
assert preprocessor.static_threshold == 500
|
||||
|
||||
def test_init_preprocessor_without_static_removal_ignores_threshold(
|
||||
self, training_repo, sample_params
|
||||
):
|
||||
"""Test preprocessor without static removal ignores static_threshold."""
|
||||
sample_params.rem_static_win = False
|
||||
sample_params.static_threshold = 500
|
||||
preprocessor = training_repo._init_data_preprocessor(sample_params)
|
||||
|
||||
assert preprocessor.static_threshold is None
|
||||
|
||||
def test_init_preprocessor_lag_configuration(self, training_repo, sample_params):
|
||||
"""Test preprocessor lag configuration."""
|
||||
sample_params.lag_train = {'var1': 5, 'var2': 5, 'var3': 5}
|
||||
|
||||
Reference in New Issue
Block a user