Skip to content

[ML] Stabilize uncertain trend forecasts - #3189

Merged
valeriy42 merged 8 commits into
elastic:mainfrom
valeriy42:codex/issue-2772-forecast-trend
Sep 16, 2026
Merged

valeriy42 merged 8 commits into
elastic:mainfrom
valeriy42:codex/issue-2772-forecast-trend

Conversation

@valeriy42

@valeriy42 valeriy42 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #2772, where an uncertain fitted trend could dominate a long forecast and produce implausible curvature, including values outside a percentage metric's natural range.

Mathematical rationale

The trend component returns a polynomial regression prediction f(x) and its covariance-derived forecast variance. Previously, a forecast evaluated the fitted polynomial directly at every future time. This means a small but poorly determined quadratic coefficient is multiplied by x^2; over a long horizon that term can become much larger than the local signal even when the model is highly uncertain about it. The existing prediction-horizon guard does not cover this case: the requested duration can be operationally reasonable while the selected trend is still statistically unreliable to extrapolate.

The fix keeps the model's near-term prediction but attenuates its change from the first forecast point according to the fraction of variance explained by the fitted trend:

w(t) = V_trend / (V_trend + V_extrapolation(t))
y_fixed(t) = f(t0) + w(t) * (f(t) - f(t0))

Here t0 is the first forecast time, V_trend is the component's long-term variance, and V_extrapolation(t) is the additional uncertainty incurred by projecting the fitted polynomial from the regression origin to t. Thus w(t) is close to one when the trend is reliable, preserving the original prediction; it falls toward zero when extrapolation uncertainty dominates, smoothly anchoring the forecast at the boundary instead of allowing an uncertain high-order term to diverge. This uses no new persisted state, settings, metric bounds, or customer-wide runtime cost.

The covariance calculation must use the regression basis. The trend model is parameterized at its regression origin, so the variance of the change f(t) - f(t0) uses the basis delta:

[0, t - t0, t^2 - t0^2]

Using [1, t, t^2] instead incorrectly includes the intercept and computes the variance of an absolute prediction rather than the extrapolated change. This PR corrects that basis.

Behaviour and coverage

  • Exact zero extrapolation variance retains the original trend prediction (w = 1); this prevents a regression for deterministic affine trends.
  • Invalid or negative numerical variances take the conservative anchored fallback.
  • The forecast remains continuous at the first prediction point.
  • Added tests cover the restored [ML] Forecasting trend is unexpected given data #2772 state, prefix continuity, affine trends, and exact zero-uncertainty trends.

This targets the intended observability workload: ordinary trend forecasts remain unchanged when their extrapolation is well supported, while temporary changes and weakly identified curvature cannot dominate a long-horizon forecast.

@valeriy42
valeriy42 requested a review from tveasey September 10, 2026 15:11
@valeriy42 valeriy42 added >bug :ml auto-backport Automatically merge backport PRs when CI passes v9.4.8 labels Sep 10, 2026
@elasticsearchmachine

Copy link
Copy Markdown

Pinging @elastic/ml-core (Team:ML)

@elasticsearchmachine

Copy link
Copy Markdown

Hi @valeriy42, I've created a changelog YAML for you.

@valeriy42

Copy link
Copy Markdown
Contributor Author

old-vs-new-synthetic-forecast-comparison-10x-horizon.pdf

Synthetic examples before and after the change

@valeriy42 valeriy42 added the ci:run-qa-tests Run a subset of the QA tests label Sep 10, 2026

@tveasey tveasey 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.

I like the strategy. I have a proposal for simplifying the edge case handling, but otherwise LGTM.

Comment on lines +602 to +614
double extrapolationWeight{0.0};
if (extrapolationVariance >= 0.0 && std::isfinite(extrapolationVariance)) {
if (extrapolationVariance == 0.0) {
extrapolationWeight = 1.0;
} else if (longTermVariance > 0.0 && std::isfinite(longTermVariance)) {
// Preserve extrapolation while its uncertainty is comparable to
// the variation observed in the series.
extrapolationWeight = std::sqrt(
std::min(2.0 * longTermVariance / extrapolationVariance, 1.0));
extrapolationWeight = std::isfinite(extrapolationWeight) ? extrapolationWeight
: 0.0;
}
}

@tveasey tveasey Sep 11, 2026

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.

I find this kind of hard to follow. How about the equivalent:

double extrapolationWeight{0.0};
if (extrapolationVariance == 0.0) {
    extrapolationWeight = 1.0;
} else if (extrapolationVariance > 0.0 && std::isfinite(extrapolationVariance) &&
           longTermVariance > 0.0 && std::isfinite(longTermVariance)) {
    // Preserve extrapolation while its uncertainty is comparable to (within 2x)
    // the variation observed in the series.
    extrapolationWeight = std::sqrt(
        std::min(2.0 * longTermVariance / extrapolationVariance, 1.0));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied the flattened exclusive-case form. Zero still maps to weight 1; invalid/negative still fall through to 0. Dropped the post-sqrt isfinite check because the argument is in [0, 1] given the guards.

@valeriy42
valeriy42 enabled auto-merge (squash) September 16, 2026 14:34
@valeriy42
valeriy42 disabled auto-merge September 16, 2026 15:59
@valeriy42
valeriy42 enabled auto-merge (squash) September 16, 2026 15:59
@valeriy42
valeriy42 merged commit aca6006 into elastic:main Sep 16, 2026
25 checks passed
@elastic-vault-github-plugin-prod

Copy link
Copy Markdown
Contributor

💔 Some backports could not be created

Status Branch Result
8.19 Backport failed because of merge conflicts
9.4
9.5

Manual backport

To create the backport manually run:

backport --pr 3189

Questions ?

Please refer to the Backport tool documentation and see the Github Action logs for details

@elastic-vault-github-plugin-prod

Copy link
Copy Markdown
Contributor

💔 All backports failed

Status Branch Result
8.19 Backport failed because of merge conflicts

Manual backport

To create the backport manually run:

backport --pr 3189

Questions ?

Please refer to the Backport tool documentation and see the Github Action logs for details

elastic-vault-github-plugin-prod Bot added a commit that referenced this pull request Sep 16, 2026
## Summary

Fixes #2772, where an uncertain fitted trend could dominate a long forecast and produce implausible curvature, including values outside a percentage metric's natural range.

## Mathematical rationale

The trend component returns a polynomial regression prediction `f(x)` and its covariance-derived forecast variance.  Previously, a forecast evaluated the fitted polynomial directly at every future time.  This means a small but poorly determined quadratic coefficient is multiplied by `x^2`; over a long horizon that term can become much larger than the local signal even when the model is highly uncertain about it.  The existing prediction-horizon guard does not cover this case: the requested duration can be operationally reasonable while the selected trend is still statistically unreliable to extrapolate.

The fix keeps the model's near-term prediction but attenuates its *change from the first forecast point* according to the fraction of variance explained by the fitted trend:

```text
w(t) = V_trend / (V_trend + V_extrapolation(t))
y_fixed(t) = f(t0) + w(t) * (f(t) - f(t0))
```

Here `t0` is the first forecast time, `V_trend` is the component's long-term variance, and `V_extrapolation(t)` is the additional uncertainty incurred by projecting the fitted polynomial from the regression origin to `t`.  Thus `w(t)` is close to one when the trend is reliable, preserving the original prediction; it falls toward zero when extrapolation uncertainty dominates, smoothly anchoring the forecast at the boundary instead of allowing an uncertain high-order term to diverge.  This uses no new persisted state, settings, metric bounds, or customer-wide runtime cost.

The covariance calculation must use the regression basis.  The trend model is parameterized at its regression origin, so the variance of the change `f(t) - f(t0)` uses the basis delta:

```text
[0, t - t0, t^2 - t0^2]
```

Using `[1, t, t^2]` instead incorrectly includes the intercept and computes the variance of an absolute prediction rather than the extrapolated change.  This PR corrects that basis.

## Behaviour and coverage

- Exact zero extrapolation variance retains the original trend prediction (`w = 1`); this prevents a regression for deterministic affine trends.
- Invalid or negative numerical variances take the conservative anchored fallback.
- The forecast remains continuous at the first prediction point.
- Added tests cover the restored #2772 state, prefix continuity, affine trends, and exact zero-uncertainty trends.

This targets the intended observability workload: ordinary trend forecasts remain unchanged when their extrapolation is well supported, while temporary changes and weakly identified curvature cannot dominate a long-horizon forecast.

(cherry picked from commit aca6006)

Co-authored-by: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
valeriy42 added a commit that referenced this pull request Sep 17, 2026
## Summary

Fixes #2772, where an uncertain fitted trend could dominate a long forecast and produce implausible curvature, including values outside a percentage metric's natural range.

## Mathematical rationale

The trend component returns a polynomial regression prediction `f(x)` and its covariance-derived forecast variance.  Previously, a forecast evaluated the fitted polynomial directly at every future time.  This means a small but poorly determined quadratic coefficient is multiplied by `x^2`; over a long horizon that term can become much larger than the local signal even when the model is highly uncertain about it.  The existing prediction-horizon guard does not cover this case: the requested duration can be operationally reasonable while the selected trend is still statistically unreliable to extrapolate.

The fix keeps the model's near-term prediction but attenuates its *change from the first forecast point* according to the fraction of variance explained by the fitted trend:

```text
w(t) = V_trend / (V_trend + V_extrapolation(t))
y_fixed(t) = f(t0) + w(t) * (f(t) - f(t0))
```

Here `t0` is the first forecast time, `V_trend` is the component's long-term variance, and `V_extrapolation(t)` is the additional uncertainty incurred by projecting the fitted polynomial from the regression origin to `t`.  Thus `w(t)` is close to one when the trend is reliable, preserving the original prediction; it falls toward zero when extrapolation uncertainty dominates, smoothly anchoring the forecast at the boundary instead of allowing an uncertain high-order term to diverge.  This uses no new persisted state, settings, metric bounds, or customer-wide runtime cost.

The covariance calculation must use the regression basis.  The trend model is parameterized at its regression origin, so the variance of the change `f(t) - f(t0)` uses the basis delta:

```text
[0, t - t0, t^2 - t0^2]
```

Using `[1, t, t^2]` instead incorrectly includes the intercept and computes the variance of an absolute prediction rather than the extrapolated change.  This PR corrects that basis.

## Behaviour and coverage

- Exact zero extrapolation variance retains the original trend prediction (`w = 1`); this prevents a regression for deterministic affine trends.
- Invalid or negative numerical variances take the conservative anchored fallback.
- The forecast remains continuous at the first prediction point.
- Added tests cover the restored #2772 state, prefix continuity, affine trends, and exact zero-uncertainty trends.

This targets the intended observability workload: ordinary trend forecasts remain unchanged when their extrapolation is well supported, while temporary changes and weakly identified curvature cannot dominate a long-horizon forecast.

(cherry picked from commit aca6006)

Co-authored-by: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
@valeriy42

Copy link
Copy Markdown
Contributor Author

💚 All backports created successfully

Status Branch Result
8.19

Questions ?

Please refer to the Backport tool documentation

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

Labels

auto-backport Automatically merge backport PRs when CI passes backport-pending >bug ci:run-qa-tests Run a subset of the QA tests :ml v8.19.22 v9.4.8 v9.5.5 v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ML] Forecasting trend is unexpected given data

3 participants