Skip to content

Commit af76392

Browse files
committed
update bench
1 parent 16730e0 commit af76392

16 files changed

Lines changed: 524 additions & 123 deletions

benchmark/formatter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
Supports console tables, CSV, JSON, and LaTeX output for papers."""
44

55
import csv
6-
from dataclasses import asdict
76
import json
87
import logging
98
import os
@@ -99,7 +98,7 @@ def to_csv(self, path: str) -> None:
9998
logger.info("Results saved to %s", path)
10099
except ImportError:
101100
# Fallback with csv module
102-
_dicts = [asdict(r) for r in self.results]
101+
_dicts = [dict(r) for r in self.results]
103102
if not _dicts:
104103
return
105104
keys = _dicts[0].keys()

benchmark/registry.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,13 @@ def get(self, name: str) -> Type[Dataset]:
6666
class _LazyDataset(Dataset):
6767
"""Lazy-loaded dataset wrapper."""
6868

69-
name = name
70-
7169
def prepare_data(self, **kwargs):
7270
return factory().prepare_data(**kwargs)
7371

7472
def get_train_valid_split(self, **kwargs):
7573
return factory().get_train_valid_split(**kwargs)
7674

75+
_LazyDataset.name = name
7776
return _LazyDataset
7877
return super().get(name)
7978

examples/run_anomaly.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import numpy as np
99
import pandas as pd
1010
from sklearn.preprocessing import StandardScaler
11+
import tensorflow as tf
1112

12-
from tfts import AutoConfig, AutoModel, AutoModelForAnomaly, KerasTrainer
13+
from tfts import AutoConfig, AutoModelForAnomaly, KerasTrainer, set_seed
1314

1415

1516
def parse_args():
@@ -28,7 +29,7 @@ def parse_args():
2829
def create_subsequences(time_series, train_length, pred_length):
2930
"""Create subsequences for training and prediction."""
3031
subsequences, next_values = [], []
31-
for i in range(len(time_series) - train_length - pred_length):
32+
for i in range(len(time_series) - train_length - pred_length + 1):
3233
subsequences.append(time_series[i : i + train_length])
3334
next_values.append(time_series[i + train_length : i + train_length + pred_length].T[0])
3435
return subsequences, next_values
@@ -53,14 +54,21 @@ def load_and_preprocess_data(args):
5354

5455
def train_model(args):
5556
"""Train the model using the specified arguments."""
57+
set_seed(args.seed)
5658
x_train, y_train, _ = load_and_preprocess_data(args)
5759

5860
config = AutoConfig.for_model(args.use_model)
5961
config.train_sequence_length = args.train_length
6062
model = AutoModelForAnomaly.from_config(config)
6163

6264
trainer = KerasTrainer(model)
63-
trainer.train((x_train, y_train), (x_train, y_train), epochs=args.epochs)
65+
trainer.train(
66+
(x_train, y_train),
67+
(x_train, y_train),
68+
optimizer=tf.keras.optimizers.Adam(args.learning_rate),
69+
epochs=args.epochs,
70+
batch_size=args.batch_size,
71+
)
6472
trainer.save_model(args.output_dir)
6573
print(f"Model trained and saved to {args.output_dir}")
6674

examples/run_classification.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sklearn.model_selection import train_test_split
99
import tensorflow as tf
1010

11-
from tfts import AutoConfig, AutoModelForClassification, KerasTrainer
11+
from tfts import AutoConfig, AutoModelForClassification, KerasTrainer, set_seed
1212

1313
logging.getLogger("tensorflow").setLevel(logging.ERROR)
1414

@@ -49,6 +49,7 @@ def readucr(filename):
4949

5050

5151
def run_train(args):
52+
set_seed(args.seed)
5253
x_train, y_train, x_test, y_test = prepare_data()
5354

5455
x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.2, random_state=42)

examples/run_tuner.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Demo to tune the model parameters by Autotune"""
22

33
import numpy as np
4+
import tensorflow as tf
45

56
from tfts import AutoConfig, AutoModel, KerasTrainer, get_data
67

@@ -30,9 +31,15 @@ def objective(self, trial):
3031
config.num_stacked_layers = num_layers
3132

3233
model = AutoModel.from_config(config, predict_sequence_length=self.predict_sequence_length)
33-
trainer = KerasTrainer(model, optimizer_config={"learning_rate": learning_rate})
34-
35-
trainer.train(self.train_data, self.valid_data, epochs=epochs, verbose=0)
34+
trainer = KerasTrainer(model)
35+
36+
trainer.train(
37+
self.train_data,
38+
self.valid_data,
39+
optimizer=tf.keras.optimizers.Adam(learning_rate),
40+
epochs=epochs,
41+
verbose=0,
42+
)
3643

3744
x_valid, y_valid = self.valid_data
3845
predictions = trainer.predict(x_valid)

tests/test_benchmark_formatter.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import builtins
2+
import csv
3+
import tempfile
4+
import unittest
5+
6+
from benchmark.formatter import BenchmarkResults
7+
from benchmark.registry import DatasetRegistry
8+
9+
10+
class BenchmarkResultsTest(unittest.TestCase):
11+
def test_to_csv_fallback_handles_dict_results_without_pandas(self):
12+
results = BenchmarkResults(
13+
[
14+
{"dataset": "synthetic", "model": "rnn", "metrics": {"mae": 0.1}},
15+
{"dataset": "synthetic", "model": "tcn", "metrics": {"mae": 0.2}},
16+
]
17+
)
18+
19+
original_import = builtins.__import__
20+
21+
def import_without_pandas(name, *args, **kwargs):
22+
if name == "pandas":
23+
raise ImportError("pandas disabled for fallback test")
24+
return original_import(name, *args, **kwargs)
25+
26+
with tempfile.NamedTemporaryFile(suffix=".csv") as tmp:
27+
try:
28+
builtins.__import__ = import_without_pandas
29+
results.to_csv(tmp.name)
30+
finally:
31+
builtins.__import__ = original_import
32+
33+
tmp.seek(0)
34+
rows = list(csv.DictReader(line.decode("utf-8") for line in tmp.readlines()))
35+
36+
self.assertEqual(rows[0]["dataset"], "synthetic")
37+
self.assertEqual(rows[0]["model"], "rnn")
38+
self.assertEqual(rows[0]["metrics"], "{'mae': 0.1}")
39+
40+
41+
class DatasetRegistryTest(unittest.TestCase):
42+
def test_lazy_dataset_registration_returns_instantiable_wrapper(self):
43+
class ToyDataset:
44+
def prepare_data(self, **kwargs):
45+
return "prepared"
46+
47+
def get_train_valid_split(self, **kwargs):
48+
return "split"
49+
50+
registry = DatasetRegistry()
51+
registry.register_lazy("toy", lambda: ToyDataset())
52+
53+
dataset_cls = registry.get("toy")
54+
dataset = dataset_cls()
55+
56+
self.assertEqual(dataset_cls.name, "toy")
57+
self.assertEqual(dataset.prepare_data(), "prepared")
58+
self.assertEqual(dataset.get_train_valid_split(), "split")
59+
60+
61+
if __name__ == "__main__":
62+
unittest.main()

tests/test_data/test_processor.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import unittest
2+
3+
import numpy as np
4+
import pandas as pd
5+
6+
from tfts.data.auto_preprocessor import AutoPreprocessor
7+
from tfts.data.processor import DataProcessor
8+
9+
10+
class DataProcessorTest(unittest.TestCase):
11+
def setUp(self):
12+
self.df = pd.DataFrame(
13+
{
14+
"date": pd.date_range("2024-01-01", periods=20, freq="D"),
15+
"value": np.arange(20, dtype=float),
16+
}
17+
)
18+
19+
def test_standard_inverse_transform(self):
20+
processor = DataProcessor(lookback=4, horizon=2, normalize="standard", validation_split=0)
21+
processor.prepare(self.df, target_col="value", time_col="date")
22+
normalized = (self.df["value"].to_numpy() - self.df["value"].mean()) / self.df["value"].std()
23+
np.testing.assert_allclose(processor.inverse_transform(normalized), self.df["value"], rtol=1e-6)
24+
25+
def test_inference_reuses_training_scaler_and_latest_window(self):
26+
processor = DataProcessor(lookback=4, horizon=2, normalize="minmax", validation_split=0)
27+
processor.prepare(self.df, target_col="value", time_col="date")
28+
scaler = dict(processor._scaler_params)
29+
30+
inference_df = self.df.tail(4).copy()
31+
inference_ds = processor.prepare_for_inference(inference_df, target_col="value", time_col="date")
32+
batches = list(inference_ds.as_numpy_iterator())
33+
34+
self.assertEqual(processor._scaler_params, scaler)
35+
self.assertEqual(len(batches), 1)
36+
self.assertEqual(batches[0][0].shape, (1, 4, 1))
37+
38+
def test_validation_split_is_chronological(self):
39+
processor = DataProcessor(
40+
lookback=3,
41+
horizon=1,
42+
normalize=None,
43+
validation_split=0.25,
44+
shuffle=False,
45+
batch_size=64,
46+
)
47+
train_ds, valid_ds = processor.prepare(self.df, target_col="value", time_col="date")
48+
train_x = next(iter(train_ds))[0].numpy()
49+
valid_x = next(iter(valid_ds))[0].numpy()
50+
self.assertLess(train_x[-1, -1, 0], valid_x[0, -1, 0])
51+
52+
def test_scaler_is_fitted_without_validation_leakage(self):
53+
processor = DataProcessor(lookback=3, horizon=1, normalize="minmax", validation_split=0.25)
54+
processor.prepare(self.df, target_col="value", time_col="date")
55+
self.assertEqual(processor._scaler_params["max"], 14.0)
56+
57+
58+
class AutoPreprocessorTest(unittest.TestCase):
59+
def test_forward_fill_preserves_unprocessed_columns(self):
60+
df = pd.DataFrame({"time": [1, 2, 3], "value": [1.0, np.nan, 3.0]})
61+
result = AutoPreprocessor(handle_missing="ffill", columns=["value"]).fit_transform(df)
62+
self.assertEqual(list(result.columns), ["time", "value"])
63+
self.assertEqual(result.loc[1, "value"], 1.0)
64+
65+
66+
if __name__ == "__main__":
67+
unittest.main()

tests/test_data/test_timeseries.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,66 @@ def test_multiple_targets(self):
551551
self.assertEqual(len(seq.target), 2)
552552
self.assertIn("value", seq.target)
553553
self.assertIn("value2", seq.target)
554+
x, y = seq[0]
555+
self.assertEqual(x.shape[-1], 2)
556+
self.assertEqual(y.shape[-1], 2)
557+
558+
def test_feature_columns_are_included_in_encoder_inputs(self):
559+
seq = TimeSeriesSequence(
560+
data=self.data,
561+
time_idx="date",
562+
target_column="value",
563+
feature_columns=["feature1", "feature2"],
564+
train_sequence_length=10,
565+
predict_sequence_length=1,
566+
)
567+
x, y = seq[0]
568+
self.assertEqual(x.shape[-1], 3)
569+
self.assertEqual(y.shape[-1], 1)
570+
np.testing.assert_allclose(x[0, :, 1], self.data["feature1"].iloc[:10])
571+
572+
def test_generated_feature_columns_are_included_in_encoder_inputs(self):
573+
config = {"date_features": {"type": "datetime", "features": ["dayofweek"], "time_col": "date"}}
574+
seq = TimeSeriesSequence(
575+
data=self.data,
576+
time_idx="date",
577+
target_column="value",
578+
feature_columns=["date_dayofweek"],
579+
train_sequence_length=10,
580+
predict_sequence_length=1,
581+
feature_config=config,
582+
)
583+
x, y = seq[0]
584+
self.assertEqual(x.shape[-1], 2)
585+
self.assertEqual(y.shape[-1], 1)
586+
np.testing.assert_allclose(x[0, :, 1], self.data["date"].dt.dayofweek.iloc[:10])
587+
588+
def test_one_step_horizon_and_boundary_continuity(self):
589+
data = pd.DataFrame({"time": [0, 1, 2, 4, 5], "value": np.arange(5)})
590+
seq = TimeSeriesSequence(
591+
data=data,
592+
time_idx="time",
593+
target_column="value",
594+
train_sequence_length=2,
595+
predict_sequence_length=1,
596+
)
597+
self.assertEqual(len(seq.sequences), 1)
598+
np.testing.assert_array_equal(seq.sequences[0][0][:, 0], [0, 1])
599+
np.testing.assert_array_equal(seq.sequences[0][1][:, 0], [2])
600+
601+
def test_inference_mode_uses_latest_complete_window(self):
602+
data = pd.DataFrame({"time": range(5), "value": np.arange(5)})
603+
seq = TimeSeriesSequence(
604+
data=data,
605+
time_idx="time",
606+
target_column="value",
607+
train_sequence_length=3,
608+
predict_sequence_length=2,
609+
mode="inference",
610+
)
611+
self.assertEqual(len(seq.sequences), 3)
612+
np.testing.assert_array_equal(seq.sequences[-1][0][:, 0], [2, 3, 4])
613+
self.assertEqual(seq.sequences[-1][1].shape, (2, 1))
554614

555615
def test_multiple_targets_as_list(self):
556616
"""Test target column provided as list."""

tests/test_models/test_auto_model.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import tempfile
12
import unittest
23

34
import numpy as np
@@ -70,3 +71,16 @@ def test_auto_model_for_uncertainty(self):
7071
x = tf.random.normal([2, 14, 4])
7172
output = model(x)
7273
print(output.shape)
74+
75+
def test_save_and_load_preserves_prediction_length(self):
76+
config = AutoConfig.for_model("rnn")
77+
model = AutoModel.from_config(config, predict_sequence_length=3)
78+
model.build_model(tf.keras.Input(shape=(8, 2)))
79+
80+
with tempfile.TemporaryDirectory() as tmpdir:
81+
model.save_pretrained(tmpdir)
82+
loaded = AutoModel.from_pretrained(tmpdir)
83+
output = loaded(tf.random.normal([2, 8, 2]))
84+
85+
self.assertEqual(loaded.predict_sequence_length, 3)
86+
self.assertEqual(output.shape, (2, 3, 1))

tfts/cli/forecasting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def predict(self, steps: Optional[int] = None, df: Optional[pd.DataFrame] = None
171171
Returns:
172172
Numpy array of shape ``(n_series, steps, n_targets)``.
173173
"""
174-
if not self._fitted and self.trainer is None:
174+
if not self._fitted:
175175
raise RuntimeError("Pipeline must be fitted before prediction. Call .fit() first.")
176176

177177
steps = steps or self.horizon

0 commit comments

Comments
 (0)