Skip to content

Commit b6fbcb8

Browse files
committed
Latest edits.
1 parent 0acdede commit b6fbcb8

5 files changed

Lines changed: 306 additions & 45 deletions

File tree

improver/calibration/load_and_apply_quantile_regression_random_forest.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,6 @@ def _get_inputs(
9797
except ValueError:
9898
qrf_model = joblib.load(file_path)
9999

100-
if not qrf_model:
101-
msg = (
102-
"No QRF model found in the provided file paths. "
103-
"A trained QRF model must be provided."
104-
)
105-
raise ValueError(msg)
106100
if not cube_inputs:
107101
msg = (
108102
"No features found in the provided file paths. "
@@ -113,18 +107,34 @@ def _get_inputs(
113107
# Extract all additional cubes which are associated with a feature in the
114108
# feature_config.
115109
forecast_constraint = iris.Constraint(name=self.target_cube_name)
116-
forecast_cube = cube_inputs.extract_cube(forecast_constraint)
110+
forecast_cube = cube_inputs.extract(forecast_constraint)
111+
112+
if forecast_cube:
113+
(forecast_cube,) = forecast_cube
114+
else:
115+
msg = (
116+
"No target forecast provided. An input file representing the target "
117+
"must be provided, even if the target will not be used as a feature. "
118+
f"The target is '{self.target_cube_name}'."
119+
)
120+
raise ValueError(msg)
121+
122+
if not qrf_model:
123+
return None, forecast_cube, None
124+
125+
if len(cube_inputs) != len(self.feature_config.keys()):
126+
msg = (
127+
"The number of cubes loaded does not match the number of features "
128+
"expected. The number of cubes loaded was: "
129+
f"{len(cube_inputs)}. The number of features expected was: "
130+
f"{len(self.feature_config.keys())}."
131+
)
132+
raise ValueError(msg)
117133

118134
# If target diagnostic not a feature in the training then remove.
119135
if self.target_cube_name not in self.feature_config.keys():
120136
cube_inputs.remove(forecast_cube)
121137

122-
if len(cube_inputs) + 1 != len(file_paths):
123-
raise ValueError(
124-
"Unable to identify the correct number of inputs. "
125-
f"The number of file paths provided was: {len(file_paths)}. "
126-
f"The number of items loaded into cubes was: {len(cube_inputs)}."
127-
)
128138
return cube_inputs, forecast_cube, qrf_model
129139

130140
@staticmethod
@@ -232,6 +242,8 @@ def process(
232242
The calibrated forecast cube.
233243
"""
234244
cube_inputs, forecast_cube, qrf_model = self._get_inputs(file_paths)
245+
if not qrf_model:
246+
return forecast_cube
235247
if forecast_cube.coords("percentile"):
236248
percentiles = self._compute_percentiles(forecast_cube, "percentile")
237249
cube_inputs = self._percentiles_to_realizations(cube_inputs)

improver/calibration/load_and_train_quantile_regression_random_forest.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import iris
1111
import numpy as np
1212
import pandas as pd
13+
import pyarrow as pa
1314
import pyarrow.parquet as pq
1415

1516
from improver import PostProcessingPlugin
@@ -67,7 +68,7 @@ def _split_cubes_and_parquet_files(self, file_paths):
6768
try:
6869
cube = load_cube(str(file_path))
6970
cube_inputs.append(cube)
70-
except (OSError, IsADirectoryError):
71+
except IsADirectoryError:
7172
# For loop here because the read_schema must read a .parquet file rather than a directory.
7273
for file in Path(file_path).glob("**/*.parquet"):
7374
try:
@@ -78,6 +79,19 @@ def _split_cubes_and_parquet_files(self, file_paths):
7879
if forecast_table_path and truth_table_path:
7980
break
8081

82+
if len(self.feature_config.keys()) not in [
83+
len(cube_inputs),
84+
len(cube_inputs) + 1,
85+
]:
86+
msg = (
87+
"The number of cubes loaded does not match the number of features "
88+
"expected. These can mismatch if the some features are coming from the "
89+
"historic forecast. The number of cubes loaded was: "
90+
f"{len(cube_inputs)}. The number of features expected was: "
91+
f"{len(self.feature_config.keys())}."
92+
)
93+
raise ValueError(msg)
94+
8195
return forecast_table_path, truth_table_path, cube_inputs
8296

8397
def _read_parquet_files(
@@ -108,12 +122,31 @@ def _read_parquet_files(
108122
("experiment", "==", self.experiment),
109123
]
110124
]
125+
for file in Path(forecast_table_path).glob("**/*.parquet"):
126+
if pq.read_schema(file).get_all_field_indices("percentile"):
127+
altered_schema = FORECAST_SCHEMA
128+
elif pq.read_schema(file).get_all_field_indices("realization"):
129+
altered_schema = FORECAST_SCHEMA.remove(
130+
FORECAST_SCHEMA.get_field_index("percentile")
131+
)
132+
altered_schema = altered_schema.append(
133+
pa.field("realization", pa.int64())
134+
)
135+
else:
136+
msg = (
137+
"The forecast parquet file is expected to contain either a "
138+
"'percentile' or 'realization' field. Neither was found."
139+
)
140+
raise ValueError(msg)
141+
break
142+
111143
forecast_df = pd.read_parquet(
112144
forecast_table_path,
113145
filters=filters,
114-
schema=FORECAST_SCHEMA,
146+
schema=altered_schema,
115147
engine="pyarrow",
116148
)
149+
117150
# Convert df columns from ms to pandas timestamp object to work with existing code
118151
for column in ["time", "forecast_reference_time", "blend_time"]:
119152
forecast_df[column] = pd.to_datetime(
@@ -294,12 +327,6 @@ def process(
294327
forecast_df, truth_df, forecast_periods
295328
)
296329

297-
# Check the number of features provided as separate files, plus 2
298-
# (the forecasts and truths for training) are equal to the total number of
299-
# file paths provided.
300-
if len(cube_inputs) + 2 != len(file_paths):
301-
raise ValueError("Unable to identify the correct number of inputs")
302-
303330
# If target_forecast is also a dynamic feature in the feature config then
304331
# add it to cube_inputs
305332
for feature_name in self.feature_config.keys():

improver/calibration/quantile_regression_random_forest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from improver import BasePlugin, PostProcessingPlugin
1717
from improver.constants import DAYS_IN_YEAR, HOURS_IN_DAY
18+
from improver.utilities.cube_manipulation import enforce_coordinate_ordering
1819

1920

2021
def _remove_item_from_list(alist: list, items: list):
@@ -364,8 +365,13 @@ def _organise_truth_data(forecast_cube: Cube, truth_cube: Cube) -> list[np.ndarr
364365
)
365366
else:
366367
# Forecast reference time and forecast period are different dimensions.
367-
for frt in list(frt_coord.cells()):
368-
for fp in fp_coord.points:
368+
enforce_coordinate_ordering(
369+
forecast_cube,
370+
["forecast_period", "forecast_reference_time"],
371+
anchor_start=True,
372+
)
373+
for fp in fp_coord.points:
374+
for frt in list(frt_coord.cells()):
369375
time_datetimes.append(
370376
frt.point._to_real_datetime()
371377
+ pd.to_timedelta(

improver_tests/calibration/quantile_regression_random_forests_calibration/test_load_and_apply_quantile_regression_random_forest.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
(100, 2, 55, 5, None, 0, {}, False, [0.5, 0.9], [[4.1, 5.1], [4.2, 5.1]]), # noqa Multiple quantiles
3232
(1, 1, 55, 5, None, 0, {}, False, [0.5], [6.2, 6.2]), # noqa Fewer estimators and reduced depth
3333
(1, 1, 73, 5, None, 0, {}, False, [0.5], [4.2, 6.2]), # Different random state
34-
(2, 2, 55, 5, "log", 10, {}, False, [0.5], [4.1, 5.1]), # Log transformation
34+
(2, 2, 55, 5, "log", 10, {}, False, [0.5], [5.11, 5.64]), # Log transformation
3535
(
3636
2,
3737
2,
@@ -42,7 +42,7 @@
4242
{},
4343
False,
4444
[0.5],
45-
[4.1, 5.1],
45+
[5.11, 5.64],
4646
), # Log10 transformation
4747
(
4848
2,
@@ -54,7 +54,7 @@
5454
{},
5555
False,
5656
[0.5],
57-
[4.1, 5.1],
57+
[5.11, 5.64],
5858
), # Square root transformation
5959
(
6060
2,
@@ -66,7 +66,7 @@
6666
{},
6767
False,
6868
[0.5],
69-
[4.1, 5.1],
69+
[5.13, 5.64],
7070
), # Cube root transformation
7171
(2, 2, 55, 5, None, 0, {"max_samples_leaf": 0.5}, False, [0.5], [5.15, 6.2]), # noqa # Different criterion
7272
(2, 5, 55, 5, None, 0, {}, True, [0.5], [5.15, 5.65]), # Include static data
@@ -155,7 +155,16 @@ def test_load_and_apply_qrf(
155155
assert result.units == "m s-1"
156156

157157

158-
@pytest.mark.parametrize("exception", ["no_model_output", "no_features"])
158+
@pytest.mark.parametrize(
159+
"exception",
160+
[
161+
"no_model_output",
162+
"no_features",
163+
"missing_target_feature",
164+
"missing_static_feature",
165+
"missing_dynamic_feature",
166+
],
167+
)
159168
def test_exceptions(
160169
tmp_path,
161170
exception,
@@ -207,6 +216,10 @@ def test_exceptions(
207216
forecast_cube, day_of_training_period, "forecast_reference_time"
208217
)
209218

219+
ancil_cube = _create_ancil_file()
220+
ancil_filepath = tmp_path / "ancil.nc"
221+
save_netcdf(ancil_cube, ancil_filepath)
222+
210223
features_dir = tmp_path / "features"
211224
features_dir.mkdir(parents=True)
212225
forecast_filepath = str(features_dir / "forecast.nc")
@@ -219,12 +232,40 @@ def test_exceptions(
219232
pre_transform_addition=pre_transform_addition,
220233
)
221234

222-
if exception == "no_qrf_model":
235+
if exception == "no_model_output":
223236
file_paths = [forecast_filepath]
224-
with pytest.raises(ValueError, match="No QRF model found"):
225-
plugin.process(file_paths=file_paths)
226-
227-
if exception == "no_features":
237+
result = plugin.process(file_paths=file_paths)
238+
assert isinstance(result, Cube)
239+
# assert result == forecast_cube
240+
assert result.name() == "wind_speed_at_10m"
241+
assert result.units == "m s-1"
242+
assert result.data.shape == forecast_cube.data.shape
243+
assert np.allclose(result.data, forecast_cube.data)
244+
elif exception == "no_features":
228245
file_paths = [model_output]
229246
with pytest.raises(ValueError, match="No features found"):
230247
plugin.process(file_paths=file_paths)
248+
elif exception == "missing_target_feature":
249+
file_paths = [model_output, ancil_filepath]
250+
with pytest.raises(ValueError, match="No target forecast provided."):
251+
plugin.process(file_paths=file_paths)
252+
elif exception == "missing_static_feature":
253+
feature_config = {
254+
"wind_speed_at_10m": ["mean", "std"],
255+
"distance_to_water": ["static"],
256+
}
257+
plugin.feature_config = feature_config
258+
file_paths = [model_output, forecast_filepath]
259+
with pytest.raises(ValueError, match="The number of cubes loaded."):
260+
plugin.process(file_paths=file_paths)
261+
elif exception == "missing_dynamic_feature":
262+
feature_config = {
263+
"wind_speed_at_10m": ["mean", "std"],
264+
"air_temperature": ["mean", "std"],
265+
}
266+
plugin.feature_config = feature_config
267+
file_paths = [model_output, forecast_filepath]
268+
with pytest.raises(ValueError, match="The number of cubes loaded."):
269+
plugin.process(file_paths=file_paths)
270+
else:
271+
raise ValueError(f"Unknown exception type: {exception}")

0 commit comments

Comments
 (0)