🎯 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")