🎯 Weighted Observations
Farseer natively supports observation weights, allowing you to give more importance to recent or reliable data points. This is perfect for emphasizing recent trends, downweighting outliers, or incorporating data quality information.
Basic Usage
import polars as pl
import numpy as np
from datetime import datetime
from farseer import Farseer
# Create data with weights
np.random.seed(42)
n = 100
df = pl.DataFrame({
'ds': pl.date_range(datetime(2020, 1, 1), periods=n, interval='1d', eager=True),
'y': np.random.randn(n).cumsum() + 50,
'weight': [2.0 if i < 50 else 1.0 for i in range(n)] # Weight recent data more
})
# Fit with weights - Farseer automatically detects 'weight' column
m = Farseer()
m.fit(df)
# Make predictions
future = m.make_future_dataframe(periods=30)
forecast = m.predict(future)
print(forecast.select(['ds', 'yhat', 'yhat_lower', 'yhat_upper']).tail())
Downweighting Outliers
import polars as pl
import numpy as np
from farseer import Farseer
# Create data with some outliers
np.random.seed(42)
n = 365
dates = pl.date_range(datetime(2020, 1, 1), periods=n, interval='1d', eager=True)
y = np.random.randn(n).cumsum() + 100
# Add outliers
outlier_indices = [50, 100, 200, 300]
y[outlier_indices] += np.random.randn(len(outlier_indices)) * 50
# Create weights: downweight outliers
weights = np.ones(n)
weights[outlier_indices] = 0.1 # Give outliers much less weight
df = pl.DataFrame({'ds': dates, 'y': y, 'weight': weights})
m = Farseer()
m.fit(df)
forecast = m.predict(m.make_future_dataframe(periods=90))
Use Cases
- Recency weighting: Give more importance to recent observations in evolving trends
- Data quality: Downweight suspicious or low-quality measurements
- Confidence scores: Incorporate measurement uncertainty
- Business logic: Emphasize important time periods (e.g., peak season)
📈 Custom Regressors
Add additional variables to your forecast model to capture effects beyond trend and seasonality. Regressors can be continuous or binary variables that influence your time series.
Adding Multiple Regressors
import polars as pl
import numpy as np
from datetime import datetime
from farseer import Farseer, regressor_coefficients
# Create sample data
np.random.seed(42)
n = 365 * 2
dates = pl.date_range(datetime(2020, 1, 1), periods=n, interval='1d', eager=True)
# Base trend and seasonality
trend = np.arange(n) * 0.3 + 100
yearly = 20 * np.sin(2 * np.pi * np.arange(n) / 365.25)
# Create regressors
is_weekend = (dates.dt.weekday() >= 5).cast(pl.Float64)
temperature = 15 + 10 * np.sin(2 * np.pi * np.arange(n) / 365.25) + np.random.randn(n) * 3
promo = np.zeros(n)
promo[np.random.choice(n, 50, replace=False)] = 1
# Combine with regressor effects
y = trend + yearly + (-10 * is_weekend) + (0.5 * temperature) + (15 * promo) + np.random.randn(n) * 3
df = pl.DataFrame({
'ds': dates,
'y': y,
'is_weekend': is_weekend,
'temperature': temperature,
'promo': promo
})
# Create model and add regressors
m = Farseer(yearly_seasonality=True)
m.add_regressor('is_weekend', prior_scale=10.0, mode='additive')
m.add_regressor('temperature', prior_scale=10.0, mode='additive')
m.add_regressor('promo', prior_scale=5.0, mode='additive')
# Split train/test
train_size = int(n * 0.8)
train = df[:train_size]
test = df[train_size:]
# Fit and forecast
m.fit(train)
forecast = m.predict(test)
# Get regressor coefficients
coeffs = regressor_coefficients(m)
print(coeffs)
Tips for Regressors
- Standardization: Continuous variables are auto-standardized; binary (0/1) are not
- Prior scale: Controls regularization; larger = more flexible, smaller = more conservative
- Mode: Use 'additive' for most cases, 'multiplicative' when effect scales with level
- Future values: Ensure regressor values are available for forecast period
🔄 Manual Changepoints
When you know specific dates where your time series trend changed (e.g., product launches, policy changes), you can specify them manually for more accurate forecasts.
Specifying Known Changepoints
import polars as pl
import numpy as np
from datetime import datetime
from farseer import Farseer
# Create data with known trend changes
np.random.seed(42)
n = 365 * 3
dates = pl.date_range(datetime(2020, 1, 1), periods=n, interval='1d', eager=True)
# Create trend with changepoints at specific dates
y = []
base = 100
for i, date in enumerate(dates.to_list()):
if date < datetime(2021, 1, 1):
slope = 0.5 # Moderate growth
y_val = base + slope * i
elif date < datetime(2022, 1, 1):
slope = 1.5 # Rapid growth (policy change)
days = (date - datetime(2021, 1, 1)).days
y_val = base + 0.5 * 365 + slope * days
else:
slope = 0.3 # Slow growth (market saturation)
days = (date - datetime(2022, 1, 1)).days
y_val = base + 0.5 * 365 + 1.5 * 365 + slope * days
yearly_season = 10 * np.sin(2 * np.pi * i / 365.25)
y.append(y_val + yearly_season + np.random.randn() * 5)
df = pl.DataFrame({'ds': dates, 'y': y})
# Specify changepoints at known dates
manual_changepoints = ['2021-01-01', '2022-01-01']
m = Farseer(
changepoints=manual_changepoints,
yearly_seasonality=True,
weekly_seasonality=False
)
# Split and fit
train_size = int(n * 0.85)
train = df[:train_size]
test = df[train_size:]
m.fit(train)
forecast = m.predict(test)
print(f"Changepoints used: {m.changepoints}")
print(f"Test MAE: {np.mean(np.abs(test['y'] - forecast['yhat'][:len(test)])):.2f}")
Automatic vs Manual Changepoints
# Automatic: Let Farseer find changepoints
m_auto = Farseer(
n_changepoints=25, # Number of potential changepoints
changepoint_range=0.8, # Consider first 80% of data
changepoint_prior_scale=0.05 # Flexibility (higher = more flexible)
)
# Manual: Specify exact dates
m_manual = Farseer(
changepoints=['2021-01-01', '2021-06-15', '2022-01-01']
)
# Hybrid: Use automatic but with custom parameters
m_hybrid = Farseer(
n_changepoints=15,
changepoint_range=0.9,
changepoint_prior_scale=0.1
)
🎄 Holiday Effects
Model the impact of holidays and special events on your time series with customizable windows before and after each event.
Creating a Holiday DataFrame
import polars as pl
from datetime import datetime
from farseer import Farseer
# Define holidays
holidays = pl.DataFrame({
'holiday': ['Christmas', 'Christmas', 'New Year', 'New Year',
'Black Friday', 'Black Friday', 'Thanksgiving', 'Thanksgiving'],
'ds': [
datetime(2020, 12, 25), datetime(2021, 12, 25),
datetime(2021, 1, 1), datetime(2022, 1, 1),
datetime(2020, 11, 27), datetime(2021, 11, 26),
datetime(2020, 11, 26), datetime(2021, 11, 25)
],
'lower_window': 0, # Days before
'upper_window': [1, 1, 1, 1, 3, 3, 2, 2] # Days after
})
# Create model with holidays
m = Farseer(holidays=holidays, yearly_seasonality=True)
# Fit model
m.fit(df)
forecast = m.predict(future)
Holiday Windows
# Christmas: affect day itself and day after
christmas = pl.DataFrame({
'holiday': 'Christmas',
'ds': pl.date_range(datetime(2020, 1, 1), datetime(2023, 12, 31), interval='1y', eager=True)
.map_elements(lambda x: datetime(x.year, 12, 25)),
'lower_window': 0,
'upper_window': 1
})
# Black Friday: affect 3 days after
black_friday = pl.DataFrame({
'holiday': 'Black Friday',
'ds': [datetime(2020, 11, 27), datetime(2021, 11, 26), datetime(2022, 11, 25)],
'lower_window': 0,
'upper_window': 3
})
# Combine holidays
all_holidays = pl.concat([christmas, black_friday])
m = Farseer(holidays=all_holidays)
m.fit(train_df)
📅 Custom Seasonality
Beyond yearly, weekly, and daily seasonality, you can add any custom periodic pattern such as monthly, quarterly, or domain-specific cycles.
Adding Monthly Seasonality
from farseer import Farseer
# Create model with custom monthly seasonality
m = Farseer(
yearly_seasonality=True,
weekly_seasonality=False,
daily_seasonality=False
)
# Add monthly seasonality (period = 30.5 days)
m.add_seasonality(
name='monthly',
period=30.5,
fourier_order=5 # Number of Fourier components
)
m.fit(df)
forecast = m.predict(future)
Quarterly Business Cycles
from farseer import Farseer
m = Farseer()
# Quarterly cycle (91.25 days)
m.add_seasonality(
name='quarterly',
period=91.25,
fourier_order=8,
mode='additive'
)
# Can also add conditional seasonality
# For example, different patterns for weekdays vs weekends
m.add_seasonality(
name='weekly_weekday',
period=7,
fourier_order=3,
condition_name='is_weekday' # Requires 'is_weekday' column in data
)
m.fit(df)
🚀 Performance Optimization
Farseer is built for speed with automatic multithreading and Polars DataFrames. Here are tips to maximize performance.
Use Polars for Best Performance
import polars as pl # Recommended
import pandas as pd
from farseer import Farseer
from datetime import datetime
# Polars (Recommended - 5-10x faster)
df_polars = pl.DataFrame({
'ds': pl.date_range(datetime(2020, 1, 1), periods=1000, interval='1d', eager=True),
'y': range(1000)
})
# Pandas (Still supported)
df_pandas = pd.DataFrame({
'ds': pd.date_range('2020-01-01', periods=1000),
'y': range(1000)
})
# Both work, but Polars is faster!
m = Farseer()
m.fit(df_polars) # Faster ⚡
Multithreading is Automatic
# Farseer automatically uses all CPU cores
# No configuration needed!
m = Farseer()
m.fit(large_df) # Automatically parallelized
# Performance scales with:
# - Number of CPU cores
# - Dataset size
# - Model complexity
Batch Processing
from concurrent.futures import ProcessPoolExecutor
from farseer import Farseer
def fit_forecast(df_segment):
"""Fit and forecast a single time series"""
m = Farseer()
m.fit(df_segment)
return m.predict(m.make_future_dataframe(periods=30))
# Process multiple time series in parallel
segments = [df1, df2, df3, df4]
with ProcessPoolExecutor() as executor:
forecasts = list(executor.map(fit_forecast, segments))
print(f"Processed {len(forecasts)} time series")