Skip to content

fix: cap the auto yearly seasonality order at what the sampling rate supports - #2774

Open
NishchayMahor wants to merge 2 commits into
meta:mainfrom
NishchayMahor:fix/yearly-fourier-order-nyquist
Open

NishchayMahor wants to merge 2 commits into
meta:mainfrom
NishchayMahor:fix/yearly-fourier-order-nyquist

Conversation

@NishchayMahor

Copy link
Copy Markdown
Contributor

Fixes #2677.

In that issue the reporter takes a 48-month series, shifts every date forward by one
year, leaves y untouched, and the 12-month forecast total moves by -25%. One
point flips sign. @Fediuld found that yearly_seasonality=False makes it go away, and
@gboulmier then asked the question this PR answers: why would turning off a seasonality
the data clearly has make the fit more stable?

Why it happens

Monthly data has one sample per month, so a yearly cycle is sampled 12 times. Above the
Nyquist limit of that spacing, floor(365.25 / (2 * 30.4)) = 6 harmonics, the higher
harmonics alias onto lower ones. Their coefficients are then set by the prior rather
than by the data, so the fitted shape can move a long way for a small change in the
input. yearly_seasonality='auto' asks for order 10 regardless of spacing, so four of
those ten harmonics are unidentifiable.

Prophet already guards the other two seasonalities on exactly this basis. forecaster.py
declines weekly once min_dt >= Timedelta(weeks=1) and daily once
min_dt >= Timedelta(days=1). Yearly has no min_dt term at all. That inconsistency is
the argument here, not a general claim about what users do.

The change

set_auto_seasonalities caps the default yearly order at the Nyquist limit of the
observed minimum spacing. Weekly and daily drop the component entirely at that point;
yearly keeps the component and lowers the order instead, so monthly data still gets a
yearly shape. An explicitly requested order is left alone.

spacing order before after
hourly 10 10
daily 10 10
weekly 10 10
monthly 10 6
quarterly 10 2
annual 10 0

Effect on the reported series

Reporter's own data, dates shifted one year, y unchanged, uncertainty_samples=0:

before after
12-month total -25.1% -0.3%
5th point -104.5% (sign flip) +10.3%
7th point +56.4% +4.6%
min multiplicative term -1.0535 -0.4688
negative forecast produced yes no

The multiplicative term below -1 is what produced the sign flip: yhat = trend * (1 + m).

Test suite: 139 passed, 6 skipped, against a 138/6 baseline I measured on 5cf15c0.
The new test asserts the order for six frequencies plus the two override paths, and
fails on current main.

What I did not verify, and the tradeoffs

  • min_dt is the minimum gap, not the typical one. A monthly series with one stray
    1-day gap keeps order 10 and is unhelped. I verified that. It is the fail-safe
    direction and it matches how the weekly and daily guards already behave, but median
    spacing would bind more often if you would rather have that.
  • Monthly users' numbers change. Anyone on auto with monthly data gets a different
    yearly shape after this. An logger.info says so and names the override. If you would
    rather not move existing fits, the warning alone (no cap) is a one-line version of this
    PR and I am happy to cut it back to that.
  • This does not fix Unexpected change in predictions on close to identical data #2676. Different cause.
  • I have not benchmarked accuracy across a corpus of real monthly series. The claim here
    is identifiability and internal consistency, not that forecasts get better on average.
  • Two docs lines describing yearly order 10 as the default become qualified for coarse
    data. I left the docs alone pending a call on the approach.

…supports

A seasonality is only identifiable up to the Nyquist limit of the spacing
between observations. Monthly data carries 12 points a year, so 6 harmonics
can be resolved, but the auto order was 10 regardless of frequency. The top
four harmonics alias onto lower ones and their coefficients are set by the
prior rather than by the data, so the fitted shape moves a long way for a
small change in the input.

On the series in meta#2677, shifting the same 48 monthly values by one calendar
year moved the 12-month forecast total by -25.1% and one point by -104.5%,
flipping it negative because the multiplicative term reached -1.05. With the
order capped to 6 the same shift moves the total by -0.3% and nothing goes
negative.

Weekly and daily seasonality already decline to turn on once the spacing
reaches their period. This applies the same reasoning to yearly, keeping the
component and lowering the order so monthly data still gets a yearly shape.
Hourly, daily and weekly sampling are unaffected, and an explicitly requested
order is left alone.
@meta-cla meta-cla Bot added the CLA Signed label Oct 30, 2026
The weekly and daily guards already carry this suppression for the same
false positive on min_dt.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dramatic change in predictions on same data shifted by 1 year Unexpected change in predictions on close to identical data

1 participant