• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

quaquel / EMAworkbench / 17440226021

03 Sep 2025 04:46PM UTC coverage: 83.291% (+3.0%) from 80.3%
17440226021

push

github

web-flow
Update ci.yml (#406)

7238 of 8690 relevant lines covered (83.29%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.56
/ema_workbench/analysis/plotting.py
1
"""this module provides functions for generating some basic figures.
2

3
The code can be used as is, or serve as an example for writing your own code.
4

5
"""
6

7
import matplotlib.pyplot as plt
1✔
8
import numpy as np
1✔
9
from matplotlib.patches import ConnectionPatch
1✔
10

11
from ..util import EMAError, get_module_logger
1✔
12

13
# from . import plotting_util
14
from .plotting_util import (
1✔
15
    TIME,
16
    Density,
17
    LegendEnum,
18
    PlotType,
19
    do_titles,
20
    do_ylabels,
21
    get_color,
22
    group_density,
23
    make_grid,
24
    make_legend,
25
    plot_envelope,
26
    prepare_data,
27
    simple_density,
28
    simple_kde,
29
)
30

31
# .. codeauthor:: jhkwakkel <j.h.kwakkel (at) tudelft (dot) nl>
32

33
__all__ = ["envelopes", "kde_over_time", "lines", "multiple_densities"]
1✔
34
_logger = get_module_logger(__name__)
1✔
35
TIME_LABEL = "Time"
1✔
36

37

38
def envelopes(
1✔
39
    experiments,
40
    outcomes,
41
    outcomes_to_show=None,
42
    group_by=None,
43
    grouping_specifiers=None,
44
    density=None,
45
    fill=False,
46
    legend=True,
47
    titles=None,
48
    ylabels=None,
49
    log=False,
50
):
51
    """Make envelop plots.
52

53
    An envelope shows over time the minimum and maximum  value for a set
54
    of runs over time. It is thus to be used in case of time series
55
    data. The function will try to find a result labeled "TIME". If this
56
    is present, these values will be used on the X-axis. In case of
57
    Vensim models, TIME is present by default.
58

59
    Parameters
60
    ----------
61
    experiments : DataFrame
62
    outcomes : OutcomesDict
63
    outcomes_to_show, str, list of str, optional
64
    group_by : str, optional
65
               name of the column in the experimentsto group results by.
66
               Alternatively, `index` can be used to use indexing
67
               arrays as the basis for grouping.
68
    grouping_specifiers : iterable or dict, optional
69
                          set of categories to be used as a basis for
70
                          grouping by. Grouping_specifiers is only
71
                          meaningful if group_by is provided as well.
72
                          In case of grouping by index, the grouping
73
                          specifiers should be in a  dictionary where
74
                          the key denotes the name of the group.
75
    density : {None, HIST, KDE, VIOLIN, BOXPLOT}, optional
76
    fill : bool, optional
77
    legend : bool, optional
78
    titles : dict, optional
79
             a way for controlling whether each of the axes should have a
80
             title. There are three possibilities. If set to None, no title
81
             will be shown for any of the axes. If set to an empty dict,
82
             the default, the title is identical to the name of the outcome of
83
             interest. If you want to override these default names, provide a
84
             dict with the outcome of interest as key and the desired title as
85
             value. This dict need only contain the outcomes for which you
86
             want to use a different title.
87
    ylabels : dict, optional
88
              way for controlling the ylabels. Works identical to titles.
89
    log : bool, optional
90
          log scale density plot
91

92
    Returns:
93
    -------
94
    Figure : Figure instance
95
    axes : dict
96
           dict with outcome as key, and axes as value. Density axes' are
97
           indexed by the outcome followed by _density.
98

99
    Note:
100
    ----
101
    the current implementation is limited to seven different categories in case
102
    of group_by, categories, and/or discretesize. This limit is due to the colors
103
    specified in COLOR_LIST.
104

105
    Examples:
106
    --------
107
    >>> import util as util
108
    >>> data = util.load_results('1000 flu cases.tar.gz')
109
    >>> envelopes(data, group_by='policy')
110

111
    will show an envelope for three different policies, for all the
112
    outcomes of interest. while
113

114
    >>> envelopes(data, group_by='policy', categories=['static policy',
115
                  'adaptive policy'])
116

117
    will only show results for the two specified policies, ignoring any results
118
    associated with no policy.
119

120
    """
121
    _logger.debug("generating envelopes")
1✔
122
    prepared_data = prepare_data(
1✔
123
        experiments,
124
        None,
125
        outcomes,
126
        outcomes_to_show,
127
        group_by,
128
        grouping_specifiers,
129
        filter_scalar=True,
130
    )
131
    experiments, outcomes, outcomes_to_show, time, grouping_labels = prepared_data
1✔
132

133
    figure, grid = make_grid(outcomes_to_show, density)
1✔
134

135
    # do the plotting
136
    axes_dict = {}
1✔
137
    for i, outcome_to_plot in enumerate(outcomes_to_show):
1✔
138
        ax = figure.add_subplot(grid[i, 0])
1✔
139
        axes_dict[outcome_to_plot] = ax
1✔
140

141
        ax_d = None
1✔
142
        if density:
1✔
143
            ax_d = figure.add_subplot(grid[i, 1], sharey=ax)
1✔
144
            axes_dict[outcome_to_plot + "_density"] = ax_d
1✔
145

146
        if group_by:
1✔
147
            group_by_envelopes(
1✔
148
                outcomes,
149
                outcome_to_plot,
150
                time,
151
                density,
152
                ax,
153
                ax_d,
154
                fill,
155
                grouping_labels,
156
                log,
157
            )
158
        else:
159
            single_envelope(
1✔
160
                outcomes, outcome_to_plot, time, density, ax, ax_d, fill, log
161
            )
162

163
        if ax_d:
1✔
164
            for tl in ax_d.get_yticklabels():
1✔
165
                tl.set_visible(False)
1✔
166

167
        ax.set_xlabel(TIME_LABEL)
1✔
168
        do_ylabels(ax, ylabels, outcome_to_plot)
1✔
169
        do_titles(ax, titles, outcome_to_plot)
1✔
170

171
    if legend and group_by:
1✔
172
        gs1 = grid[0, 0]
1✔
173

174
        for ax in figure.axes:
1✔
175
            gs2 = ax._subplotspec
1✔
176
            if all(
1✔
177
                (
178
                    gs1._gridspec == gs2._gridspec,
179
                    gs1.num1 == gs2.num1,
180
                    gs1.num2 == gs2.num2,
181
                )
182
            ):
183
                break
1✔
184
        if fill:
1✔
185
            make_legend(grouping_labels, ax, alpha=0.3, legend_type=LegendEnum.PATCH)
1✔
186
        else:
187
            make_legend(grouping_labels, ax, legend_type=LegendEnum.LINE)
1✔
188

189
    return figure, axes_dict
1✔
190

191

192
def group_by_envelopes(
1✔
193
    outcomes, outcome_to_plot, time, density, ax, ax_d, fill, group_labels, log
194
):
195
    """Helper function responsible for generating an envelope plot based on a grouping.
196

197
    Parameters
198
    ----------
199
    outcomes : OutcomesDict
200
               a dictionary containing the various outcomes to plot
201
    outcome_to_plot : str
202
                      the specific outcome to plot
203
    time : str
204
           the name of the time dimension
205
    density :  {None, HIST, KDE, VIOLIN, BOXPLOT}
206
    ax : Axes instance
207
         the ax on which to plot
208
    ax_d : Axes instance
209
           the ax on which to plot the density
210
    fill : bool
211
    group_by_labels : list of str
212
                      order in which groups should be plotted
213
    log : bool
214

215
    """
216
    for j, key in enumerate(group_labels):
1✔
217
        value = outcomes[key]
1✔
218
        value = value[outcome_to_plot]
1✔
219
        try:
1✔
220
            plot_envelope(ax, j, time, value, fill)
1✔
221
        except ValueError:
×
222
            _logger.exception(f"ValueError when plotting for {key}")
×
223
            raise
×
224

225
    if density:
1✔
226
        group_density(ax_d, density, outcomes, outcome_to_plot, group_labels, log)
1✔
227

228
        ax_d.get_yaxis().set_view_interval(
1✔
229
            ax.get_yaxis().get_view_interval()[0], ax.get_yaxis().get_view_interval()[1]
230
        )
231

232

233
def single_envelope(outcomes, outcome_to_plot, time, density, ax, ax_d, fill, log):
1✔
234
    """Helper function for generating a single envelope plot.
235

236
    Parameters
237
    ----------
238
    outcomes : OutcomesDict
239
               a dictionary containing the various outcomes to plot
240
    outcome_to_plot : str
241
                      the specific outcome to plot
242
    time : str
243
           the name of the time dimension
244
    density :  {None, HIST, KDE, VIOLIN, BOXPLOT}
245
    ax : Axes instance
246
         the ax on which to plot
247
    ax_d : Axes instance
248
           the ax on which to plot the density
249
    fill : bool
250
    group_by_labels : list of str
251
                      order in which groups should be plotted
252
    log : bool
253

254
    """
255
    value = outcomes[outcome_to_plot]
1✔
256

257
    plot_envelope(ax, 0, time, value, fill)
1✔
258
    if density:
1✔
259
        simple_density(density, value, ax_d, ax, log)
1✔
260

261

262
def lines(
1✔
263
    experiments,
264
    outcomes,
265
    outcomes_to_show=None,
266
    group_by=None,
267
    grouping_specifiers=None,
268
    density="",
269
    legend=True,
270
    titles=None,
271
    ylabels=None,
272
    experiments_to_show=None,
273
    show_envelope=False,
274
    log=False,
275
):
276
    """Visualize results from experiments as line plots.
277

278
    It is thus to be used in case of time
279
    series data. The function will try to find a result labeled "TIME". If this
280
    is present, these values will be used on the X-axis. In case of Vensim
281
    models, TIME is present by default.
282

283
    Parameters
284
    ----------
285
    experiments : DataFrame
286
    outcomes : OutcomesDict
287
    outcomes_to_show : list of str, optional
288
                       list of outcome of interest you want to plot. If empty,
289
                       all outcomes are plotted. **Note**:  just names.
290
    group_by : str, optional
291
               name of the column in the cases array to group results by.
292
               Alternatively, `index` can be used to use indexing arrays as the
293
               basis for grouping.
294
    grouping_specifiers : iterable or dict, optional
295
                          set of categories to be used as a basis for grouping
296
                          by. Grouping_specifiers is only meaningful if
297
                          group_by is provided as well. In case of grouping by
298
                          index, the grouping specifiers should be in a
299
                          dictionary where the key denotes the name of the
300
                          group.
301
    density : {None, HIST, KDE, VIOLIN, BOXPLOT}, optional
302
    legend : bool, optional
303
    titles : dict, optional
304
             a way for controlling whether each of the axes should have a
305
             title. There are three possibilities. If set to None, no title
306
             will be shown for any of the axes. If set to an empty dict,
307
             the default, the title is identical to the name of the outcome of
308
             interest. If you want to override these default names, provide a
309
             dict with the outcome of interest as key and the desired title as
310
             value. This dict need only contain the outcomes for which you
311
             want to use a different title.
312
    ylabels : dict, optional
313
              way for controlling the ylabels. Works identical to titles.
314
    experiments_to_show : ndarray, optional
315
                          indices of experiments to show lines for,
316
                          defaults to None.
317
    show_envelope : bool, optional
318
                    show envelope of outcomes. This envelope is the based on
319
                    the minimum at each column and the maximum at each column.
320
    log : bool, optional
321
          log scale density plot
322

323
    Returns:
324
    -------
325
    fig : Figure instance
326
    axes : dict
327
           dict with outcome as key, and axes as value. Density axes' are
328
           indexed by the outcome followed by _density.
329

330
    Note:
331
    ----
332
    the current implementation is limited to seven different categories in case
333
    of group_by, categories, and/or discretesize. This limit is due to the colors
334
    specified in COLOR_LIST.
335

336
    """
337
    _logger.debug("generating line graph")
1✔
338

339
    # make sure we have the data
340

341
    if show_envelope:
1✔
342
        return plot_lines_with_envelopes(
1✔
343
            experiments,
344
            outcomes,
345
            outcomes_to_show=outcomes_to_show,
346
            group_by=group_by,
347
            legend=legend,
348
            density=density,
349
            grouping_specifiers=grouping_specifiers,
350
            experiments_to_show=experiments_to_show,
351
            titles=titles,
352
            ylabels=ylabels,
353
            log=log,
354
        )
355

356
    data = prepare_data(
1✔
357
        experiments,
358
        experiments_to_show,
359
        outcomes,
360
        outcomes_to_show,
361
        group_by,
362
        grouping_specifiers,
363
    )
364
    experiments, outcomes, outcomes_to_show, time, grouping_labels = data
1✔
365

366
    figure, grid = make_grid(outcomes_to_show, density)
1✔
367
    axes_dict = {}
1✔
368

369
    # do the plotting
370
    for i, outcome_to_plot in enumerate(outcomes_to_show):
1✔
371
        ax = figure.add_subplot(grid[i, 0])
1✔
372
        axes_dict[outcome_to_plot] = ax
1✔
373

374
        ax_d = None
1✔
375
        if density:
1✔
376
            ax_d = figure.add_subplot(grid[i, 1], sharey=ax)
1✔
377
            axes_dict[outcome_to_plot + "_density"] = ax_d
1✔
378

379
            for tl in ax_d.get_yticklabels():
1✔
380
                tl.set_visible(False)
1✔
381

382
        if group_by:
1✔
383
            group_by_lines(
1✔
384
                outcomes, outcome_to_plot, time, density, ax, ax_d, grouping_labels, log
385
            )
386
        else:
387
            simple_lines(outcomes, outcome_to_plot, time, density, ax, ax_d, log)
1✔
388
        ax.set_xlabel(TIME_LABEL)
1✔
389
        do_ylabels(ax, ylabels, outcome_to_plot)
1✔
390
        do_titles(ax, titles, outcome_to_plot)
1✔
391

392
    if legend and group_by:
1✔
393
        gs1 = grid[0, 0]
1✔
394

395
        for ax in figure.axes:
1✔
396
            gs2 = ax._subplotspec
1✔
397
            if all(
1✔
398
                (
399
                    gs1._gridspec == gs2._gridspec,
400
                    gs1.num1 == gs2.num1,
401
                    gs1.num2 == gs2.num2,
402
                )
403
            ):
404
                break
1✔
405

406
        make_legend(grouping_labels, ax)
1✔
407

408
    return figure, axes_dict
1✔
409

410

411
def plot_lines_with_envelopes(
1✔
412
    experiments,
413
    outcomes,
414
    outcomes_to_show=None,
415
    group_by=None,
416
    grouping_specifiers=None,
417
    density="",
418
    legend=True,
419
    titles=None,
420
    ylabels=None,
421
    experiments_to_show=None,
422
    log=False,
423
):
424
    """Helper function for generating a plot which both an envelope and lines.
425

426
    Parameters
427
    ----------
428
    experiments : DataFrame
429
    outcomes : OutcomesDict
430
    outcomes_to_show : list of str, optional
431
                       list of outcome of interest you want to plot. If empty,
432
                       all outcomes are plotted. **Note**:  just names.
433
    group_by : str, optional
434
               name of the column in the cases array to group results by.
435
               Alternatively, `index` can be used to use indexing arrays as the
436
               basis for grouping.
437
    grouping_specifiers : iterable or dict, optional
438
                          set of categories to be used as a basis for grouping
439
                          by. Grouping_specifiers is only meaningful if
440
                          group_by is provided as well. In case of grouping by
441
                          index, the grouping specifiers should be in a
442
                          dictionary where the key denotes the name of the
443
                          group.
444
    density : {None, HIST, KDE, VIOLIN, BOXPLOT}, optional
445
    legend : bool, optional
446
    titles : dict, optional
447
             a way for controlling whether each of the axes should have a
448
             title. There are three possibilities. If set to None, no title
449
             will be shown for any of the axes. If set to an empty dict,
450
             the default, the title is identical to the name of the outcome of
451
             interest. If you want to override these default names, provide a
452
             dict with the outcome of interest as key and the desired title as
453
             value. This dict need only contain the outcomes for which you
454
             want to use a different title.
455
    ylabels : dict, optional
456
              way for controlling the ylabels. Works identical to titles.
457
    experiments_to_show : ndarray, optional
458
                          indices of experiments to show lines for,
459
                          defaults to None.
460
    log : bool, optional
461

462
    Returns:
463
    -------
464
    Figure
465
        a figure instance
466
    dict
467
        dict with outcome as key, and axes as value. Density axes' are
468
        indexed by the outcome followed by _density
469
    """
470
    full_outcomes = prepare_data(
1✔
471
        experiments, None, outcomes, outcomes_to_show, group_by, grouping_specifiers
472
    )[1]
473
    data = prepare_data(
1✔
474
        experiments,
475
        experiments_to_show,
476
        outcomes,
477
        outcomes_to_show,
478
        group_by,
479
        grouping_specifiers,
480
    )
481
    experiments, outcomes, outcomes_to_show, time, grouping_labels = data
1✔
482

483
    figure, grid = make_grid(outcomes_to_show, density)
1✔
484
    axes_dict = {}
1✔
485

486
    # do the plotting
487
    for i, outcome_to_plot in enumerate(outcomes_to_show):
1✔
488
        ax = figure.add_subplot(grid[i, 0])
1✔
489
        axes_dict[outcome_to_plot] = ax
1✔
490

491
        ax_d = None
1✔
492
        if density:
1✔
493
            ax_d = figure.add_subplot(grid[i, 1], sharey=ax)
1✔
494
            axes_dict[outcome_to_plot + "_density"] = ax_d
1✔
495

496
            for tl in ax_d.get_yticklabels():
1✔
497
                tl.set_visible(False)
1✔
498

499
        if group_by:
1✔
500
            for j, key in enumerate(grouping_labels):
1✔
501
                full_value = full_outcomes[key][outcome_to_plot]
1✔
502
                plot_envelope(ax, j, time, full_value, fill=True)
1✔
503
            for j, key in enumerate(grouping_labels):
1✔
504
                value = outcomes[key][outcome_to_plot]
1✔
505
                full_value = full_outcomes[key][outcome_to_plot]
1✔
506
                ax.plot(time.T[:, np.newaxis], value.T, c=get_color(j))
1✔
507

508
            if density:
1✔
509
                group_density(
1✔
510
                    ax_d, density, full_outcomes, outcome_to_plot, grouping_labels, log
511
                )
512

513
                ax_d.get_yaxis().set_view_interval(
1✔
514
                    ax.get_yaxis().get_view_interval()[0],
515
                    ax.get_yaxis().get_view_interval()[1],
516
                )
517

518
        else:
519
            value = full_outcomes[outcome_to_plot]
×
520
            plot_envelope(ax, 0, time, value, fill=True)
×
521
            if density:
×
522
                simple_density(density, value, ax_d, ax, log)
×
523

524
            value = outcomes[outcome_to_plot]
×
525
            ax.plot(time.T, value.T)
×
526

527
        ax.set_xlim(left=time[0], right=time[-1])
1✔
528
        ax.set_xlabel(TIME_LABEL)
1✔
529
        do_ylabels(ax, ylabels, outcome_to_plot)
1✔
530
        do_titles(ax, titles, outcome_to_plot)
1✔
531

532
    if legend and group_by:
1✔
533
        gs1 = grid[0, 0]
1✔
534

535
        for ax in figure.axes:
1✔
536
            gs2 = ax._subplotspec
1✔
537
            if all(
1✔
538
                (
539
                    gs1._gridspec == gs2._gridspec,
540
                    gs1.num1 == gs2.num1,
541
                    gs1.num2 == gs2.num2,
542
                )
543
            ):
544
                break
1✔
545
        make_legend(grouping_labels, ax)
1✔
546

547
    return figure, axes_dict
1✔
548

549

550
def group_by_lines(
1✔
551
    outcomes, outcome_to_plot, time, density, ax, ax_d, group_by_labels, log
552
):
553
    """Helper function responsible for generating a grouped lines plot.
554

555
    Parameters
556
    ----------
557
    results : tupule
558
              return from :meth:`perform_experiments`.
559
    outcome_to_plot : str
560
    time : str
561
    density : {None, HIST, KDE, VIOLIN, BOXPLOT}
562
    ax : Axes instance
563
    ax_d : Axes instance
564
    group_by_labels : list of str
565
    log : bool
566

567
    """
568
    for j, key in enumerate(group_by_labels):
1✔
569
        value = outcomes[key]
1✔
570
        value = value[outcome_to_plot]
1✔
571

572
        color = get_color(j)
1✔
573
        ax.plot(time.T[:, np.newaxis], value.T, c=color, ms=1, markevery=5)
1✔
574

575
    if density:
1✔
576
        group_density(ax_d, density, outcomes, outcome_to_plot, group_by_labels, log)
1✔
577

578
        ax_d.get_yaxis().set_view_interval(
1✔
579
            ax.get_yaxis().get_view_interval()[0], ax.get_yaxis().get_view_interval()[1]
580
        )
581

582

583
def simple_lines(outcomes, outcome_to_plot, time, density, ax, ax_d, log):
1✔
584
    """Helper function responsible for generating a simple lines plot.
585

586
    Parameters
587
    ----------
588
    outcomes : OutcomesDict
589
    outcomes_to_plot : str
590
    time : str
591
    density : {None, HIST, KDE, VIOLIN, BOXPLOT}
592
    ax : Axes instance
593
    ax_d : Axes instance
594
    log : bool
595

596
    """
597
    value = outcomes[outcome_to_plot]
1✔
598
    ax.plot(time.T, value.T)
1✔
599
    if density:
1✔
600
        simple_density(density, value, ax_d, ax, log)
1✔
601

602

603
def kde_over_time(
1✔
604
    experiments,
605
    outcomes,
606
    outcomes_to_show=None,
607
    group_by=None,
608
    grouping_specifiers=None,
609
    colormap="viridis",
610
    log=True,
611
):
612
    """Plot a KDE over time. The KDE is visualized through a heatmap.
613

614
    Parameters
615
    ----------
616
    experiments : DataFrame
617
    outcomes : OutcomesDict
618
    outcomes_to_show : list of str, optional
619
                       list of outcome of interest you want to plot. If
620
                       empty, all outcomes are plotted.
621
                       **Note**:  just names.
622
    group_by : str, optional
623
               name of the column in the cases array to group results
624
               by. Alternatively, `index` can be used to use indexing
625
               arrays as the basis for grouping.
626
    grouping_specifiers : iterable or dict, optional
627
                          set of categories to be used as a basis for
628
                          grouping by. Grouping_specifiers is only
629
                          meaningful if group_by is provided as well.
630
                          In case of grouping by index, the grouping
631
                          specifiers should be in a dictionary where
632
                          the key denotes the name of the group.
633
    colormap : str, optional
634
               valid matplotlib color map name
635
    log : bool, optional
636

637
    Returns:
638
    -------
639
    list of Figure instances
640
        a figure instance for each group for each outcome
641
    dict
642
        dict with outcome as key, and axes as value. Density axes' are
643
        indexed by the outcome followed by _density
644

645
    """
646
    # determine the minima and maxima over all runs
647
    minima = {}
1✔
648
    maxima = {}
1✔
649
    for key, value in outcomes.items():
1✔
650
        minima[key] = np.min(value)
1✔
651
        maxima[key] = np.max(value)
1✔
652

653
    prepared_data = prepare_data(
1✔
654
        experiments,
655
        None,
656
        outcomes,
657
        outcomes_to_show,
658
        group_by,
659
        grouping_specifiers,
660
        filter_scalar=True,
661
    )
662
    experiments, outcomes, outcomes_to_show, time, grouping_specifiers = prepared_data
1✔
663
    del time
1✔
664

665
    if group_by:
1✔
666
        figures = []
1✔
667
        axes_dicts = {}
1✔
668
        for key, value in outcomes.items():
1✔
669
            fig, axes_dict = simple_kde(
1✔
670
                value, outcomes_to_show, colormap, log, minima, maxima
671
            )
672
            fig.suptitle(key)
1✔
673
            figures.append(fig)
1✔
674
            axes_dicts[key] = axes_dict
1✔
675

676
        return figures, axes_dicts
1✔
677
    else:
678
        return simple_kde(outcomes, outcomes_to_show, colormap, log, minima, maxima)
1✔
679

680

681
def multiple_densities(
1✔
682
    experiments,
683
    outcomes,
684
    points_in_time=None,
685
    outcomes_to_show=None,
686
    group_by=None,
687
    grouping_specifiers=None,
688
    density=Density.KDE,
689
    legend=True,
690
    titles=None,
691
    ylabels=None,
692
    experiments_to_show=None,
693
    plot_type=PlotType.ENVELOPE,
694
    log=False,
695
    **kwargs,
696
):
697
    """Make an envelope plot with multiple density plots over the run time.
698

699
    Parameters
700
    ----------
701
    experiments : DataFrame
702
    outcomes : OutcomesDict
703
    points_in_time : list
704
                     a list of points in time for which you want to see the
705
                     density. At the moment up to 6 points in time are
706
                     supported
707
    outcomes_to_show : list of str, optional
708
                       list of outcome of interest you want to plot. If empty,
709
                       all outcomes are plotted. **Note**:  just names.
710
    group_by : str, optional
711
               name of the column in the cases array to group results by.
712
               Alternatively, `index` can be used to use indexing arrays as the
713
               basis for grouping.
714
    grouping_specifiers : iterable or dict, optional
715
                          set of categories to be used as a basis for grouping
716
                          by. Grouping_specifiers is only meaningful if
717
                          group_by is provided as well. In case of grouping by
718
                          index, the grouping specifiers should be in a
719
                          dictionary where the key denotes the name of the
720
                          group.
721
    density : {Density.KDE, Density.HIST, Density.VIOLIN, Density.BOXPLOT},
722
              optional
723
    legend : bool, optional
724
    titles : dict, optional
725
             a way for controlling whether each of the axes should have a
726
             title. There are three possibilities. If set to None, no title
727
             will be shown for any of the axes. If set to an empty dict,
728
             the default, the title is identical to the name of the outcome of
729
             interest. If you want to override these default names, provide a
730
             dict with the outcome of interest as key and the desired title as
731
             value. This dict need only contain the outcomes for which you
732
             want to use a different title.
733
    ylabels : dict, optional
734
              way for controlling the ylabels. Works identical to titles.
735
    experiments_to_show : ndarray, optional
736
                          indices of experiments to show lines for,
737
                          defaults to None.
738
    plot_type : {PlotType.ENVELOPE, PlotType.ENV_LIN, PlotType.LINES}, optional
739
    log : bool, optional
740

741
    Returns:
742
    -------
743
    fig : Figure instance
744
    axes : dict
745
           dict with outcome as key, and axes as value. Density axes' are
746
           indexed by the outcome followed by _density.
747

748
    Note:
749
    ----
750
    the current implementation is limited to seven different categories
751
    in case of group_by, categories, and/or discretesize. This limit is
752
    due to the colors specified in COLOR_LIST.
753

754
    Note:
755
    ----
756
    the connection patches are for some reason not drawn if log scaling is
757
    used for the density plots. This appears to be an issue in matplotlib
758
    itself.
759

760
    """
761
    if not outcomes_to_show:
1✔
762
        outcomes_to_show = [k for k, v in outcomes.items() if v.ndim == 2]
1✔
763
        outcomes_to_show.remove(TIME)
1✔
764
    elif isinstance(outcomes_to_show, str):
1✔
765
        outcomes_to_show = [outcomes_to_show]
1✔
766

767
    data = prepare_data(
1✔
768
        experiments, None, outcomes, outcomes_to_show, group_by, grouping_specifiers
769
    )
770
    _, outcomes, _, time, grouping_labels = data
1✔
771

772
    axes_dicts = {}
1✔
773
    figures = []
1✔
774
    for outcome_to_show in outcomes_to_show:
1✔
775
        axes_dict = {}
1✔
776
        axes_dicts[outcome_to_show] = axes_dict
1✔
777

778
        # start of plotting
779
        fig = plt.figure()
1✔
780
        figures.append(fig)
1✔
781

782
        # making of grid
783
        if not points_in_time:
1✔
784
            raise EMAError(
×
785
                "No points in time specified, should be a list of length 1-6"
786
            )
787
        if len(points_in_time) == 1:
1✔
788
            ax_env = plt.subplot2grid((2, 3), (0, 0), colspan=3)
1✔
789
            ax1 = plt.subplot2grid((2, 3), (1, 1), sharey=ax_env)
1✔
790
            kde_axes = [ax1]
1✔
791
        elif len(points_in_time) == 2:
1✔
792
            ax_env = plt.subplot2grid((2, 2), (0, 0), colspan=2)
1✔
793
            ax1 = plt.subplot2grid((2, 2), (1, 0), sharey=ax_env)
1✔
794
            ax2 = plt.subplot2grid((2, 2), (1, 1), sharex=ax1, sharey=ax_env)
1✔
795
            kde_axes = [ax1, ax2]
1✔
796
        elif len(points_in_time) == 3:
1✔
797
            ax_env = plt.subplot2grid((2, 3), (0, 0), colspan=3)
1✔
798
            ax1 = plt.subplot2grid((2, 3), (1, 0), sharey=ax_env)
1✔
799
            ax2 = plt.subplot2grid((2, 3), (1, 1), sharex=ax1, sharey=ax_env)
1✔
800
            ax3 = plt.subplot2grid((2, 3), (1, 2), sharex=ax1, sharey=ax_env)
1✔
801
            kde_axes = [ax1, ax2, ax3]
1✔
802
        elif len(points_in_time) == 4:
1✔
803
            ax_env = plt.subplot2grid((2, 4), (0, 1), colspan=2)
1✔
804
            ax1 = plt.subplot2grid((2, 4), (1, 0), sharey=ax_env)
1✔
805
            ax2 = plt.subplot2grid((2, 4), (1, 1), sharex=ax1, sharey=ax_env)
1✔
806
            ax3 = plt.subplot2grid((2, 4), (1, 2), sharex=ax1, sharey=ax_env)
1✔
807
            ax4 = plt.subplot2grid((2, 4), (1, 3), sharex=ax1, sharey=ax_env)
1✔
808
            kde_axes = [ax1, ax2, ax3, ax4]
1✔
809
        elif len(points_in_time) == 5:
1✔
810
            ax_env = plt.subplot2grid((2, 5), (0, 1), colspan=3)
1✔
811
            ax1 = plt.subplot2grid((2, 5), (1, 0), sharey=ax_env)
1✔
812
            ax2 = plt.subplot2grid((2, 5), (1, 1), sharex=ax1, sharey=ax_env)
1✔
813
            ax3 = plt.subplot2grid((2, 5), (1, 2), sharex=ax1, sharey=ax_env)
1✔
814
            ax4 = plt.subplot2grid((2, 5), (1, 3), sharex=ax1, sharey=ax_env)
1✔
815
            ax5 = plt.subplot2grid((2, 5), (1, 4), sharex=ax1, sharey=ax_env)
1✔
816
            kde_axes = [ax1, ax2, ax3, ax4, ax5]
1✔
817
        elif len(points_in_time) == 6:
1✔
818
            ax_env = plt.subplot2grid((2, 6), (0, 1), colspan=4)
1✔
819
            ax1 = plt.subplot2grid((2, 6), (1, 0), sharey=ax_env)
1✔
820
            ax2 = plt.subplot2grid((2, 6), (1, 1), sharex=ax1, sharey=ax_env)
1✔
821
            ax3 = plt.subplot2grid((2, 6), (1, 2), sharex=ax1, sharey=ax_env)
1✔
822
            ax4 = plt.subplot2grid((2, 6), (1, 3), sharex=ax1, sharey=ax_env)
1✔
823
            ax5 = plt.subplot2grid((2, 6), (1, 4), sharex=ax1, sharey=ax_env)
1✔
824
            ax6 = plt.subplot2grid((2, 6), (1, 5), sharex=ax1, sharey=ax_env)
1✔
825
            kde_axes = [ax1, ax2, ax3, ax4, ax5, ax6]
1✔
826
        else:
827
            raise EMAError(
×
828
                f"Too many points in time specified: {len(points_in_time)}, max is 6"
829
            )
830

831
        axes_dict["main plot"] = ax_env
1✔
832
        for n, entry in enumerate(kde_axes):
1✔
833
            axes_dict[f"density_{n}"] = entry
1✔
834

835
            # turn of ticks for all but the first density
836
            if n > 0:
1✔
837
                for tl in entry.get_yticklabels():
1✔
838
                    tl.set_visible(False)
1✔
839

840
        # bit of a trick to avoid duplicating code. If no subgroups are
841
        # specified, nest the outcomes one step deeper in the dict so the
842
        # iteration below can proceed normally.
843
        if not grouping_labels:
1✔
844
            grouping_labels = [""]
×
845
            outcomes[""] = outcomes
×
846

847
        for j, key in enumerate(grouping_labels):
1✔
848
            value = outcomes[key][outcome_to_show]
1✔
849

850
            if plot_type == PlotType.ENVELOPE:
1✔
851
                plot_envelope(ax_env, j, time, value, **kwargs)
1✔
852
            elif plot_type == PlotType.LINES:
1✔
853
                ax_env.plot(time.T, value.T)
×
854
            elif plot_type == PlotType.ENV_LIN:
1✔
855
                plot_envelope(ax_env, j, time, value, **kwargs)
1✔
856
                if experiments_to_show is not None:
1✔
857
                    ax_env.plot(time.T, value[experiments_to_show].T)
1✔
858
                else:
859
                    ax_env.plot(time.T, value.T)
×
860
            ax_env.set_xlim(time[0], time[-1])
1✔
861

862
            ax_env.set_xlabel(TIME_LABEL)
1✔
863
            do_ylabels(ax_env, ylabels, outcome_to_show)
1✔
864
            do_titles(ax_env, titles, outcome_to_show)
1✔
865

866
        for ax, time_value in zip(kde_axes, points_in_time):
1✔
867
            index = np.where(time == time_value)[0][0]
1✔
868

869
            # TODO grouping labels, boxplots, and sharex
870
            # create a problem
871
            group_density(
1✔
872
                ax,
873
                density,
874
                outcomes,
875
                outcome_to_show,
876
                grouping_labels,
877
                index=index,
878
                log=log,
879
            )
880

881
        min_y, max_y = ax_env.get_ylim()
1✔
882
        ax_env.autoscale(enable=False, axis="y")
1✔
883
        # draw line to connect each point in time in the main plot
884
        # to the associated density plot
885
        for i, ax in enumerate(kde_axes):
1✔
886
            time_value = points_in_time[i]
1✔
887

888
            ax_env.plot([time_value, time_value], [min_y, max_y], c="k", ls="--")
1✔
889
            con = ConnectionPatch(
1✔
890
                xyA=(time_value, min_y),
891
                xyB=(ax.get_xlim()[0], max_y),
892
                coordsA=ax_env.transData,
893
                coordsB=ax.transData,
894
            )
895
            ax_env.add_artist(con)
1✔
896

897
        if legend and group_by:
1✔
898
            lt = LegendEnum.PATCH
1✔
899
            alpha = 0.3
1✔
900
            if plot_type == PlotType.LINES:
1✔
901
                lt = LegendEnum.LINE
×
902
                alpha = 1
×
903
            make_legend(grouping_labels, ax_env, legend_type=lt, alpha=alpha)
1✔
904
    return figures, axes_dicts
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc