|
| 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 | + # ) |
0 commit comments