Examples - Farseer

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