Skip to content

Commit 8fda0ab

Browse files
authored
opt the tft model benchmark
1 parent a47beb6 commit 8fda0ab

25 files changed

Lines changed: 779 additions & 94 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ CHECK_DIRS := tfts examples tests
77
style: ## Run formatters and linters (black, isort, flake8, pre-commit)
88
black $(CHECK_DIRS)
99
isort $(CHECK_DIRS)
10-
flake8 $(check_dirs)
10+
flake8 $(CHECK_DIRS)
1111
pre-commit run --all-files
1212

1313
## Run all unit tests

examples/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Dive deeper with these notebooks:
1313
- [single step prediction](https://nbviewer.org/github/LongxingTan/Time-series-prediction/blob/master/examples/notebooks/single_step_weather_prediction.ipynb): A guided example on predicting the next time point in weather data.
1414
- [multi steps prediction](https://nbviewer.org/github/LongxingTan/Time-series-prediction/blob/master/examples/notebooks/multi_steps_sales_prediction.ipynb): Learn how to forecast multiple future time points in a sales dataset.
1515

16+
## 📊 Benchmark
17+
- [Kaggle - Forecasting Sticker Sales](https://www.kaggle.com/competitions/playground-series-s5e1)
18+
1619

1720
## 🏆 More examples
1821
Check out these advanced examples and competition-winning implementations:

examples/benchmarks/CMI_detect_sleep_states/README.md

Whitespace-only changes.

examples/benchmarks/CMI_detect_sleep_states/conf.yaml

Whitespace-only changes.

examples/benchmarks/CMI_detect_sleep_states/dataset.py

Whitespace-only changes.

examples/benchmarks/forecasting_sticker_sales/README.md

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
seed: 315
2+
3+
data:
4+
data_path: "data.csv"
5+
target_column: "target"
6+
freq: 'h'
7+
8+
model:
9+
name: bert
10+
predict_sequence_length: 32
11+
n_layers: 2
12+
hidden_size: 128
13+
n_features: 10
14+
n_output: 1
15+
16+
17+
training:
18+
batch_size: 128
19+
epochs: 30
20+
learning_rate: 0.001
21+
loss: "MSE"
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import warnings
2+
3+
from joblib import Parallel, delayed
4+
import numpy as np
5+
from omegaconf import OmegaConf
6+
import pandas as pd
7+
import requests
8+
from sklearn.preprocessing import StandardScaler
9+
from tensorflow.keras.utils import Sequence
10+
11+
warnings.filterwarnings("ignore")
12+
13+
14+
# https://www.kaggle.com/code/cdeotte/transformer-starter-lb-0-052
15+
class TimeSeriesProcessor:
16+
def __init__(self, use_internet=True, path="./"):
17+
self.use_internet = use_internet
18+
self.path = path
19+
self.scales = {}
20+
self.gdp_data = None
21+
22+
def fetch_gdp(self, df):
23+
"""Unified GDP fetching logic."""
24+
alpha3_map = {
25+
"Canada": "CAN",
26+
"Finland": "FIN",
27+
"Italy": "ITA",
28+
"Kenya": "KEN",
29+
"Norway": "NOR",
30+
"Singapore": "SGP",
31+
}
32+
df["alpha3"] = df["country"].map(alpha3_map)
33+
df["year"] = df["date"].dt.year
34+
years = df["year"].unique()
35+
36+
if self.use_internet:
37+
gdp_dict = {}
38+
for country, a3 in alpha3_map.items():
39+
try:
40+
url = f"https://api.worldbank.org/v2/country/{a3}/indicator/NY.GDP.PCAP.CD?date={min(years)}:{max(years)}&format=json" # noqa: E501,E231
41+
res = requests.get(url).json()[1]
42+
for entry in res:
43+
gdp_dict[(a3, int(entry["date"]))] = entry["value"]
44+
except Exception as e:
45+
print(f"Error fetching GDP for {a3}: {e}")
46+
self.gdp_data = gdp_dict
47+
else:
48+
# Assume local file exists
49+
gdp_df = pd.read_csv(f"{self.path}gdp.csv").set_index("alpha3")
50+
self.gdp_data = gdp_df.to_dict()
51+
52+
return df
53+
54+
def process_features(self, df, is_train=True):
55+
"""Calculates GDP ratios and store-based normalization."""
56+
df = df.copy()
57+
df["date"] = pd.to_datetime(df["date"])
58+
59+
if self.gdp_data is None:
60+
df = self.fetch_gdp(df)
61+
62+
df["GDP"] = df.apply(lambda x: self.gdp_data.get((x["alpha3"], x["year"]), 1.0), axis=1)
63+
64+
# 1. GDP Normalization
65+
df["scaled_target"] = df["num_sold"] / df["GDP"]
66+
67+
# 2. Store Ratio (calculate during train, apply during test)
68+
if is_train:
69+
self.store_ratios = df.groupby("store")["scaled_target"].mean().to_dict()
70+
71+
df["scaled_target"] /= df["store"].map(self.store_ratios)
72+
73+
# 3. Kenya Fudge Factor
74+
df.loc[df["country"] == "Kenya", "scaled_target"] *= 1.15
75+
76+
return df
77+
78+
def dataframe_to_tensor(self, df):
79+
"""
80+
Pivots the dataframe into a 3D tensor: (Products, Time, Series)
81+
Series = Country + Store combinations.
82+
"""
83+
# Create a unique key for each Country/Store combination
84+
df["series_key"] = df["country"] + "_" + df["store"]
85+
86+
products = sorted(df["product"].unique())
87+
series_keys = sorted(df["series_key"].unique())
88+
89+
tensor_list = []
90+
for prod in products:
91+
# Efficient pivoting instead of nested loops
92+
subset = df[df["product"] == prod].pivot(index="date", columns="series_key", values="scaled_target")
93+
94+
# Save scaling params per product
95+
if prod not in self.scales:
96+
self.scales[prod] = {"mean": subset.values.mean(), "std": subset.values.std()}
97+
98+
# Standard Scale
99+
scaled_val = (subset.values - self.scales[prod]["mean"]) / self.scales[prod]["std"]
100+
tensor_list.append(scaled_val)
101+
102+
return np.stack(tensor_list), products, series_keys
103+
104+
def inverse_transform(self, pred, product_name, country, store, date):
105+
"""Reverses all transformations to get the original num_sold scale."""
106+
# 1. Reverse Standard Scale
107+
val = (pred * self.scales[product_name]["std"]) + self.scales[product_name]["mean"]
108+
109+
# 2. Reverse Kenya Factor
110+
if country == "Kenya":
111+
val /= 1.15
112+
113+
# 3. Reverse Store Ratio
114+
val *= self.store_ratios[store]
115+
116+
# 4. Reverse GDP
117+
year = pd.to_datetime(date).year
118+
# Note: You'd need a helper to get alpha3 from country
119+
alpha3 = {
120+
"Canada": "CAN",
121+
"Finland": "FIN",
122+
"Italy": "ITA",
123+
"Kenya": "KEN",
124+
"Norway": "NOR",
125+
"Singapore": "SGP",
126+
}[country]
127+
val *= self.gdp_data.get((alpha3, year), 1.0)
128+
129+
return val
130+
131+
132+
class TimeSeriesDataset(Sequence):
133+
def __init__(
134+
self,
135+
data,
136+
mode="train", # "train" or "test"
137+
product_idx=0,
138+
train_sequence_length=1440,
139+
predict_sequence_length=32,
140+
batch_size=32,
141+
):
142+
self.data = data[product_idx] # Shape: (Time, Series)
143+
self.mode = mode
144+
self.product_idx = product_idx
145+
self.train_sequence_length = train_sequence_length
146+
self.predict_sequence_length = predict_sequence_length
147+
self.batch_size = batch_size
148+
149+
nans = np.isnan(self.data).astype("float32")
150+
self.combined_data = np.stack([np.nan_to_num(self.data), nans], axis=-1)
151+
152+
def __len__(self):
153+
return int(np.ceil(self.data.shape[1] / self.batch_size))
154+
155+
def __getitem__(self, idx):
156+
if self.mode == "train":
157+
return self._get_train_batch()
158+
else:
159+
return self._get_test_batch(idx)
160+
161+
def _get_train_batch(self):
162+
X = np.zeros((self.batch_size, self.train_sequence_length, 2), dtype="float32")
163+
y = np.zeros((self.batch_size, self.predict_sequence_length), dtype="float32")
164+
165+
for i in range(self.batch_size):
166+
series_idx = np.random.randint(0, self.data.shape[1])
167+
start = np.random.randint(0, self.data.shape[0] - self.train_sequence_length - self.predict_sequence_length)
168+
169+
X[i] = self.combined_data[start : start + self.train_sequence_length, series_idx, :]
170+
y[i] = self.combined_data[
171+
start + self.train_sequence_length : start + self.train_sequence_length + self.predict_sequence_length,
172+
series_idx,
173+
0,
174+
]
175+
return X, y
176+
177+
def _get_test_batch(self, idx):
178+
"""Returns the LAST train_len for each category for prediction."""
179+
start_series = idx * self.batch_size
180+
end_series = min((idx + 1) * self.batch_size, self.data.shape[1])
181+
actual_bs = end_series - start_series
182+
183+
X = np.zeros((actual_bs, self.train_sequence_length, 2), dtype="float32")
184+
185+
for i, s_idx in enumerate(range(start_series, end_series)):
186+
# Always take the very tail of the data
187+
X[i] = self.combined_data[-self.train_sequence_length :, s_idx, :]
188+
189+
return X
190+
191+
192+
if __name__ == "__main__":
193+
# 1. Process Data
194+
processor = TimeSeriesProcessor(use_internet=True)
195+
df_train = pd.read_csv("/kaggle/input/playground-series-s5e1/train.csv")
196+
df_processed = processor.process_features(df_train, is_train=True)
197+
tensor, product_names, series_names = processor.dataframe_to_tensor(df_processed)
198+
199+
# 2. Create Train Dataset for Product 0
200+
train_gen = TimeSeriesDataset(tensor, mode="train", product_idx=0)
201+
202+
# 3. Create Test Dataset (the last window for all series in Product 0)
203+
test_gen = TimeSeriesDataset(tensor, mode="test", product_idx=0)
204+
205+
# # 4. Predict
206+
# predictions = model.predict(test_gen) # (Total Series, pred_len)
207+
208+
# # 5. Reverse Scaling for a specific prediction
209+
# raw_pred = processor.inverse_transform(
210+
# pred=predictions[0, 0],
211+
# product_name=product_names[0],
212+
# country="Canada",
213+
# store="KaggleMart",
214+
# date="2026-01-01"
215+
# )
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import argparse
2+
import math
3+
import random
4+
5+
from dataset import DataReader, TrainDataset
6+
import numpy as np
7+
from omegaconf import OmegaConf
8+
import pandas as pd
9+
import tensorflow as tf
10+
11+
from tfts import AutoConfig, AutoModel, Pipeline, set_seed
12+
13+
14+
def parse_args():
15+
parser = argparse.ArgumentParser(description="tfts forecasting")
16+
parser.add_argument("--config_path", type=str, default="conf.yaml", help="Path to base config file")
17+
parser.add_argument("--debug", type=bool, default=False, help="Enable debug mode")
18+
parser.add_argument("--is_training", type=bool, default=True, help="Whether to train or predict")
19+
parser.add_argument("--model_name", type=str, default=None, help="Model name, e.g., BERT, LSTM")
20+
parser.add_argument("--batch_size", type=int, default=None, help="Batch size")
21+
parser.add_argument("--epochs", type=int, default=None, help="Number of epochs")
22+
23+
args = parser.parse_args()
24+
return args
25+
26+
27+
# def run_inference(product_idx):
28+
# """Runs the recursive prediction for a specific product."""
29+
# # Ensure history has the 2nd channel (NaN indicator)
30+
# # history_tensor shape: (5, 2557, 18) -> Needs expansion to (1, LEN, 18, 2)
31+
# data = np.expand_dims(self.history_tensor, axis=-1)
32+
# nans = np.isnan(data).astype('float32')
33+
# data = np.concatenate([data, nans], axis=-1)
34+
35+
# product_preds = np.zeros((18, self.PRED_LEN * self.STEPS))
36+
# bad_rows = []
37+
38+
# for jj in range(18):
39+
# # Get last window of training data for this series
40+
# # Shape: (1, LEN, 2)
41+
# current_window = data[product_idx:product_idx+1, -self.LEN:, jj, :].copy()
42+
43+
# if np.isnan(current_window[:, :, 0]).sum() == self.LEN:
44+
# bad_rows.append(jj)
45+
# continue
46+
47+
# series_predictions = []
48+
49+
# for step in range(self.STEPS):
50+
# # Predict next 32 days
51+
# # Input shape: (1, LEN, 2)
52+
# p2 = self.model(np.nan_to_num(current_window))
53+
# p2 = p2.numpy().reshape((1, self.PRED_LEN, 1))
54+
55+
# # Add dummy NaN indicator (0.0) to predictions for the next step
56+
# p2_with_nan = np.concatenate([p2, np.zeros_like(p2)], axis=-1)
57+
# series_predictions.append(p2_with_nan)
58+
59+
# # Update window: Slide window forward
60+
# # Remove oldest 32, append newest 32
61+
# current_window = np.concatenate([current_window[:, self.PRED_LEN:, :], p2_with_nan], axis=1)
62+
63+
# # Combine all steps and remove the NaN indicator channel
64+
# product_preds[jj, :] = np.concatenate([z[:, :, 0] for z in series_predictions], axis=1).flatten()
65+
66+
# # Handle bad rows (series with no training data)
67+
# if bad_rows:
68+
# fill_val = np.nanmean(product_preds, axis=0)
69+
# for r in bad_rows:
70+
# product_preds[r, :] = fill_val
71+
72+
# return product_preds
73+
74+
75+
def main():
76+
args = parse_args()
77+
cfg = OmegaConf.load(args.config_path)
78+
79+
set_seed(cfg.seed)
80+
81+
data_reader = DataReader()
82+
train_df = data_reader.load_data("/kaggle/input/playground-series-s5e1/train.csv")
83+
train_df = data_reader.add_features(train_df)
84+
data_tensor = data_reader.reshape_to_tensor(train_df)
85+
86+
train_dataset = TrainDataset(
87+
data=data_tensor, product_idx=0, batch_size=64, train_sequence_length=1440, predict_sequence_length=32
88+
)
89+
90+
forecaster = Pipeline(cfg)
91+
92+
forecaster.train(train_dataset=train_dataset)
93+
94+
95+
if __name__ == "__main__":
96+
main()

examples/notebooks/single_step_stock_prediction.ipynb

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,35 @@
44
"cell_type": "markdown",
55
"metadata": {},
66
"source": [
7-
"# Financial stock prediction"
7+
"# Financial stock prediction\n",
8+
"- the data is from binance API\n"
89
]
10+
},
11+
{
12+
"cell_type": "code",
13+
"execution_count": null,
14+
"metadata": {
15+
"vscode": {
16+
"languageId": "plaintext"
17+
}
18+
},
19+
"outputs": [],
20+
"source": [
21+
"import pandas as pd\n",
22+
"\n",
23+
"from tfts import AutoConfig, AutoModel"
24+
]
25+
},
26+
{
27+
"cell_type": "code",
28+
"execution_count": null,
29+
"metadata": {
30+
"vscode": {
31+
"languageId": "plaintext"
32+
}
33+
},
34+
"outputs": [],
35+
"source": []
936
}
1037
],
1138
"metadata": {

0 commit comments

Comments
 (0)