diff --git a/docs/_source/_plots/conditional_quantiles_cali-ref_plot-2.png b/docs/_source/_plots/conditional_quantiles_cali-ref_plot-2.png deleted file mode 100644 index bedc075b8cc3bc75e1dabfbbec02cbdb6c69123a..0000000000000000000000000000000000000000 Binary files a/docs/_source/_plots/conditional_quantiles_cali-ref_plot-2.png and /dev/null differ diff --git a/docs/_source/_plots/conditional_quantiles_cali-ref_plot-3.png b/docs/_source/_plots/conditional_quantiles_cali-ref_plot-3.png deleted file mode 100644 index ccc454211e5dbf16374ebbee522ea584e24a4fbd..0000000000000000000000000000000000000000 Binary files a/docs/_source/_plots/conditional_quantiles_cali-ref_plot-3.png and /dev/null differ diff --git a/docs/_source/_plots/conditional_quantiles_cali-ref_plot-1.png b/docs/_source/_plots/conditional_quantiles_cali-ref_plot.png similarity index 100% rename from docs/_source/_plots/conditional_quantiles_cali-ref_plot-1.png rename to docs/_source/_plots/conditional_quantiles_cali-ref_plot.png diff --git a/docs/_source/_plots/conditional_quantiles_like-bas_plot-2.png b/docs/_source/_plots/conditional_quantiles_like-bas_plot-2.png deleted file mode 100644 index c851f8f58a33cc2b37917e8964faa65243b3e8a6..0000000000000000000000000000000000000000 Binary files a/docs/_source/_plots/conditional_quantiles_like-bas_plot-2.png and /dev/null differ diff --git a/docs/_source/_plots/conditional_quantiles_like-bas_plot-3.png b/docs/_source/_plots/conditional_quantiles_like-bas_plot-3.png deleted file mode 100644 index 302862bc61d881f879a4bb7c860a2a55d46a76af..0000000000000000000000000000000000000000 Binary files a/docs/_source/_plots/conditional_quantiles_like-bas_plot-3.png and /dev/null differ diff --git a/docs/_source/_plots/conditional_quantiles_like-bas_plot-1.png b/docs/_source/_plots/conditional_quantiles_like-bas_plot.png similarity index 100% rename from docs/_source/_plots/conditional_quantiles_like-bas_plot-1.png rename to docs/_source/_plots/conditional_quantiles_like-bas_plot.png diff --git a/src/helpers.py b/src/helpers.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/helpers/time_tracking.py b/src/helpers/time_tracking.py index 3a4dad595d63eec17bc9ba35f08421fd341a3d57..c85a6a047943a589a9d076584ae40186634db767 100644 --- a/src/helpers/time_tracking.py +++ b/src/helpers/time_tracking.py @@ -3,6 +3,7 @@ import datetime as dt import logging import math import time +import types from functools import wraps from typing import Optional @@ -38,6 +39,10 @@ class TimeTrackingWrapper: with TimeTracking(name=self.__wrapped__.__name__): return self.__wrapped__(*args, **kwargs) + def __get__(self, instance, cls): + """Create bound method object and supply self argument to the decorated method.""" + return types.MethodType(self, instance) + class TimeTracking(object): """ diff --git a/src/plotting/postprocessing_plotting.py b/src/plotting/postprocessing_plotting.py index 36039073c16e26f430ad607639b8c31e3efb7467..e1bf1c1a59ba308539597087a68b0df493612d6b 100644 --- a/src/plotting/postprocessing_plotting.py +++ b/src/plotting/postprocessing_plotting.py @@ -256,10 +256,29 @@ class PlotStationMap(AbstractPlotClass): @TimeTrackingWrapper class PlotConditionalQuantiles(AbstractPlotClass): """ - This class creates cond.quantile plots as originally proposed by Murphy, Brown and Chen (1989) [But in log scale] + Create cond.quantile plots as originally proposed by Murphy, Brown and Chen (1989) [But in log scale]. Link to paper: https://journals.ametsoc.org/doi/pdf/10.1175/1520-0434%281989%29004%3C0485%3ADVOTF%3E2.0.CO%3B2 + + .. image:: ../../../../../_source/_plots/conditional_quantiles_cali-ref_plot.png + :width: 400 + + .. image:: ../../../../../_source/_plots/conditional_quantiles_like-bas_plot.png + :width: 400 + + For each time step ahead a separate plot is created. If parameter plot_per_season is true, data is split by season + and conditional quantiles are plotted for each season in addition. + + :param stations: all stations to plot + :param data_pred_path: path to dir which contains the forecasts as .nc files + :param plot_folder: path where the plots are stored + :param plot_per_seasons: if `True' create cond. quantile plots for _seasons (DJF, MAM, JJA, SON) individually + :param rolling_window: smoothing of quantiles (3 is used by Murphy et al.) + :param model_mame: name of the model prediction as stored in netCDF file (for example "CNN") + :param obs_name: name of observation as stored in netCDF file (for example "obs") + :param kwargs: Some further arguments which are listed in self._opts """ + # ignore warnings if nans appear in quantile grouping warnings.filterwarnings("ignore", message="All-NaN slice encountered") # ignore warnings if mean is calculated on nans @@ -269,46 +288,33 @@ class PlotConditionalQuantiles(AbstractPlotClass): def __init__(self, stations: List, data_pred_path: str, plot_folder: str = ".", plot_per_seasons=True, rolling_window: int = 3, model_mame: str = "CNN", obs_name: str = "obs", **kwargs): - """ - - :param stations: all stations to plot - :param data_pred_path: path to dir which contains the forecasts as .nc files - :param plot_folder: path where the plots are stored - :param plot_per_seasons: if `True' create cond. quantile plots for seasons (DJF, MAM, JJA, SON) individually - :param rolling_window: smoothing of quantiles (3 is used by Murphy et al.) - :param model_mame: name of the model prediction as stored in netCDF file (for example "CNN") - :param obs_name: name of observation as stored in netCDF file (for example "obs") - :param kwargs: Some further arguments which are listed in self._opts - """ + """Initialise.""" super().__init__(plot_folder, "conditional_quantiles") - self._data_pred_path = data_pred_path self._stations = stations self._rolling_window = rolling_window self._model_name = model_mame self._obs_name = obs_name - - self._opts = {"q": kwargs.get("q", [.1, .25, .5, .75, .9]), - "linetype": kwargs.get("linetype", [':', '-.', '--', '-.', ':']), - "legend": kwargs.get("legend", - ['.10th and .90th quantile', '.25th and .75th quantile', '.50th quantile', - 'reference 1:1']), - "data_unit": kwargs.get("data_unit", "ppb"), - } - if plot_per_seasons is True: - self.seasons = ['DJF', 'MAM', 'JJA', 'SON'] - else: - self.seasons = "" + self._opts = self._get_opts(kwargs) + self._seasons = ['DJF', 'MAM', 'JJA', 'SON'] if plot_per_seasons is True else "" self._data = self._load_data() self._bins = self._get_bins_from_rage_of_data() - self._plot() - def _load_data(self): + @staticmethod + def _get_opts(kwargs): + """Extract options from kwargs.""" + return {"q": kwargs.get("q", [.1, .25, .5, .75, .9]), + "linetype": kwargs.get("linetype", [':', '-.', '--', '-.', ':']), + "legend": kwargs.get("legend", ['.10th and .90th quantile', '.25th and .75th quantile', + '.50th quantile', 'reference 1:1']), + "data_unit": kwargs.get("data_unit", "ppb"), } + + def _load_data(self) -> xr.DataArray: """ - This method loads forcast data + Load plot data. - :return: + :return: plot data """ logging.debug("... load data") data_collector = [] @@ -319,13 +325,14 @@ class PlotConditionalQuantiles(AbstractPlotClass): res = xr.concat(data_collector, dim='station').transpose('index', 'type', 'ahead', 'station') return res - def _segment_data(self, data, x_model): + def _segment_data(self, data: xr.DataArray, x_model: str) -> xr.DataArray: """ - This method creates segmented data which is used for cond. quantile plots + Segment data into bins. - :param data: - :param x_model: - :return: + :param data: data to segment + :param x_model: name of x dimension + + :return: segmented data """ logging.debug("... segment data") # combine index and station to multi index @@ -334,17 +341,18 @@ class PlotConditionalQuantiles(AbstractPlotClass): data.coords['z'] = range(len(data.coords['z'])) # segment data of x_model into bins data.loc[x_model, ...] = data.loc[x_model, ...].to_pandas().T.apply(pd.cut, bins=self._bins, - labels=self._bins[1:]).T.values + labels=self._bins[1:]).T.values return data @staticmethod - def _labels(plot_type, data_unit="ppb"): + def _labels(plot_type: str, data_unit: str = "ppb") -> Tuple[str, str]: """ - Helper method to correctly assign (x,y) labels to plots, depending on like-base or cali-ref factorization + Assign (x,y) labels to plots correctly, depending on like-base or cali-ref factorization. - :param plot_type: - :param data_unit: - :return: + :param plot_type: type of plot, either `obs` or a model name + :param data_unit: unit of data to add to labels (default ppb) + + :return: tuple with y and x labels """ names = (f"forecast concentration (in {data_unit})", f"observed concentration (in {data_unit})") if plot_type == "obs": @@ -352,22 +360,23 @@ class PlotConditionalQuantiles(AbstractPlotClass): else: return names[::-1] - def _get_bins_from_rage_of_data(self): + def _get_bins_from_rage_of_data(self) -> np.ndarray: """ - Get array of bins to use for quantiles + Get array of bins to use for quantiles. - :return: + :return: range from 0 to data's maximum + 1 (rounded down) """ return np.arange(0, math.ceil(self._data.max().max()) + 1, 1).astype(int) - def _create_quantile_panel(self, data, x_model, y_model): + def _create_quantile_panel(self, data: xr.DataArray, x_model: str, y_model: str) -> xr.DataArray: """ - Clculate quantiles + Calculate quantiles. - :param data: - :param x_model: - :param y_model: - :return: + :param data: data to calculate quantiles + :param x_model: name of x dimension + :param y_model: name of y dimension + + :return: quantile panel with binned data """ logging.debug("... create quantile panel") # create empty xarray with dims: time steps ahead, quantiles, bin index (numbers create in previous step) @@ -379,83 +388,69 @@ class PlotConditionalQuantiles(AbstractPlotClass): # calculate for each bin of the pred_name data the quantiles of the ref_name data for bin in self._bins[1:]: mask = (data.loc[x_model, ...] == bin) - quantile_panel.loc[..., bin] = data.loc[y_model, ...].where(mask).quantile(self._opts["q"], - dim=['z']).T + quantile_panel.loc[..., bin] = data.loc[y_model, ...].where(mask).quantile(self._opts["q"], dim=['z']).T return quantile_panel @staticmethod - def add_affix(x): + def add_affix(affix: str) -> str: """ - Helper method to add additional information on plot name + Add additional information to plot name with leading underscore or add empty string if affix is empty. - :param x: - :return: + :param affix: string to add + + :return: affix with leading underscore or empty string. """ - return f"_{x}" if len(x) > 0 else "" + return f"_{affix}" if len(affix) > 0 else "" - def _prepare_plots(self, data, x_model, y_model): + def _prepare_plots(self, data: xr.DataArray, x_model: str, y_model: str) -> Tuple[xr.DataArray, xr.DataArray]: """ - Get segmented_data and quantile_panel + Get segmented data and quantile panel. - :param data: - :param x_model: - :param y_model: - :return: + :param data: plot data + :param x_model: name of x dimension + :param y_model: name of y dimension + + :return: segmented data and quantile panel """ segmented_data = self._segment_data(data, x_model) quantile_panel = self._create_quantile_panel(segmented_data, x_model, y_model) return segmented_data, quantile_panel def _plot(self): - """ - Main plot call - - :return: - """ - logging.info(f"start plotting {self.__class__.__name__}, scheduled number of plots: {(len(self.seasons) + 1) * 2}") + """Start plotting routines: overall plot and seasonal (if enabled).""" + logging.info(f"start plotting {self.__class__.__name__}, scheduled number of plots: {(len(self._seasons) + 1) * 2}") - if len(self.seasons) > 0: + if len(self._seasons) > 0: self._plot_seasons() self._plot_all() def _plot_seasons(self): - """ - Seasonal plot call - - :return: - """ - for season in self.seasons: + """Create seasonal plots.""" + for season in self._seasons: self._plot_base(data=self._data.where(self._data['index.season'] == season), x_model=self._model_name, y_model=self._obs_name, plot_name_affix="cali-ref", season=season) self._plot_base(data=self._data.where(self._data['index.season'] == season), x_model=self._obs_name, y_model=self._model_name, plot_name_affix="like-base", season=season) def _plot_all(self): - """ - Full plot call - - :return: - """ + """Plot overall conditional quantiles on full data.""" self._plot_base(data=self._data, x_model=self._model_name, y_model=self._obs_name, plot_name_affix="cali-ref") self._plot_base(data=self._data, x_model=self._obs_name, y_model=self._model_name, plot_name_affix="like-base") @TimeTrackingWrapper - def _plot_base(self, data, x_model, y_model, plot_name_affix, season=""): + def _plot_base(self, data: xr.DataArray, x_model: str, y_model: str, plot_name_affix: str, season: str = ""): """ - Base method to create cond. quantile plots. Is called from _plot_all and _plot_seasonal + Create conditional quantile plots. :param data: data which is used to create cond. quantile plot :param x_model: name of model on x axis (can also be obs) :param y_model: name of model on y axis (can also be obs) :param plot_name_affix: should be `cali-ref' or `like-base' - :param season: List of seasons to use - :return: + :param season: List of _seasons to use """ - segmented_data, quantile_panel = self._prepare_plots(data, x_model, y_model) ylabel, xlabel = self._labels(x_model, self._opts["data_unit"]) plot_name = f"{self.plot_name}{self.add_affix(season)}{self.add_affix(plot_name_affix)}_plot.pdf" - #f"{base_name}{add_affix(season)}{add_affix(plot_name_affix)}_plot.pdf" plot_path = os.path.join(os.path.abspath(self.plot_folder), plot_name) pdf_pages = matplotlib.backends.backend_pdf.PdfPages(plot_path) logging.debug(f"... plot path is {plot_path}")