Skip to content

Commit cecd99e

Browse files
authored
Merge pull request #17 from gavinevans/mobt_877_implement_qrf_8
Modifications following review comments
2 parents 3948062 + 1f0da55 commit cecd99e

20 files changed

Lines changed: 1450 additions & 773 deletions

doc/source/conf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,11 +370,11 @@
370370
# Example configuration for intersphinx: refer to the Python standard library.
371371
intersphinx_mapping = {
372372
"python": ("https://docs.python.org/3/", None),
373-
"iris": ("https://scitools-iris.readthedocs.io/en/latest/", None),
374-
"cartopy": ("https://scitools.org.uk/cartopy/docs/latest/", None),
373+
"iris": ("https://scitools-iris.readthedocs.io/en/stable/", None),
374+
"cartopy": ("https://cartopy.readthedocs.io/stable/", None),
375375
"cf_units": ("https://cf-units.readthedocs.io/en/stable/", None),
376376
"numpy": ("https://numpy.org/doc/stable/", None),
377-
"scipy": ("https://docs.scipy.org/doc/scipy-1.6.2/reference/", None),
377+
"scipy": ("https://docs.scipy.org/doc/scipy/reference", None),
378378
"pandas": ("https://pandas.pydata.org/pandas-docs/dev/", None),
379379
}
380380

improver/calibration/__init__.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
"""
88

99
from collections import OrderedDict
10+
from pathlib import Path
1011
from typing import Dict, List, Optional, Tuple, Union
1112

13+
import iris
14+
import joblib
15+
import pandas as pd
1216
from iris.cube import Cube, CubeList
1317

1418
from improver.metadata.probabilistic import (
@@ -51,6 +55,7 @@ def __init__(self):
5155
("altitude", pa.float32()),
5256
("time", pa.timestamp("s", "utc")),
5357
("wmo_id", pa.string()),
58+
("station_id", pa.string()),
5459
("ob_value", pa.float32()),
5560
]
5661
)
@@ -263,6 +268,85 @@ def split_forecasts_and_bias_files(cubes: CubeList) -> Tuple[Cube, Optional[Cube
263268
return forecast_cube, bias_cubes
264269

265270

271+
def split_pickle_parquet_and_netcdf(files):
272+
"""Split the input files into pickle, parquet, and netcdf files.
273+
Only a single pickle file is expected.
274+
275+
Args:
276+
files:
277+
A list of input file paths which will be split into pickle,
278+
parquet, and netcdf files.
279+
Returns:
280+
- A flattened cube list containing all the cubes contained within the
281+
provided paths to NetCDF files.
282+
- A list of paths to Parquet files.
283+
- A loaded pickle file.
284+
Raises:
285+
ValueError: If multiple pickle files provided, as only one is ever expected.
286+
"""
287+
cubes = iris.cube.CubeList()
288+
loaded_pickles = []
289+
parquets = []
290+
291+
for file_path in files:
292+
if not file_path.exists():
293+
continue
294+
295+
# Directories indicate we are working with parquet files.
296+
if file_path.is_dir():
297+
parquets.append(file_path)
298+
continue
299+
300+
try:
301+
cube = iris.load(file_path)
302+
cubes.extend(cube)
303+
except ValueError:
304+
try:
305+
loaded_pickles.append(joblib.load(file_path))
306+
except Exception as e:
307+
msg = f"Failed to load {file_path}: {e}"
308+
raise ValueError(msg)
309+
310+
if len(loaded_pickles) > 1:
311+
msg = "Multiple pickle inputs have been provided. Only one is expected."
312+
raise ValueError(msg)
313+
314+
return (
315+
cubes if cubes else None,
316+
parquets if parquets else None,
317+
loaded_pickles[0] if loaded_pickles else None,
318+
)
319+
320+
321+
def identify_parquet_type(parquet_paths: List[Path]):
322+
"""Determine whether the provided parquet paths contain forecast or truth data.
323+
This is done by checking the columns within the parquet files for the presence
324+
of a forecast_period column which is only present for forecast data.
325+
Args:
326+
parquet_paths:
327+
A list of paths to Parquet files.
328+
Returns:
329+
- The path to the Parquet file containing the historical forecasts.
330+
- The path to the Parquet file containing the truths.
331+
"""
332+
import pyarrow.parquet as pq
333+
334+
forecast_table_path = None
335+
truth_table_path = None
336+
for file_path in parquet_paths:
337+
try:
338+
example_file_path = next(file_path.glob("**/*.parquet"))
339+
except StopIteration:
340+
continue
341+
try:
342+
pq.read_schema(example_file_path).field("forecast_period")
343+
forecast_table_path = file_path
344+
except KeyError:
345+
truth_table_path = file_path
346+
347+
return forecast_table_path, truth_table_path
348+
349+
266350
def validity_time_check(forecast: Cube, validity_times: List[str]) -> bool:
267351
"""Check the validity time of the forecast matches the accepted validity times
268352
within the validity times list.
@@ -307,3 +391,25 @@ def add_warning_comment(forecast: Cube) -> Cube:
307391
"however, no calibration has been applied."
308392
)
309393
return forecast
394+
395+
396+
def get_training_period_cycles(
397+
cycletime: str, forecast_period: Union[int, str], training_length: int
398+
):
399+
"""Generate a list of forecast reference times for the training period.
400+
401+
Args:
402+
cycletime: The time at which the forecast is issued in a format understood by
403+
pandas.Timestamp e.g. 20170109T0000Z.
404+
forecast_period: The forecast period in seconds.
405+
training_length: The number of days in the training period.
406+
"""
407+
forecast_period_td = pd.Timedelta(int(forecast_period), unit="seconds")
408+
409+
return pd.date_range(
410+
end=pd.Timestamp(cycletime)
411+
- pd.Timedelta(1, unit="days")
412+
- forecast_period_td.floor("D"),
413+
periods=int(training_length),
414+
freq="D",
415+
)

improver/calibration/dataframe_utilities.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def _unique_check(df: DataFrame, column: str) -> None:
128128
raise ValueError(msg)
129129

130130

131-
def _quantile_check(df: DataFrame) -> None:
131+
def quantile_check(df: DataFrame) -> None:
132132
"""Check that the percentiles provided can be considered to be
133133
quantiles with equal spacing spanning the percentile range.
134134
@@ -142,7 +142,7 @@ def _quantile_check(df: DataFrame) -> None:
142142

143143
if not np.allclose(expected_percentiles, df["percentile"].unique()):
144144
msg = (
145-
"The forecast percentiles can not be considered as quantiles. "
145+
"Forecast percentiles must be equally spaced. "
146146
f"The forecast percentiles are {df['percentile'].unique()}."
147147
"Based on the number of percentiles provided, the expected "
148148
f"percentiles would be {expected_percentiles}."
@@ -447,7 +447,7 @@ def _prepare_dataframes(
447447

448448
# Check the percentiles can be considered to be equally space quantiles.
449449
if representation_type == "percentile":
450-
_quantile_check(forecast_df)
450+
quantile_check(forecast_df)
451451

452452
# Remove forecast duplicates.
453453
forecast_cols = [

0 commit comments

Comments
 (0)