diff --git a/CHANGELOG b/CHANGELOG index 611c3aecbe30..480466f4ffd6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,11 @@ argument. A 30x30 grid is now used for both density=1 and density=(1, 1). +2013-12-03 Added a pure boxplot-drawing method that allow a more complete + customization of boxplots. It takes a list of dicts contains stats. + Also created a function (`cbook.boxplot_stats`) that generates the + stats needed. + 2013-11-28 Added qhull extension module to perform Delaunay triangulation more robustly than before. It is used by tri.Triangulation (and hence all pyplot.tri* methods) and mlab.griddata. Deprecated diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index 7d059632084a..0f984255132e 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -32,6 +32,31 @@ Phil Elson rewrote of the documentation and userguide for both Legend and PathEf New plotting features --------------------- +Fully customizable boxplots +```````````````````````````` +Paul Hobson overhauled the :func:`~matplotlib.pyplot.boxplot` method such +that it is now completely customizable in terms of the styles and positions +of the individual artists. Under the hood, :func:`~matplotlib.pyplot.boxplot` +relies on a new function (:func:`~matplotlib.cbook.boxplot_stats`), which +accepts any data structure currently compatible with +:func:`~matplotlib.pyplot.boxplot`, and returns a list of dictionaries +containing the positions for each element of the boxplots. Then +a second method, :func:`~matplotlib.Axes.bxp` is called to draw the boxplots +based on the stats. + +The :func:~matplotlib.pyplot.boxplot function can be used as before to +generate boxplots from data in one step. But now the user has the +flexibility to generate the statistics independently, or to modify the +output of :func:~matplotlib.cbook.boxplot_stats prior to plotting +with :func:~matplotlib.Axes.bxp. + +Lastly, each artist (e.g., the box, outliers, cap, notches) can now be +toggled on or off and their styles can be passed in through individual +kwargs. See the examples: +:ref:`~examples/statistics/boxplot_demo.py` and +:ref:`~examples/statistics/bxp_demo.py` + + Support for datetime axes in 2d plots ````````````````````````````````````` Andrew Dawson added support for datetime axes to diff --git a/examples/statistics/boxplot_demo.py b/examples/statistics/boxplot_demo.py new file mode 100644 index 000000000000..f810f6700d7f --- /dev/null +++ b/examples/statistics/boxplot_demo.py @@ -0,0 +1,77 @@ +""" +Demo of the new boxplot functionality +""" + +import numpy as np +import matplotlib.pyplot as plt + +# fake data +np.random.seed(937) +data = np.random.lognormal(size=(37, 4), mean=1.5, sigma=1.75) +labels = list('ABCD') +fs = 10 # fontsize + +# demonstrate how to toggle the display of different elements: +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(6,6)) +axes[0, 0].boxplot(data, labels=labels) +axes[0, 0].set_title('Default', fontsize=fs) + +axes[0, 1].boxplot(data, labels=labels, showmeans=True) +axes[0, 1].set_title('showmeans=True', fontsize=fs) + +axes[0, 2].boxplot(data, labels=labels, showmeans=True, meanline=True) +axes[0, 2].set_title('showmeans=True,\nmeanline=True', fontsize=fs) + +axes[1, 0].boxplot(data, labels=labels, showbox=False, showcaps=False) +axes[1, 0].set_title('Tufte Style \n(showbox=False,\nshowcaps=False)', fontsize=fs) + +axes[1, 1].boxplot(data, labels=labels, notch=True, bootstrap=10000) +axes[1, 1].set_title('notch=True,\nbootstrap=10000', fontsize=fs) + +axes[1, 2].boxplot(data, labels=labels, showfliers=False) +axes[1, 2].set_title('showfliers=False', fontsize=fs) + +for ax in axes.flatten(): + ax.set_yscale('log') + ax.set_yticklabels([]) + +fig.subplots_adjust(hspace=0.4) +plt.show() + + +# demonstrate how to customize the display different elements: +boxprops = dict(linestyle='--', linewidth=3, color='darkgoldenrod') +flierprops = dict(marker='o', markerfacecolor='green', markersize=12, + linestyle='none') +medianprops = dict(linestyle='-.', linewidth=2.5, color='firebrick') +meanpointprops = dict(marker='D', markeredgecolor='black', + markerfacecolor='firebrick') +meanlineprops = dict(linestyle='--', linewidth=2.5, color='purple') + +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(6,6)) +axes[0, 0].boxplot(data, boxprops=boxprops) +axes[0, 0].set_title('Custom boxprops', fontsize=fs) + +axes[0, 1].boxplot(data, flierprops=flierprops, medianprops=medianprops) +axes[0, 1].set_title('Custom medianprops\nand flierprops', fontsize=fs) + +axes[0, 2].boxplot(data, whis='range') +axes[0, 2].set_title('whis="range"', fontsize=fs) + +axes[1, 0].boxplot(data, meanprops=meanpointprops, meanline=False, + showmeans=True) +axes[1, 0].set_title('Custom mean\nas point', fontsize=fs) + +axes[1, 1].boxplot(data, meanprops=meanlineprops, meanline=True, showmeans=True) +axes[1, 1].set_title('Custom mean\nas line', fontsize=fs) + +axes[1, 2].boxplot(data, whis=[15, 85]) +axes[1, 2].set_title('whis=[15, 85]\n#percentiles', fontsize=fs) + +for ax in axes.flatten(): + ax.set_yscale('log') + ax.set_yticklabels([]) + +fig.suptitle("I never said they'd be pretty") +fig.subplots_adjust(hspace=0.4) +plt.show() diff --git a/examples/statistics/bxp_demo.py b/examples/statistics/bxp_demo.py new file mode 100644 index 000000000000..74d60b0b27bb --- /dev/null +++ b/examples/statistics/bxp_demo.py @@ -0,0 +1,83 @@ +""" +Demo of the new boxplot drawer function +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.cbook as cbook + +# fake data +np.random.seed(937) +data = np.random.lognormal(size=(37, 4), mean=1.5, sigma=1.75) +labels = list('ABCD') + +# compute the boxplot stats +stats = cbook.boxplot_stats(data, labels=labels, bootstrap=10000) +# After we've computed the stats, we can go through and change anything. +# Just to prove it, I'll set the median of each set to the median of all +# the data, and double the means +for n in range(len(stats)): + stats[n]['med'] = np.median(data) + stats[n]['mean'] *= 2 + +print(stats[0].keys()) +fs = 10 # fontsize + +# demonstrate how to toggle the display of different elements: +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(6,6)) +axes[0, 0].bxp(stats) +axes[0, 0].set_title('Default', fontsize=fs) + +axes[0, 1].bxp(stats, showmeans=True) +axes[0, 1].set_title('showmeans=True', fontsize=fs) + +axes[0, 2].bxp(stats, showmeans=True, meanline=True) +axes[0, 2].set_title('showmeans=True,\nmeanline=True', fontsize=fs) + +axes[1, 0].bxp(stats, showbox=False, showcaps=False) +axes[1, 0].set_title('Tufte Style\n(showbox=False,\nshowcaps=False)', fontsize=fs) + +axes[1, 1].bxp(stats, shownotches=True) +axes[1, 1].set_title('notch=True', fontsize=fs) + +axes[1, 2].bxp(stats, showfliers=False) +axes[1, 2].set_title('showfliers=False', fontsize=fs) + +for ax in axes.flatten(): + ax.set_yscale('log') + ax.set_yticklabels([]) + +fig.subplots_adjust(hspace=0.4) +plt.show() + + +# demonstrate how to customize the display different elements: +boxprops = dict(linestyle='--', linewidth=3, color='darkgoldenrod') +flierprops = dict(marker='o', markerfacecolor='green', markersize=12, + linestyle='none') +medianprops = dict(linestyle='-.', linewidth=2.5, color='firebrick') +meanpointprops = dict(marker='D', markeredgecolor='black', + markerfacecolor='firebrick') +meanlineprops = dict(linestyle='--', linewidth=2.5, color='purple') + +fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(6,6)) +axes[0, 0].bxp(stats, boxprops=boxprops) +axes[0, 0].set_title('Custom boxprops', fontsize=fs) + +axes[0, 1].bxp(stats, flierprops=flierprops, medianprops=medianprops) +axes[0, 1].set_title('Custom medianprops\nand flierprops', fontsize=fs) + +axes[1, 0].bxp(stats, meanprops=meanpointprops, meanline=False, + showmeans=True) +axes[1, 0].set_title('Custom mean\nas point', fontsize=fs) + +axes[1, 1].bxp(stats, meanprops=meanlineprops, meanline=True, showmeans=True) +axes[1, 1].set_title('Custom mean\nas line', fontsize=fs) + +for ax in axes.flatten(): + ax.set_yscale('log') + ax.set_yticklabels([]) + +fig.suptitle("I never said they'd be pretty") +fig.subplots_adjust(hspace=0.4) +plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 4717a6ecf64f..5b63a753fe03 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2789,15 +2789,21 @@ def xywhere(xs, ys, mask): def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, positions=None, widths=None, patch_artist=False, - bootstrap=None, usermedians=None, conf_intervals=None): + bootstrap=None, usermedians=None, conf_intervals=None, + meanline=False, showmeans=False, showcaps=True, + showbox=True, showfliers=True, boxprops=None, labels=None, + flierprops=None, medianprops=None, meanprops=None): """ Make a box and whisker plot. Call signature:: - boxplot(x, notch=False, sym='+', vert=True, whis=1.5, + boxplot(x, notch=False, sym='b+', vert=True, whis=1.5, positions=None, widths=None, patch_artist=False, - bootstrap=None, usermedians=None, conf_intervals=None) + bootstrap=None, usermedians=None, conf_intervals=None, + meanline=False, showmeans=False, showcaps=True, + showbox=True, showfliers=True, boxprops=None, labels=None, + flierprops=None, medianprops=None, meanprops=None) Make a box and whisker plot for each column of *x* or each vector in sequence *x*. The box extends from the lower to @@ -2805,29 +2811,38 @@ def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, The whiskers extend from the box to show the range of the data. Flier points are those past the end of the whiskers. - Function Arguments: + Parameters + ---------- - *x* : - Array or a sequence of vectors. + x : Array or a sequence of vectors. + The input data. - *notch* : [ False (default) | True ] - If False (default), produces a rectangular box plot. + notch : bool, default = False + If False, produces a rectangular box plot. If True, will produce a notched box plot - *sym* : [ default 'b+' ] + sym : str, default = 'b+' The default symbol for flier points. Enter an empty string ('') if you don't want to show fliers. - *vert* : [ False | True (default) ] + vert : bool, default = False If True (default), makes the boxes vertical. If False, makes horizontal boxes. - *whis* : [ default 1.5 ] - Defines the length of the whiskers as a function of the inner - quartile range. They extend to the most extreme data point - within ( ``whis*(75%-25%)`` ) data range. - - *bootstrap* : [ *None* (default) | integer ] + whis : float, sequence (default = 1.5) or string + As a float, determines the reach of the whiskers past the first + and third quartiles (e.g., Q3 + whis*IQR, IQR = interquartile + range, Q3-Q1). Beyond the whiskers, data are considered outliers + and are plotted as individual points. Set this to an unreasonably + high value to force the whiskers to show the min and max values. + Alternatively, set this to an ascending sequence of percentile + (e.g., [5, 95]) to set the whiskers at specific percentiles of + the data. Finally, *whis* can be the string 'range' to force the + whiskers to the min and max of the data. In the edge case that + the 25th and 75th percentiles are equivalent, *whis* will be + automatically set to 'range'. + + bootstrap : None (default) or integer Specifies whether to bootstrap the confidence intervals around the median for notched boxplots. If bootstrap==None, no bootstrapping is performed, and notches are calculated @@ -2837,14 +2852,14 @@ def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, bootstrap the median to determine it's 95% confidence intervals. Values between 1000 and 10000 are recommended. - *usermedians* : [ default None ] + usermedians : array-like or None (default) An array or sequence whose first dimension (or length) is compatible with *x*. This overrides the medians computed by matplotlib for each element of *usermedians* that is not None. When an element of *usermedians* == None, the median will be - computed directly as normal. + computed by matplotlib as normal. - *conf_intervals* : [ default None ] + conf_intervals : array-like or None (default) Array or sequence whose first dimension (or length) is compatible with *x* and whose second dimension is 2. When the current element of *conf_intervals* is not None, the notch locations computed by @@ -2852,20 +2867,57 @@ def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, element of *conf_intervals* is None, boxplot compute notches the method specified by the other kwargs (e.g., *bootstrap*). - *positions* : [ default 1,2,...,n ] - Sets the horizontal positions of the boxes. The ticks and limits + positions : array-like, default = [1, 2, ..., n] + Sets the positions of the boxes. The ticks and limits are automatically set to match the positions. - *widths* : [ default 0.5 ] + widths : array-like, default = 0.5 Either a scalar or a vector and sets the width of each box. The default is 0.5, or ``0.15*(distance between extreme positions)`` if that is smaller. - *patch_artist* : [ False (default) | True ] + labels : sequence or None (default) + Labels for each dataset. Length must be compatible with + dimensions of *x* + + patch_artist : bool, default = False If False produces boxes with the Line2D artist If True produces boxes with the Patch artist - Returns a dictionary mapping each component of the boxplot + showmeans : bool, default = False + If True, will toggle one the rendering of the means + + showcaps : bool, default = True + If True, will toggle one the rendering of the caps + + showbox : bool, default = True + If True, will toggle one the rendering of box + + showfliers : bool, default = True + If True, will toggle one the rendering of the fliers + + boxprops : dict or None (default) + If provided, will set the plotting style of the boxes + + flierprops : dict or None (default) + If provided, will set the plotting style of the fliers + + medianprops : dict or None (default) + If provided, will set the plotting style of the medians + + meanprops : dict or None (default) + If provided, will set the plotting style of the means + + meanline : bool, default = False + If True (and *showmeans* is True), will try to render the mean + as a line spanning the full width of the box according to + *meanprops*. Not recommended if *shownotches* is also True. + Otherwise, means will be shown as points. + + Returns + ------- + + A dictionary mapping each component of the boxplot to a list of the :class:`matplotlib.lines.Line2D` instances created. That dictionary has the following keys (assuming vertical boxplots): @@ -2878,266 +2930,393 @@ def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5, - caps: the horizontal lines at the ends of the whiskers. - fliers: points representing data that extend beyone the whiskers (outliers). + - means: points or lines representing the means. - **Example:** + Examples + -------- - .. plot:: pyplots/boxplot_demo.py + .. plot:: examples/statistics/boxplot_demo.py """ - def bootstrapMedian(data, N=5000): - # determine 95% confidence intervals of the median - M = len(data) - percentile = [2.5, 97.5] - estimate = np.zeros(N) - for n in range(N): - bsIndex = np.random.random_integers(0, M - 1, M) - bsData = data[bsIndex] - estimate[n] = mlab.prctile(bsData, 50) - CI = mlab.prctile(estimate, percentile) - return CI - - def computeConfInterval(data, med, iq, bootstrap): - if bootstrap is not None: - # Do a bootstrap estimate of notch locations. - # get conf. intervals around median - CI = bootstrapMedian(data, N=bootstrap) - notch_min = CI[0] - notch_max = CI[1] - else: - # Estimate notch locations using Gaussian-based - # asymptotic approximation. - # - # For discussion: McGill, R., Tukey, J.W., - # and Larsen, W.A. (1978) "Variations of - # Boxplots", The American Statistician, 32:12-16. - N = len(data) - notch_min = med - 1.57 * iq / np.sqrt(N) - notch_max = med + 1.57 * iq / np.sqrt(N) - return notch_min, notch_max - - if not self._hold: - self.cla() - holdStatus = self._hold - whiskers, caps, boxes, medians, fliers = [], [], [], [], [] + bxpstats = cbook.boxplot_stats(x, whis=whis, bootstrap=bootstrap, + labels=labels) + if sym == 'b+' and flierprops is None: + flierprops = dict(linestyle='none', marker='+', + markeredgecolor='blue') - # convert x to a list of vectors - if hasattr(x, 'shape'): - if len(x.shape) == 1: - if hasattr(x[0], 'shape'): - x = list(x) - else: - x = [x, ] - elif len(x.shape) == 2: - nr, nc = x.shape - if nr == 1: - x = [x] - elif nc == 1: - x = [x.ravel()] - else: - x = [x[:, i] for i in xrange(nc)] + # replace medians if necessary: + if usermedians is not None: + if (len(np.ravel(usermedians)) != len(bxpstats) or + np.shape(usermedians)[0] != len(bxpstats)): + medmsg = 'usermedians length not compatible with x' + raise ValueError(medmsg) else: - raise ValueError("input x can have no more than 2 dimensions") - if not hasattr(x[0], '__len__'): - x = [x] - col = len(x) + # reassign medians as necessary + for stats, med in zip(bxpstats, usermedians): + if med is not None: + stats['med'] = med - # sanitize user-input medians - msg1 = "usermedians must either be a list/tuple or a 1d array" - msg2 = "usermedians' length must be compatible with x" - if usermedians is not None: - if hasattr(usermedians, 'shape'): - if len(usermedians.shape) != 1: - raise ValueError(msg1) - elif usermedians.shape[0] != col: - raise ValueError(msg2) - elif len(usermedians) != col: - raise ValueError(msg2) - - #sanitize user-input confidence intervals - msg1 = "conf_intervals must either be a list of tuples or a 2d array" - msg2 = "conf_intervals' length must be compatible with x" - msg3 = "each conf_interval, if specificied, must have two values" if conf_intervals is not None: - if hasattr(conf_intervals, 'shape'): - if len(conf_intervals.shape) != 2: - raise ValueError(msg1) - elif conf_intervals.shape[0] != col: - raise ValueError(msg2) - elif conf_intervals.shape[1] != 2: - raise ValueError(msg3) + if np.shape(conf_intervals)[0] != len(bxpstats): + raise ValueError('conf_intervals length not ' + 'compatible with x') else: - if len(conf_intervals) != col: - raise ValueError(msg2) - for ci in conf_intervals: - if ci is not None and len(ci) != 2: - raise ValueError(msg3) + for stats, ci in zip(bxpstats, conf_intervals): + if ci is not None: + if len(ci) != 2: + raise ValueError('each confidence interval must ' + 'have two values') + else: + if ci[0] is not None: + stats['cilo'] = ci[0] + if ci[1] is not None: + stats['cihi'] = ci[1] + + artists = self.bxp(bxpstats, positions=positions, widths=widths, + vert=vert, patch_artist=patch_artist, + shownotches=notch, showmeans=showmeans, + showcaps=showcaps, showbox=showbox, + boxprops=boxprops, flierprops=flierprops, + medianprops=medianprops, meanprops=meanprops, + meanline=meanline, showfliers=showfliers) + return artists + + def bxp(self, bxpstats, positions=None, widths=None, vert=True, + patch_artist=False, shownotches=False, showmeans=False, + showcaps=True, showbox=True, showfliers=True, + boxprops=None, flierprops=None, medianprops=None, + meanprops=None, meanline=False): + """ + Drawing function for box and whisker plots. - # get some plot info - if positions is None: - positions = list(xrange(1, col + 1)) - if widths is None: - distance = max(positions) - min(positions) - widths = min(0.15 * max(distance, 1.0), 0.5) - if isinstance(widths, float) or isinstance(widths, int): - widths = np.ones((col,), float) * widths + Call signature:: - # loop through columns, adding each to plot - self.hold(True) - for i, pos in enumerate(positions): - d = np.ravel(x[i]) - row = len(d) - if row == 0: - # no data, skip this position - continue + bxp(bxpstats, positions=None, widths=None, vert=True, + patch_artist=False, shownotches=False, showmeans=False, + showcaps=True, showbox=True, showfliers=True, + boxprops=None, flierprops=None, medianprops=None, + meanprops=None, meanline=False) - # get median and quartiles - q1, med, q3 = mlab.prctile(d, [25, 50, 75]) + Make a box and whisker plot for each column of *x* or each + vector in sequence *x*. The box extends from the lower to + upper quartile values of the data, with a line at the median. + The whiskers extend from the box to show the range of the + data. Flier points are those past the end of the whiskers. - # replace with input medians if available - if usermedians is not None: - if usermedians[i] is not None: - med = usermedians[i] + Parameters + ---------- - # get high extreme - iq = q3 - q1 - hi_val = q3 + whis * iq - wisk_hi = np.compress(d <= hi_val, d) - if len(wisk_hi) == 0 or np.max(wisk_hi) < q3: - wisk_hi = q3 - else: - wisk_hi = max(wisk_hi) + bxpstats : list of dicts + A list of dictionaries containing stats for each boxplot. + Required keys are: + 'med' - The median (scalar float). + 'q1' - The first quartile (25th percentile) (scalar float). + 'q3' - The first quartile (50th percentile) (scalar float). + 'whislo' - Lower bound of the lower whisker (scalar float). + 'whishi' - Upper bound of the upper whisker (scalar float). + Optional keys are + 'mean' - The mean (scalar float). Needed if showmeans=True. + 'fliers' - Data beyond the whiskers (sequence of floats). + Needed if showfliers=True. + 'cilo' & 'ciho' - Lower and upper confidence intervals about + the median. Needed if shownotches=True. + 'label' - Name of the dataset (string). If available, this + will be used a tick label for the boxplot + + positions : array-like, default = [1, 2, ..., n] + Sets the positions of the boxes. The ticks and limits + are automatically set to match the positions. - # get low extreme - lo_val = q1 - whis * iq - wisk_lo = np.compress(d >= lo_val, d) - if len(wisk_lo) == 0 or np.min(wisk_lo) > q1: - wisk_lo = q1 - else: - wisk_lo = min(wisk_lo) - - # get fliers - if we are showing them - flier_hi = [] - flier_lo = [] - flier_hi_x = [] - flier_lo_x = [] - if len(sym) != 0: - flier_hi = np.compress(d > wisk_hi, d) - flier_lo = np.compress(d < wisk_lo, d) - flier_hi_x = np.ones(flier_hi.shape[0]) * pos - flier_lo_x = np.ones(flier_lo.shape[0]) * pos - - # get x locations for fliers, whisker, whisker cap and box sides - box_x_min = pos - widths[i] * 0.5 - box_x_max = pos + widths[i] * 0.5 - - wisk_x = np.ones(2) * pos - - cap_x_min = pos - widths[i] * 0.25 - cap_x_max = pos + widths[i] * 0.25 - cap_x = [cap_x_min, cap_x_max] - - # get y location for median - med_y = [med, med] - - # calculate 'notch' plot - if notch: - # conf. intervals from user, if available - if (conf_intervals is not None and - conf_intervals[i] is not None): - notch_max = np.max(conf_intervals[i]) - notch_min = np.min(conf_intervals[i]) - else: - notch_min, notch_max = computeConfInterval(d, med, iq, - bootstrap) - - # make our notched box vectors - box_x = [box_x_min, box_x_max, box_x_max, cap_x_max, box_x_max, - box_x_max, box_x_min, box_x_min, cap_x_min, box_x_min, - box_x_min] - box_y = [q1, q1, notch_min, med, notch_max, q3, q3, notch_max, - med, notch_min, q1] - # make our median line vectors - med_x = [cap_x_min, cap_x_max] - med_y = [med, med] - # calculate 'regular' plot - else: - # make our box vectors - box_x = [box_x_min, box_x_max, box_x_max, box_x_min, box_x_min] - box_y = [q1, q1, q3, q3, q1] - # make our median line vectors - med_x = [box_x_min, box_x_max] - - def to_vc(xs, ys): - # convert arguments to verts and codes - verts = [] - #codes = [] - for xi, yi in zip(xs, ys): - verts.append((xi, yi)) - verts.append((0, 0)) # ignored - codes = [mpath.Path.MOVETO] + \ - [mpath.Path.LINETO] * (len(verts) - 2) + \ - [mpath.Path.CLOSEPOLY] - return verts, codes - - def patch_list(xs, ys): - verts, codes = to_vc(xs, ys) - path = mpath.Path(verts, codes) - patch = mpatches.PathPatch(path) - self.add_artist(patch) - return [patch] - - # vertical or horizontal plot? - if vert: - - def doplot(*args): - return self.plot(*args) - - def dopatch(xs, ys): - return patch_list(xs, ys) - else: + widths : array-like, default = 0.5 + Either a scalar or a vector and sets the width of each box. The + default is 0.5, or ``0.15*(distance between extreme positions)`` + if that is smaller. - def doplot(*args): - shuffled = [] - for i in xrange(0, len(args), 3): - shuffled.extend([args[i + 1], args[i], args[i + 2]]) - return self.plot(*shuffled) + vert : bool, default = False + If True (default), makes the boxes vertical. + If False, makes horizontal boxes. - def dopatch(xs, ys): - xs, ys = ys, xs # flip X, Y - return patch_list(xs, ys) + patch_artist : bool, default = False + If False produces boxes with the Line2D artist + If True produces boxes with the Patch artist - if patch_artist: - median_color = 'k' - else: - median_color = 'r' - - whiskers.extend(doplot(wisk_x, [q1, wisk_lo], 'b--', - wisk_x, [q3, wisk_hi], 'b--')) - caps.extend(doplot(cap_x, [wisk_hi, wisk_hi], 'k-', - cap_x, [wisk_lo, wisk_lo], 'k-')) - if patch_artist: - boxes.extend(dopatch(box_x, box_y)) + shownotches : bool, default = False + If False (default), produces a rectangular box plot. + If True, will produce a notched box plot + + showmeans : bool, default = False + If True, will toggle one the rendering of the means + + showcaps : bool, default = True + If True, will toggle one the rendering of the caps + + showbox : bool, default = True + If True, will toggle one the rendering of box + + showfliers : bool, default = True + If True, will toggle one the rendering of the fliers + + boxprops : dict or None (default) + If provided, will set the plotting style of the boxes + + flierprops : dict or None (default) + If provided, will set the plotting style of the fliers + + medianprops : dict or None (default) + If provided, will set the plotting style of the medians + + meanprops : dict or None (default) + If provided, will set the plotting style of the means + + meanline : bool, default = False + If True (and *showmeans* is True), will try to render the mean + as a line spanning the full width of the box according to + *meanprops*. Not recommended if *shownotches* is also True. + Otherwise, means will be shown as points. + + Returns + ------- + + A dictionary mapping each component of the boxplot + to a list of the :class:`matplotlib.lines.Line2D` + instances created. That dictionary has the following keys + (assuming vertical boxplots): + + - boxes: the main body of the boxplot showing the quartiles + and the median's confidence intervals if enabled. + - medians: horizonal lines at the median of each box. + - whiskers: the vertical lines extending to the most extreme, + n-outlier data points. + - caps: the horizontal lines at the ends of the whiskers. + - fliers: points representing data that extend beyone the + whiskers (fliers). + - means: points or lines representing the means. + + Examples + -------- + + .. plot:: examples/statistics/bxp_demo.py + """ + # lists of artists to be output + whiskers = [] + caps = [] + boxes = [] + medians = [] + means = [] + fliers = [] + + # empty list of xticklabels + datalabels = [] + + # translates between line2D and patch linestyles + linestyle_map = { + 'solid': '-', + 'dashed': '--', + 'dashdot': '-.', + 'dotted': ':' + } + + # box properties + if patch_artist: + final_boxprops = dict(linestyle='solid', edgecolor='black', + facecolor='white', linewidth=1) + else: + final_boxprops = dict(linestyle='-', color='black', linewidth=1) + + if boxprops is not None: + final_boxprops.update(boxprops) + + # other (cap, whisker) properties + if patch_artist: + otherprops = dict( + linestyle=linestyle_map[final_boxprops['linestyle']], + color=final_boxprops['edgecolor'], + linewidth=final_boxprops.get('linewidth', 1) + ) + else: + otherprops = dict(linestyle=final_boxprops['linestyle'], + color=final_boxprops['color'], + linewidth=final_boxprops.get('linewidth', 1)) + + # flier (outlier) properties + final_flierprops = dict(linestyle='none', marker='+', + markeredgecolor='blue') + if flierprops is not None: + final_flierprops.update(flierprops) + + # median line properties + final_medianprops = dict(linestyle='-', color='blue') + if medianprops is not None: + final_medianprops.update(medianprops) + + # mean (line or point) properties + if meanline: + final_meanprops = dict(linestyle='--', color='red') + else: + final_meanprops = dict(linestyle='none', markerfacecolor='red', + marker='s') + if meanprops is not None: + final_meanprops.update(meanprops) + + def to_vc(xs, ys): + # convert arguments to verts and codes + verts = [] + #codes = [] + for xi, yi in zip(xs, ys): + verts.append((xi, yi)) + verts.append((0, 0)) # ignored + codes = [mpath.Path.MOVETO] + \ + [mpath.Path.LINETO] * (len(verts) - 2) + \ + [mpath.Path.CLOSEPOLY] + return verts, codes + + def patch_list(xs, ys, **kwargs): + verts, codes = to_vc(xs, ys) + path = mpath.Path(verts, codes) + patch = mpatches.PathPatch(path, **kwargs) + self.add_artist(patch) + return [patch] + + # vertical or horizontal plot? + if vert: + def doplot(*args, **kwargs): + return self.plot(*args, **kwargs) + + def dopatch(xs, ys, **kwargs): + return patch_list(xs, ys, **kwargs) + + else: + def doplot(*args, **kwargs): + shuffled = [] + for i in xrange(0, len(args), 2): + shuffled.extend([args[i + 1], args[i]]) + return self.plot(*shuffled, **kwargs) + + def dopatch(xs, ys, **kwargs): + xs, ys = ys, xs # flip X, Y + return patch_list(xs, ys, **kwargs) + + # input validation + N = len(bxpstats) + datashape_message = ("List of boxplot statistics and `{0}` " + "values must have same the length") + # check position + if positions is None: + positions = list(xrange(1, N + 1)) + elif len(positions) != N: + raise ValueError(datashape_message.format("positions")) + + # width + if widths is None: + distance = max(positions) - min(positions) + widths = [min(0.15 * max(distance, 1.0), 0.5)] * N + elif np.isscalar(widths): + widths = [widths] * N + elif len(widths) != N: + raise ValueError(datashape_message.format("widths")) + + # check and save the `hold` state of the current axes + if not self._hold: + self.cla() + holdStatus = self._hold + + for pos, width, stats in zip(positions, widths, bxpstats): + # try to find a new label + datalabels.append(stats.get('label', pos)) + + # fliers coords + flier_x = np.ones(len(stats['fliers'])) * pos + flier_y = stats['fliers'] + + # whisker coords + whisker_x = np.ones(2) * pos + whiskerlo_y = np.array([stats['q1'], stats['whislo']]) + whiskerhi_y = np.array([stats['q3'], stats['whishi']]) + + # cap coords + cap_left = pos - width * 0.25 + cap_right = pos + width * 0.25 + cap_x = np.array([cap_left, cap_right]) + cap_lo = np.ones(2) * stats['whislo'] + cap_hi = np.ones(2) * stats['whishi'] + + # box and median coords + box_left = pos - width * 0.5 + box_right = pos + width * 0.5 + med_y = [stats['med'], stats['med']] + + # notched boxes + if shownotches: + box_x = [box_left, box_right, box_right, cap_right, box_right, + box_right, box_left, box_left, cap_left, box_left, + box_left] + box_y = [stats['q1'], stats['q1'], stats['cilo'], + stats['med'], stats['cihi'], stats['q3'], + stats['q3'], stats['cihi'], stats['med'], + stats['cilo'], stats['q1']] + med_x = cap_x + + # plain boxes else: - boxes.extend(doplot(box_x, box_y, 'b-')) + box_x = [box_left, box_right, box_right, box_left, box_left] + box_y = [stats['q1'], stats['q1'], stats['q3'], stats['q3'], + stats['q1']] + med_x = [box_left, box_right] + + # maybe draw the box: + if showbox: + if patch_artist: + boxes.extend(dopatch(box_x, box_y, **final_boxprops)) + else: + boxes.extend(doplot(box_x, box_y, **final_boxprops)) + + # draw the whiskers + whiskers.extend(doplot(whisker_x, whiskerlo_y, **otherprops)) + whiskers.extend(doplot(whisker_x, whiskerhi_y, **otherprops)) + + # maybe draw the caps: + if showcaps: + caps.extend(doplot(cap_x, cap_lo, **otherprops)) + caps.extend(doplot(cap_x, cap_hi, **otherprops)) + + # draw the medians + medians.extend(doplot(med_x, med_y, **final_medianprops)) + + # maybe draw the means + if showmeans: + if meanline: + means.extend(doplot( + [box_left, box_right], [stats['mean'], stats['mean']], + **final_meanprops + )) + else: + means.extend(doplot( + [pos], [stats['mean']], **final_meanprops + )) - medians.extend(doplot(med_x, med_y, median_color + '-')) - fliers.extend(doplot(flier_hi_x, flier_hi, sym, - flier_lo_x, flier_lo, sym)) + # maybe draw the fliers + if showfliers: + fliers.extend(doplot(flier_x, flier_y, **final_flierprops)) # fix our axes/ticks up a little if vert: - setticks, setlim = self.set_xticks, self.set_xlim + setticks = self.set_xticks + setlim = self.set_xlim + setlabels = self.set_xticklabels else: - setticks, setlim = self.set_yticks, self.set_ylim + setticks = self.set_yticks + setlim = self.set_ylim + setlabels = self.set_yticklabels newlimits = min(positions) - 0.5, max(positions) + 0.5 setlim(newlimits) setticks(positions) + setlabels(datalabels) # reset hold status self.hold(holdStatus) return dict(whiskers=whiskers, caps=caps, boxes=boxes, - medians=medians, fliers=fliers) + medians=medians, fliers=fliers, means=means) @docstring.dedent_interpd def scatter(self, x, y, s=20, c='b', marker='o', cmap=None, norm=None, diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index c5e96a5bda5f..e9333d9406d9 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -171,13 +171,13 @@ def new_function(): pending : bool, optional If True, uses a PendingDeprecationWarning instead of a DeprecationWarning. - + Example ------- @deprecated('1.4.0') def the_function_to_deprecate(): pass - + """ def deprecate(func, message=message, name=name, alternative=alternative, pending=pending): @@ -1846,6 +1846,179 @@ def delete_masked_points(*args): return margs +def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None): + ''' + Returns list of dictionaries of staticists to be use to draw a series of + box and whisker plots. See the `Returns` section below to the required + keys of the dictionary. Users can skip this function and pass a user- + defined set of dictionaries to the new `axes.bxp` method instead of + relying on MPL to do the calcs. + + Parameters + ---------- + X : array-like + Data that will be represented in the boxplots. Should have 2 or fewer + dimensions. + + whis : float, string, or sequence (default = 1.5) + As a float, determines the reach of the whiskers past the first and + third quartiles (e.g., Q3 + whis*IQR, QR = interquartile range, Q3-Q1). + Beyond the whiskers, data are considered outliers and are plotted as + individual points. Set this to an unreasonably high value to force the + whiskers to show the min and max data. Alternatively, set this to an + ascending sequence of percentile (e.g., [5, 95]) to set the whiskers + at specific percentiles of the data. Finally, can `whis` be the + string 'range' to force the whiskers to the min and max of the data. + In the edge case that the 25th and 75th percentiles are equivalent, + `whis` will be automatically set to 'range' + + bootstrap : int or None (default) + Number of times the confidence intervals around the median should + be bootstrapped (percentile method). + + labels : sequence + Labels for each dataset. Length must be compatible with dimensions + of `X` + + Returns + ------- + bxpstats : A list of dictionaries containing the results for each column + of data. Keys are as + + Notes + ----- + Non-bootstrapping approach to confidence interval uses Gaussian-based + asymptotic approximation. + + General approach from: + McGill, R., Tukey, J.W., and Larsen, W.A. (1978) "Variations of + Boxplots", The American Statistician, 32:12-16. + ''' + + def _bootstrap_median(data, N=5000): + # determine 95% confidence intervals of the median + M = len(data) + percentiles = [2.5, 97.5] + + ii = np.random.randint(M, size=(N, M)) + bsData = x[ii] + estimate = np.median(bsData, axis=1, overwrite_input=True) + + CI = np.percentile(estimate, percentiles) + return CI + + def _compute_conf_interval(data, med, iqr, bootstrap): + if bootstrap is not None: + # Do a bootstrap estimate of notch locations. + # get conf. intervals around median + CI = _bootstrap_median(data, N=bootstrap) + notch_min = CI[0] + notch_max = CI[1] + else: + + N = len(data) + notch_min = med - 1.57 * iqr / np.sqrt(N) + notch_max = med + 1.57 * iqr / np.sqrt(N) + + return notch_min, notch_max + + # output is a list of dicts + bxpstats = [] + + # convert X to a list of lists + if hasattr(X, 'shape'): + # one item + if len(X.shape) == 1: + if hasattr(X[0], 'shape'): + X = list(X) + else: + X = [X, ] + + # several items + elif len(X.shape) == 2: + nrows, ncols = X.shape + if nrows == 1: + X = [X] + elif ncols == 1: + X = [X.ravel()] + else: + X = [X[:, i] for i in xrange(ncols)] + else: + raise ValueError("input `X` must have 2 or fewer dimensions") + + if not hasattr(X[0], '__len__'): + X = [X] + + ncols = len(X) + if labels is None: + labels = [str(i) for i in range(ncols)] + elif len(labels) != ncols: + raise ValueError("Dimensions of labels and X must be compatible") + + for ii, (x, label) in enumerate(zip(X, labels), start=0): + # empty dict + stats = {} + stats['label'] = label + + # arithmetic mean + stats['mean'] = np.mean(x) + + # medians and quartiles + q1, med, q3 = np.percentile(x, [25, 50, 75]) + + # interquartile range + stats['iqr'] = q3 - q1 + if stats['iqr'] == 0: + whis = 'range' + + # conf. interval around median + stats['cilo'], stats['cihi'] = _compute_conf_interval( + x, med, stats['iqr'], bootstrap + ) + + # lowest/highest non-outliers + if np.isscalar(whis): + if np.isreal(whis): + loval = q1 - whis * stats['iqr'] + hival = q3 + whis * stats['iqr'] + elif whis in ['range', 'limit', 'limits', 'min/max']: + loval = np.min(x) + hival = np.max(x) + else: + whismsg = ('whis must be a float, valid string, or ' + 'list of percentiles') + raise ValueError(whismsg) + else: + loval = np.percentile(x, whis[0]) + hival = np.percentile(x, whis[1]) + + # get high extreme + wiskhi = np.compress(x <= hival, x) + if len(wiskhi) == 0 or np.max(wiskhi) < q3: + stats['whishi'] = q3 + else: + stats['whishi'] = np.max(wiskhi) + + # get low extreme + wisklo = np.compress(x >= loval, x) + if len(wisklo) == 0 or np.min(wisklo) > q1: + stats['whislo'] = q1 + else: + stats['whislo'] = np.min(wisklo) + + # compute a single array of outliers + stats['fliers'] = np.hstack([ + np.compress(x < stats['whislo'], x), + np.compress(x > stats['whishi'], x) + ]) + + # add in teh remaining stats and append to final output + stats['q1'], stats['med'], stats['q3'] = q1, med, q3 + bxpstats.append(stats) + + return bxpstats + + # FIXME I don't think this is used anywhere def unmasked_index_ranges(mask, compressed=True): """ diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 5b64afa364f7..12480b43552a 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -132,7 +132,6 @@ def switch_backend(newbackend): def show(*args, **kw): """ Display a figure. - When running in ipython with its pylab mode, display all figures and return to the ipython prompt. @@ -2602,7 +2601,9 @@ def broken_barh(xranges, yrange, hold=None, **kwargs): @_autogen_docstring(Axes.boxplot) def boxplot(x, notch=False, sym='b+', vert=True, whis=1.5, positions=None, widths=None, patch_artist=False, bootstrap=None, usermedians=None, - conf_intervals=None, hold=None): + conf_intervals=None, meanline=False, showmeans=False, showcaps=True, + showbox=True, showfliers=True, boxprops=None, labels=None, + flierprops=None, medianprops=None, meanprops=None, hold=None): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2613,7 +2614,13 @@ def boxplot(x, notch=False, sym='b+', vert=True, whis=1.5, positions=None, ret = ax.boxplot(x, notch=notch, sym=sym, vert=vert, whis=whis, positions=positions, widths=widths, patch_artist=patch_artist, bootstrap=bootstrap, - usermedians=usermedians, conf_intervals=conf_intervals) + usermedians=usermedians, + conf_intervals=conf_intervals, meanline=meanline, + showmeans=showmeans, showcaps=showcaps, + showbox=showbox, showfliers=showfliers, + boxprops=boxprops, labels=labels, + flierprops=flierprops, medianprops=medianprops, + meanprops=meanprops) draw_if_interactive() finally: ax.hold(washold) @@ -2624,8 +2631,8 @@ def boxplot(x, notch=False, sym='b+', vert=True, whis=1.5, positions=None, # changes will be lost @_autogen_docstring(Axes.cohere) def cohere(x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, - window=mlab.window_hanning, noverlap=0, pad_to=None, - sides='default', scale_by_freq=None, hold=None, **kwargs): + window=mlab.window_hanning, noverlap=0, pad_to=None, sides='default', + scale_by_freq=None, hold=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2747,9 +2754,9 @@ def errorbar(x, y, yerr=None, xerr=None, fmt='-', ecolor=None, elinewidth=None, # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.eventplot) -def eventplot(positions, orientation='horizontal', lineoffsets=1, - linelengths=1, linewidths=None, colors=None, linestyles='solid', - hold=None, **kwargs): +def eventplot(positions, orientation='horizontal', lineoffsets=1, linelengths=1, + linewidths=None, colors=None, linestyles='solid', hold=None, + **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2897,8 +2904,8 @@ def hist2d(x, y, bins=10, range=None, normed=False, weights=None, cmin=None, # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.hlines) -def hlines(y, xmin, xmax, colors='k', linestyles='solid', label='', - hold=None, **kwargs): +def hlines(y, xmin, xmax, colors='k', linestyles='solid', label='', hold=None, + **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3390,8 +3397,8 @@ def triplot(*args, **kwargs): # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.vlines) -def vlines(x, ymin, ymax, colors='k', linestyles='solid', label='', - hold=None, **kwargs): +def vlines(x, ymin, ymax, colors='k', linestyles='solid', label='', hold=None, + **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot.pdf b/lib/matplotlib/tests/baseline_images/test_axes/boxplot.pdf index 1ef558832b91..6603608c6832 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/boxplot.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot.png b/lib/matplotlib/tests/baseline_images/test_axes/boxplot.png index 797d133e371e..f17404f5ad48 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/boxplot.png and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot.svg b/lib/matplotlib/tests/baseline_images/test_axes/boxplot.svg index 9d3db78b84d1..e123c4a2db1c 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/boxplot.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/boxplot.svg @@ -5,7 +5,7 @@ @@ -30,42 +30,42 @@ z +M166.86 236.45 +L200.34 236.45 +L200.34 222.672 +L191.97 216 +L200.34 209.328 +L200.34 195.55 +L166.86 195.55 +L166.86 209.328 +L175.23 216 +L166.86 222.672 +L166.86 236.45" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +M183.6 236.45 +L183.6 256.32" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +M183.6 195.55 +L183.6 175.68" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +L191.97 256.32" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +M175.23 175.68 +L191.97 175.68" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +L191.97 216" style="fill:none;stroke:#0000ff;stroke-linecap:square;"/> @@ -73,86 +73,78 @@ L191.97 216" style="fill:none;stroke:#ff0000;"/> M-3 0 L3 0 M0 3 -L0 -3" id="me594928b4b" style="stroke:#0000ff;stroke-linecap:butt;stroke-width:0.5;"/> +L0 -3" id="md4acab13e0" style="stroke:#0000ff;stroke-width:0.5;"/> - + + - - - + +L406.8 256.32" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +L406.8 175.68" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +M398.43 256.32 +L415.17 256.32" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +M398.43 175.68 +L415.17 175.68" style="fill:none;stroke:#000000;stroke-linecap:square;"/> +M398.43 216 +L415.17 216" style="fill:none;stroke:#0000ff;stroke-linecap:square;"/> - - - - - - - - - + + - + +L0 -4" id="m93b0483c22" style="stroke:#000000;stroke-width:0.5;"/> - + - + +L0 4" id="m741efc42ff" style="stroke:#000000;stroke-width:0.5;"/> - + @@ -173,20 +165,20 @@ L12.4062 0 z " id="BitstreamVeraSans-Roman-31"/> - + - + - + - + - + @@ -216,7 +208,7 @@ Q49.8594 40.875 45.4062 35.4062 Q44.1875 33.9844 37.6406 27.2188 Q31.1094 20.4531 19.1875 8.29688" id="BitstreamVeraSans-Roman-32"/> - + @@ -224,24 +216,24 @@ Q31.1094 20.4531 19.1875 8.29688" id="BitstreamVeraSans-Roman-32"/> - + +L4 0" id="m728421d6d4" style="stroke:#000000;stroke-width:0.5;"/> - + - + +L-4 0" id="mcb0005524f" style="stroke:#000000;stroke-width:0.5;"/> - + @@ -305,7 +297,7 @@ Q6.59375 17.9688 6.59375 36.375 Q6.59375 54.8281 13.0625 64.5156 Q19.5312 74.2188 31.7812 74.2188" id="BitstreamVeraSans-Roman-30"/> - + @@ -313,19 +305,19 @@ Q19.5312 74.2188 31.7812 74.2188" id="BitstreamVeraSans-Roman-30"/> - + - + - + - + - + @@ -333,19 +325,19 @@ Q19.5312 74.2188 31.7812 74.2188" id="BitstreamVeraSans-Roman-30"/> - + - + - + - + - + @@ -353,75 +345,75 @@ Q19.5312 74.2188 31.7812 74.2188" id="BitstreamVeraSans-Roman-30"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -431,22 +423,22 @@ Q19.5312 74.2188 31.7812 74.2188" id="BitstreamVeraSans-Roman-30"/> +L518.4 43.2" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;"/> +L518.4 43.2" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;"/> +L518.4 388.8" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;"/> +L72 43.2" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;"/> diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.pdf b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.pdf new file mode 100644 index 000000000000..dfdb5b6f3f9c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.png b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.png new file mode 100644 index 000000000000..3e4d8726d3a3 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.svg b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.svg new file mode 100644 index 000000000000..0d070e52b984 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_autorange_whiskers.svg @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_no_inverted_whisker.png b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_no_inverted_whisker.png index ea43a079599c..cba447aca198 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_no_inverted_whisker.png and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_no_inverted_whisker.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_with_CIarray.png b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_with_CIarray.png index c3136d0e13e7..9fb197604e80 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/boxplot_with_CIarray.png and b/lib/matplotlib/tests/baseline_images/test_axes/boxplot_with_CIarray.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_baseline.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_baseline.png new file mode 100644 index 000000000000..146241bd347c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_baseline.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_custombox.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custombox.png new file mode 100644 index 000000000000..6329a2c51a71 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custombox.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_custommedian.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custommedian.png new file mode 100644 index 000000000000..1c3f6dec22a0 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custommedian.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_customoutlier.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_customoutlier.png new file mode 100644 index 000000000000..53bb0c2bd6e6 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_customoutlier.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompatchartist.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompatchartist.png new file mode 100644 index 000000000000..9ed745791428 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompatchartist.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompositions.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompositions.png new file mode 100644 index 000000000000..35a0de4fa8ee Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompositions.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_customwidths.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_customwidths.png new file mode 100644 index 000000000000..9d063c9bded0 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_customwidths.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_horizontal.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_horizontal.png new file mode 100644 index 000000000000..8c79adacf6c4 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_horizontal.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_nobox.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_nobox.png new file mode 100644 index 000000000000..b12088d71ee7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_nobox.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_nocaps.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_nocaps.png new file mode 100644 index 000000000000..235d1da545d2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_nocaps.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_patchartist.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_patchartist.png new file mode 100644 index 000000000000..6e54ca819a24 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_patchartist.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_precentilewhis.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_precentilewhis.png new file mode 100644 index 000000000000..dd6b368e7196 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_precentilewhis.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_rangewhis.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_rangewhis.png new file mode 100644 index 000000000000..8df2d30167c8 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_rangewhis.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_scalarwidth.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_scalarwidth.png new file mode 100644 index 000000000000..90ff37016736 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_scalarwidth.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_with_xlabels.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_with_xlabels.png new file mode 100644 index 000000000000..fda6da5059fd Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_with_xlabels.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_with_ylabels.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_with_ylabels.png new file mode 100644 index 000000000000..07867fa6ddbc Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_with_ylabels.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_custompoint.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_custompoint.png new file mode 100644 index 000000000000..74ec31b2e25e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_custompoint.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_line.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_line.png new file mode 100644 index 000000000000..73520f0118d7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_line.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_point.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_point.png new file mode 100644 index 000000000000..77ffc6ca3da1 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withmean_point.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_withnotch.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withnotch.png new file mode 100644 index 000000000000..c95f103856ff Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_withnotch.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 835d701d428b..d36c1c5bc8d3 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -989,7 +989,6 @@ def test_log_scales(): ax.invert_yaxis() ax.set_xscale('log', basex=9.0) - @image_comparison(baseline_images=['stackplot_test_image']) def test_stackplot(): fig = plt.figure() @@ -1002,7 +1001,6 @@ def test_stackplot(): ax.set_xlim((0, 10)) ax.set_ylim((0, 70)) - @image_comparison(baseline_images=['stackplot_test_baseline'], remove_text=True) def test_stackplot_baseline(): @@ -1037,19 +1035,316 @@ def bump(a): plt.subplot(2, 2, 4) plt.stackplot(list(xrange(100)), d.T, baseline='weighted_wiggle') +@image_comparison(baseline_images=['bxp_baseline'], + extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_baseline(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats) + +@image_comparison(baseline_images=['bxp_rangewhis'], + extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_rangewhis(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)), + whis='range' + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats) + +@image_comparison(baseline_images=['bxp_precentilewhis'], + extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_precentilewhis(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)), + whis=[5, 95] + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats) + +@image_comparison(baseline_images=['bxp_with_xlabels'], + extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_with_xlabels(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + for stats, label in zip(logstats, list('ABCD')): + stats['label'] = label + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats) + +@image_comparison(baseline_images=['bxp_horizontal'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_horizontal(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_xscale('log') + ax.bxp(logstats, vert=False) + +@image_comparison(baseline_images=['bxp_with_ylabels'], + extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_with_ylabels(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + for stats, label in zip(logstats, list('ABCD')): + stats['label'] = label + + fig, ax = plt.subplots() + ax.set_xscale('log') + ax.bxp(logstats, vert=False) + +@image_comparison(baseline_images=['bxp_patchartist'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_patchartist(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, patch_artist=True) + +@image_comparison(baseline_images=['bxp_custompatchartist'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_custompatchartist(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + boxprops = dict(facecolor='yellow', edgecolor='green', linestyle='dotted') + ax.bxp(logstats, patch_artist=True, boxprops=boxprops) + +@image_comparison(baseline_images=['bxp_customoutlier'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_customoutlier(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + flierprops = dict(linestyle='none', marker='d', markerfacecolor='g') + ax.bxp(logstats, flierprops=flierprops) + +@image_comparison(baseline_images=['bxp_withmean_custompoint'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_showcustommean(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + meanprops = dict(linestyle='none', marker='d', markerfacecolor='green') + ax.bxp(logstats, showmeans=True, meanprops=meanprops) + +@image_comparison(baseline_images=['bxp_custombox'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_custombox(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + boxprops = dict(linestyle='--', color='b', linewidth=3) + ax.bxp(logstats, boxprops=boxprops) + +@image_comparison(baseline_images=['bxp_custommedian'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_custommedian(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + medianprops = dict(linestyle='--', color='b', linewidth=3) + ax.bxp(logstats, medianprops=medianprops) + +@image_comparison(baseline_images=['bxp_withnotch'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_shownotches(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, shownotches=True) + +@image_comparison(baseline_images=['bxp_nocaps'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_nocaps(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, showcaps=False) + +@image_comparison(baseline_images=['bxp_nobox'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_nobox(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, showbox=False) + +@image_comparison(baseline_images=['bxp_withmean_point'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_showmean(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, showmeans=True, meanline=False) + +@image_comparison(baseline_images=['bxp_withmean_line'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_showmeanasline(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, showmeans=True, meanline=True) + +@image_comparison(baseline_images=['bxp_scalarwidth'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_scalarwidth(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, widths=0.25) + +@image_comparison(baseline_images=['bxp_customwidths'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_customwidths(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, widths=[0.10, 0.25, 0.65, 0.85]) + +@image_comparison(baseline_images=['bxp_custompositions'], + remove_text=True, extensions=['png'], + savefig_kwarg={'dpi': 40}) +def test_bxp_custompositions(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.bxp(logstats, positions=[1, 5, 6, 7]) + +def test_bxp_bad_widths(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + assert_raises(ValueError, ax.bxp, logstats, widths=[1]) + +def test_bxp_bad_positions(): + np.random.seed(937) + logstats = matplotlib.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37,4)) + ) + + fig, ax = plt.subplots() + ax.set_yscale('log') + assert_raises(ValueError, ax.bxp, logstats, positions=[2,3]) + @image_comparison(baseline_images=['boxplot']) def test_boxplot(): x = np.linspace(-7, 7, 140) x = np.hstack([-25, x, 25]) - fig = plt.figure() - ax = fig.add_subplot(111) + fig, ax = plt.subplots() - # show 1 boxplot with mpl medians/conf. interfals, 1 with manual values - ax.boxplot([x, x], bootstrap=10000, usermedians=[None, 1.0], - conf_intervals=[None, (-1.0, 3.5)], notch=1) + ax.boxplot([x, x], bootstrap=10000, notch=1) ax.set_ylim((-30, 30)) +@image_comparison(baseline_images=['boxplot_autorange_whiskers']) +def test_boxplot_autorange_whiskers(): + x = np.ones(140) + x = np.hstack([0, x, 2]) + fig, ax = plt.subplots() + + ax.boxplot([x, x], bootstrap=10000, notch=1) + ax.set_ylim((-5, 5)) + @image_comparison(baseline_images=['boxplot_with_CIarray'], remove_text=True, extensions=['png'], savefig_kwarg={'dpi': 40}) @@ -1077,8 +1372,33 @@ def test_boxplot_no_weird_whisker(): ax1.yaxis.grid(False, which='minor') ax1.xaxis.grid(False) -@image_comparison(baseline_images=['errorbar_basic', - 'errorbar_mixed']) +def test_boxplot_bad_medians_1(): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + assert_raises(ValueError, ax.boxplot, x, usermedians=[1, 2]) + +def test_boxplot_bad_medians_1(): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + assert_raises(ValueError, ax.boxplot, [x, x], usermedians=[[1, 2],[1, 2]]) + +def test_boxplot_bad_ci_1(): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + assert_raises(ValueError, ax.boxplot, [x, x], + conf_intervals=[[1, 2]]) + +def test_boxplot_bad_ci_2(): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + assert_raises(ValueError, ax.boxplot, [x, x], + conf_intervals=[[1, 2], [1]]) + +@image_comparison(baseline_images=['errorbar_basic', 'errorbar_mixed']) def test_errorbar(): x = np.arange(0.1, 4, 0.5) y = np.exp(-x) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 1aba8e02eacb..994e780911b4 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -6,8 +6,9 @@ from datetime import datetime import numpy as np -from numpy.testing.utils import assert_array_equal -from nose.tools import assert_equal, raises +from numpy.testing.utils import (assert_array_equal, assert_approx_equal, + assert_array_almost_equal) +from nose.tools import assert_equal, raises, assert_true import matplotlib.cbook as cbook import matplotlib.colors as mcolors @@ -92,3 +93,154 @@ def test_allequal(): assert(cbook.allequal([])) assert(cbook.allequal(('a', 'a'))) assert(not cbook.allequal(('a', 'b'))) + + +class Test_boxplot_stats: + def setup(self): + np.random.seed(937) + self.nrows = 37 + self.ncols = 4 + self.data = np.random.lognormal(size=(self.nrows, self.ncols), + mean=1.5, sigma=1.75) + self.known_keys = sorted([ + 'mean', 'med', 'q1', 'q3', 'iqr', + 'cilo', 'cihi', 'whislo', 'whishi', + 'fliers', 'label' + ]) + self.std_results = cbook.boxplot_stats(self.data) + + self.known_nonbootstrapped_res = { + 'cihi': 6.8161283264444847, + 'cilo': -0.1489815330368689, + 'iqr': 13.492709959447094, + 'mean': 13.00447442387868, + 'med': 3.3335733967038079, + 'fliers': np.array([ + 92.55467075, 87.03819018, 42.23204914, 39.29390996 + ]), + 'q1': 1.3597529879465153, + 'q3': 14.85246294739361, + 'whishi': 27.899688243699629, + 'whislo': 0.042143774965502923, + 'label': 0 + } + + self.known_bootstrapped_ci = { + 'cihi': 8.939577523357828, + 'cilo': 1.8692703958676578, + } + + self.known_whis3_res = { + 'whishi': 42.232049135969874, + 'whislo': 0.042143774965502923, + 'fliers': np.array([92.55467075, 87.03819018]), + } + + self.known_res_with_labels = { + 'label': 'Test1' + } + + self.known_res_percentiles = { + 'whislo': 0.1933685896907924, + 'whishi': 42.232049135969874 + } + + self.known_res_range = { + 'whislo': 0.042143774965502923, + 'whishi': 92.554670752188699 + + } + + def test_form_main_list(self): + assert_true(isinstance(self.std_results, list)) + + def test_form_each_dict(self): + for res in self.std_results: + assert_true(isinstance(res, dict)) + + def test_form_dict_keys(self): + for res in self.std_results: + keys = sorted(list(res.keys())) + for key in keys: + assert_true(key in self.known_keys) + + def test_results_baseline(self): + res = self.std_results[0] + for key in list(self.known_nonbootstrapped_res.keys()): + if key != 'fliers': + assert_statement = assert_approx_equal + else: + assert_statement = assert_array_almost_equal + + assert_statement( + res[key], + self.known_nonbootstrapped_res[key] + ) + + def test_results_bootstrapped(self): + results = cbook.boxplot_stats(self.data, bootstrap=10000) + res = results[0] + for key in list(self.known_bootstrapped_ci.keys()): + assert_approx_equal( + res[key], + self.known_bootstrapped_ci[key] + ) + + def test_results_whiskers_float(self): + results = cbook.boxplot_stats(self.data, whis=3) + res = results[0] + for key in list(self.known_whis3_res.keys()): + if key != 'fliers': + assert_statement = assert_approx_equal + else: + assert_statement = assert_array_almost_equal + + assert_statement( + res[key], + self.known_whis3_res[key] + ) + + def test_results_whiskers_range(self): + results = cbook.boxplot_stats(self.data, whis='range') + res = results[0] + for key in list(self.known_res_range.keys()): + if key != 'fliers': + assert_statement = assert_approx_equal + else: + assert_statement = assert_array_almost_equal + + assert_statement( + res[key], + self.known_res_range[key] + ) + + def test_results_whiskers_percentiles(self): + results = cbook.boxplot_stats(self.data, whis=[5, 95]) + res = results[0] + for key in list(self.known_res_percentiles.keys()): + if key != 'fliers': + assert_statement = assert_approx_equal + else: + assert_statement = assert_array_almost_equal + + assert_statement( + res[key], + self.known_res_percentiles[key] + ) + + def test_results_withlabels(self): + labels = ['Test1', 2, 3, 4] + results = cbook.boxplot_stats(self.data, labels=labels) + res = results[0] + for key in list(self.known_res_with_labels.keys()): + assert_equal(res[key], self.known_res_with_labels[key]) + + @raises(ValueError) + def test_label_error(self): + labels = [1, 2] + results = cbook.boxplot_stats(self.data, labels=labels) + + @raises(ValueError) + def test_bad_dims(self): + data = np.random.normal(size=(34, 34, 34)) + results = cbook.boxplot_stats(data)