Source code for pyleoclim.core.ensembleseries

"""
The EnsembleSeries class is a child of MultipleSeries, designed for ensemble applications (e.g. draws from a posterior distribution of ages, model ensembles with randomized initial conditions, or some other stochastic ensemble).
In addition to a MultipleSeries object, an EnsembleSeries object has the following properties:
- All series members are assumed to share the same units and other metadata.
- The class enables ensemble-oriented methods for computation (e.g., quantiles) and visualization (e.g., envelope plot).    
"""

from ..utils import plotting
from ..utils import correlation as corrutils
from ..core.series import Series
from ..core.correns import CorrEns
from ..core.multipleseries import MultipleSeries

import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from copy import deepcopy

from matplotlib.ticker import FormatStrFormatter
import matplotlib.transforms as transforms
import matplotlib as mpl
from tqdm import tqdm
from scipy.stats.mstats import mquantiles

[docs]class EnsembleSeries(MultipleSeries): ''' EnsembleSeries object The EnsembleSeries object is a child of the MultipleSeries object, that is, a special case of MultipleSeries, aiming for ensembles of similar series. Ensembles usually arise from age modeling or Bayesian calibrations. All members of an EnsembleSeries object are assumed to share identical labels and units. All methods available for MultipleSeries are available for EnsembleSeries. Some functions were modified for the special case of ensembles. The class enables ensemble-oriented methods for computation (e.g., quantiles) and visualization (e.g., envelope plot) that are unavailable to other classes. ''' def __init__(self, series_list): self.series_list = series_list
[docs] def make_labels(self): '''Initialization of labels Returns ------- time_header : str Label for the time axis value_header : str Label for the value axis ''' ts_list = self.series_list if ts_list[0].time_name is not None: time_name_str = ts_list[0].time_name else: time_name_str = 'time' if ts_list[0].value_name is not None: value_name_str = ts_list[0].value_name else: value_name_str = 'value' if ts_list[0].value_unit is not None: value_header = f'{value_name_str} [{ts_list[0].value_unit}]' else: value_header = f'{value_name_str}' if ts_list[0].time_unit is not None: time_header = f'{time_name_str} [{ts_list[0].time_unit}]' else: time_header = f'{time_name_str}' return time_header, value_header
[docs] def quantiles(self, qs=[0.05, 0.5, 0.95]): '''Calculate quantiles of an EnsembleSeries object Reuses [scipy.stats.mstats.mquantiles](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.mstats.mquantiles.html) function. Parameters ---------- qs : list, optional List of quantiles to consider for the calculation. The default is [0.05, 0.5, 0.95]. Returns ------- ens_qs : pyleoclim.EnsembleSeries EnsembleSeries object containing empirical quantiles of original Examples -------- .. ipython:: python :okwarning: :okexcept: nn = 30 # number of noise realizations nt = 500 series_list = [] t,v = pyleo.utils.gen_ts(model='colored_noise',nt=nt,alpha=1.0) signal = pyleo.Series(t,v) for idx in range(nn): # noise noise = np.random.randn(nt,nn)*100 ts = pyleo.Series(time=signal.time, value=signal.value+noise[:,idx]) series_list.append(ts) ts_ens = pyleo.EnsembleSeries(series_list) ens_qs = ts_ens.quantiles() ''' time = np.copy(self.series_list[0].time) vals = [] for ts in self.series_list: if not np.array_equal(ts.time, time): raise ValueError('Time axis not consistent across the ensemble!') vals.append(ts.value) vals = np.array(vals) ens_qs = mquantiles(vals, qs, axis=0) ts_list = [] for i, quant in enumerate(ens_qs): ts = Series(time=time, value=quant, label=f'{qs[i]*100:g}%') ts_list.append(ts) ens_qs = EnsembleSeries(series_list=ts_list) return ens_qs
[docs] def correlation(self, target=None, timespan=None, alpha=0.05, settings=None, fdr_kwargs=None, common_time_kwargs=None, mute_pbar=False, seed=None): ''' Calculate the correlation between an EnsembleSeries object to a target. If the target is not specified, then the 1st member of the ensemble will be the target Note that the FDR approach is applied by default to determine the significance of the p-values (more information in See Also below). Parameters ---------- target : pyleoclim.Series or pyleoclim.EnsembleSeries A pyleoclim Series object or EnsembleSeries object. When the target is also an EnsembleSeries object, then the calculation of correlation is performed in a one-to-one sense, and the ourput list of correlation values and p-values will be the size of the series_list of the self object. That is, if the self object contains n Series, and the target contains n+m Series, then only the first n Series from the object will be used for the calculation; otherwise, if the target contains only n-m Series, then the first m Series in the target will be used twice in sequence. timespan : tuple The time interval over which to perform the calculation alpha : float The significance level (0.05 by default) settings : dict Parameters for the correlation function, including: nsim : int the number of simulations (default: 1000) method : str, {'ttest','isopersistent','isospectral' (default)} method for significance testing fdr_kwargs : dict Parameters for the FDR function common_time_kwargs : dict Parameters for the method MultipleSeries.common_time() mute_pbar : bool; {True,False} If True, the progressbar will be muted. Default is False. seed : float or int random seed for isopersistent and isospectral methods Returns ------- corr_ens : pyleoclim.CorrEns The resulting object, see pyleoclim.CorrEns See also -------- pyleoclim.utils.correlation.corr_sig : Correlation function pyleoclim.utils.correlation.fdr : False Discovery Rate pyleoclim.core.correns.CorrEns : The correlation ensemble object Examples -------- .. ipython:: python :okwarning: :okexcept: nn = 50 # number of noise realizations nt = 100 series_list = [] time, signal = pyleo.utils.gen_ts(model='colored_noise',nt=nt,alpha=2.0) ts = pyleo.Series(time=time, value = signal).standardize() noise = np.random.randn(nt,nn) for idx in range(nn): # noise ts = pyleo.Series(time=time, value=ts.value+5*noise[:,idx]) series_list.append(ts) ts_ens = pyleo.EnsembleSeries(series_list) # set an arbitrary random seed to fix the result corr_res = ts_ens.correlation(ts, seed=2333) print(corr_res) The `print` function tabulates the output, and conveys the p-value according to the correlation test applied ("isospec", by default). To plot the result: .. ipython:: python :okwarning: :okexcept: @savefig ens_corrplot.png corr_res.plot() pyleo.closefig(fig) ''' if target is None: target = self.series_list[0] r_list = [] p_list = [] signif_list = [] print("Looping over "+ str(len(self.series_list)) +" Series in the ensemble") for idx, ts1 in tqdm(enumerate(self.series_list), total=len(self.series_list), disable=mute_pbar): if hasattr(target, 'series_list'): nEns = np.size(target.series_list) if idx < nEns: value2 = target.series_list[idx].value time2 = target.series_list[idx].time else: value2 = target.series_list[idx-nEns].value time2 = target.series_list[idx-nEns].time else: value2 = target.value time2 = target.time ts2 = Series(time=time2, value=value2, verbose=idx==0) corr_res = ts1.correlation(ts2, timespan=timespan, settings=settings, common_time_kwargs=common_time_kwargs, seed=seed) r_list.append(corr_res.r) signif_list.append(corr_res.signif) p_list.append(corr_res.p) r_list = np.array(r_list) p_list = np.array(p_list) signif_fdr_list = [] fdr_kwargs = {} if fdr_kwargs is None else fdr_kwargs.copy() args = {} args.update(fdr_kwargs) for i in range(np.size(signif_list)): signif_fdr_list.append(False) fdr_res = corrutils.fdr(p_list, **fdr_kwargs) if fdr_res is not None: for i in fdr_res: signif_fdr_list[i] = True corr_ens = CorrEns(r_list, p_list, signif_list, signif_fdr_list, alpha) return corr_ens
[docs] def plot_traces(self, figsize=[10, 4], xlabel=None, ylabel=None, title=None, num_traces=10, seed=None, xlim=None, ylim=None, linestyle='-', savefig_settings=None, ax=None, plot_legend=True, color=sns.xkcd_rgb['pale red'], lw=0.5, alpha=0.3, lgd_kwargs=None): '''Plot EnsembleSeries as a subset of traces. Parameters ---------- figsize : list, optional The figure size. The default is [10, 4]. xlabel : str, optional x-axis label. The default is None. ylabel : str, optional y-axis label. The default is None. title : str, optional Plot title. The default is None. xlim : list, optional x-axis limits. The default is None. ylim : list, optional y-axis limits. The default is None. color : str, optional Color of the traces. The default is sns.xkcd_rgb['pale red']. alpha : float, optional Transparency of the lines representing the multiple members. The default is 0.3. linestyle : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} Set the linestyle of the line lw : float, optional Width of the lines representing the multiple members. The default is 0.5. num_traces : int, optional Number of traces to plot. The default is 10. savefig_settings : dict, optional the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existed or non-existed path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} The default is None. ax : matplotlib.ax, optional Matplotlib axis on which to return the plot. The default is None. plot_legend : bool; {True,False}, optional Whether to plot the legend. The default is True. lgd_kwargs : dict, optional Parameters for the legend. The default is None. seed : int, optional Set the seed for the random number generator. Useful for reproducibility. The default is None. Returns ------- fig : matplotlib.figure the figure object from matplotlib See [matplotlib.pyplot.figure](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.figure.html) for details. ax : matplotlib.axis the axis object from matplotlib See [matplotlib.axes](https://matplotlib.org/api/axes_api.html) for details. See also -------- pyleoclim.utils.plotting.savefig : Saving figure in Pyleoclim Examples -------- .. ipython:: python :okwarning: :okexcept: nn = 30 # number of noise realizations nt = 500 series_list = [] t,v = pyleo.utils.gen_ts(model='colored_noise',nt=nt,alpha=1.0) signal = pyleo.Series(t,v) for idx in range(nn): # noise noise = np.random.randn(nt,nn)*100 ts = pyleo.Series(time=signal.time, value=signal.value+noise[:,idx]) series_list.append(ts) ts_ens = pyleo.EnsembleSeries(series_list) @savefig ens_plot_traces.png fig, ax = ts_ens.plot_traces(alpha=0.2,num_traces=8) pyleo.closefig(fig) #Optional close fig after plotting ''' savefig_settings = {} if savefig_settings is None else savefig_settings.copy() lgd_kwargs = {} if lgd_kwargs is None else lgd_kwargs.copy() # generate default axis labels time_label, value_label = self.make_labels() if xlabel is None: xlabel = time_label if ylabel is None: ylabel = value_label if ax is None: fig, ax = plt.subplots(figsize=figsize) if num_traces > 0: if seed is not None: np.random.seed(seed) nts = np.size(self.series_list) random_draw_idx = np.random.choice(nts, num_traces) for idx in random_draw_idx: self.series_list[idx].plot(xlabel=xlabel, ylabel=ylabel, zorder=99, linewidth=lw, xlim=xlim, ylim=ylim, ax=ax, color=color, alpha=alpha,linestyle='-') ax.plot(np.nan, np.nan, color=color, label=f'example members (n={num_traces})',linestyle='-') if title is not None: ax.set_title(title) if plot_legend: lgd_args = {'frameon': False} lgd_args.update(lgd_kwargs) ax.legend(**lgd_args) if 'fig' in locals(): if 'path' in savefig_settings: plotting.savefig(fig, settings=savefig_settings) return fig, ax else: return ax
[docs] def plot_envelope(self, figsize=[10, 4], qs=[0.025, 0.25, 0.5, 0.75, 0.975], xlabel=None, ylabel=None, title=None, xlim=None, ylim=None, savefig_settings=None, ax=None, plot_legend=True, curve_clr=sns.xkcd_rgb['pale red'], curve_lw=2, shade_clr=sns.xkcd_rgb['pale red'], shade_alpha=0.2, inner_shade_label='IQR', outer_shade_label='95% CI', lgd_kwargs=None): ''' Plot EnsembleSeries as an envelope. Parameters ---------- figsize : list, optional The figure size. The default is [10, 4]. qs : list, optional The significance levels to consider. The default is [0.025, 0.25, 0.5, 0.75, 0.975] (median, interquartile range, and central 95% region) xlabel : str, optional x-axis label. The default is None. ylabel : str, optional y-axis label. The default is None. title : str, optional Plot title. The default is None. xlim : list, optional x-axis limits. The default is None. ylim : list, optional y-axis limits. The default is None. savefig_settings : dict, optional the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existed or non-existed path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} The default is None. ax : matplotlib.ax, optional Matplotlib axis on which to return the plot. The default is None. plot_legend : bool; {True,False}, optional Wether to plot the legend. The default is True. curve_clr : str, optional Color of the main line (median). The default is sns.xkcd_rgb['pale red']. curve_lw : str, optional Width of the main line (median). The default is 2. shade_clr : str, optional Color of the shaded envelope. The default is sns.xkcd_rgb['pale red']. shade_alpha : float, optional Transparency on the envelope. The default is 0.2. inner_shade_label : str, optional Label for the envelope. The default is 'IQR'. outer_shade_label : str, optional Label for the envelope. The default is '95\% CI'. lgd_kwargs : dict, optional Parameters for the legend. The default is None. Returns ------- fig : matplotlib.figure the figure object from matplotlib See [matplotlib.pyplot.figure](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.figure.html) for details. ax : matplotlib.axis the axis object from matplotlib See [matplotlib.axes](https://matplotlib.org/api/axes_api.html) for details. See also -------- pyleoclim.utils.plotting.savefig : Saving figure in Pyleoclim Examples -------- .. ipython:: python :okwarning: :okexcept: nn = 30 # number of noise realizations nt = 500 series_list = [] t,v = pyleo.utils.gen_ts(model='colored_noise',nt=nt,alpha=1.0) signal = pyleo.Series(t,v) for idx in range(nn): # noise noise = np.random.randn(nt,nn)*100 ts = pyleo.Series(time=signal.time, value=signal.value+noise[:,idx]) series_list.append(ts) ts_ens = pyleo.EnsembleSeries(series_list) @savefig ens_plot_envelope.png fig, ax = ts_ens.plot_envelope(curve_lw=1.5) pyleo.closefig(fig) #Optional close fig after plotting ''' savefig_settings = {} if savefig_settings is None else savefig_settings.copy() lgd_kwargs = {} if lgd_kwargs is None else lgd_kwargs.copy() # generate default axis labels time_label, value_label = self.make_labels() if xlabel is None: xlabel = time_label if ylabel is None: ylabel = value_label if ax is None: fig, ax = plt.subplots(figsize=figsize) ts_qs = self.quantiles(qs=qs) if inner_shade_label is None: inner_shade_label = f'{ts_qs.series_list[1].label}-{ts_qs.series_list[-2].label}' if outer_shade_label is None: outer_shade_label = f'{ts_qs.series_list[0].label}-{ts_qs.series_list[-1].label}' time = ts_qs.series_list[0].time # plot outer envelope ax.fill_between( time, ts_qs.series_list[0].value, ts_qs.series_list[4].value, color=shade_clr, alpha=shade_alpha, edgecolor=shade_clr, label=outer_shade_label, ) # plot inner envelope on top ax.fill_between( time, ts_qs.series_list[1].value, ts_qs.series_list[3].value, color=shade_clr, alpha=2*shade_alpha, edgecolor=shade_clr, label=inner_shade_label, ) # plot the median ts_qs.series_list[2].plot(xlabel=xlabel, ylabel=ylabel, linewidth=curve_lw, color=curve_clr, xlim=xlim, ylim=ylim, ax=ax, zorder=100, label = 'median' ) if title is not None: ax.set_title(title) if plot_legend: lgd_args = {'frameon': False} lgd_args.update(lgd_kwargs) ax.legend(**lgd_args) if 'fig' in locals(): if 'path' in savefig_settings: plotting.savefig(fig, settings=savefig_settings) return fig, ax else: return ax
[docs] def stackplot(self, figsize=[5, 15], savefig_settings=None, xlim=None, fill_between_alpha=0.2, colors=None, cmap='tab10', norm=None, spine_lw=1.5, grid_lw=0.5, font_scale=0.8, label_x_loc=-0.15, v_shift_factor=3/4, linewidth=1.5): ''' Stack plot of multiple series Note that the plotting style is uniquely designed for this one and cannot be properly reset with `pyleoclim.set_style()`. Parameters ---------- figsize : list Size of the figure. colors : list Colors for plotting. If None, the plotting will cycle the 'tab10' colormap; if only one color is specified, then all curves will be plotted with that single color; if a list of colors are specified, then the plotting will cycle that color list. cmap : str The colormap to use when "colors" is None. norm : matplotlib.colors.Normalize like The nomorlization for the colormap. If None, a linear normalization will be used. savefig_settings : dictionary the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existed or non-existed path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} The default is None. xlim : list The x-axis limit. fill_between_alpha : float The transparency for the fill_between shades. spine_lw : float The linewidth for the spines of the axes. grid_lw : float The linewidth for the gridlines. linewidth : float The linewidth for the curves. font_scale : float The scale for the font sizes. Default is 0.8. label_x_loc : float The x location for the label of each curve. v_shift_factor : float The factor for the vertical shift of each axis. The default value 3/4 means the top of the next axis will be located at 3/4 of the height of the previous one. Returns ------- fig : matplotlib.figure the figure object from matplotlib See [matplotlib.pyplot.figure](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.figure.html) for details. ax : matplotlib.axis the axis object from matplotlib See [matplotlib.axes](https://matplotlib.org/api/axes_api.html) for details. See also -------- pyleoclim.utils.plotting.savefig : Saving figure in Pyleoclim Examples -------- .. ipython:: python :okwarning: :okexcept: nn = 10 # number of noise realizations nt = 200 series_list = [] t, v = pyleo.utils.gen_ts(model='colored_noise',nt=nt,alpha=1.0) signal, _, _ = pyleo.utils.standardize(v) noise = np.random.randn(nt,nn) for idx in range(nn): # noise ts = pyleo.Series(time=t, value=signal+noise[:,idx], label='trace #'+str(idx+1)) series_list.append(ts) ts_ens = pyleo.EnsembleSeries(series_list) @savefig ens_stackplot.png fig, ax = ts_ens.stackplot() pyleo.closefig(fig) #Optional close fig after plotting ''' current_style = deepcopy(mpl.rcParams) plotting.set_style('journal', font_scale=font_scale) savefig_settings = {} if savefig_settings is None else savefig_settings.copy() n_ts = len(self.series_list) fig = plt.figure(figsize=figsize) if xlim is None: time_min = np.inf time_max = -np.inf for ts in self.series_list: if np.min(ts.time) <= time_min: time_min = np.min(ts.time) if np.max(ts.time) >= time_max: time_max = np.max(ts.time) xlim = [time_min, time_max] ax = {} left = 0 width = 1 height = 1/n_ts bottom = 1 for idx, ts in enumerate(self.series_list): if colors is None: cmap_obj = plt.get_cmap(cmap) if hasattr(cmap_obj, 'colors'): nc = len(cmap_obj.colors) else: nc = len(self.series_list) if norm is None: norm = mpl.colors.Normalize(vmin=0, vmax=nc-1) clr = cmap_obj(norm(idx%nc)) elif type(colors) is str: clr = colors elif type(colors) is list: nc = len(colors) clr = colors[idx%nc] else: raise TypeError('"colors" should be a list of, or one, Python supported color code (a string of hex code or a tuple of rgba values)') bottom -= height*v_shift_factor ax[idx] = fig.add_axes([left, bottom, width, height]) ax[idx].plot(ts.time, ts.value, color=clr, lw=linewidth) ax[idx].patch.set_alpha(0) ax[idx].set_xlim(xlim) time_label, value_label = ts.make_labels() ax[idx].set_ylabel(value_label, weight='bold') mu = np.mean(ts.value) std = np.std(ts.value) ylim = [mu-4*std, mu+4*std] ax[idx].fill_between(ts.time, ts.value, y2=mu, alpha=fill_between_alpha, color=clr) trans = transforms.blended_transform_factory(ax[idx].transAxes, ax[idx].transData) if ts.label is not None: ax[idx].text(label_x_loc, mu, ts.label, horizontalalignment='right', transform=trans, color=clr, weight='bold') ax[idx].set_ylim(ylim) ax[idx].set_yticks(ylim) ax[idx].yaxis.set_major_formatter(FormatStrFormatter('%.1f')) ax[idx].grid(False) if idx % 2 == 0: ax[idx].spines['left'].set_visible(True) ax[idx].spines['left'].set_linewidth(spine_lw) ax[idx].spines['left'].set_color(clr) ax[idx].spines['right'].set_visible(False) ax[idx].yaxis.set_label_position('left') ax[idx].yaxis.tick_left() else: ax[idx].spines['left'].set_visible(False) ax[idx].spines['right'].set_visible(True) ax[idx].spines['right'].set_linewidth(spine_lw) ax[idx].spines['right'].set_color(clr) ax[idx].yaxis.set_label_position('right') ax[idx].yaxis.tick_right() ax[idx].yaxis.label.set_color(clr) ax[idx].tick_params(axis='y', colors=clr) ax[idx].spines['top'].set_visible(False) ax[idx].spines['bottom'].set_visible(False) ax[idx].tick_params(axis='x', which='both', length=0) ax[idx].set_xlabel('') ax[idx].set_xticklabels([]) xt = ax[idx].get_xticks()[1:-1] for x in xt: ax[idx].axvline(x=x, color='lightgray', linewidth=grid_lw, ls='-', zorder=-1) ax[idx].axhline(y=mu, color='lightgray', linewidth=grid_lw, ls='-', zorder=-1) bottom -= height*(1-v_shift_factor) ax[n_ts] = fig.add_axes([left, bottom, width, height]) ax[n_ts].set_xlabel(time_label) ax[n_ts].spines['left'].set_visible(False) ax[n_ts].spines['right'].set_visible(False) ax[n_ts].spines['bottom'].set_visible(True) ax[n_ts].spines['bottom'].set_linewidth(spine_lw) ax[n_ts].set_yticks([]) ax[n_ts].patch.set_alpha(0) ax[n_ts].set_xlim(xlim) ax[n_ts].grid(False) ax[n_ts].tick_params(axis='x', which='both', length=3.5) xt = ax[n_ts].get_xticks()[1:-1] for x in xt: ax[n_ts].axvline(x=x, color='lightgray', linewidth=grid_lw, ls='-', zorder=-1) if 'fig' in locals(): if 'path' in savefig_settings: plotting.savefig(fig, settings=savefig_settings) mpl.rcParams.update(current_style) return fig, ax else: # reset the plotting style mpl.rcParams.update(current_style) return ax
[docs] def distplot(self, figsize=[10, 4], title=None, savefig_settings=None, ax=None, ylabel='KDE', vertical=False, edgecolor='w', **plot_kwargs): """ Plots the distribution of the timeseries across ensembles Reuses seaborn [histplot](https://seaborn.pydata.org/generated/seaborn.histplot.html) function. Parameters ---------- figsize : list, optional The size of the figure. The default is [10, 4]. title : str, optional Title for the figure. The default is None. savefig_settings : dict, optional the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existed or non-existed path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"}. The default is None. ax : matplotlib.axis, optional A matplotlib axis. The default is None. ylabel : str, optional Label for the count axis. The default is 'KDE'. vertical : bool; {True,False}, optional Whether to flip the plot vertically. The default is False. edgecolor : matplotlib.color, optional The color of the edges of the bar. The default is 'w'. plot_kwargs : dict Plotting arguments for seaborn histplot: https://seaborn.pydata.org/generated/seaborn.histplot.html. See also -------- pyleoclim.utils.plotting.savefig : Saving figure in Pyleoclim Examples -------- .. ipython:: python :okwarning: :okexcept: nn = 30 # number of noise realizations nt = 500 series_list = [] time, signal = pyleo.utils.gen_ts(model='colored_noise',nt=nt,alpha=1.0) ts = pyleo.Series(time=time, value = signal).standardize() noise = np.random.randn(nt,nn) for idx in range(nn): # noise ts = pyleo.Series(time=time, value=signal+noise[:,idx]) series_list.append(ts) ts_ens = pyleo.EnsembleSeries(series_list) @savefig ens_distplot.png fig, ax = ts_ens.distplot() pyleo.closefig(fig) """ savefig_settings = {} if savefig_settings is None else savefig_settings.copy() if ax is None: fig, ax = plt.subplots(figsize=figsize) #make the data into a dataframe so we can flip the figure time_label, value_label = self.make_labels() #append all the values together for the plot val = self.series_list[0].value for i in range(1,len(self.series_list)): val=np.append(val,self.series_list[i].value) if vertical == True: data=pd.DataFrame({'value':val}) ax = sns.histplot(data=data, y="value", ax=ax, kde=True, edgecolor=edgecolor, **plot_kwargs) ax.set_ylabel(value_label) ax.set_xlabel(ylabel) else: ax = sns.histplot(val, ax=ax, kde=True, edgecolor=edgecolor, **plot_kwargs) ax.set_xlabel(value_label) ax.set_ylabel(ylabel) if title is not None: ax.set_title(title) if 'fig' in locals(): if 'path' in savefig_settings: plotting.savefig(fig, settings=savefig_settings) return fig, ax else: return ax