Skip to content

Commit d413536

Browse files
committed
Add basic integration tests
1 parent 654b911 commit d413536

4 files changed

Lines changed: 163 additions & 26 deletions

File tree

src/scmdata/plotting.py

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def lineplot(self, time_axis=None, **kwargs): # pragma: no cover
7575
kwargs.setdefault("y", "value")
7676
if "scenario" in self.meta_attributes:
7777
kwargs.setdefault("hue", "scenario")
78+
7879
kwargs.setdefault("ci", "sd")
7980
kwargs.setdefault("estimator", np.median)
8081

@@ -93,7 +94,6 @@ def plumeplot( # pragma: no cover
9394
self,
9495
ax=None,
9596
quantiles_plumes=[((0.05, 0.95), 0.5), ((0.5,), 1.0),],
96-
quantile_over=("model",),
9797
hue_var="scenario",
9898
hue_label="Scenario",
9999
palette=None,
@@ -102,6 +102,8 @@ def plumeplot( # pragma: no cover
102102
dashes=None,
103103
linewidth=2,
104104
time_axis=None,
105+
pre_calculated=False,
106+
quantile_over=("ensemble_member",),
105107
):
106108
"""
107109
Make a plume plot, showing plumes for custom quantiles
@@ -112,51 +114,104 @@ def plumeplot( # pragma: no cover
112114
Axes on which to make the plot
113115
114116
quantiles_plumes : list[tuple[tuple, float]]
115-
Configuration to use when plotting quantiles. Each element is a tuple, the first element of which is itself a tuple and the second element of which is the alpha to use for the quantile. If the first element has length two, these two elements are the quantiles to plot and a plume will be made between these two quantiles. If the first element has length one, then a line will be plotted to represent this quantile.
116-
117-
quantile_over : tuple[str]
118-
Columns of ``self.meta`` over which the quantiles should be calculated.
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.
119123
120124
hue_var : str
121-
The column of ``self.meta`` which should be used to distinguish different hues.
125+
The column of ``self.meta`` which should be used to distinguish
126+
different hues.
122127
123128
hue_label : str
124129
Label to use in the legend for ``hue_var``.
125130
126131
palette : dict
127-
Dictionary defining the colour to use for different values of ``hue_var``.
132+
Dictionary defining the colour to use for different values of
133+
``hue_var``.
128134
129135
style_var : str
130-
The column of ``self.meta`` which should be used to distinguish different styles.
136+
The column of ``self.meta`` which should be used to distinguish
137+
different styles.
131138
132139
style_label : str
133140
Label to use in the legend for ``style_var``.
134141
135142
dashes : dict
136-
Dictionary defining the style to use for different values of ``style_var``.
143+
Dictionary defining the style to use for different values of
144+
``style_var``.
137145
138146
linewidth : float
139-
Width of lines to use (for quantiles which are not to be shown as plumes)
147+
Width of lines to use (for quantiles which are not to be shown as
148+
plumes)
140149
141150
time_axis : str
142151
Time axis to use for the plot (see :meth:`~ScmRun.timeseries`)
143152
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 ``True``.
162+
144163
Returns
145164
-------
146165
:obj:`matplotlib.axes._subplots.AxesSubplot`, list
147166
Axes on which the plot was made and the legend items we have made (in
148167
case the user wants to move the legend to a different position for
149168
example)
150169
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+
151197
Note
152198
----
153199
``scmdata`` is not a plotting library so this function is provided as is,
154-
with no testing. In some ways, it is more intended as inspiration for other
155-
users than as a robust plotting tool.
200+
with little testing. In some ways, it is more intended as inspiration for
201+
other users than as a robust plotting tool.
156202
"""
157203
if not has_matplotlib:
158204
raise ImportError("matplotlib is not installed. Run 'pip install matplotlib'")
159205

206+
if not pre_calculated:
207+
quantiles = [v for qv in quantiles_plumes for v in qv[0]]
208+
_pdf = type(self)(
209+
self.quantiles_over(quantile_over, quantiles=quantiles)
210+
)
211+
else:
212+
_pdf = self
213+
214+
160215
if ax is None:
161216
ax = plt.figure().add_subplot(111)
162217

@@ -169,9 +224,11 @@ def plumeplot( # pragma: no cover
169224
else:
170225
_dashes = dashes
171226

227+
_plotted_lines = False
228+
172229
quantile_labels = {}
173230
for q, alpha in quantiles_plumes:
174-
for hdf in self.groupby(hue_var):
231+
for hdf in _pdf.groupby(hue_var):
175232
hue_value = hdf.get_unique_meta(hue_var, no_duplicates=True)
176233
pkwargs = {"alpha": alpha}
177234

@@ -196,6 +253,8 @@ def plumeplot( # pragma: no cover
196253
_palette[hue_value] = p.get_facecolor()[0]
197254

198255
elif len(q) == 1:
256+
_plotted_lines = True
257+
199258
if style_value in _dashes:
200259
pkwargs["linestyle"] = _dashes[style_value]
201260
else:
@@ -206,6 +265,7 @@ def plumeplot( # pragma: no cover
206265
label = q[0]
207266
else:
208267
label = "{:.0f}th".format(q[0] * 100)
268+
209269
p = ax.plot(
210270
xaxis,
211271
hsdf.filter(quantile=q[0]).values.squeeze(),
@@ -232,26 +292,34 @@ def plumeplot( # pragma: no cover
232292
for hue_value in self.get_unique_meta(hue_var)
233293
]
234294

235-
style_val_lines = [
236-
mlines.Line2D(
237-
[0],
238-
[0],
239-
**{"linestyle": _dashes[style_value]},
240-
label=style_value,
241-
color="gray"
242-
)
243-
for style_value in self.get_unique_meta(style_var)
244-
]
245-
246295
legend_items = [
247296
mpatches.Patch(alpha=0, label="Quantiles"),
248297
*quantile_labels.values(),
249298
mpatches.Patch(alpha=0, label=hue_label),
250299
*hue_val_lines,
251-
mpatches.Patch(alpha=0, label=style_label),
252-
*style_val_lines,
253300
]
254301

302+
if _plotted_lines:
303+
style_val_lines = [
304+
mlines.Line2D(
305+
[0],
306+
[0],
307+
**{"linestyle": _dashes[style_value]},
308+
label=style_value,
309+
color="gray"
310+
)
311+
for style_value in self.get_unique_meta(style_var)
312+
]
313+
legend_items += [
314+
mpatches.Patch(alpha=0, label=style_label),
315+
*style_val_lines,
316+
]
317+
else:
318+
if dashes is not None:
319+
warnings.warn(
320+
"`dashes` was passed but no lines were plotted, the style settings will not be used"
321+
)
322+
255323
ax.legend(handles=legend_items, loc="best")
256324

257325
units = self.get_unique_meta("unit")

tests/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,23 @@ def test_processing_scm_df():
327327
)
328328

329329

330+
@pytest.fixture(scope="function")
331+
def plumeplot_scmrun():
332+
yield ScmRun(
333+
data=np.random.random((10, 3)).T,
334+
columns={
335+
"model": ["a_iam"],
336+
"climate_model": ["a_model"] * 5 + ["a_model_2"] * 5,
337+
"scenario": ["a_scenario"] * 5 + ["a_scenario_2"] * 5,
338+
"ensemble_member": list(range(5)) + list(range(5)),
339+
"region": ["World"],
340+
"variable": ["Surface Air Temperature Change"],
341+
"unit": ["K"],
342+
},
343+
index=[datetime(2005, 1, 1), datetime(2010, 1, 1), datetime(2015, 6, 12)],
344+
)
345+
346+
330347
append_scm_df_pairs_cols = {
331348
"model": ["a_iam"],
332349
"climate_model": ["a_model"],
@@ -491,6 +508,7 @@ def test_append_scm_runs(request):
491508
def iamdf_type():
492509
if not IamDataFrame:
493510
pytest.skip("pyam not installed")
511+
494512
return IamDataFrame
495513

496514

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import pytest
2+
3+
from scmdata import ScmRun
4+
5+
6+
sample_quantiles_plumes = pytest.mark.parametrize("quantiles_plumes", (
7+
(((0.05, 0.95), 0.5), ((0.5,), 1.0),),
8+
(((0.17, 0.83), 0.7),),
9+
))
10+
11+
12+
def test_plumeplot_default(plumeplot_scmrun):
13+
plumeplot_scmrun.plumeplot()
14+
15+
16+
@sample_quantiles_plumes
17+
def test_plumeplot(plumeplot_scmrun, quantiles_plumes):
18+
plumeplot_scmrun.plumeplot(quantiles_plumes=quantiles_plumes)
19+
20+
21+
@sample_quantiles_plumes
22+
def test_plumeplot_pre_calculated(plumeplot_scmrun, quantiles_plumes):
23+
quantiles = [v for qv in quantiles_plumes for v in qv[0]]
24+
summary_stats = ScmRun(
25+
plumeplot_scmrun.quantiles_over("ensemble_member", quantiles=quantiles)
26+
)
27+
summary_stats.plumeplot(
28+
quantiles_plumes=quantiles_plumes,
29+
pre_calculated=True,
30+
)
31+
32+
33+
def test_plumeplot_warns_dashes_without_lines(scm_run):
34+
with pytest.warns(UserWarning) as record:
35+
scm_run.plumeplot(
36+
quantiles_plumes=(((0.17, 0.83), 0.7),),
37+
quantile_over="ensemble_member",
38+
dashes={"Surface Air Temperature Change": "--"},
39+
)
40+
41+
assert len(record) == 1
42+
assert record[0].message.args[0] == (
43+
"`dashes` was passed but no lines were plotted, the style "
44+
"settings will not be used"
45+
)

tests/unit/test_plotting.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,9 @@ def test_lineplot_base(mock_seaborn_lineplot, base_scm_run, scm_run):
117117
scm_run.lineplot(time_axis="year")
118118
call_args, call_kwargs = mock_seaborn_lineplot.call_args_list[0]
119119
assert "hue" in call_kwargs
120+
121+
122+
# runs
123+
# calculate_quantiles argument works
124+
# sensible error if non-unique for given set of filters
125+
# sensible error if missing style etc.

0 commit comments

Comments
 (0)