33
44See the example notebook 'plotting-with-seaborn.ipynb' for usage examples
55"""
6-
76import warnings
7+ from itertools import cycle
88
99import numpy as np
1010
11+ try :
12+ import matplotlib .lines as mlines
13+ import matplotlib .patches as mpatches
14+ import matplotlib .pyplot as plt
15+
16+ has_matplotlib = True
17+ except ImportError : # pragma: no cover
18+ plt = None
19+ has_matplotlib = False
20+
1121try :
1222 import seaborn as sns
1323
@@ -65,6 +75,7 @@ def lineplot(self, time_axis=None, **kwargs): # pragma: no cover
6575 kwargs .setdefault ("y" , "value" )
6676 if "scenario" in self .meta_attributes :
6777 kwargs .setdefault ("hue" , "scenario" )
78+
6879 kwargs .setdefault ("ci" , "sd" )
6980 kwargs .setdefault ("estimator" , np .median )
7081
@@ -79,6 +90,284 @@ def lineplot(self, time_axis=None, **kwargs): # pragma: no cover
7990 return ax
8091
8192
93+ def plumeplot ( # pragma: no cover
94+ self ,
95+ ax = None ,
96+ quantiles_plumes = [((0.05 , 0.95 ), 0.5 ), ((0.5 ,), 1.0 ),],
97+ hue_var = "scenario" ,
98+ hue_label = "Scenario" ,
99+ palette = None ,
100+ style_var = "variable" ,
101+ style_label = "Variable" ,
102+ dashes = None ,
103+ linewidth = 2 ,
104+ time_axis = None ,
105+ pre_calculated = False ,
106+ quantile_over = ("ensemble_member" ,),
107+ ):
108+ """
109+ Make a plume plot, showing plumes for custom quantiles
110+
111+ Parameters
112+ ----------
113+ ax : :obj:`matplotlib.axes._subplots.AxesSubplot`
114+ Axes on which to make the plot
115+
116+ quantiles_plumes : list[tuple[tuple, float]]
117+ Configuration to use when plotting quantiles. Each element is a tuple,
118+ the first element of which is itself a tuple and the second element of
119+ which is the alpha to use for the quantile. If the first element has
120+ length two, these two elements are the quantiles to plot and a plume
121+ will be made between these two quantiles. If the first element has
122+ length one, then a line will be plotted to represent this quantile.
123+
124+ hue_var : str
125+ The column of ``self.meta`` which should be used to distinguish
126+ different hues.
127+
128+ hue_label : str
129+ Label to use in the legend for ``hue_var``.
130+
131+ palette : dict
132+ Dictionary defining the colour to use for different values of
133+ ``hue_var``.
134+
135+ style_var : str
136+ The column of ``self.meta`` which should be used to distinguish
137+ different styles.
138+
139+ style_label : str
140+ Label to use in the legend for ``style_var``.
141+
142+ dashes : dict
143+ Dictionary defining the style to use for different values of
144+ ``style_var``.
145+
146+ linewidth : float
147+ Width of lines to use (for quantiles which are not to be shown as
148+ plumes)
149+
150+ time_axis : str
151+ Time axis to use for the plot (see :meth:`~ScmRun.timeseries`)
152+
153+ pre_calculated : bool
154+ Are the quantiles pre-calculated? If no, the quantiles will be
155+ calculated within this function. Pre-calculating the quantiles using
156+ :meth:`ScmRun.quantiles_over` can lead to faster plotting if multiple
157+ plots are to be made with the same quantiles.
158+
159+ quantile_over : str, tuple[str]
160+ Columns of ``self.meta`` over which the quantiles should be calculated.
161+ Only used if ``pre_calculated`` is ``False``.
162+
163+ Returns
164+ -------
165+ :obj:`matplotlib.axes._subplots.AxesSubplot`, list
166+ Axes on which the plot was made and the legend items we have made (in
167+ case the user wants to move the legend to a different position for
168+ example)
169+
170+ Examples
171+ --------
172+ >>> scmrun = ScmRun(
173+ ... data=np.random.random((10, 3)).T,
174+ ... columns={
175+ ... "model": ["a_iam"],
176+ ... "climate_model": ["a_model"] * 5 + ["a_model_2"] * 5,
177+ ... "scenario": ["a_scenario"] * 5 + ["a_scenario_2"] * 5,
178+ ... "ensemble_member": list(range(5)) + list(range(5)),
179+ ... "region": ["World"],
180+ ... "variable": ["Surface Air Temperature Change"],
181+ ... "unit": ["K"],
182+ ... },
183+ ... index=[2005, 2010, 2015],
184+ ... )
185+
186+ Plot the plumes, calculated over the different ensemble members.
187+
188+ >>> scmrun.plumeplot(quantile_over="ensemble_member")
189+
190+ Pre-calculate the quantiles, then plot
191+
192+ >>> summary_stats = ScmRun(
193+ ... scmrun.quantiles_over("ensemble_member", quantiles=quantiles)
194+ ... )
195+ >>> summary_stats.plumeplot(pre_calculated=True)
196+
197+ Note
198+ ----
199+ ``scmdata`` is not a plotting library so this function is provided as is,
200+ with little testing. In some ways, it is more intended as inspiration for
201+ other users than as a robust plotting tool.
202+ """
203+ if not has_matplotlib :
204+ raise ImportError ("matplotlib is not installed. Run 'pip install matplotlib'" )
205+
206+ if not pre_calculated :
207+ quantiles = [v for qv in quantiles_plumes for v in qv [0 ]]
208+ _pdf = type (self )(self .quantiles_over (quantile_over , quantiles = quantiles ))
209+ else :
210+ _pdf = self
211+
212+ if ax is None :
213+ ax = plt .figure ().add_subplot (111 )
214+
215+ _palette = {} if palette is None else palette
216+
217+ if dashes is None :
218+ _dashes = {}
219+ lines = ["-" , "--" , "-." , ":" ]
220+ linestyle_cycler = cycle (lines )
221+ else :
222+ _dashes = dashes
223+
224+ _plotted_lines = False
225+
226+ quantile_labels = {}
227+ for q , alpha in quantiles_plumes :
228+ for hdf in _pdf .groupby (hue_var ):
229+ hue_value = hdf .get_unique_meta (hue_var , no_duplicates = True )
230+ pkwargs = {"alpha" : alpha }
231+
232+ for hsdf in hdf .groupby (style_var ):
233+ style_value = hsdf .get_unique_meta (style_var , no_duplicates = True )
234+
235+ xaxis = hsdf .timeseries (time_axis = time_axis ).columns .tolist ()
236+ if palette is not None :
237+ try :
238+ pkwargs ["color" ] = _palette [hue_value ]
239+ except KeyError as exc :
240+ error_msg = "{} not in palette: {}" .format (hue_value , palette )
241+ raise KeyError (error_msg ) from exc
242+
243+ elif hue_value in _palette :
244+ pkwargs ["color" ] = _palette [hue_value ]
245+
246+ if len (q ) == 2 :
247+ label = "{:.0f}th - {:.0f}th" .format (q [0 ] * 100 , q [1 ] * 100 )
248+ p = ax .fill_between (
249+ xaxis ,
250+ _get_1d_or_raise (
251+ hsdf .filter (quantile = q [0 ]), hue_var , style_var
252+ ),
253+ _get_1d_or_raise (
254+ hsdf .filter (quantile = q [1 ]), hue_var , style_var
255+ ),
256+ label = label ,
257+ ** pkwargs
258+ )
259+
260+ if palette is None :
261+ _palette [hue_value ] = p .get_facecolor ()[0 ]
262+
263+ elif len (q ) == 1 :
264+ _plotted_lines = True
265+
266+ if dashes is not None :
267+ try :
268+ pkwargs ["linestyle" ] = _dashes [style_value ]
269+ except KeyError as exc :
270+ error_msg = "{} not in dashes: {}" .format (
271+ style_value , dashes
272+ )
273+ raise KeyError (error_msg ) from exc
274+ else :
275+ _dashes [style_value ] = next (linestyle_cycler )
276+ pkwargs ["linestyle" ] = _dashes [style_value ]
277+
278+ if isinstance (q [0 ], str ):
279+ label = q [0 ]
280+ else :
281+ label = "{:.0f}th" .format (q [0 ] * 100 )
282+
283+ p = ax .plot (
284+ xaxis ,
285+ _get_1d_or_raise (
286+ hsdf .filter (quantile = q [0 ]), hue_var , style_var
287+ ),
288+ label = label ,
289+ linewidth = linewidth ,
290+ ** pkwargs
291+ )[0 ]
292+
293+ if dashes is None :
294+ _dashes [style_value ] = p .get_linestyle ()
295+
296+ else :
297+ raise ValueError (
298+ "quantiles to plot must be of length one or two, "
299+ "received: {}" .format (q )
300+ )
301+
302+ if label not in quantile_labels :
303+ quantile_labels [label ] = p
304+
305+ # Fake the line handles for the legend
306+ hue_val_lines = [
307+ mlines .Line2D ([0 ], [0 ], color = _palette [hue_value ], label = hue_value )
308+ for hue_value in self .get_unique_meta (hue_var )
309+ ]
310+
311+ legend_items = [
312+ mpatches .Patch (alpha = 0 , label = "Quantiles" ),
313+ * quantile_labels .values (),
314+ mpatches .Patch (alpha = 0 , label = hue_label ),
315+ * hue_val_lines ,
316+ ]
317+
318+ if _plotted_lines :
319+ style_val_lines = [
320+ mlines .Line2D (
321+ [0 ],
322+ [0 ],
323+ linestyle = _dashes [style_value ],
324+ label = style_value ,
325+ color = "gray" ,
326+ linewidth = linewidth ,
327+ )
328+ for style_value in self .get_unique_meta (style_var )
329+ ]
330+ legend_items += [
331+ mpatches .Patch (alpha = 0 , label = style_label ),
332+ * style_val_lines ,
333+ ]
334+ else :
335+ if dashes is not None :
336+ warnings .warn (
337+ "`dashes` was passed but no lines were plotted, the style settings will not be used"
338+ )
339+
340+ ax .legend (handles = legend_items , loc = "best" )
341+
342+ units = self .get_unique_meta ("unit" )
343+ if len (units ) == 1 :
344+ ax .set_ylabel (units [0 ])
345+
346+ return ax , legend_items
347+
348+
349+ def _get_1d_or_raise (in_scmrun , hue_var , style_var ):
350+ out_arr = in_scmrun .values .squeeze ()
351+ if len (out_arr .shape ) > 1 :
352+ quantile = in_scmrun .get_unique_meta ("quantile" , True )
353+ hue_var_value = in_scmrun .get_unique_meta (hue_var , True )
354+ style_var_value = in_scmrun .get_unique_meta (style_var , True )
355+ error_msg = (
356+ "More than one timeseries for "
357+ "quantile: {}, "
358+ "{}: {}, "
359+ "{}: {}.\n "
360+ "Please process your data to create unique quantile timeseries "
361+ "before calling :meth:`plumeplot`.\n "
362+ "Found: {}" .format (
363+ quantile , hue_var , hue_var_value , style_var , style_var_value , in_scmrun ,
364+ )
365+ )
366+ raise ValueError (error_msg )
367+
368+ return out_arr
369+
370+
82371def _deprecated_line_plot (self , ** kwargs ): # pragma: no cover
83372 """
84373 Make a line plot via `seaborn's lineplot <https://seaborn.pydata.org/generated/seaborn.lineplot.html>`_
@@ -112,6 +401,7 @@ def inject_plotting_methods(cls):
112401 methods = [
113402 ("lineplot" , lineplot ),
114403 ("line_plot" , _deprecated_line_plot ), # for compatibility
404+ ("plumeplot" , plumeplot ),
115405 ]
116406
117407 for name , f in methods :
0 commit comments