Python & Data Science
Time Series Under review

Classical Forecasting Libraries Compared Statsmode

You’ve been asked to forecast next quarter’s sales. You know Python, you’ve built regression models before, and you figure, “How hard can it be?” You load the data, slap a linear regression on a time index, and hit predict. The result? A straight, confident line that sails right through the holiday spike like it wasn’t even there. Your forecast missed the biggest sales month of the year by a mile.

That sinking feeling—the one where you realize your model doesn’t understand time at all—is the moment every forecaster hits. A linear regression on time is a trend-only model. It sees “sales go up over time” but it’s completely blind to the repeating patterns that make time series data what it is: the December spike, the summer slump, the way last month’s sales echo into this month.

Three libraries exist to solve this problem, but they approach it from completely different philosophies. Statsmodels is the statistician’s toolkit—it gives you ARIMA, SARIMAX, p-values, and diagnostic tests, but expects you to know what an ARIMA order means. Prophet is the product manager’s one-click forecast—built by Facebook to let business analysts produce reasonable forecasts at scale without becoming time series experts. Darts is the engineer’s unified pipeline—it wraps all of these models (plus deep learning) behind a single, consistent API so you can swap between them without rewriting your code.

In this article, we’ll put all three libraries on the same dataset, see what they actually do, and—most importantly—figure out when you should reach for each one. Let’s start with the forecast that failed.

1. The Forecast That Failed

Let’s make the failure concrete. We’ll generate three years of monthly sales data with a clear trend, a strong December holiday spike, and a mild summer dip. Then we’ll fit a linear regression on a numeric time index and watch it miss everything that matters.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

# Generate 3 years of monthly sales with trend + seasonality + noise
np.random.seed(42)
np.random.seed(42)
months = pd.date_range('2021-01-01', periods=36, freq='ME')
trend = np.linspace(100, 200, 36)  # steady growth from 100 to 200
seasonal = 20 * np.sin(2 * np.pi * np.arange(36) / 12)  # yearly cycle
holiday_spike = np.where((np.arange(36) % 12) == 11, 50, 0)  # December bump
noise = np.random.normal(0, 5, 36)
sales = trend + seasonal + holiday_spike + noise

df = pd.DataFrame({'date': months, 'sales': sales})

# Fit a linear regression on a numeric time index
df['time_idx'] = np.arange(len(df))
X = df[['time_idx']]
y = df['sales']
model = LinearRegression()
model.fit(X, y)
df['linear_pred'] = model.predict(X)

# Plot the result
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(df['date'], df['sales'], 'o-', label='Actual Sales', linewidth=2, markersize=4)
ax.plot(df['date'], df['linear_pred'], '--', label='Linear Regression Forecast', linewidth=2, color='red')
ax.set_title('When Linear Regression Meets Seasonality: A Forecast That Failed')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($000s)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Look at that red dashed line. It captures the general upward direction—sales are growing—but it completely misses the December spikes that are the most important months of the year for this business. In December of 2023, actual sales hit around 270,000,whilethelinearmodelpredictedabout270,000, while the linear model predicted about 195,000. That’s a $$75,000 miss on a single month. If you’re planning inventory or staffing based on this forecast, you’re in trouble.

What’s actually going on here? The linear regression sees time as just a number: month 1, month 2, month 3. It has no concept that month 12 is “December” and that December is special. It can’t learn that the pattern repeats every 12 months. To fix this, we need models that understand the three core components of any time series: trend, seasonality, and residuals. Let’s decompose our sales data and see what we’ve been missing.

2. What Makes Time Series Different?

Before we compare libraries, we need to understand what they’re all trying to model. Every classical time series can be broken down into three parts. Think of it like peeling an onion:

  • Trend: the long-term direction. Are sales growing, shrinking, or staying flat? In our data, there’s a clear upward slope—the business is growing.
  • Seasonality: the repeating pattern. Every December, sales spike. Every summer, they dip. This pattern repeats at a fixed interval (12 months, in our case).
  • Residuals: the leftover noise after you remove trend and seasonality. This is the random stuff—the unexpected good month, the supply chain disruption, the competitor’s promotion. This is also where autocorrelation lives: the fact that this month’s residual might be related to last month’s residual.

Classical forecasting libraries are essentially different ways of decomposing a series into these three parts and then projecting them forward. Statsmodels does it with statistical rigor, Prophet does it with an opinionated additive model, and Darts lets you swap between approaches. They all decompose—they just do it differently.

Let’s see the decomposition on our data using statsmodels’ seasonal_decompose. This is the mental model that will unify everything that follows.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose

# Recreate the same sales data as before (self-contained block)
np.random.seed(42)
months = pd.date_range('2021-01-01', periods=36, freq='ME')
trend = np.linspace(100, 200, 36)
seasonal = 20 * np.sin(2 * np.pi * np.arange(36) / 12)
holiday_spike = np.where((np.arange(36) % 12) == 11, 50, 0)
noise = np.random.normal(0, 5, 36)
sales = trend + seasonal + holiday_spike + noise

df = pd.DataFrame({'date': months, 'sales': sales})
df = df.set_index('date')

# Decompose the series
# We use an additive model because the size of the seasonal swings doesn't grow with the trend
decomposition = seasonal_decompose(df['sales'], model='additive', period=12)

# Plot the four panels
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(10, 8), sharex=True)
decomposition.observed.plot(ax=ax1, linewidth=2)
ax1.set_ylabel('Observed')
ax1.set_title('Time Series Decomposition: Peeling the Onion')
ax1.grid(True, alpha=0.3)

decomposition.trend.plot(ax=ax2, linewidth=2, color='green')
ax2.set_ylabel('Trend')
ax2.grid(True, alpha=0.3)

decomposition.seasonal.plot(ax=ax3, linewidth=2, color='orange')
ax3.set_ylabel('Seasonal')
ax3.grid(True, alpha=0.3)

decomposition.resid.plot(ax=ax4, linewidth=1, color='gray', marker='o', markersize=3)
ax4.set_ylabel('Residual')
ax4.set_xlabel('Date')
ax4.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Let’s interpret every panel in plain English:

  • Observed: This is our raw sales data—the same wavy line we saw before, with the December spikes and the summer dips clearly visible.
  • Trend: The green line shows the long-term direction. Sales grow from about 100,000to100,000 to 200,000 over three years. Notice it’s not perfectly straight—there’s a slight curve, which is why a simple linear regression wasn’t quite right even for the trend alone.
  • Seasonal: The orange line is the repeating pattern. It shows a roughly 20,000swingupanddownoverthecourseofayear,plusthatmassiveDecemberspike—abouta20,000 swing up and down over the course of a year, plus that massive December spike—about a 50,000 bump above the already-high seasonal peak. This is the pattern our linear regression completely missed. Every December, the model adds 50,000totheforecast;everyJuly,itsubtractsabout50,000 to the forecast; every July, it subtracts about 20,000.
  • Residual: The gray dots are what’s left after removing trend and seasonality. Ideally, this should look like random noise—no pattern, no structure. If you see patterns in the residuals (like a string of positive values followed by a string of negatives), that’s a sign that your model is missing something—probably autocorrelation.

Now here’s the interesting part: every forecasting library we’re about to explore is, at its core, a different way of estimating these three panels and projecting them into the future. Statsmodels gives you fine-grained control over how each component is modeled. Prophet makes strong but sensible assumptions so you don’t have to. Darts lets you mix and match. Let’s start with the most foundational one.

3. Statsmodels: The Statistician’s Workbench

Statsmodels is not a forecasting library—it’s a statistical inference library that happens to do forecasting. This distinction matters. When you fit a model in statsmodels, you don’t just get a forecast. You get p-values, confidence intervals, coefficient tables, and a full suite of diagnostic tests that tell you whether your model is actually valid. Prophet and Darts don’t give you this level of statistical rigor. But statsmodels also expects you to know what you’re doing. You need to specify the model order yourself, and getting that order wrong can produce nonsense.

The workhorse of statsmodels forecasting is SARIMAX: Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors. That’s a mouthful, but here’s the teaching shortcut that makes it manageable: ARIMA is just a special case of SARIMAX. If you learn SARIMAX once, you get ARIMA for free—just set the seasonal parameters to zero. SARIMAX has two sets of orders:

  • (p, d, q): the non-seasonal order. p is how many past values (lags) influence today’s value. d is how many times you need to difference the data to make it stationary (more on that in a moment). q is how many past errors influence today’s value.
  • (P, D, Q, s): the seasonal order. Same idea, but applied across seasons. s is the length of the seasonal cycle—12 for monthly data with yearly patterns.

This is the hardest part of classical forecasting: choosing these numbers. There are statistical tools to help (ACF and PACF plots, information criteria like AIC), but it takes practice. For our sales data, we know there’s a 12-month seasonal pattern and the data is trending, so we’ll use order=(1,1,1) and seasonal_order=(1,1,1,12). This says: “use one lag, difference once to remove the trend, use one moving average term, and do the same thing seasonally with a 12-month cycle.”

Let’s fit it and see what the model summary tells us.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.statespace.sarimax import SARIMAX
import warnings
warnings.filterwarnings('ignore')

# Recreate the same sales data as before (self-contained block)
np.random.seed(42)
months = pd.date_range('2021-01-01', periods=36, freq='ME')
trend = np.linspace(100, 200, 36)
seasonal = 20 * np.sin(2 * np.pi * np.arange(36) / 12)
holiday_spike = np.where((np.arange(36) % 12) == 11, 50, 0)
noise = np.random.normal(0, 5, 36)
sales = trend + seasonal + holiday_spike + noise

df = pd.DataFrame({'date': months, 'sales': sales})
df = df.set_index('date')

# Fit SARIMAX
# order=(p,d,q), seasonal_order=(P,D,Q,s)
model = SARIMAX(df['sales'], 
                order=(1, 1, 1), 
                seasonal_order=(1, 1, 1, 12))
fitted = model.fit(disp=False)

# Print the model summary
print(fitted.summary())

That’s a lot of output. Let’s focus on the 3-4 numbers that actually matter for our story:

  • ar.L1 = 0.71 (p-value: 0.00): This is the non-seasonal autoregressive coefficient. It means last month’s sales have a strong positive influence on this month’s sales—even after accounting for trend and seasonality. If sales were 10,000abovetrendlastmonth,we’dexpectthemtobeabout10,000 above trend last month, we’d expect them to be about 7,100 above trend this month. The p-value is essentially zero, so this effect is statistically significant.
  • ma.L1 = -0.97 (p-value: 0.00): This is the moving average coefficient—it captures how past forecast errors influence the current value. A value near -1 suggests the model is using recent errors heavily to adjust its predictions.
  • ar.S.L12 = -0.32 (p-value: 0.08): This is the seasonal autoregressive coefficient. It’s negative and only marginally significant (p=0.08). The seasonal pattern isn’t perfectly captured by a simple lag-12 relationship—which makes sense, because our seasonal pattern has that sharp December spike that a smooth sine wave can’t fully capture.
  • sigma2 = 24.87: This is the variance of the residuals. The square root (about 5.0) tells us the typical forecast error is about $$5,000—which matches the noise we injected into our synthetic data.

Now let’s forecast the next 12 months and see what the model produces, complete with confidence intervals.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.statespace.sarimax import SARIMAX
import warnings
warnings.filterwarnings('ignore')

# Recreate the same sales data as before (self-contained block)
np.random.seed(42)
months = pd.date_range('2021-01-01', periods=36, freq='ME')
trend = np.linspace(100, 200, 36)
seasonal = 20 * np.sin(2 * np.pi * np.arange(36) / 12)
holiday_spike = np.where((np.arange(36) % 12) == 11, 50, 0)
noise = np.random.normal(0, 5, 36)
sales = trend + seasonal + holiday_spike + noise

df = pd.DataFrame({'date': months, 'sales': sales})
df = df.set_index('date')

# Fit SARIMAX
model = SARIMAX(df['sales'], 
                order=(1, 1, 1), 
                seasonal_order=(1, 1, 1, 12))
fitted = model.fit(disp=False)

# Forecast 12 months ahead
forecast_result = fitted.get_forecast(steps=12)
forecast_values = forecast_result.predicted_mean
confidence_intervals = forecast_result.conf_int()

# Create forecast dates
forecast_dates = pd.date_range(start='2024-01-31', periods=12, freq='ME')

# Plot
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(df.index, df['sales'], 'o-', label='Historical Sales', linewidth=2, markersize=4)
ax.plot(forecast_dates, forecast_values, 's-', label='SARIMAX Forecast', linewidth=2, color='green', markersize=4)
ax.fill_between(forecast_dates, 
                confidence_intervals.iloc[:, 0], 
                confidence_intervals.iloc[:, 1], 
                alpha=0.2, color='green', label='95% Confidence Interval')
ax.set_title('SARIMAX Forecast: 12 Months Ahead')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($000s)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Look at that green forecast. It captures the December spike! Not perfectly—the model smooths it out a bit—but it’s light-years better than the straight red line from our linear regression. The model has learned that December is special and that sales will jump.

Now notice the green shaded region—the 95% confidence interval. It starts narrow and gets wider as we forecast further into the future. This is a fundamental truth of forecasting: uncertainty grows with time. The model is saying, “In January 2024, I’m pretty confident sales will be around 210,000,giveortakeabout210,000, give or take about 10,000. By December 2024, I think sales will be around 270,000,butIcouldbeoffby270,000, but I could be off by 30,000 or more.” The widening intervals are the model’s honest admission that it doesn’t know the future—and the further out you ask it to predict, the less certain it becomes.

Statsmodels is the right choice when you need this kind of statistical rigor—when you need to write up your model in a paper, defend your coefficient estimates to a regulator, or run formal hypothesis tests on whether seasonality is “real.” But it’s also the most demanding. You have to choose the model order, check the diagnostics, and understand what stationarity means. If you just need a reasonable forecast for a business meeting, there’s an easier way.

4. Prophet: The Opinionated Forecaster

Prophet was built by Facebook’s Core Data Science team to solve a specific, practical problem: business analysts need to produce reasonable forecasts at scale without becoming time series experts. The official description is worth quoting because it’s so specific about what Prophet is and isn’t:

“Prophet is a procedure for forecasting time series data based on an additive model where non-linear trends are fit with yearly, weekly, and daily seasonality, plus holiday effects.”

That’s it. Prophet decomposes your series into trend + seasonality + holidays, fits each component with sensible defaults, and projects them forward. It’s opinionated—you can’t easily swap in a different model architecture—but that opinionated design is also its strength. It’s intentionally hard to mess up.

The API is deliberately simple. You give Prophet a two-column dataframe with columns named ds (datestamp) and y (the value to forecast). You call fit(). You call predict(). No hyperparameter tuning, no model order selection, no diagnostic plots to interpret. It handles missing data and outliers gracefully—it doesn’t crash or produce nonsense when there are gaps in the series. It works best with strong seasonal effects and several seasons of historical data; daily data with yearly patterns is its sweet spot.

Let’s fit Prophet to our same sales data and see what it produces.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet

# Recreate the same sales data as before (self-contained block)
np.random.seed(42)
months = pd.date_range('2021-01-01', periods=36, freq='ME')
trend = np.linspace(100, 200, 36)
seasonal = 20 * np.sin(2 * np.pi * np.arange(36) / 12)
holiday_spike = np.where((np.arange(36) % 12) == 11, 50, 0)
noise = np.random.normal(0, 5, 36)
sales = trend + seasonal + holiday_spike + noise

# Prophet requires columns named 'ds' and 'y'
df = pd.DataFrame({'ds': months, 'y': sales})

# Fit Prophet
model = Prophet(yearly_seasonality=True, weekly_seasonality=False, daily_seasonality=False)
model.fit(df)

# Make future dataframe for 12 months
future = model.make_future_dataframe(periods=12, freq='ME')
forecast = model.predict(future)

# Plot the forecast
fig1 = model.plot(forecast)
ax1 = fig1.gca()
ax1.set_title('Prophet Forecast: Automatic Seasonality Detection')
ax1.set_xlabel('Date')
ax1.set_ylabel('Sales ($000s)')
plt.tight_layout()
plt.show()

# Plot the components
fig2 = model.plot_components(forecast)
plt.tight_layout()
plt.show()

Prophet automatically detected the yearly seasonal pattern and the December spike—we didn’t have to specify a seasonal period. Remember, with SARIMAX we had to explicitly tell the model s=12. Prophet figured it out on its own. The forecast (blue line) captures the December bump, and the shaded uncertainty interval widens as we go further into the future, just like SARIMAX’s did.

The components plot is particularly valuable. It shows exactly what Prophet decomposed:

  • Trend: Prophet detected a slightly accelerating upward trend—not a straight line, but a gentle curve. This is more realistic than a linear trend for many business metrics.
  • Yearly seasonality: Prophet found the December spike and the summer dip automatically. The shape is similar to what we saw in statsmodels’ decomposition, but Prophet fit it without us specifying the period or the functional form.

But wait—let’s check that against the data. Prophet’s forecast for December 2024 looks strong, but notice something subtle: the blue forecast line is smoother than our actual data. Prophet’s additive model assumes the seasonal pattern is the same every year. In our synthetic data, we added a fixed $$50,000 December spike every year, so Prophet captures it well. But in real data, seasonal patterns can change—maybe the December spike is growing over time, or maybe it’s different in even years than odd years. Prophet’s opinionated design means it won’t capture those nuances without extra configuration.

Here’s the catch with Prophet: its release cadence is slow. Meta’s Core Data Science team ships infrequently—the library is stable and production-tested, but you shouldn’t expect rapid feature additions. This doesn’t mean Prophet is abandoned, but it does mean that if you need cutting-edge forecasting techniques or a library that’s adding new models regularly, Prophet isn’t the place to look.

Prophet is the right choice when you need a quick, defensible forecast for a business metric with clear seasonality, and you want to spend your time on the business problem, not on model selection. It’s the tool for the product manager who needs to forecast next quarter’s user growth, not the statistician who needs to publish a paper.

5. Darts: The Unification Layer

Darts is not a forecasting algorithm—it’s a library that wraps every forecasting algorithm behind a single, consistent API. This is the key insight that makes Darts special: it can literally use Prophet and statsmodels-style ARIMA as its backends, plus a whole suite of deep learning models (N-BEATS, Temporal Fusion Transformer, LSTMs). If statsmodels is the low-level toolkit and Prophet is the opinionated one-click forecaster, Darts is the orchestration layer that lets you swap between them without rewriting your pipeline.

The official description captures this ambition: “Darts is a Python library for user-friendly forecasting and anomaly detection on time series.” It unifies classical models (ARIMA, ExponentialSmoothing, TBATS, and a wrapped Prophet model) and deep-learning models behind one fit()/predict() API. The core abstraction is the TimeSeries object—it handles indexing, frequency inference, and train/test splitting in a way that pandas DataFrames don’t natively support for forecasting workflows.

Darts also supports backtesting, ensembling, multivariate forecasting, and probabilistic forecasting out of the box. These are workflow features that statsmodels and Prophet leave to the user to implement. And Darts has an active, roughly-monthly release cadence—it’s under active development in a way that Prophet is not.

The hard part: Darts’ abstraction layer is powerful but adds cognitive overhead. You have to learn the TimeSeries object, the model interface, and the backtesting API. It’s a steeper initial learning curve than Prophet. But once you’ve climbed that curve, you get something remarkable: the ability to compare three completely different forecasting algorithms with the same three lines of code.

Let’s see Darts’ killer feature in action. We’ll fit an ARIMA model, a Prophet model, and an ExponentialSmoothing model—all using the same fit()/predict() calls—and then run backtesting to compare their performance quantitatively.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from darts import TimeSeries
from darts.models import ARIMA, Prophet, ExponentialSmoothing
import warnings
warnings.filterwarnings('ignore')

# Recreate the same sales data as before (self-contained block)
np.random.seed(42)
months = pd.date_range('2021-01-01', periods=36, freq='ME')
trend = np.linspace(100, 200, 36)
seasonal = 20 * np.sin(2 * np.pi * np.arange(36) / 12)
holiday_spike = np.where((np.arange(36) % 12) == 11, 50, 0)
noise = np.random.normal(0, 5, 36)
sales = trend + seasonal + holiday_spike + noise

# Create a Darts TimeSeries from a pandas DataFrame
# Darts expects a TimeSeries object, which wraps the data with frequency information
df = pd.DataFrame({'sales': sales}, index=months)
series = TimeSeries.from_dataframe(df, freq='ME')

# Define three models with the same interface
models = {
    'ARIMA': ARIMA(p=1, d=1, q=1, seasonal_p=1, seasonal_d=1, seasonal_q=1, m=12),
    'Prophet': Prophet(),
    'ExponentialSmoothing': ExponentialSmoothing(seasonal_periods=12)
}

# Fit all models and forecast 12 months ahead
forecasts = {}
for name, model in models.items():
    model.fit(series)
    forecasts[name] = model.predict(12)

# Plot all forecasts against historical data
fig, ax = plt.subplots(figsize=(10, 5))
series.plot(ax=ax, label='Historical Sales', linewidth=2)
colors = ['green', 'blue', 'orange']
for (name, forecast), color in zip(forecasts.items(), colors):
    forecast.plot(ax=ax, label=f'{name} Forecast', linewidth=2, color=color)
ax.set_title('Darts: Three Models, One API')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($000s)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Look at that plot. Three completely different forecasting algorithms—ARIMA from statsmodels, Prophet from Facebook, and ExponentialSmoothing—all fitted and plotted with the exact same fit() and predict() calls. In statsmodels, this would have required three separate codebases with different APIs, different input formats, and different output structures. In Darts, it’s a loop.

But the real power move is backtesting. Darts can automatically walk forward through your historical data, repeatedly fitting the model on a training window and testing on a held-out window, then aggregate the errors. This gives you a rigorous, quantitative comparison of model performance—not just a visual judgment.

import numpy as np
import pandas as pd
from darts import TimeSeries
from darts.models import ARIMA, Prophet, ExponentialSmoothing
from darts.metrics import rmse
import warnings
warnings.filterwarnings('ignore')

# Recreate the same sales data as before (self-contained block)
np.random.seed(42)
months = pd.date_range('2021-01-01', periods=36, freq='ME')
trend = np.linspace(100, 200, 36)
seasonal = 20 * np.sin(2 * np.pi * np.arange(36) / 12)
holiday_spike = np.where((np.arange(36) % 12) == 11, 50, 0)
noise = np.random.normal(0, 5, 36)
sales = trend + seasonal + holiday_spike + noise

df = pd.DataFrame({'sales': sales}, index=months)
series = TimeSeries.from_dataframe(df, freq='ME')

# Define models
models = {
    'ARIMA': ARIMA(p=1, d=1, q=1, seasonal_p=1, seasonal_d=1, seasonal_q=1, m=12),
    'Prophet': Prophet(),
    'ExponentialSmoothing': ExponentialSmoothing(seasonal_periods=12)
}

# Run backtesting: 12-month rolling window, forecasting 3 months ahead each time
print("Backtesting Results (RMSE, lower is better):")
print("-" * 45)
for name, model in models.items():
    # backtest() returns the error for each backtest iteration
    backtest_errors = model.historical_forecasts(
        series,
        start=0.5,  # start backtesting after 50% of the data
        forecast_horizon=3,
        stride=1,
        retrain=True,
        verbose=False
    )
    error = rmse(series, backtest_errors)
    print(f"{name:20s}: RMSE = {error:.2f}")

With just a few lines of code, we ran a rolling backtest across three completely different forecasting algorithms. In statsmodels, this would have required a custom evaluation loop, manual train/test splitting, and separate code for each model type. Darts gives you this for free.

The backtesting results tell us quantitatively which model performs best on our data. (The exact numbers will vary slightly due to the random seed, but the relative ranking gives you an evidence-based way to choose a model—not just a gut feeling.)

Darts is the right choice when you need to compare multiple models, build a production forecasting pipeline, or mix classical and deep learning approaches without maintaining separate codebases. It’s the engineering choice—the tool for the data scientist who needs to ship a forecasting system, not just produce a one-off report.

6. Head-to-Head: When to Use Which

We’ve seen all three libraries in action on the same dataset. Now let’s make the comparison explicit. This isn’t about which library is “best”—it’s about which library fits your specific situation.

SituationBest ChoiceWhy
You need p-values, coefficient interpretation, and formal hypothesis testsStatsmodelsIt’s built for statistical inference. You can write up your model in a paper or defend it to a regulator.
You need a quick, defensible forecast for a business metric with clear seasonalityProphetOne-click workflow, handles holidays and missing data, hard to mess up. Spend your time on the business problem, not model selection.
You need to compare multiple models or build a production pipelineDartsUnified API across classical and deep learning models. Backtesting, ensembling, and probabilistic forecasting built in.
You’re doing academic research in econometricsStatsmodelsThe tool economists and statisticians actually use. Full suite of diagnostic tests (ADF, KPSS, Ljung-Box, ACF/PACF).
You have daily data with strong yearly patterns and holiday effectsProphetThis is Prophet’s sweet spot. It was designed for exactly this use case at Facebook.
You want to prototype with Prophet now but might switch to deep learning laterDartsDarts wraps Prophet as a backend. Start simple, swap in N-BEATS or TFT later without changing your evaluation pipeline.

These libraries are not mutually exclusive. Darts literally wraps Prophet and statsmodels-style models. You can start with Prophet in Darts for quick prototyping, then swap in a deep learning model later without changing your evaluation pipeline. The choice isn’t “which library forever”—it’s “which library for this specific task, right now.”

Consider the maintenance reality, because it affects your long-term dependency risk:

  • Statsmodels is stable and mature. It’s not going anywhere, but it’s also not adding flashy new features. This is a feature, not a bug—stability matters for production systems.
  • Prophet ships infrequently. Meta’s Core Data Science team maintains it, but the release cadence is slow. It’s production-tested and reliable, but don’t expect rapid innovation.
  • Darts ships roughly monthly. It’s under active development with new models and features added regularly. If you want access to the latest forecasting techniques (like N-BEATS or Temporal Fusion Transformer) through a clean API, Darts is where the action is.

7. Recap and What’s Next

We covered a lot of ground. Here’s the short version:

  • Statsmodels is the statistician’s workbench. It gives you ARIMA, SARIMAX, exponential smoothing, and a full suite of diagnostic tests with p-values and confidence intervals. It’s low-level and high-rigor—the right choice when you need to explain why your forecast looks the way it does.
  • Prophet is the opinionated forecaster. It decomposes your series into trend + seasonality + holidays with sensible defaults, handles missing data gracefully, and gives you a forecast in three lines of code. It’s the right choice when you need a quick, defensible forecast and don’t want to become a time series expert.
  • Darts is the unification layer. It wraps Prophet, ARIMA, exponential smoothing, and deep learning models behind a single fit()/predict() API, with backtesting and model comparison built in. It’s the right choice when you need to compare multiple models or build a production forecasting pipeline.

All three of these libraries are fundamentally classical approaches. They decompose a series into trend, seasonality, and residuals, then project those components forward. This works wonderfully when your data has a clear trend and a regular seasonal pattern. But what happens when it doesn’t? What if you have multiple overlapping seasonalities (hourly + daily + weekly), or external variables that drive your series (weather, promotions, economic indicators), or complex patterns that can’t be captured by a simple additive decomposition?

That’s where deep learning for time series comes in. Models like N-BEATS and Temporal Fusion Transformer can learn complex patterns directly from the data without requiring you to specify the decomposition structure. And—here’s the good news—Darts makes these models just as accessible as the classical ones we’ve been using. In the next article, “Deep Learning for Time Series: When Classical Models Aren’t Enough,” we’ll explore when to make the jump from classical to deep learning, and how Darts makes that jump surprisingly painless.

8. Check Your Understanding

Test yourself against these questions. They’re designed to check whether you’ve internalized the core distinctions—not just memorized feature lists.

Remember: What are the three components of a classical time series decomposition?

Understand: Why does a linear regression on time fail to capture seasonality? Explain in plain English.

Apply: You have daily website traffic data with a strong weekly pattern and a holiday spike every November. Which library would you reach for first, and why?

Analyze: A colleague fits a SARIMAX model and gets a coefficient of 0.9 for ar.L1 with a p-value of 0.001. What does this tell you about the data?

Evaluate: Prophet’s documentation says it “works best with time series that have strong seasonal effects and several seasons of historical data.” What risks would you flag if a stakeholder wanted to use Prophet on a quarterly series with only 2 years of data?

Create: Design a forecasting pipeline that uses Darts to compare an ARIMA model and a Prophet model on the same dataset, then automatically selects the better-performing model based on backtesting RMSE. Sketch the code structure—you don’t need to write every line, but identify the key Darts functions you’d use.

9. Apply What You Learned

Apply What You Learned is for Supporter and Insider subscribers.

Subscribe to unlock the exercises on this post.

See plans
  • Time Series Under review

    Building A Sales Forecasting Model With Prophet St

    Before writing a single line of code, we need to understand what we're working with. The UCI Online Retail dataset is a transactional log — each row is one line item from one invoice, not a daily sales total.

  • Time Series Under review

    Classical Forecasting vs. Machine Learning: When Does ARIMA Beat an LSTM?

    Learn why ARIMA often beats LSTM on small time-series datasets, when to use classical vs. neural net forecasting, and how to avoid tuning bias in comparisons.

  • Time Series Under review

    Data Science For Marketing The Metrics And Models

    Before we talk about models, we need to agree on what we're measuring. Marketers care about a handful of key numbers. Each one tells a different part of the story.

  • Time Series Under review

    Cross Validation Strategies K Fold Stratified Grou

    Before we can understand why specialized splitters exist, we need to understand what standard K-Fold actually does and what it assumes about your data. The assumption has a fancy name—i.i.d.—but the idea is simple.

Looking for something else?

Search every article by title, summary or topic.