Skip to content

fix(arima): keep seasonal period in search_arima fallback (#1276) - #1285

Open
Ajay Mathuriya (ajaycodesitbetter) wants to merge 7 commits into
Nixtla:mainfrom
ajaycodesitbetter:issue/1276
Open

Ajay Mathuriya (ajaycodesitbetter) wants to merge 7 commits into
Nixtla:mainfrom
ajaycodesitbetter:issue/1276

Conversation

@ajaycodesitbetter

Copy link
Copy Markdown

Fixes #1276.

When the non-approximate refit gives infinite ic, search_arima recursed without period, so the fallback search used the default period=1 and returned a non-seasonal model. This passes period=period in that recursive call.

Adds a regression test that forces the fallback and checks that the seasonal period is kept for every model fitted (fails before the fix, passes after). The full tests/test_arima.py passes locally.

Note: the failing path is rare (stepwise=False, approximation=True, exact refit fails). I forced it with a mock in the test.

I used AI assistance to help write and verify this change; I reviewed it myself.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 01:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CLAassistant

CLAassistant commented Oct 14, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@jmoralez

Copy link
Copy Markdown
Contributor

Thanks for the fix! Could you also add an end-to-end test that goes through auto_arima_f with stepwise=False, so we also cover how the arguments reach search_arima and not only search_arima itself? Something like this fails on main (assert 1 == 12) and passes with your change:

def test_auto_arima_nonstepwise_fallback_keeps_seasonal_period(monkeypatch):
    original = arima_module.myarima
    state = {"failed": False}

    def myarima_refit_fails(*args, **kwargs):
        fit = original(*args, **kwargs)
        if kwargs.get("approximation") is False and not state["failed"]:
            state["failed"] = True
            fit = {**fit, "ic": math.inf}
        return fit

    monkeypatch.setattr(arima_module, "myarima", myarima_refit_fails)
    fit = arima_module.auto_arima_f(
        ap, period=12, stepwise=False, approximation=True, max_order=3
    )
    assert state["failed"]
    assert fit["arma"][4] == 12

@jmoralez José Morales (jmoralez) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix! I left a suggestion to simplify the tests a bit.

Comment thread tests/test_arima.py Outdated
Comment on lines +1526 to +1584
def test_search_arima_fallback_keeps_seasonal_period(monkeypatch):
# when the non-approximate refit of the best model has infinite ic,
# search_arima falls back to a full search that must keep the seasonal period
rng = np.random.default_rng(0)
season = 12
x = np.sin(np.arange(120) * 2 * np.pi / season) + rng.normal(size=120) * 0.3
original = arima_module.myarima
state = {"failed": False}
seen_periods = []

def myarima_refit_fails(*args, **kwargs):
seen_periods.append(kwargs["seasonal"]["period"])
fit = original(*args, **kwargs)
if kwargs.get("approximation") is False and not state["failed"]:
state["failed"] = True
fit = {**fit, "ic": math.inf}
return fit

monkeypatch.setattr(arima_module, "myarima", myarima_refit_fails)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fit = arima_module.search_arima(
x,
max_p=1,
max_q=1,
max_P=1,
max_Q=1,
max_order=3,
approximation=True,
offset=0,
period=season,
)
assert state["failed"]
# every model fitted, including those in the fallback search, uses the period
assert set(seen_periods) == {season}
assert fit["arma"][4] == season


def test_auto_arima_nonstepwise_fallback_keeps_seasonal_period(monkeypatch):
# end-to-end: the period given to auto_arima_f(stepwise=False) must survive
# the fallback search that search_arima runs when the exact refit fails
original = arima_module.myarima
state = {"failed": False}

def myarima_refit_fails(*args, **kwargs):
fit = original(*args, **kwargs)
if kwargs.get("approximation") is False and not state["failed"]:
state["failed"] = True
fit = {**fit, "ic": math.inf}
return fit

monkeypatch.setattr(arima_module, "myarima", myarima_refit_fails)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fit = auto_arima_f(
ap, period=12, stepwise=False, approximation=True, max_order=3
)
assert state["failed"]
assert fit["arma"][4] == 12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two tests repeat the same myarima wrapper, so this moves it into a fixture. It also limits the auto_arima_f grid to order 1 so the end-to-end test runs in ~1.5s instead of ~5s, checks that every model it fits uses period 12, and restores the blank lines before the next test block. Both tests still fail without the period=period fix.

Suggested change
def test_search_arima_fallback_keeps_seasonal_period(monkeypatch):
# when the non-approximate refit of the best model has infinite ic,
# search_arima falls back to a full search that must keep the seasonal period
rng = np.random.default_rng(0)
season = 12
x = np.sin(np.arange(120) * 2 * np.pi / season) + rng.normal(size=120) * 0.3
original = arima_module.myarima
state = {"failed": False}
seen_periods = []
def myarima_refit_fails(*args, **kwargs):
seen_periods.append(kwargs["seasonal"]["period"])
fit = original(*args, **kwargs)
if kwargs.get("approximation") is False and not state["failed"]:
state["failed"] = True
fit = {**fit, "ic": math.inf}
return fit
monkeypatch.setattr(arima_module, "myarima", myarima_refit_fails)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fit = arima_module.search_arima(
x,
max_p=1,
max_q=1,
max_P=1,
max_Q=1,
max_order=3,
approximation=True,
offset=0,
period=season,
)
assert state["failed"]
# every model fitted, including those in the fallback search, uses the period
assert set(seen_periods) == {season}
assert fit["arma"][4] == season
def test_auto_arima_nonstepwise_fallback_keeps_seasonal_period(monkeypatch):
# end-to-end: the period given to auto_arima_f(stepwise=False) must survive
# the fallback search that search_arima runs when the exact refit fails
original = arima_module.myarima
state = {"failed": False}
def myarima_refit_fails(*args, **kwargs):
fit = original(*args, **kwargs)
if kwargs.get("approximation") is False and not state["failed"]:
state["failed"] = True
fit = {**fit, "ic": math.inf}
return fit
monkeypatch.setattr(arima_module, "myarima", myarima_refit_fails)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fit = auto_arima_f(
ap, period=12, stepwise=False, approximation=True, max_order=3
)
assert state["failed"]
assert fit["arma"][4] == 12
@pytest.fixture
def exact_refit_fails(monkeypatch):
# makes the first non-approximate refit return an infinite ic, which forces
# search_arima into its fallback search, and records every seasonal period fitted
original = arima_module.myarima
state = {"failed": False, "periods": set()}
def myarima_refit_fails(*args, **kwargs):
state["periods"].add(kwargs["seasonal"]["period"])
fit = original(*args, **kwargs)
if kwargs.get("approximation") is False and not state["failed"]:
state["failed"] = True
fit = {**fit, "ic": math.inf}
return fit
monkeypatch.setattr(arima_module, "myarima", myarima_refit_fails)
return state
def test_search_arima_fallback_keeps_seasonal_period(exact_refit_fails):
rng = np.random.default_rng(0)
season = 12
x = np.sin(np.arange(120) * 2 * np.pi / season) + rng.normal(size=120) * 0.3
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fit = arima_module.search_arima(
x,
max_p=1,
max_q=1,
max_P=1,
max_Q=1,
max_order=3,
approximation=True,
offset=0,
period=season,
)
assert exact_refit_fails["failed"]
assert exact_refit_fails["periods"] == {season}
assert fit["arma"][4] == season
def test_auto_arima_nonstepwise_fallback_keeps_seasonal_period(exact_refit_fails):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fit = auto_arima_f(
ap,
period=12,
stepwise=False,
approximation=True,
max_p=1,
max_q=1,
max_P=1,
max_Q=1,
max_order=3,
)
assert exact_refit_fails["failed"]
assert exact_refit_fails["periods"] == {12}
assert fit["arma"][4] == 12

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Model] search_arima fallback search drops the seasonal period

4 participants