| """Feature matrix assembly tests (RESEARCH Pattern 0).""" |
|
|
| from pathlib import Path |
|
|
| import numpy as np |
| import pytest |
|
|
| from model.features import ( |
| ANOMALY_FEATURES, |
| CATEGORICAL_FEATURES, |
| CLASSES, |
| CLASSIFIER_FEATURES, |
| load_anomaly_features, |
| load_split, |
| ) |
| from model.synth.state_machines import GENERATORS |
|
|
|
|
| def test_classes_match_generators_order() -> None: |
| """CLASSES MUST mirror GENERATORS insertion order (byte-identicality anchor).""" |
| assert CLASSES == list(GENERATORS.keys()) |
| assert len(CLASSES) == 10 |
|
|
|
|
| def test_classifier_features_shape() -> None: |
| """20 features = 9 numerics + 8 categoricals + 3 misc.""" |
| assert len(CLASSIFIER_FEATURES) == 20 |
| assert len(ANOMALY_FEATURES) == 9 |
| assert len(CATEGORICAL_FEATURES) == 8 |
|
|
|
|
| def test_classifier_features_no_leakage_columns() -> None: |
| """`bssid` and `timestamp` MUST NOT be in CLASSIFIER_FEATURES (Pitfall 2).""" |
| assert "bssid" not in CLASSIFIER_FEATURES |
| assert "timestamp" not in CLASSIFIER_FEATURES |
|
|
|
|
| @pytest.mark.skipif( |
| not Path("data/train.parquet").exists(), |
| reason="data/train.parquet not generated (run `make synth` first)", |
| ) |
| def test_load_split_train_shape() -> None: |
| X, y, names = load_split(Path("data/train.parquet")) |
| assert X.dtype == np.float64 |
| assert y.dtype == np.int64 |
| assert X.shape[0] == 3_000_000 |
| assert X.shape[1] == len(CLASSIFIER_FEATURES) |
| assert names == list(CLASSIFIER_FEATURES) |
| assert y.min() >= 0 and y.max() < len(CLASSES) |
|
|
|
|
| @pytest.mark.skipif( |
| not Path("data/train.parquet").exists(), |
| reason="data/train.parquet not generated (run `make synth` first)", |
| ) |
| def test_load_anomaly_features_shape() -> None: |
| X_anom, y, ts = load_anomaly_features(Path("data/train.parquet")) |
| assert X_anom.shape[0] == 3_000_000 |
| assert X_anom.shape[1] == len(ANOMALY_FEATURES) |
| assert X_anom.dtype == np.float64 |
| assert ts.shape[0] == 3_000_000 |
| |
| assert (y >= 0).all() and (y < len(CLASSES)).all() |
|
|