import numpy as np
import pandas as pd
from pathlib import Path
from datetime import datetime, timedelta
SEED = 42
N_OBSERVATIONS = 100
SEASONAL_PERIOD = 12
DATA_DIR = Path(__file__).parent / "data"
def generate_timestamps(n: int, start: str = "2020-01-01") -> list[datetime]:
start_date = datetime.fromisoformat(start)
return [start_date + timedelta(days=30 * i) for i in range(n)]
def generate_stationary(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 50.0
std = 5.0
return mean + rng.normal(0, std, n)
def generate_trend(n: int, rng: np.random.Generator) -> np.ndarray:
intercept = 10.0
slope = 0.5
noise_std = 3.0
t = np.arange(n)
return intercept + slope * t + rng.normal(0, noise_std, n)
def generate_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 50.0
amplitude = 10.0
noise_std = 2.0
t = np.arange(n)
seasonal = amplitude * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
return mean + seasonal + rng.normal(0, noise_std, n)
def generate_trend_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
intercept = 20.0
slope = 0.3
amplitude = 8.0
noise_std = 2.0
t = np.arange(n)
trend = slope * t
seasonal = amplitude * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
return intercept + trend + seasonal + rng.normal(0, noise_std, n)
def generate_seasonal_negative(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 5.0 amplitude = 10.0 noise_std = 1.0
t = np.arange(n)
seasonal = amplitude * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
return mean + seasonal + rng.normal(0, noise_std, n)
def generate_multiplicative_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
intercept = 50.0
slope = 0.5
t = np.arange(n)
level = intercept + slope * t
seasonal_factor = 1.0 + 0.3 * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
noise_factor = 1.0 + rng.normal(0, 0.02, n)
return level * seasonal_factor * noise_factor
def generate_intermittent(n: int, rng: np.random.Generator) -> np.ndarray:
demand_prob = 0.3
has_demand = rng.random(n) < demand_prob
mean_demand = 5.0
demand_sizes = rng.poisson(mean_demand, n) + 1
series = np.where(has_demand, demand_sizes, 0).astype(float)
return series
def generate_high_frequency(n: int, rng: np.random.Generator) -> np.ndarray:
daily_period = 24
weekly_period = 168
t = np.arange(n)
daily = 5.0 * np.sin(2 * np.pi * t / daily_period)
weekly = 3.0 * np.sin(2 * np.pi * t / weekly_period)
trend = 0.01 * t
noise = rng.normal(0, 1.5, n)
return 50.0 + trend + daily + weekly + noise
def generate_structural_break(n: int, rng: np.random.Generator) -> np.ndarray:
mean_before = 50.0
mean_after = 70.0 noise_std = 3.0
break_point = n // 2
values = np.zeros(n)
values[:break_point] = mean_before + rng.normal(0, noise_std, break_point)
values[break_point:] = mean_after + rng.normal(0, noise_std, n - break_point)
return values
def generate_long_memory(n: int, rng: np.random.Generator) -> np.ndarray:
d = 0.3
K = min(100, n) psi = np.zeros(K)
psi[0] = 1.0
for k in range(1, K):
psi[k] = psi[k-1] * (k - 1 + d) / k
epsilon = rng.normal(0, 1, n + K)
values = np.zeros(n)
for t in range(n):
values[t] = np.sum(psi * epsilon[t:t+K][::-1])
values = 50.0 + 5.0 * values
return values
def generate_noisy_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 50.0
amplitude = 5.0 noise_std = 8.0
t = np.arange(n)
seasonal = amplitude * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
return mean + seasonal + rng.normal(0, noise_std, n)
def generate_exponential_trend(n: int, rng: np.random.Generator) -> np.ndarray:
t = np.arange(n)
base = 10.0 * np.exp(0.02 * t) noise = rng.normal(0, 0.5, n) * (1.0 + 0.01 * t) return base + noise
def generate_damped_trend(n: int, rng: np.random.Generator) -> np.ndarray:
level = 20.0
b = 1.0
phi = 0.9
noise_std = 2.0
values = np.zeros(n)
cumulative_trend = 0.0
for t in range(n):
cumulative_trend += b * phi ** (t + 1)
values[t] = level + cumulative_trend + rng.normal(0, noise_std)
return values
def generate_strong_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 100.0
amplitude = 40.0
noise_std = 2.0
t = np.arange(n)
seasonal = amplitude * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
return mean + seasonal + rng.normal(0, noise_std, n)
def generate_quarterly_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
period = 4
mean = 60.0
amplitude = 12.0
noise_std = 3.0
t = np.arange(n)
seasonal = amplitude * np.sin(2 * np.pi * t / period)
return mean + seasonal + rng.normal(0, noise_std, n)
def generate_multiplicative_trend_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
t = np.arange(n)
level = 30.0 * np.exp(0.01 * t) seasonal_factor = 1.0 + 0.25 * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
noise_factor = 1.0 + rng.normal(0, 0.03, n)
return level * seasonal_factor * noise_factor
def generate_heteroscedastic(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 50.0
t = np.arange(n, dtype=float)
variance = 1.0 + 0.2 * t noise = rng.normal(0, 1, n) * np.sqrt(variance)
return mean + noise
def generate_random_walk(n: int, rng: np.random.Generator) -> np.ndarray:
increments = rng.normal(0, 1, n)
return 50.0 + np.cumsum(increments)
def generate_ar1(n: int, rng: np.random.Generator) -> np.ndarray:
phi = 0.7
noise_std = 2.0
values = np.zeros(n)
values[0] = 50.0
for t in range(1, n):
values[t] = 50.0 * (1 - phi) + phi * values[t - 1] + rng.normal(0, noise_std)
return values
def generate_outlier_series(n: int, rng: np.random.Generator) -> np.ndarray:
t = np.arange(n)
base = 50.0 + 0.3 * t + 8.0 * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
noise = rng.normal(0, 2.0, n)
values = base + noise
n_outliers = max(1, n // 20)
outlier_idx = rng.choice(n, size=n_outliers, replace=False)
outlier_signs = rng.choice([-1, 1], size=n_outliers)
outlier_magnitudes = rng.uniform(15, 30, size=n_outliers)
values[outlier_idx] += outlier_signs * outlier_magnitudes
return values
def generate_step_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 50.0
amplitude = 10.0
noise_std = 2.0
t = np.arange(n)
phase = (t % SEASONAL_PERIOD) / SEASONAL_PERIOD
seasonal = np.where(phase < 0.5, amplitude, -amplitude)
return mean + seasonal + rng.normal(0, noise_std, n)
def generate_bimodal_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 60.0
noise_std = 2.5
t = np.arange(n)
peak1 = 8.0 * np.sin(2 * np.pi * t / SEASONAL_PERIOD) peak2 = 5.0 * np.sin(4 * np.pi * t / SEASONAL_PERIOD + 1.0) return mean + peak1 + peak2 + rng.normal(0, noise_std, n)
def generate_asymmetric_seasonal(n: int, rng: np.random.Generator) -> np.ndarray:
mean = 50.0
amplitude = 12.0
noise_std = 2.0
t = np.arange(n)
phase = (t % SEASONAL_PERIOD) / SEASONAL_PERIOD
seasonal = amplitude * (1.0 - (1.0 - phase) ** 2) * 2.0 - amplitude
return mean + seasonal + rng.normal(0, noise_std, n)
def generate_seasonal_trend_break(n: int, rng: np.random.Generator) -> np.ndarray:
t = np.arange(n)
mid = n // 2
amplitude = 10.0
noise_std = 2.0
seasonal = amplitude * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
trend = np.zeros(n)
trend[:mid] = 0.5 * np.arange(mid)
trend[mid:] = trend[mid - 1] - 0.5 * np.arange(n - mid)
return 40.0 + trend + seasonal + rng.normal(0, noise_std, n)
def generate_low_count(n: int, rng: np.random.Generator) -> np.ndarray:
t = np.arange(n)
rate = 5.0 + 2.0 * np.sin(2 * np.pi * t / SEASONAL_PERIOD)
rate = np.maximum(rate, 0.5) values = rng.poisson(rate)
return values.astype(float)
def save_series(name: str, timestamps: list[datetime], values: np.ndarray) -> Path:
df = pd.DataFrame({
"timestamp": timestamps,
"value": values
})
filepath = DATA_DIR / f"{name}.csv"
df.to_csv(filepath, index=False)
print(f" Saved {name}.csv ({len(values)} observations)")
return filepath
def main():
print("Generating synthetic time series data...")
print(f" Seed: {SEED}")
print(f" Observations: {N_OBSERVATIONS}")
print(f" Seasonal period: {SEASONAL_PERIOD}")
print()
DATA_DIR.mkdir(parents=True, exist_ok=True)
rng = np.random.default_rng(SEED)
timestamps = generate_timestamps(N_OBSERVATIONS)
series_generators = [
("stationary", generate_stationary),
("trend", generate_trend),
("seasonal", generate_seasonal),
("trend_seasonal", generate_trend_seasonal),
("seasonal_negative", generate_seasonal_negative),
("multiplicative_seasonal", generate_multiplicative_seasonal),
("intermittent", generate_intermittent),
("high_frequency", generate_high_frequency),
("structural_break", generate_structural_break),
("long_memory", generate_long_memory),
("noisy_seasonal", generate_noisy_seasonal),
("exponential_trend", generate_exponential_trend),
("damped_trend", generate_damped_trend),
("strong_seasonal", generate_strong_seasonal),
("quarterly_seasonal", generate_quarterly_seasonal),
("multiplicative_trend_seasonal", generate_multiplicative_trend_seasonal),
("heteroscedastic", generate_heteroscedastic),
("random_walk", generate_random_walk),
("ar1", generate_ar1),
("outlier_series", generate_outlier_series),
("step_seasonal", generate_step_seasonal),
("bimodal_seasonal", generate_bimodal_seasonal),
("asymmetric_seasonal", generate_asymmetric_seasonal),
("seasonal_trend_break", generate_seasonal_trend_break),
("low_count", generate_low_count),
]
for name, generator in series_generators:
values = generator(N_OBSERVATIONS, rng)
save_series(name, timestamps, values)
print()
print(f"Data saved to: {DATA_DIR.absolute()}")
if __name__ == "__main__":
main()