Skip to content

Forecast capping if calibrate forecast is too far from the uncalibrated - #2449

Merged
Kat-90 merged 10 commits into
metoppv:masterfrom
Kat-90:forecast_capping
Sep 17, 2026
Merged

Kat-90 merged 10 commits into
metoppv:masterfrom
Kat-90:forecast_capping

Conversation

@Kat-90

@Kat-90 Kat-90 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Addresses #GitHubissuenum

This change adds a forecast cap, if set, that caps the calibrated forecast to an amount away from the original uncalibrated forecast. It acts as a risk mitigation for erroneous observations e.g. Fiji in the global calibration.
This is an optional argument that doesn't need to affect calibration of other diagnostics unless set in the suite.

Testing:

  • Ran tests and they passed OK
  • Added new tests for the new feature(s)

CLA

  • If a new developer, signed up to CLA

@Kat-90 Kat-90 self-assigned this Sep 4, 2026
…ons, so that these values can be used as bounds to clip the calibrated forecast.

@gavinevans gavinevans 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 @Kat-90 👍

I've added a comment, as I don't think the original forecast that you're trying to compare to, is quite right, in terms of identifying the original forecast that we want to compare to. I've therefore created this PR (Kat-90#25) to your PR to identify the min and max within the original forecast and use these as the lower and bound when clipping the calibrated forecast.

Comment on lines +619 to +623
original_forecast = forecast_df.loc[
forecast_df[representation_name]
== forecast_df[representation_name].iloc[0],
self.target_name,
].values

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.

There's an assumption in this pandas selection that we only want to use the zeroth item in forecast_df[representation_name] as the original forecast. This could be the 5th percentile, for example, or a single realization, and we don't really want to be clipping based on a single realization or the 5th percentile. I've therefore created a PR to this PR (Kat-90#25) with the aim of finding the min and max across realization or percentile, and using these values as the lower and upper bounds for clipping.

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.

Thanks Gavin, I've reviewed and merged in your changes.

Correction to "Forecast capping if calibrate forecast is too far from the uncalibrated"
gavinevans
gavinevans previously approved these changes Sep 15, 2026
gavinevans
gavinevans previously approved these changes Sep 16, 2026

@mo-kbogue mo-kbogue 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.

Limited experience on the actual application of QRF in the suite but from what I can see it looks good. Just a few notes on possibly rewording some comments, and on the testing,

provided, the forecast period found in the first forecast cube
will be used.
max_allowed_difference (float, optional):
The maximum allowed difference for the forecast calibration. If not

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

minor note but I'd prefer the below;

Suggested change
The maximum allowed difference for the forecast calibration. If not
The maximum allowed difference between the uncalibrated and calibrated forecast. If not

unique_site_id_keys: The names of the coordinates that uniquely identify
each site, e.g. "wmo_id" or ["latitude", "longitude"].
max_allowed_difference (float, optional):
The maximum allowed difference for the forecast calibration. If not

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same as above, personally I'd go for;

Suggested change
The maximum allowed difference for the forecast calibration. If not
The maximum allowed difference between the uncalibrated and calibrated forecast. If not

provided, the first forecast period found in the forecast cube
will be used.
max_allowed_difference (float, optional):
The maximum allowed difference for the forecast calibration. If not

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same again

Suggested change
The maximum allowed difference for the forecast calibration. If not
The maximum allowed difference between the uncalibrated and calibrated forecast. If not

Comment on lines +1044 to +1048
[0.5],
np.array([6], dtype=np.float32),
np.array([17.0, 0.0], dtype=np.float32),
5.0,
np.array([11.0, 3.0], dtype=np.float32),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If I'm reading this correctly, we're inputting one single uncalibrated forecast value, 6, but our fake QRF calibrated forecast returns two values, 17 and 0? This test would maybe make more sense to me if the fake QRF calibrated forecast was 17 (rather than 17 and 0) and the expected output was 11 (rather than 11 and 3). Or equivalently, if the input forecast was [6.0, 6.0].
Some extra comments here might also just be useful, even if only for this first example, e.g the below;

Suggested change
[0.5],
np.array([6], dtype=np.float32),
np.array([17.0, 0.0], dtype=np.float32),
5.0,
np.array([11.0, 3.0], dtype=np.float32),
[0.5], #50th percentile, i.e median
np.array([6.0], [6.0], dtype=np.float32), #input uncalibrated forecast
np.array([17.0, 0.0], dtype=np.float32), #qrf calibrated forecast
5.0, #maximum difference
np.array([11.0, 3.0], dtype=np.float32), #expected output

),
(
[0.1, 0.5, 0.9],
np.array([6, 12, 18], dtype=np.float32),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As in the previous point, the uncalibrated and calibrated examples here not being the same length threw me off - might be better if they have the same format

),
(
[0.1, 0.5, 0.9],
np.array([6, 12, 18], dtype=np.float32),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same as above

# 18 + 5 = 23 is the upper bound for the first site, while 6 - 5 = 3 is
# the lower bound for the second site. Values beyond these limits are
# clipped, while points already inside the allowed range remain unchanged.
np.array([[23.0, 23.0, 22.0], [3.0, 3.0, 9.0]], dtype=np.float32),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If we're inputting the 10th, 50th and 90th percentile as specified above, would we not expect the result to be in ascending order? Maybe that goes beyond the scope of this PR? Could be worth including as a warning perhaps

mo-kbogue
mo-kbogue previously approved these changes Sep 17, 2026

@mo-kbogue mo-kbogue 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.

Thanks for the changes made - conversation offline helped me understand how this code works.

@mo-kbogue mo-kbogue 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.

Thanks for the changes made - conversation offline helped me understand how this code works.

@Kat-90
Kat-90 merged commit e68a8d4 into metoppv:master Sep 17, 2026
7 checks passed
gavinevans added a commit to gavinevans/improver that referenced this pull request Sep 18, 2026
…sion

* upstream/master:
  Spatial morphing (metoppv#2441)
  Stochastic noise extensions (metoppv#2442)
  Forecast capping if calibrate forecast is too far from the uncalibrated (metoppv#2449)
  Vicinity metadata from the threshold plugin (metoppv#2439)
  Update cluster sources bookkeeping, so that forecast periods are only stored in one forecast source. (metoppv#2437)
  Correct realization numbering bug in temporal interpolation (metoppv#2436)
  Modify precedence ordering when clustering (metoppv#2435)
MoseleyS added a commit that referenced this pull request Sep 18, 2026
* master:
  Spatial Morphing - refinement of Google FILM output (#2451)
  Spatial morphing (#2441)
  Stochastic noise extensions (#2442)
  Forecast capping if calibrate forecast is too far from the uncalibrated (#2449)
  Vicinity metadata from the threshold plugin (#2439)
  Update cluster sources bookkeeping, so that forecast periods are only stored in one forecast source. (#2437)
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.

3 participants