def gen_ylabel(self): r""" Generate automatic label for y-axis Uses attributes already set in class >>> TSP = TimeSeriesPlot() >>> TSP.phenomena_short_name = 'O3' >>> TSP.units = 'ug/m3' >>> label = TSP.gen_ylabel() >>> print(label) O3 ($\mu g\ m^{-3}$) """ ylabel = '' if self.phenomena_short_name is not None: ylabel += self.phenomena_short_name if self.units is not None: units = plotting_functions.units_str(self.units) if self.phenomena_short_name is not None: ylabel += ' (' ylabel += units if self.phenomena_short_name is not None: ylabel += ')' return ylabel
def plot_regime_stats(ini_dict, xstat_list=None, ystat_list=None, statsfile=None): """ Produce scatter plots of each xstat vs each ystat, labelling points by corresponding weather regime. By default, picks out meanobs vs a selected group of useful stats. :param ini_dict: Dictionary of a :class:`inifile` object. Should contain: * 'plot_dir' - location of statistics file to read in, plus output location * 'short_name_list' :param xstat_list: List of stats to plot on the x axis, in turn. If set to None, defaults to ['meanobs'] :param ystat_list: List of stats to plot on the y axis, in turn. If set to None, defaults to ['mnmb', 'fge', 'bias', 'meanmod'] *Available stats for plotting:* 'nsites': 'Number of sites', 'n': 'Number of points', 'correlation': 'Correlation', 'bias': 'Bias', 'nmb': 'Normalised Mean Bias', 'mnmb': 'Modified Normalised Mean Bias', 'nmge': 'Normalised Mean Gross Error', 'fge': 'Fractional Gross Error', 'rmse': 'Root Mean Square Error', 'fac2': 'Factor of 2', 'ioa': 'Index of Agreement', 'threshold': 'Threshold', 'ORSS': 'Odds Ratio Skill Score', 'hitrate': 'Hitrate', 'falsealarmrate': 'False Alarm Rate', 'falsealarmratio': 'False Alarm Ratio', 'o>=t_m>=t': 'Number Obs >= Threshold and Model >= Threshold', 'o<t_m>=t': 'Number Obs < Threshold and Model >= Threshold', 'o>=t_m<t': 'Number Obs >= Threshold and Model < Threshold', 'o<t_m<t': 'Number Obs < Threshold and Model < Threshold', 'maxobs': 'Maximum Observation Value', 'maxmod': 'Maximum Model Value', 'meanobs': 'Mean Observation Value', 'meanmod': 'Mean Model Value', 'sdobs': 'Standard Deviation of Observations', 'sdmod': 'Standard Deviation of Model', 'perc_correct': 'Percentage of Correct values', 'perc_over': 'Percentage of Over-predicions', 'perc_under': 'Percentage of Under-predictions', 'units': 'Units' Firstly get some example data, and ensure that plot_dir is set. Also shorten the short_names required to be plotted. >>> import adaq_functions >>> ini_dict, sites_data, od, md_list = adaq_functions.get_exampledata( ... exampletype="full") # doctest: +ELLIPSIS Reading inifile .../example_data_5days.ini Number of sites: 5 >>> import config >>> ini_dict['plot_dir'] = config.CODE_DIR + "/adaqdocs/figures/" >>> ini_dict['short_name_list'] = ['O3'] >>> ini_dict['calc_stats_format_list'] = ['csv'] Before any plotting can be done, first need to calculate some statistics: >>> statsfile = 'regime_stats' >>> tsstats = adaq_functions.calc_stats( ... ini_dict, od, md_list, statsfile_prefix=statsfile) # doctest: +ELLIPSIS Statistics saved to .../regime_stats.csv Can now read in the file which contains statistics and plot these. >>> plot_regime_stats(ini_dict, ystat_list=['bias'], ... statsfile=statsfile) # doctest: +ELLIPSIS Statistics found at .../regime_stats.csv Saved figure .../meanobs_vs_bias_O3.png """ #Set up defaults if xstat_list is None: xstat_list = ['meanobs'] if ystat_list is None: ystat_list = ['mnmb', 'fge', 'bias', 'meanmod'] plotdir = ini_dict['plot_dir'] if plotdir[-1] != '/': plotdir += '/' if statsfile is None: filein = plotdir + 'stats.csv' else: filein = plotdir + statsfile + '.csv' try: #Read in stats.csv file from plot directory stats = np.genfromtxt(filein, dtype=str, skip_header=1, autostrip=True, delimiter=',') print('Statistics found at ', filein) except: #If stats file not found in directory, raise error raise IOError("No statistics file found in plotdir for reading") #Initialise certain stats to carry units needs_units = ['meanobs', 'meanmod', 'maxobs', 'maxmod', 'bias', 'rmse'] regimes = [] #Pick out regime numbers from model label #Ignore first two columns, 'Phenomenon' and 'Statistic' for iregime in stats[0][2:]: if iregime[-2] in ['1', '2', '3']: regimes.append(iregime[-2:]) else: regimes.append(iregime[-1]) #Produce species-specific plots for short_name in ini_dict['short_name_list']: for xstat in xstat_list: for ystat in ystat_list: for item in stats: #Create list of selected stat if item[0] == short_name and item[1].split()[0] == xstat: x = item[2:] elif item[0] == short_name and item[1].split()[0] == ystat: y = item[2:] elif item[0] == short_name and item[1] == 'units': units = item[2] units = plotting_functions.units_str(units) plt.figure(figsize=(8, 7.25)) plt.scatter(x, y, s=0) #Add units to axes labels if appropriate if xstat in needs_units: xunits = ' (' + units + ')' else: xunits = '' if ystat in needs_units: yunits = ' (' + units + ')' else: yunits = '' plt.xlabel(xstat + xunits) plt.ylabel(ystat + yunits) plt.title(short_name) #Use regime number instead of dot to indicate data point for i, regime in enumerate(regimes): plt.annotate(regime, (x[i], y[i]), horizontalalignment='center', verticalalignment='center', color='teal') ymin, ymax = plt.ylim() xmin, xmax = plt.xlim() #Add axis line at y=0 if there's +ve and -ve data if ymin < 0 < ymax: plt.axhline(color='gray') #Equalise axes when plotting similar stats if xstat == 'meanobs' and ystat == 'meanmod': plt.ylim(min(xmin, ymin), max(xmax, ymax)) plt.xlim(min(xmin, ymin), max(xmax, ymax)) filename = xstat + '_vs_' + ystat + '_' + short_name + '.png' plt.savefig(plotdir + filename) print('Saved figure ', plotdir + filename) plt.close()
def plot(self): """ Plot quantile-quantile plot. Returns figure object for further plotting if needed. """ if not self.lines: raise ValueError( "Quantile-Quantile plot: no lines have been added") self.get_percentiles() self.fig = plt.figure() ax = plt.gca() axis_max = np.nanmax(self.xpercentiles) axis_min = np.nanmin(self.xpercentiles) for line in self.lines: if line['marker'] is None: #Ensure a marker is set line['marker'] = 'o' if 'ypercentiles' not in line: #ypercentiles were not calculated due to all nan data #therefore cannot plot this line. continue plt.scatter(self.xpercentiles, line['ypercentiles'], color=np.atleast_1d(line['colour']), label=line['label'], linewidth=line['linewidth'], marker=line['marker']) axis_max = np.nanmax([axis_max, np.nanmax(line['ypercentiles'])]) axis_min = np.nanmin([axis_min, np.nanmin(line['ypercentiles'])]) #Set x/y lims if axis_min > 0 and axis_min < axis_max / 25.: #Ensure zero is plotted if close to zero. axis_min = 0 ax.set_xlim([axis_min, axis_max]) ax.set_ylim([axis_min, axis_max]) #Add 1-1 line if self.one2one: plt.plot([axis_min, axis_max], [axis_min, axis_max], color='k', linestyle='-') #Add title if self.title is None: self.gen_title() ax.set_title(self.title) #Add axis labels if self.xlabel is None: self.xlabel = self.xcube.attributes['label'] if self.units is not None: units = plotting_functions.units_str(self.units) self.xlabel += ' (' + units + ')' ax.set_xlabel(self.xlabel) if self.ylabel is None: if self.units is not None: units = plotting_functions.units_str(self.units) if len(self.lines) == 1: self.ylabel = self.lines[0]['cube'].attributes['label'] if self.units is not None: self.ylabel += ' (' + units + ')' else: self.ylabel = self.phenomena_short_name + ' (' + units + ')' ax.set_ylabel(self.ylabel) #Add legend if needed if len(self.lines) > 1: plotting_functions.add_legend_belowaxes(scatterpoints=1) #Add gridlines if self.gridlines: plt.grid() # Apply branding if self.mobrand: line_plot.add_mobranding() return self.fig
def plot(self): """ Plot histogram. Returns figure object for further plotting if needed """ if not self.lines: raise ValueError("Histogram: no lines have been added") if self.fig is None: self.fig = plt.figure() ax = self.fig.add_subplot(111) if self.bin_edges is None: self.gen_binedges() max_x = None for line in self.lines: #Get a 1d array of data data = np.reshape(line['cube'].data, -1) if sum(np.isfinite(data)) == 0: #All nan data, so don't include this line warnings.warn('All nan data') continue returned_tuple = ax.hist(data, bins=self.bin_edges, histtype=self.histtype, normed=self.normed, color=line['colour'], label=line['label'], linewidth=line['linewidth'], range=[np.nanmin(data), np.nanmax(data)]) if self.maxperc != 100: cumulative_sum = np.cumsum(returned_tuple[0]) #Get maximum value to be 1 cumulative_sum /= cumulative_sum[-1] indices = np.where(cumulative_sum * 100. > self.maxperc) if indices[0].size: max_x_line = returned_tuple[1][indices[0][0]] if max_x is None: max_x = max_x_line else: max_x = np.max([max_x, max_x_line]) #Add legend if self.legend: plotting_functions.add_legend_belowaxes() #Add title if self.title is None: self.gen_title() ax.set_title(self.title) #Add ylabel if self.ylabel is None: self.ylabel = 'Frequency' ax.set_ylabel(self.ylabel) #Add xlabel (units) if self.xlabel is None: if self.units is not None: units = plotting_functions.units_str(self.units) self.xlabel = units else: self.xlabel = '' ax.set_xlabel(self.xlabel) #Set maximum x limit if max_x is not None: ax.set_xlim(right=max_x) #Add gridlines if self.gridlines: plt.grid() # Apply branding if self.mobrand: line_plot.add_mobranding() return self.fig
def tsp_statistic(statscube_list, statistic, plotdir='./', filesuffix='', colours_list=None, xcoordname=None): """ Function to plot timeseries of a statistic, given a list of statistics cubes from :class:`TimeSeriesStats`. Note this routine is also called from the higher level :func:`adaq_plotting.plot_timeseries_of_stats` :param statscube_list: List of statistics cubes created by \ :meth:`timeseries_stats.TimeSeriesStats.convert_to_cube` and then merged such that each cube has more than one time coordinate. :param statistic: Name of statistic to plot. Should be one of the keys of the :data:`timeseries_stats.STATS_INFO` dictionary :param plotdir: String - directory for plot to be saved. :param filesuffix: String to add to end of filename for specific naming purposes. By default adds nothing. :param colours_list: List of colours to use to match in order against cubes in statscube_list. If not set, defaults to plotting_functions.COLOURS :param xcoordname: Name of coordinate to use on x-axis. If not set, defaults to first dimension coordinate (usually time). Example usage: >>> import adaq_functions >>> ini_dict, stats_cubes = adaq_functions.get_exampledata( ... exampletype='stats') # doctest: +ELLIPSIS Reading inifile ...example_data_10days.ini >>> import config >>> plotdir = config.CODE_DIR + "/adaqdocs/figures" Extract a list of just the O3 cubes: >>> stats_cubes_o3 = [] >>> for cubelist in stats_cubes: ... stat_cube_o3 = cubelist.extract(iris.AttributeConstraint( ... short_name='O3'), strict=True) ... stats_cubes_o3.append(stat_cube_o3) >>> print(stats_cubes_o3) [<iris 'Cube' of mass_concentration_of_ozone_in_air / (1) \ (istatistic: 32; time: 10)>] >>> print(stats_cubes_o3[0]) mass_concentration_of_ozone_in_air / (1) (istatistic: 32; time: 10) Dimension coordinates: istatistic x - time - x Auxiliary coordinates: statistic x - statistic_long_name x - statistic_units x - Attributes: Conventions: CF-1.5 label: aqum_oper obs: AURN short_name: O3 >>> print(stats_cubes_o3[0].coord('statistic').points) \ # doctest: +NORMALIZE_WHITESPACE ['mdi' 'nsites' 'npts' 'correlation' 'bias' 'nmb' 'mnmb' 'mge' 'nmge' \ 'fge' 'rmse' 'fac2' 'ioa' 'threshold' 'orss' 'odds_ratio' 'hitrate' \ 'falsealarmrate' 'falsealarmratio' 'o>=t_m>=t' 'o<t_m>=t' 'o>=t_m<t' \ 'o<t_m<t' 'maxobs' 'maxmod' 'meanobs' 'meanmod' 'sdobs' 'sdmod' \ 'perc_correct' 'perc_over' 'perc_under'] Choose 'bias' from the list of available statistics and plot this: >>> tsp = tsp_statistic(stats_cubes_o3, 'bias', plotdir=plotdir) ... # doctest: +ELLIPSIS Saved figure .../adaqdocs/figures/Timeseries_of_bias_O3.png .. image:: ../adaqdocs/figures/Timeseries_of_bias_O3.png :scale: 50% """ if not colours_list: colours_list = plotting_functions.COLOURS[1:] #Ignore black #Information dictionary about statistic statsinfo = timeseries_stats.STATS_INFO[statistic] #Set up TimeSeriesPlot tsp = TimeSeriesPlot() short_name = '' #Loop through each cube, adding it as a line to the plot for icube, statscube in enumerate(statscube_list): #Extract statistic cube = statscube.extract(iris.Constraint(statistic=statistic)) if cube is not None: short_name = cube.attributes['short_name'] if sum(np.isfinite(cube.data)) >= 2: #Need at least 2 valid (non-nan) points to produce # sensible plot (To draw a line between two points) #Label according to model only label = cube.attributes['label'] if xcoordname is not None: xcoord = cube.coord(xcoordname) elif 'time' in [c.name() for c in cube.coords()]: #Default to time coord if possible xcoord = cube.coord('time') else: xcoord = None tsp.add_line(cube, x=xcoord, label=label, colour=colours_list[icube]) if not tsp.lines: warnings.warn("No Timeseries of " + statistic + " plot for " + \ short_name + " (Not enough obs&model points)") #Don't plot this statistic return tsp #Add statistic and units to y axis label #Get units from last cube units = cube.coord('statistic_units').points[0] tsp.ylabel = statsinfo['long_name'] if units is not None and units != '1': #Add units to ylabel, converting to latex units if in LATEX, #otherwise leaving as units. tsp.ylabel += ' (' + plotting_functions.units_str(units) + ')' #Also add a perfect-value line if appropriate perfect_value = statsinfo.get('perfect_value', None) if perfect_value is not None: tsp.horiz_y = perfect_value #Set up title tsp.title = 'Time series of ' + statsinfo['long_name'] tsp.title += '\n ' + tsp.phenomena_name.replace('_', ' ') #Generate plot tsp.plot() #Save plot filename = 'Timeseries_of_' + statistic + '_' + \ short_name + filesuffix+'.png' tsp.save_fig(plotdir=plotdir, filename=filename) return tsp
def plot_cube_cross_section(cube, waypoints, short_name, ini_dict, bl_depth=None, titleprefix=None, plotdir=None, filesuffix='', tight=False, verbose=1): """ Plot vertical cross section of a cube along a given set of waypoints. Defaults (where parameters are set to None or 'default') are set to the defaults in :class:`field_layer.FieldLayer` or :class:`field_plot.FieldPlot`. :param cube: Iris cube which should contain X,Y and Z coordinates, plus 'short_name' and 'label' attributes. :param waypoints: List of dictionaries, whose keys are 'latitude' and 'longitude' and whose values are the lat/lon points that should be included in the cross-section. :param short_name: string to match to short_name attribute in cubes. :param ini_dict: Dictionary of values used to define appearance of plot. :param bl_depth: Iris cube containing boundary layer depth. This should contain X and Y coordinates, plus 'short_name' and 'label' attributes. If given, the boundary layer height will also be plotted on the cross-section plot. :param titleprefix: String to prefix to default title (time of plot). :param plotdir: Output directory for plot to be saved to. :param filesuffix: String to add to end of filename for specific naming purposes. By default adds nothing. :param tight: If set to True, will adjust figure size to be tight to bounding box of figure and minimise whitespace. Note this will change size of output file and may not be consistent amongst different plots (particularly if levels on colorbar are different). :param verbose: Level of print output: * 0 = No extra printing * 1 = Standard level (recommended) * 2 = Extra level for debugging :returns: Iris cube containing the section that was plotted. This will have additional coordinate 'i_sample_points' instead of X and Y coordinates. Load example cubes: >>> import config >>> import adaq_functions >>> sample_datadir = config.SAMPLE_DATADIR + 'aqum_output/oper/3d/' >>> tcube = iris.load_cube(sample_datadir + ... 'prodm_op_aqum_20170701_18.006.pp', 'air_temperature') >>> ini_dict = inifile.get_inidict(defaultfilename= ... 'adaqcode/adaq_vertical_plotting.ini') # doctest: +ELLIPSIS Reading inifile .../adaqcode/adaq_vertical_plotting.ini >>> tcube.attributes['short_name'] = 'T' >>> tcube.attributes['label'] = 'aqum' >>> tcube.coord('time').points = tcube.coord('time').bounds[:, 1] >>> print(tcube.summary(True)) # doctest: +NORMALIZE_WHITESPACE air_temperature / (K) (time: 2; model_level_number: 63; grid_latitude: 182; grid_longitude: 146) >>> blcube = iris.load_cube(sample_datadir + ... 'prods_op_aqum_20170701_18.000.pp', ... 'atmosphere_boundary_layer_thickness') >>> blcube.attributes['short_name'] = 'bl_depth' >>> blcube.attributes['label'] = 'aqum' Set up dictionary of way points and then plot cross section >>> waypoints = [{'latitude':51.1, 'longitude':-0.6}, ... {'latitude':51.7, 'longitude':0.2}, ... {'latitude':52.0, 'longitude':-1.0}] >>> plotdir = config.CODE_DIR + "/adaqdocs/" + "figures/adaq_plotting" >>> section = plot_cube_cross_section(tcube, waypoints, 'T', ini_dict, ... bl_depth=blcube, plotdir=plotdir) # doctest: +ELLIPSIS Saved figure .../CrossSection_aqum_T_201707020300.png Saved figure .../CrossSection_aqum_T_201707020600.png .. image:: ../adaqdocs/figures/adaq_plotting/ CrossSection_aqum_T_201707020600.png :scale: 75% >>> print(section.summary(True)) # doctest: +NORMALIZE_WHITESPACE air_temperature / (K) (time: 2; level_height: 26; i_sample_point: 30) """ #Get some variables from ini_dict if possible. max_height = ini_dict.get('max_height', None) if max_height is not None: max_height = float(max_height) #If levels not already set, get levels from ini_dict if possible #(and corresponding number of levels) levels_dict = ini_dict.get('levels_dict', {}) if short_name in levels_dict: levels = levels_dict[short_name] else: levels = ini_dict.get('levels_list', None) if levels is not None: levels = [float(v) for v in levels] if levels is not None: nlevels = len(levels) else: nlevels = ini_dict.get('nlevels', 10) cmap = ini_dict.get('cmap', 'YlGnBu') cbar_label = ini_dict.get('cbar_label', 'default') cbar = ini_dict.get('cbar', True) cbar_num_fmt = ini_dict.get('cbar_num_fmt', None) line_colours_list = ini_dict.get('line_colours_list', COLOURS) line_colour = line_colours_list[0] cbar_orientation = ini_dict.get('cbar_orientation', 'vertical') # Extract a section along the waypoints section = cube_functions.extract_section(cube, waypoints) #Plot against a sensible vertical coordinate if section.coords('model_level_number') and section.coords('level_height'): section.remove_coord('model_level_number') iris.util.promote_aux_coord_to_dim_coord(section, 'level_height') zcoordname, = cube_functions.guess_coord_names(section, ['Z']) if zcoordname is None: print('Not plotting cross section for ' + cube.attributes['short_name'] + ' from ' + cube.attributes['label'] + ' (no vertical coordinate)') return None scoordname = 'i_sample_point' #Limit section to maximum required height if max_height is not None: if section.coords('level_height'): section = section.extract( iris.Constraint(level_height=lambda c: c <= max_height)) else: raise UserWarning('Cannot limit to max_height as level_height' + 'coordinate does not exist') if bl_depth: bl_depth_section = cube_functions.extract_section(bl_depth, waypoints) #--- #Set up field layer flr = field_layer.FieldLayer(section) flr.set_layerstyle(plottype='pcolormesh', colorscale='linear', levels=levels, nlevels=nlevels, mask=True, cmap=cmap) flr.cbar = cbar flr.cbar_orientation = cbar_orientation flr.cbar_label = cbar_label flr.cbar_num_fmt = cbar_num_fmt # Set colour for boundary layer line plots line_colour = line_colour if line_colour else 'black' #--- #Loop over fields within layer (sample point & Z coords) for layer_slice in flr.layer_slice([scoordname, zcoordname]): fplt = field_plot.FieldPlot(ini_dict) #Don't plot fields which are entirely nan data if isinstance(layer_slice.cube.data, np.ma.MaskedArray): if np.sum(np.isfinite(layer_slice.cube.data.data)) == 0: #All nan data warnings.warn('All nan data, no gridded field plot created') continue else: #Not a masked array if np.sum(np.isfinite(layer_slice.cube.data)) == 0: #All nan data warnings.warn('All nan data, no gridded field plot created') continue fplt.add_layer(layer_slice) if titleprefix is None: titleprefix = ('Cross section for ' + cube.name().replace('_', ' ') + '\n') fplt.titleprefix = titleprefix # Set a specific figsize for this type of plot: fplt.plot(figsize=[15.0, 6.0]) # Get the coordinate that was actually used on the vertical axis. vert_axis_coord = iplt._get_plot_defn(flr.cube, iris.coords.BOUND_MODE, ndims=2).coords[0] plt.gca().set_ylabel(vert_axis_coord.name().replace('_', ' ') + ' (' + units_str(str(vert_axis_coord.units)) + ')') if vert_axis_coord.has_bounds(): plt.gca().set_ylim([0, vert_axis_coord.bounds[-1, 1]]) plt.gca().set_xlabel('Section point') plt.gca().set_xlim([-0.5, 29.5]) # n_sample_points=30. # Also plot BL depth at the same time if bl_depth: time = layer_slice.cube.coord('time').units.num2date( layer_slice.cube.coord('time').points[0]) bl_depth_slice = bl_depth_section.extract( iris.Constraint(time=time)) if bl_depth_slice: # found matching time # Plot iplt.plot(bl_depth_slice, label='Boundary Layer Height', color=line_colour, linestyle='--') # Add label plt.gca().legend() fplt.save_fig(plotdir=plotdir, fileprefix='CrossSection_', filesuffix=filesuffix, tight=tight, verbose=verbose) return section