Skip to content
This repository

User-specified medians and conf. intervals in boxplots #906

Closed
wants to merge 22 commits into from

4 participants

Paul Hobson Phil Elson Michael Droettboom Benjamin Root
Paul Hobson
phobson commented May 27, 2012

First, here's a test script: https://gist.github.com/2814818

I know I've submitted at least one other PR for this, but this time I've think I've got it right (or at least much closer).

Basically, the user provides a list of medians and confidence intervals where (if using numpy arrays)
usermedians.shape = (N,)
conf_intervals.shape = (N,2)

and the data to be plotted has: data.shape = (M,N).

All of this allows the user to compute the medians and its confidence intervals outside of the boxplot function using more statistically robust methods of his or her choice.

I've used a lot of assert statements to verify that the usermedians and conf_intervals inputs are compatible with the data being plotted (x in the axes.boxplot call signature).

Hope this is useful and can be incorporated into the library.

Thanks for all of the hard work
-paul

Phil Elson
Collaborator
pelson commented May 29, 2012

I've been thinking of implementing something similar recently, but you've beat me to it ;-)

I notice your spacing is not looking so hot. What editor are you using? Does it put tabs in instead of 4 spaces? Would it be easy for you be able to correct this before we go much further in the review?

Paul Hobson
phobson commented May 29, 2012
Paul Hobson
phobson commented May 29, 2012

OK. This is looking good on my system now. Rebuilt matplotlib from scratch, ran my script, and everything worked as expected.

Paul Hobson phobson commented on the diff May 29, 2012
lib/matplotlib/axes.py
... ...
@@ -5613,33 +5678,16 @@ def boxplot(self, x, notch=0, sym='b+', vert=1, whis=1.5,
5613 5678
                 med_x = [box_x_min, box_x_max]
5614 5679
             # calculate 'notch' plot
5615 5680
             else:
5616  
-                if bootstrap is not None:
5617  
-                    # Do a bootstrap estimate of notch locations.
5618  
-                    def bootstrapMedian(data, N=5000):
5619  
-                        # determine 95% confidence intervals of the median
5620  
-                        M = len(data)
5621  
-                        percentile = [2.5,97.5]
5622  
-                        estimate = np.zeros(N)
5623  
-                        for n in range(N):
5624  
-                            bsIndex = np.random.random_integers(0,M-1,M)
5625  
-                            bsData = data[bsIndex]
5626  
-                            estimate[n] = mlab.prctile(bsData, 50)
5627  
-                        CI = mlab.prctile(estimate, percentile)
1
Paul Hobson
phobson added a note May 29, 2012

Just occurred to me that we should use numpy.percentile now (assuming it's available in minimum version of numy that MPL supports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/axes.py
((6 lines not shown))
5454 5453
         """
5455  
-        Call signature::
  5454
+        call signature::
1
Phil Elson Collaborator
pelson added a note May 30, 2012

Should be capitalised and the signature below it indented (based on looking at scatter & errorbar).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/axes.py
((40 lines not shown))
5502  
-            the boxes. The ticks and limits are automatically set to match
5503  
-            the positions.
5504  
-
5505  
-          *widths* : [ scalar | array ]
5506  
-            Either a scalar or a vector to set the width of each box.
5507  
-            The default is 0.5, or ``0.15*(distance between extreme
5508  
-            positions)`` if that is smaller.
5509  
-
5510  
-          *patch_artist* : boolean
5511  
-            If *False* (default), produce boxes with the
5512  
-            :class:`~matplotlib.lines.Line2D` artist.
5513  
-            If *True*, produce boxes with the
5514  
-            :class:`~matplotlib.patches.Patch` artist.
  5466
+        *x* is an array or a sequence of vectors.
  5467
+
  5468
+        - *notch* = 0 (default) produces a rectangular box plot.
2
Phil Elson Collaborator
pelson added a note May 30, 2012

I'm not sure about this formatting. Unfortunately I can't find a de-facto matplotlib docstring. Do any of the devs have any resources to look at regarding the target format for mpl docstrings?

Michael Droettboom Owner
mdboom added a note June 01, 2012

The matplotlib docstring format predates the numpydoc format, so there isn't anything quite so consistent about it. (It would be a great but very large TODO list item to update all of the docstring formatting).

In general, it follows the formatting of Python stdlib docstrings (which are fairly loose as well) -- the important thing being that argument names are italicized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Michael Droettboom
Owner
mdboom commented June 01, 2012

This looks good. Can you add an example and a unit test?

examples/pylab_examples/boxplot_demo3.py
((48 lines not shown))
19 43
 text_transform= mtransforms.blended_transform_factory(ax.transData,
20 44
                                                      ax.transAxes)
21 45
 ax.set_xlabel('treatment')
22 46
 ax.set_ylabel('response')
23  
-ax.set_ylim(-0.2, 1.4)
  47
+#ax.set_ylim(-0.2, 1.4)
1
Phil Elson Collaborator
pelson added a note June 15, 2012

This comment serves no purpose other than to make it harder to read. Would you mind just nuking it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/axes.py
((19 lines not shown))
5477 5477
             Enter an empty string ('') if you don't want to show fliers.
5478 5478
 
5479  
-          *vert* : [1 (default) | 0]
5480  
-            If 1, make the boxes vertical.
5481  
-            If 0, make horizontal boxes. (Odd, but kept for compatibility
5482  
-            with MATLAB boxplots)
  5479
+          *vert* : [ 0 (default) | 1]
2
Phil Elson Collaborator
pelson added a note June 15, 2012

Perhaps you could change this to True & False (and double check it all still works as expected)?

Paul Hobson
phobson added a note July 15, 2012

took care of this. Both 1's/0's an Trues/Falses still work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Phil Elson pelson commented on the diff June 15, 2012
lib/matplotlib/axes.py
((100 lines not shown))
5515 5526
 
5516 5527
         Returns a dictionary mapping each component of the boxplot
5517  
-        to a list of the :class:`~matplotlib.lines.Line2D`
5518  
-        instances created (unless *patch_artist* was *True*. See above.).
1
Phil Elson Collaborator
pelson added a note June 15, 2012

I agree that the line (unless *patch_artist* was *True*. See above.) was incorrect and that it should be taken out.

Would you mind adding some information about the keys that exist in the returning dictionary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/axes.py
((9 lines not shown))
5621  
-                        percentile = [2.5,97.5]
5622  
-                        estimate = np.zeros(N)
5623  
-                        for n in range(N):
5624  
-                            bsIndex = np.random.random_integers(0,M-1,M)
5625  
-                            bsData = data[bsIndex]
5626  
-                            estimate[n] = mlab.prctile(bsData, 50)
5627  
-                        CI = mlab.prctile(estimate, percentile)
5628  
-                        return CI
5629  
-
5630  
-                    # get conf. intervals around median
5631  
-                    CI = bootstrapMedian(d, N=bootstrap)
5632  
-                    notch_max = CI[1]
5633  
-                    notch_min = CI[0]
  5691
+                # conf. intervals from user, if available
  5692
+                if conf_intervals is not None:
  5693
+                    if conf_intervals[i] is not None:
1
Phil Elson Collaborator
pelson added a note June 15, 2012

Consider combining this into a single if statement:

if conf_intervals is not None and conf_intervals[i] is not None:
    ...
else:
    ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Phil Elson
Collaborator
pelson commented June 15, 2012

Would you mind adding a simple unit test?

Other than that, this change gets my thumbs up. Great work!

Paul Hobson
Phil Elson
Collaborator
pelson commented June 15, 2012

@phobson: No problems. Have a great break!

Paul Hobson

@pelson

I addressed your comments. Sorry for the delay. In summary:
1) switch the vert/notch kwargs over to true/false (though 1/0 still work as in the demos) and updated the docstring to reflect this
2) combined my conf_intervals if-logic into a single statement and removed the lines that became extraneous as a result
3) added a unit test + baseline images
4) modified a demo to show this functionality
5) enhanced the docs about the dictionary that is returned

Benjamin Root
Collaborator

@pelson, has the OP addressed your concerns here?

lib/matplotlib/axes.py
... ...
@@ -5546,6 +5597,32 @@ def boxplot(self, x, notch=0, sym='b+', vert=1, whis=1.5,
5546 5597
             x = [x]
5547 5598
         col = len(x)
5548 5599
 
  5600
+        # sanitize user-input medians
  5601
+        msg1 = "usermedians must either be a list/tuple or a 1d array"
  5602
+        msg2 = "usermedians' length must be compatible with x"
  5603
+        if usermedians is not None:
  5604
+            if hasattr(usermedians, 'shape'):
  5605
+                assert len(usermedians.shape) == 1, msg1
1
Benjamin Root Collaborator
WeatherGod added a note July 21, 2012

Don't do asserts for checking user inputs. Raise a ValueError instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Benjamin Root
Collaborator

Neat work. We are close to getting this merged in. One more very important thing to add is a note in doc/users/whats_new.rst. Also, you need to run boilerplate.py in the matplotlib's source directory in order to regenerate the pyplot file.

lib/matplotlib/axes.py
((20 lines not shown))
5623  
-                        for n in range(N):
5624  
-                            bsIndex = np.random.random_integers(0,M-1,M)
5625  
-                            bsData = data[bsIndex]
5626  
-                            estimate[n] = mlab.prctile(bsData, 50)
5627  
-                        CI = mlab.prctile(estimate, percentile)
5628  
-                        return CI
5629  
-
5630  
-                    # get conf. intervals around median
5631  
-                    CI = bootstrapMedian(d, N=bootstrap)
5632  
-                    notch_max = CI[1]
5633  
-                    notch_min = CI[0]
  5692
+            if notch:
  5693
+                # conf. intervals from user, if available
  5694
+                if conf_intervals is not None and \
  5695
+                   conf_intervals[i] is not None:
  5696
+		    notch_max = np.max(conf_intervals[i])
2
Phil Elson Collaborator
pelson added a note July 22, 2012

This indentation looks a little funny. Have tabs been used here?

Paul Hobson
phobson added a note July 22, 2012

I think it looked a little funny since the conditional was wrapped around the second line. python parsed it fine, but i've lined everything up to look nicer. will see with my next commit/push

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Phil Elson
Collaborator
pelson commented July 22, 2012

@pelson, has the OP addressed your concerns here?

Yes. I am finding it hard to assert that the logic/statistics are identical (thanks to the improvement of layout). But what I can see looks good. Gets my +1.

Benjamin Root
Collaborator

@pelson, good to know. Tasks that remain: get pyplot.py regenerated, odd indentation fixed (or explained), and the new entry to the whats_new.rst,

Paul Hobson

@WeatherGod Sorry to require so much hand-holding, here. I ran boilerplate.py like this:
paul@flint ~/sources/matplotlib $ python boilerplate.py
[prints lots of stuff to terminal, then...]

paul@flint ~/sources/matplotlib $ git status

On branch manual-boxplots-2

nothing to commit (working directory clean)
paul@flint ~/sources/matplotlib $

There don't seem to be any calls to sys.argv, so i'm not sure how to get this to behave properly. Any advice would be much appreciated. Thanks.

Phil Elson
Collaborator
pelson commented July 22, 2012

It may be that your branch is based on a version before the bolierplate script was updated. My advice at this stage would be to rebase your branch (the rebase is needed anyway), and then run the boilerplate.py script. The workflow goes something like:

git checkout manual-boxplots-2
git fetch upstream
git rebase upstream/master
# Fix conflicts, if any
git push -f origin manual-boxplots-2
added some commits May 27, 2012
Paul Hobson users can specify the median and it's confidence interval when creati…
…ng boxplots
c96118f
Paul Hobson added assert messages to help user 6491ce0
Paul Hobson fixed embarrassing tabs vs 4spaces 4328635
Paul Hobson fixed bad indent on 1 line in axes.boxplot 1767e28
Paul Hobson formatted (indented) the docstring os axes.boxplot a0d30f5
Paul Hobson weird text issue - accidentally copied text from other buffer 146a6e2
Paul Hobson tried to standardized arg/kwarg doc format in axes.boxplot 9efe05a
Paul Hobson added in "function arguments" header ae69ae1
Paul Hobson modified an example to include the new functionality 45711e5
Paul Hobson minor tweaks to clean up my boxplot example and the logic handling th…
…e confidence intervals
ba8890a
Paul Hobson Got rid of a lot cruft in the example. the transform line wasn't doin…
…g anything and there's no point in adjusting the subplot spacing.
88d50ce
Paul Hobson no more subplot adjusting 1c79eb8
Paul Hobson added test for new boxplot functionality 5464e34
Paul Hobson fixed my data construction with np.hstack ffc8345
Paul Hobson np.hstack/np.linspace errors d3b3d5c
Paul Hobson added baseline images and unit test for new boxplot functionality e6403b2
Paul Hobson switched notch and vert kwargs over to True/False instead of 0 or 1 c052d07
Paul Hobson switched from assert statements to raiseing value errors per ben root…
…'s request
3db645a
Paul Hobson cleaned up ambiguous indentation around a multiline if statement e157dd1
Paul Hobson added entry to what's new 3172ef2
Paul Hobson reran boilerplate.py to update pyplot 0b65488
Paul Hobson

@pelson thanks for the git-fu! worked like a charm.

lib/matplotlib/tests/test_axes.py
@@ -670,6 +670,7 @@ def test_hist_log():
670 670
     ax.set_xticks([])
671 671
     ax.set_yticks([])
672 672
 
  673
+<<<<<<< HEAD
2
Benjamin Root Collaborator
WeatherGod added a note July 23, 2012

Looks like you missed something while resolving conflicts. This means you didn't run the tests after rebasing. Please run the tests to make sure everything looks good. This may also impact the resulting test images because changes may have occurred to the rendering algorithms (line snapping, text anti-aliasing, etc.).

Paul Hobson
phobson added a note July 24, 2012

@WeatherGod: Just cleared that up. Sorry for the slow response. All tests just passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Phil Elson
Collaborator

I've just merged this by hand (447a7cc), squashing the commits and resolving the remaining conflicts.

@phobson: Thanks for doing this work! If you have any more enhancements, I would be more than happy to review them!

Phil Elson pelson closed this August 11, 2012
Paul Hobson

@pelson Thanks for squashing and merging! Really glad I could contribute back to matplotlib! I also really appreciate the feedback and guidance through the whole process from you and everyone else.

Paul Hobson phobson deleted the branch January 27, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 22 unique commits by 1 author.

Jul 22, 2012
Paul Hobson users can specify the median and it's confidence interval when creati…
…ng boxplots
c96118f
Paul Hobson added assert messages to help user 6491ce0
Paul Hobson fixed embarrassing tabs vs 4spaces 4328635
Paul Hobson fixed bad indent on 1 line in axes.boxplot 1767e28
Paul Hobson formatted (indented) the docstring os axes.boxplot a0d30f5
Paul Hobson weird text issue - accidentally copied text from other buffer 146a6e2
Paul Hobson tried to standardized arg/kwarg doc format in axes.boxplot 9efe05a
Paul Hobson added in "function arguments" header ae69ae1
Paul Hobson modified an example to include the new functionality 45711e5
Paul Hobson minor tweaks to clean up my boxplot example and the logic handling th…
…e confidence intervals
ba8890a
Paul Hobson Got rid of a lot cruft in the example. the transform line wasn't doin…
…g anything and there's no point in adjusting the subplot spacing.
88d50ce
Paul Hobson no more subplot adjusting 1c79eb8
Paul Hobson added test for new boxplot functionality 5464e34
Paul Hobson fixed my data construction with np.hstack ffc8345
Paul Hobson np.hstack/np.linspace errors d3b3d5c
Paul Hobson added baseline images and unit test for new boxplot functionality e6403b2
Paul Hobson switched notch and vert kwargs over to True/False instead of 0 or 1 c052d07
Paul Hobson switched from assert statements to raiseing value errors per ben root…
…'s request
3db645a
Paul Hobson cleaned up ambiguous indentation around a multiline if statement e157dd1
Paul Hobson added entry to what's new 3172ef2
Paul Hobson reran boilerplate.py to update pyplot 0b65488
Jul 24, 2012
Paul Hobson fixed remaining conflict in test_axes.py a2085a6
This page is out of date. Refresh to see the latest.
10  doc/users/whats_new.rst
Source Rendered
@@ -57,6 +57,16 @@ minimum and maximum colorbar extensions.
57 57
 
58 58
     plt.show()
59 59
 
  60
+New Boxplot Functionality
  61
+-------------------------
  62
+
  63
+Users can now incorporate their own methods for computing the median and its
  64
+confidence intervals into the boxplot method. For every column of data passed
  65
+to boxplot, the user can specify an accompanying median and confidence
  66
+interval.
  67
+:meth: `matplotlib.axes.boxplot`
  68
+.. plot:: examples/pylab_examples/boxplot_demo3.py
  69
+
60 70
 .. _whats-new-1-1:
61 71
 
62 72
 new in matplotlib-1.1
42  examples/pylab_examples/boxplot_demo3.py
@@ -2,26 +2,48 @@
2 2
 import matplotlib.transforms as mtransforms
3 3
 import numpy as np
4 4
 
  5
+def fakeBootStrapper(n):
  6
+    '''
  7
+    This is just a placeholder for the user's method of
  8
+    bootstrapping the median and its confidence intervals.
  9
+
  10
+    Returns an arbitrary median and confidence intervals
  11
+    packed into a tuple
  12
+    '''
  13
+    if n == 1:
  14
+        med = 0.1
  15
+        CI = (-0.25, 0.25)
  16
+    else:
  17
+        med = 0.2
  18
+        CI = (-0.35, 0.50)
  19
+
  20
+    return med, CI
  21
+
  22
+
  23
+
5 24
 np.random.seed(2)
6 25
 inc = 0.1
7  
-e1 = np.random.uniform(0,1, size=(500,))
8  
-e2 = np.random.uniform(0,1, size=(500,))
9  
-e3 = np.random.uniform(0,1 + inc, size=(500,))
10  
-e4 = np.random.uniform(0,1 + 2*inc, size=(500,))
  26
+e1 = np.random.normal(0, 1, size=(500,))
  27
+e2 = np.random.normal(0, 1, size=(500,))
  28
+e3 = np.random.normal(0, 1 + inc, size=(500,))
  29
+e4 = np.random.normal(0, 1 + 2*inc, size=(500,))
11 30
 
12 31
 treatments = [e1,e2,e3,e4]
  32
+med1, CI1 = fakeBootStrapper(1)
  33
+med2, CI2 = fakeBootStrapper(2)
  34
+medians = [None, None, med1, med2]
  35
+conf_intervals = [None, None, CI1, CI2]
13 36
 
14 37
 fig = plt.figure()
15 38
 ax = fig.add_subplot(111)
16 39
 pos = np.array(range(len(treatments)))+1
17  
-bp = ax.boxplot( treatments, sym='k+', patch_artist=True,
18  
-                 positions=pos, notch=1, bootstrap=5000 )
19  
-text_transform= mtransforms.blended_transform_factory(ax.transData,
20  
-                                                     ax.transAxes)
  40
+bp = ax.boxplot(treatments, sym='k+', positions=pos,
  41
+                notch=1, bootstrap=5000,
  42
+                usermedians=medians,
  43
+                conf_intervals=conf_intervals)
  44
+
21 45
 ax.set_xlabel('treatment')
22 46
 ax.set_ylabel('response')
23  
-ax.set_ylim(-0.2, 1.4)
24 47
 plt.setp(bp['whiskers'], color='k',  linestyle='-' )
25 48
 plt.setp(bp['fliers'], markersize=3.0)
26  
-fig.subplots_adjust(right=0.99,top=0.99)
27 49
 plt.show()
232  lib/matplotlib/axes.py
@@ -35,7 +35,6 @@
35 35
 import matplotlib.ticker as mticker
36 36
 import matplotlib.transforms as mtransforms
37 37
 import matplotlib.tri as mtri
38  
-
39 38
 from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
40 39
 
41 40
 iterable = cbook.iterable
@@ -5469,14 +5468,15 @@ def xywhere(xs, ys, mask):
5469 5468
 
5470 5469
         return errorbar_container # (l0, caplines, barcols)
5471 5470
 
5472  
-    def boxplot(self, x, notch=0, sym='b+', vert=1, whis=1.5,
  5471
+    def boxplot(self, x, notch=False, sym='b+', vert=True, whis=1.5,
5473 5472
                 positions=None, widths=None, patch_artist=False,
5474  
-                bootstrap=None):
  5473
+                bootstrap=None, usermedians=None, conf_intervals=None):
5475 5474
         """
5476 5475
         Call signature::
5477 5476
 
5478  
-          boxplot(x, notch=0, sym='+', vert=1, whis=1.5,
5479  
-                  positions=None, widths=None, patch_artist=False)
  5477
+          boxplot(x, notch=False, sym='+', vert=True, whis=1.5,
  5478
+                  positions=None, widths=None, patch_artist=False,
  5479
+                  bootstrap=None, usermedians=None, conf_intervals=None)
5480 5480
 
5481 5481
         Make a box and whisker plot for each column of *x* or each
5482 5482
         vector in sequence *x*.  The box extends from the lower to
@@ -5489,59 +5489,110 @@ def boxplot(self, x, notch=0, sym='b+', vert=1, whis=1.5,
5489 5489
           *x* :
5490 5490
             Array or a sequence of vectors.
5491 5491
 
5492  
-          *notch* : [ 0 (default) | 1]
5493  
-            If 0, produce a rectangular box plot.
5494  
-            If 1, produce a notched box plot
  5492
+          *notch* : [ False (default) | True ]
  5493
+            If False (default), produces a rectangular box plot.
  5494
+            If True, will produce a notched box plot
5495 5495
 
5496  
-          *sym* :
5497  
-            (default 'b+') is the default symbol for flier points.
  5496
+          *sym* : [ default 'b+' ]
  5497
+            The default symbol for flier points.
5498 5498
             Enter an empty string ('') if you don't want to show fliers.
5499 5499
 
5500  
-          *vert* : [1 (default) | 0]
5501  
-            If 1, make the boxes vertical.
5502  
-            If 0, make horizontal boxes. (Odd, but kept for compatibility
5503  
-            with MATLAB boxplots)
  5500
+          *vert* : [ False | True (default) ]
  5501
+            If True (default), makes the boxes vertical.
  5502
+            If False, makes horizontal boxes.
5504 5503
 
5505  
-          *whis* : (default 1.5)
5506  
-            Defines the length of the whiskers as
5507  
-            a function of the inner quartile range.  They extend to the
5508  
-            most extreme data point within ( ``whis*(75%-25%)`` ) data range.
  5504
+          *whis* : [ default 1.5 ]
  5505
+            Defines the length of the whiskers as a function of the inner
  5506
+            quartile range.  They extend to the most extreme data point
  5507
+            within ( ``whis*(75%-25%)`` ) data range.
5509 5508
 
5510 5509
           *bootstrap* : [ *None* (default) | integer ]
5511 5510
             Specifies whether to bootstrap the confidence intervals
5512  
-            around the median for notched boxplots. If *None*, no
5513  
-            bootstrapping is performed, and notches are calculated
5514  
-            using a Gaussian-based asymptotic approximation
5515  
-            (see McGill, R., Tukey, J.W., and Larsen, W.A.,
5516  
-            1978, and Kendall and Stuart, 1967). Otherwise, bootstrap
5517  
-            specifies the number of times to bootstrap the median to
5518  
-            determine its 95% confidence intervals. Values between 1000
5519  
-            and 10000 are recommended.
5520  
-
5521  
-          *positions* : (default 1,2,...,n)
5522  
-            Sets the horizontal positions of
5523  
-            the boxes. The ticks and limits are automatically set to match
5524  
-            the positions.
5525  
-
5526  
-          *widths* : [ scalar | array ]
5527  
-            Either a scalar or a vector to set the width of each box.
5528  
-            The default is 0.5, or ``0.15*(distance between extreme
5529  
-            positions)`` if that is smaller.
5530  
-
5531  
-          *patch_artist* : boolean
5532  
-            If *False* (default), produce boxes with the
5533  
-            :class:`~matplotlib.lines.Line2D` artist.
5534  
-            If *True*, produce boxes with the
5535  
-            :class:`~matplotlib.patches.Patch` artist.
  5511
+            around the median for notched boxplots. If bootstrap==None,
  5512
+            no bootstrapping is performed, and notches are calculated
  5513
+            using a Gaussian-based asymptotic approximation  (see McGill, R.,
  5514
+            Tukey, J.W., and Larsen, W.A., 1978, and Kendall and Stuart,
  5515
+            1967). Otherwise, bootstrap specifies the number of times to
  5516
+            bootstrap the median to determine it's 95% confidence intervals.
  5517
+            Values between 1000 and 10000 are recommended.
  5518
+
  5519
+          *usermedians* : [ default None ]
  5520
+            An array or sequence whose first dimension (or length) is
  5521
+            compatible with *x*. This overrides the medians computed by
  5522
+            matplotlib for each element of *usermedians* that is not None.
  5523
+            When an element of *usermedians* == None, the median will be
  5524
+            computed directly as normal.
  5525
+
  5526
+          *conf_intervals* : [ default None ]
  5527
+            Array or sequence whose first dimension (or length) is compatible
  5528
+            with *x* and whose second dimension is 2. When the current element
  5529
+            of *conf_intervals* is not None, the notch locations computed by
  5530
+            matplotlib are overridden (assuming notch is True). When an element of
  5531
+            *conf_intervals* is None, boxplot compute notches the method
  5532
+            specified by the other kwargs (e.g. *bootstrap*).
  5533
+
  5534
+          *positions* : [ default 1,2,...,n ]
  5535
+            Sets the horizontal positions of the boxes. The ticks and limits
  5536
+            are automatically set to match the positions.
  5537
+
  5538
+          *widths* : [ default 0.5 ]
  5539
+            Either a scalar or a vector and sets the width of each box. The
  5540
+            default is 0.5, or ``0.15*(distance between extreme positions)``
  5541
+            if that is smaller.
  5542
+
  5543
+          *patch_artist* : [ False (default) | True ]
  5544
+            If False produces boxes with the Line2D artist
  5545
+            If True produces boxes with the Patch artist
5536 5546
 
5537 5547
         Returns a dictionary mapping each component of the boxplot
5538  
-        to a list of the :class:`~matplotlib.lines.Line2D`
5539  
-        instances created (unless *patch_artist* was *True*. See above.).
  5548
+        to a list of the :class:`matplotlib.lines.Line2D`
  5549
+        instances created. That disctionary has the following keys
  5550
+        (assuming vertical boxplots):
  5551
+            boxes: the main body of the boxplot showing the quartiles
  5552
+                and the median's confidence intervals if enabled.
  5553
+            medians: horizonal lines at the median of each box.
  5554
+            whiskers: the vertical lines extending to the most extreme,
  5555
+                non-outlier data points.
  5556
+            caps: the horizontal lines at the ends of the whiskers.
  5557
+            fliers: points representing data that extend beyone the
  5558
+                whiskers (outliers).
  5559
+
5540 5560
 
5541 5561
         **Example:**
5542 5562
 
5543 5563
         .. plot:: pyplots/boxplot_demo.py
5544 5564
         """
  5565
+        def bootstrapMedian(data, N=5000):
  5566
+            # determine 95% confidence intervals of the median
  5567
+            M = len(data)
  5568
+            percentile = [2.5,97.5]
  5569
+            estimate = np.zeros(N)
  5570
+            for n in range(N):
  5571
+                bsIndex = np.random.random_integers(0,M-1,M)
  5572
+                bsData = data[bsIndex]
  5573
+                estimate[n] = mlab.prctile(bsData, 50)
  5574
+            CI = mlab.prctile(estimate, percentile)
  5575
+            return CI
  5576
+
  5577
+        def computeConfInterval(data, med, iq, bootstrap):
  5578
+            if bootstrap is not None:
  5579
+                # Do a bootstrap estimate of notch locations.
  5580
+                # get conf. intervals around median
  5581
+                CI = bootstrapMedian(data, N=bootstrap)
  5582
+                notch_min = CI[0]
  5583
+                notch_max = CI[1]
  5584
+            else:
  5585
+                # Estimate notch locations using Gaussian-based
  5586
+                # asymptotic approximation.
  5587
+                #
  5588
+                # For discussion: McGill, R., Tukey, J.W.,
  5589
+                # and Larsen, W.A. (1978) "Variations of
  5590
+                # Boxplots", The American Statistician, 32:12-16.
  5591
+                N = len(data)
  5592
+                notch_min = med - 1.57*iq/np.sqrt(N)
  5593
+                notch_max = med + 1.57*iq/np.sqrt(N)
  5594
+            return notch_min, notch_max
  5595
+
5545 5596
         if not self._hold: self.cla()
5546 5597
         holdStatus = self._hold
5547 5598
         whiskers, caps, boxes, medians, fliers = [], [], [], [], []
@@ -5567,6 +5618,38 @@ def boxplot(self, x, notch=0, sym='b+', vert=1, whis=1.5,
5567 5618
             x = [x]
5568 5619
         col = len(x)
5569 5620
 
  5621
+        # sanitize user-input medians
  5622
+        msg1 = "usermedians must either be a list/tuple or a 1d array"
  5623
+        msg2 = "usermedians' length must be compatible with x"
  5624
+        if usermedians is not None:
  5625
+            if hasattr(usermedians, 'shape'):
  5626
+                if len(usermedians.shape) != 1:
  5627
+                    raise ValueError(msg1)
  5628
+                elif usermedians.shape[0] != col:
  5629
+                    raise ValueError(msg2)
  5630
+            elif len(usermedians) != col:
  5631
+                raise ValueError(msg2)
  5632
+
  5633
+        #sanitize user-input confidence intervals
  5634
+        msg1 = "conf_intervals must either be a list of tuples or a 2d array"
  5635
+        msg2 = "conf_intervals' length must be compatible with x"
  5636
+        msg3 = "each conf_interval, if specificied, must have two values"
  5637
+        if conf_intervals is not None:
  5638
+            if hasattr(conf_intervals, 'shape'):
  5639
+                if len(conf_intervals.shape) != 2:
  5640
+                    raise ValueError(msg1)
  5641
+                elif conf_intervals.shape[0] != col:
  5642
+                    raise ValueError(msg2)
  5643
+                elif conf_intervals.shape[1] == 2:
  5644
+                    raise ValueError(msg3)
  5645
+            else:
  5646
+                if len(conf_intervals) != col:
  5647
+                    raise ValueError(msg2)
  5648
+                for ci in conf_intervals:
  5649
+                    if ci is not None and len(ci) != 2:
  5650
+                        raise ValueError(msg3)
  5651
+
  5652
+
5570 5653
         # get some plot info
5571 5654
         if positions is None:
5572 5655
             positions = range(1, col + 1)
@@ -5578,14 +5661,21 @@ def boxplot(self, x, notch=0, sym='b+', vert=1, whis=1.5,
5578 5661
 
5579 5662
         # loop through columns, adding each to plot
5580 5663
         self.hold(True)
5581  
-        for i,pos in enumerate(positions):
  5664
+        for i, pos in enumerate(positions):
5582 5665
             d = np.ravel(x[i])
5583 5666
             row = len(d)
5584 5667
             if row==0:
5585 5668
                 # no data, skip this position
5586 5669
                 continue
  5670
+
5587 5671
             # get median and quartiles
5588 5672
             q1, med, q3 = mlab.prctile(d,[25,50,75])
  5673
+
  5674
+            # replace with input medians if available
  5675
+            if usermedians is not None:
  5676
+                if usermedians[i] is not None:
  5677
+                    med = usermedians[i]
  5678
+
5589 5679
             # get high extreme
5590 5680
             iq = q3 - q1
5591 5681
             hi_val = q3 + whis*iq
@@ -5625,42 +5715,16 @@ def boxplot(self, x, notch=0, sym='b+', vert=1, whis=1.5,
5625 5715
             # get y location for median
5626 5716
             med_y = [med, med]
5627 5717
 
5628  
-            # calculate 'regular' plot
5629  
-            if notch == 0:
5630  
-                # make our box vectors
5631  
-                box_x = [box_x_min, box_x_max, box_x_max, box_x_min, box_x_min ]
5632  
-                box_y = [q1, q1, q3, q3, q1 ]
5633  
-                # make our median line vectors
5634  
-                med_x = [box_x_min, box_x_max]
5635 5718
             # calculate 'notch' plot
5636  
-            else:
5637  
-                if bootstrap is not None:
5638  
-                    # Do a bootstrap estimate of notch locations.
5639  
-                    def bootstrapMedian(data, N=5000):
5640  
-                        # determine 95% confidence intervals of the median
5641  
-                        M = len(data)
5642  
-                        percentile = [2.5,97.5]
5643  
-                        estimate = np.zeros(N)
5644  
-                        for n in range(N):
5645  
-                            bsIndex = np.random.random_integers(0,M-1,M)
5646  
-                            bsData = data[bsIndex]
5647  
-                            estimate[n] = mlab.prctile(bsData, 50)
5648  
-                        CI = mlab.prctile(estimate, percentile)
5649  
-                        return CI
5650  
-
5651  
-                    # get conf. intervals around median
5652  
-                    CI = bootstrapMedian(d, N=bootstrap)
5653  
-                    notch_max = CI[1]
5654  
-                    notch_min = CI[0]
  5719
+            if notch:
  5720
+                # conf. intervals from user, if available
  5721
+                if conf_intervals is not None and conf_intervals[i] is not None:
  5722
+                    notch_max = np.max(conf_intervals[i])
  5723
+                    notch_min = np.min(conf_intervals[i])
5655 5724
                 else:
5656  
-                    # Estimate notch locations using Gaussian-based
5657  
-                    # asymptotic approximation.
5658  
-                    #
5659  
-                    # For discussion: McGill, R., Tukey, J.W.,
5660  
-                    # and Larsen, W.A. (1978) "Variations of
5661  
-                    # Boxplots", The American Statistician, 32:12-16.
5662  
-                    notch_max = med + 1.57*iq/np.sqrt(row)
5663  
-                    notch_min = med - 1.57*iq/np.sqrt(row)
  5725
+                    notch_min, notch_max = computeConfInterval(d, med, iq,
  5726
+                                                               bootstrap)
  5727
+
5664 5728
                 # make our notched box vectors
5665 5729
                 box_x = [box_x_min, box_x_max, box_x_max, cap_x_max, box_x_max,
5666 5730
                          box_x_max, box_x_min, box_x_min, cap_x_min, box_x_min,
@@ -5670,6 +5734,13 @@ def bootstrapMedian(data, N=5000):
5670 5734
                 # make our median line vectors
5671 5735
                 med_x = [cap_x_min, cap_x_max]
5672 5736
                 med_y = [med, med]
  5737
+            # calculate 'regular' plot
  5738
+            else:
  5739
+                # make our box vectors
  5740
+                box_x = [box_x_min, box_x_max, box_x_max, box_x_min, box_x_min ]
  5741
+                box_y = [q1, q1, q3, q3, q1 ]
  5742
+                # make our median line vectors
  5743
+                med_x = [box_x_min, box_x_max]
5673 5744
 
5674 5745
             def to_vc(xs,ys):
5675 5746
                 # convert arguments to verts and codes
@@ -5719,12 +5790,13 @@ def dopatch(xs,ys):
5719 5790
                 boxes.extend(dopatch(box_x, box_y))
5720 5791
             else:
5721 5792
                 boxes.extend(doplot(box_x, box_y, 'b-'))
  5793
+
5722 5794
             medians.extend(doplot(med_x, med_y, median_color+'-'))
5723 5795
             fliers.extend(doplot(flier_hi_x, flier_hi, sym,
5724 5796
                                  flier_lo_x, flier_lo, sym))
5725 5797
 
5726 5798
         # fix our axes/ticks up a little
5727  
-        if 1 == vert:
  5799
+        if vert:
5728 5800
             setticks, setlim = self.set_xticks, self.set_xlim
5729 5801
         else:
5730 5802
             setticks, setlim = self.set_yticks, self.set_ylim
8  lib/matplotlib/pyplot.py
@@ -2411,8 +2411,9 @@ def broken_barh(xranges, yrange, hold=None, **kwargs):
2411 2411
 # This function was autogenerated by boilerplate.py.  Do not edit as
2412 2412
 # changes will be lost
2413 2413
 @autogen_docstring(Axes.boxplot)
2414  
-def boxplot(x, notch=0, sym='b+', vert=1, whis=1.5, positions=None, widths=None,
2415  
-            patch_artist=False, bootstrap=None, hold=None):
  2414
+def boxplot(x, notch=False, sym='b+', vert=True, whis=1.5, positions=None,
  2415
+            widths=None, patch_artist=False, bootstrap=None, usermedians=None,
  2416
+            conf_intervals=None, hold=None):
2416 2417
     ax = gca()
2417 2418
     # allow callers to override the hold state by passing hold=True|False
2418 2419
     washold = ax.ishold()
@@ -2422,7 +2423,8 @@ def boxplot(x, notch=0, sym='b+', vert=1, whis=1.5, positions=None, widths=None,
2422 2423
     try:
2423 2424
         ret = ax.boxplot(x, notch=notch, sym=sym, vert=vert, whis=whis,
2424 2425
                          positions=positions, widths=widths,
2425  
-                         patch_artist=patch_artist, bootstrap=bootstrap)
  2426
+                         patch_artist=patch_artist, bootstrap=bootstrap,
  2427
+                         usermedians=usermedians, conf_intervals=conf_intervals)
2426 2428
         draw_if_interactive()
2427 2429
     finally:
2428 2430
         ax.hold(washold)
BIN  lib/matplotlib/tests/baseline_images/test_axes/boxplot.pdf
Binary file not shown
BIN  lib/matplotlib/tests/baseline_images/test_axes/boxplot.png
458  lib/matplotlib/tests/baseline_images/test_axes/boxplot.svg
... ...
@@ -0,0 +1,458 @@
  1
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
  2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  3
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
  4
+<!-- Created with matplotlib (http://matplotlib.sourceforge.net/) -->
  5
+<svg height="432pt" version="1.1" viewBox="0 0 576 432" width="576pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  6
+ <defs>
  7
+  <style type="text/css">
  8
+*{stroke-linecap:square;stroke-linejoin:round;}
  9
+  </style>
  10
+ </defs>
  11
+ <g id="figure_1">
  12
+  <g id="patch_1">
  13
+   <path d="
  14
+M0 432
  15
+L576 432
  16
+L576 0
  17
+L0 0
  18
+z
  19
+" style="fill:#ffffff;"/>
  20
+  </g>
  21
+  <g id="axes_1">
  22
+   <g id="patch_2">
  23
+    <path d="
  24
+M72 388.8
  25
+L518.4 388.8
  26
+L518.4 43.2
  27
+L72 43.2
  28
+z
  29
+" style="fill:#ffffff;"/>
  30
+   </g>
  31
+   <g id="line2d_1">
  32
+    <path clip-path="url(#p7ff5b81e1d)" d="
  33
+M183.6 236.45
  34
+L183.6 256.32" style="fill:none;stroke:#0000ff;stroke-dasharray:6.000000,6.000000;stroke-dashoffset:0.0;stroke-linecap:butt;"/>
  35
+   </g>
  36
+   <g id="line2d_2">
  37
+    <path clip-path="url(#p7ff5b81e1d)" d="
  38
+M183.6 195.55
  39
+L183.6 175.68" style="fill:none;stroke:#0000ff;stroke-dasharray:6.000000,6.000000;stroke-dashoffset:0.0;stroke-linecap:butt;"/>
  40
+   </g>
  41
+   <g id="line2d_3">
  42
+    <path clip-path="url(#p7ff5b81e1d)" d="
  43
+M175.23 175.68
  44
+L191.97 175.68" style="fill:none;stroke:#000000;"/>
  45
+   </g>
  46
+   <g id="line2d_4">
  47
+    <path clip-path="url(#p7ff5b81e1d)" d="
  48
+M175.23 256.32
  49
+L191.97 256.32" style="fill:none;stroke:#000000;"/>
  50
+   </g>
  51
+   <g id="line2d_5">
  52
+    <path clip-path="url(#p7ff5b81e1d)" d="
  53
+M166.86 236.45
  54
+L200.34 236.45
  55
+L200.34 222.962
  56
+L191.97 216
  57
+L200.34 209.038
  58
+L200.34 195.55
  59
+L166.86 195.55
  60
+L166.86 209.038
  61
+L175.23 216
  62
+L166.86 222.962
  63
+L166.86 236.45" style="fill:none;stroke:#0000ff;"/>
  64
+   </g>
  65
+   <g id="line2d_6">
  66
+    <path clip-path="url(#p7ff5b81e1d)" d="
  67
+M175.23 216
  68
+L191.97 216" style="fill:none;stroke:#ff0000;"/>
  69
+   </g>
  70
+   <g id="line2d_7">
  71
+    <defs>
  72
+     <path d="
  73
+M-3 0
  74
+L3 0
  75
+M0 3
  76
+L0 -3" id="me594928b4b" style="stroke:#0000ff;stroke-linecap:butt;stroke-width:0.5;"/>
  77
+    </defs>
  78
+    <g clip-path="url(#p7ff5b81e1d)">
  79
+     <use style="fill:#0000ff;stroke:#0000ff;stroke-linecap:butt;stroke-width:0.5;" x="183.6" xlink:href="#me594928b4b" y="72.0"/>
  80
+    </g>
  81
+   </g>
  82
+   <g id="line2d_8">
  83
+    <g clip-path="url(#p7ff5b81e1d)">
  84
+     <use style="fill:#0000ff;stroke:#0000ff;stroke-linecap:butt;stroke-width:0.5;" x="183.6" xlink:href="#me594928b4b" y="360.0"/>
  85
+    </g>
  86
+   </g>
  87
+   <g id="line2d_9">
  88
+    <path clip-path="url(#p7ff5b81e1d)" d="
  89
+M406.8 236.45
  90
+L406.8 256.32" style="fill:none;stroke:#0000ff;stroke-dasharray:6.000000,6.000000;stroke-dashoffset:0.0;stroke-linecap:butt;"/>
  91
+   </g>
  92
+   <g id="line2d_10">
  93
+    <path clip-path="url(#p7ff5b81e1d)" d="
  94
+M406.8 195.55
  95
+L406.8 175.68" style="fill:none;stroke:#0000ff;stroke-dasharray:6.000000,6.000000;stroke-dashoffset:0.0;stroke-linecap:butt;"/>
  96
+   </g>
  97
+   <g id="line2d_11">
  98
+    <path clip-path="url(#p7ff5b81e1d)" d="
  99
+M398.43 175.68
  100
+L415.17 175.68" style="fill:none;stroke:#000000;"/>
  101
+   </g>
  102
+   <g id="line2d_12">
  103
+    <path clip-path="url(#p7ff5b81e1d)" d="
  104
+M398.43 256.32
  105
+L415.17 256.32" style="fill:none;stroke:#000000;"/>
  106
+   </g>
  107
+   <g id="line2d_13">
  108
+    <path clip-path="url(#p7ff5b81e1d)" d="
  109
+M390.06 236.45
  110
+L423.54 236.45
  111
+L423.54 221.76
  112
+L415.17 210.24
  113
+L423.54 195.84
  114
+L423.54 195.55
  115
+L390.06 195.55
  116
+L390.06 195.84
  117
+L398.43 210.24
  118
+L390.06 221.76
  119
+L390.06 236.45" style="fill:none;stroke:#0000ff;"/>
  120
+   </g>
  121
+   <g id="line2d_14">
  122
+    <path clip-path="url(#p7ff5b81e1d)" d="
  123
+M398.43 210.24
  124
+L415.17 210.24" style="fill:none;stroke:#ff0000;"/>
  125
+   </g>
  126
+   <g id="line2d_15">
  127
+    <g clip-path="url(#p7ff5b81e1d)">
  128
+     <use style="fill:#0000ff;stroke:#0000ff;stroke-linecap:butt;stroke-width:0.5;" x="406.8" xlink:href="#me594928b4b" y="72.0"/>
  129
+    </g>
  130
+   </g>
  131
+   <g id="line2d_16">
  132
+    <g clip-path="url(#p7ff5b81e1d)">
  133
+     <use style="fill:#0000ff;stroke:#0000ff;stroke-linecap:butt;stroke-width:0.5;" x="406.8" xlink:href="#me594928b4b" y="360.0"/>
  134
+    </g>
  135
+   </g>
  136
+   <g id="matplotlib.axis_1">
  137
+    <g id="xtick_1">
  138
+     <g id="line2d_17">
  139
+      <defs>
  140
+       <path d="
  141
+M0 0
  142
+L0 -4" id="mcb557df647" style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;"/>
  143
+      </defs>
  144
+      <g>
  145
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="183.6" xlink:href="#mcb557df647" y="388.8"/>
  146
+      </g>
  147
+     </g>
  148
+     <g id="line2d_18">
  149
+      <defs>
  150
+       <path d="
  151
+M0 0
  152
+L0 4" id="mdad270ee8e" style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;"/>
  153
+      </defs>
  154
+      <g>
  155
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="183.6" xlink:href="#mdad270ee8e" y="43.2"/>
  156
+      </g>
  157
+     </g>
  158
+     <g id="text_1">
  159
+      <!-- 1 -->
  160
+      <defs>
  161
+       <path d="
  162
+M12.4062 8.29688
  163
+L28.5156 8.29688
  164
+L28.5156 63.9219
  165
+L10.9844 60.4062
  166
+L10.9844 69.3906
  167
+L28.4219 72.9062
  168
+L38.2812 72.9062
  169
+L38.2812 8.29688
  170
+L54.3906 8.29688
  171
+L54.3906 0
  172
+L12.4062 0
  173
+z
  174
+" id="BitstreamVeraSans-Roman-31"/>
  175
+      </defs>
  176
+      <g transform="translate(180.995625 401.54875)scale(0.12 -0.12)">
  177
+       <use xlink:href="#BitstreamVeraSans-Roman-31"/>
  178
+      </g>
  179
+     </g>
  180
+    </g>
  181
+    <g id="xtick_2">
  182
+     <g id="line2d_19">
  183
+      <g>
  184
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="406.8" xlink:href="#mcb557df647" y="388.8"/>
  185
+      </g>
  186
+     </g>
  187
+     <g id="line2d_20">
  188
+      <g>
  189
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="406.8" xlink:href="#mdad270ee8e" y="43.2"/>
  190
+      </g>
  191
+     </g>
  192
+     <g id="text_2">
  193
+      <!-- 2 -->
  194
+      <defs>
  195
+       <path d="
  196
+M19.1875 8.29688
  197
+L53.6094 8.29688
  198
+L53.6094 0
  199
+L7.32812 0
  200
+L7.32812 8.29688
  201
+Q12.9375 14.1094 22.625 23.8906
  202
+Q32.3281 33.6875 34.8125 36.5312
  203
+Q39.5469 41.8438 41.4219 45.5312
  204
+Q43.3125 49.2188 43.3125 52.7812
  205
+Q43.3125 58.5938 39.2344 62.25
  206
+Q35.1562 65.9219 28.6094 65.9219
  207
+Q23.9688 65.9219 18.8125 64.3125
  208
+Q13.6719 62.7031 7.8125 59.4219
  209
+L7.8125 69.3906
  210
+Q13.7656 71.7812 18.9375 73
  211
+Q24.125 74.2188 28.4219 74.2188
  212
+Q39.75 74.2188 46.4844 68.5469
  213
+Q53.2188 62.8906 53.2188 53.4219
  214
+Q53.2188 48.9219 51.5312 44.8906
  215
+Q49.8594 40.875 45.4062 35.4062
  216
+Q44.1875 33.9844 37.6406 27.2188
  217
+Q31.1094 20.4531 19.1875 8.29688" id="BitstreamVeraSans-Roman-32"/>
  218
+      </defs>
  219
+      <g transform="translate(404.023125 401.70625)scale(0.12 -0.12)">
  220
+       <use xlink:href="#BitstreamVeraSans-Roman-32"/>
  221
+      </g>
  222
+     </g>
  223
+    </g>
  224
+   </g>
  225
+   <g id="matplotlib.axis_2">
  226
+    <g id="ytick_1">
  227
+     <g id="line2d_21">
  228
+      <defs>
  229
+       <path d="
  230
+M0 0
  231
+L4 0" id="mc8fcea1516" style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;"/>
  232
+      </defs>
  233
+      <g>
  234
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="72.0" xlink:href="#mc8fcea1516" y="388.8"/>
  235
+      </g>
  236
+     </g>
  237
+     <g id="line2d_22">
  238
+      <defs>
  239
+       <path d="
  240
+M0 0
  241
+L-4 0" id="m0d5b0a6425" style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;"/>
  242
+      </defs>
  243
+      <g>
  244
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="518.4" xlink:href="#m0d5b0a6425" y="388.8"/>
  245
+      </g>
  246
+     </g>
  247
+     <g id="text_3">
  248
+      <!-- −30 -->
  249
+      <defs>
  250
+       <path d="
  251
+M10.5938 35.5
  252
+L73.1875 35.5
  253
+L73.1875 27.2031
  254
+L10.5938 27.2031
  255
+z
  256
+" id="BitstreamVeraSans-Roman-2212"/>
  257
+       <path d="
  258
+M40.5781 39.3125
  259
+Q47.6562 37.7969 51.625 33
  260
+Q55.6094 28.2188 55.6094 21.1875
  261
+Q55.6094 10.4062 48.1875 4.48438
  262
+Q40.7656 -1.42188 27.0938 -1.42188
  263
+Q22.5156 -1.42188 17.6562 -0.515625
  264
+Q12.7969 0.390625 7.625 2.20312
  265
+L7.625 11.7188
  266
+Q11.7188 9.32812 16.5938 8.10938
  267
+Q21.4844 6.89062 26.8125 6.89062
  268
+Q36.0781 6.89062 40.9375 10.5469
  269
+Q45.7969 14.2031 45.7969 21.1875
  270
+Q45.7969 27.6406 41.2812 31.2656
  271
+Q36.7656 34.9062 28.7188 34.9062
  272
+L20.2188 34.9062
  273
+L20.2188 43.0156
  274
+L29.1094 43.0156
  275
+Q36.375 43.0156 40.2344 45.9219
  276
+Q44.0938 48.8281 44.0938 54.2969
  277
+Q44.0938 59.9062 40.1094 62.9062
  278
+Q36.1406 65.9219 28.7188 65.9219
  279
+Q24.6562 65.9219 20.0156 65.0312
  280
+Q15.375 64.1562 9.8125 62.3125
  281
+L9.8125 71.0938
  282
+Q15.4375 72.6562 20.3438 73.4375
  283
+Q25.25 74.2188 29.5938 74.2188
  284
+Q40.8281 74.2188 47.3594 69.1094
  285
+Q53.9062 64.0156 53.9062 55.3281
  286
+Q53.9062 49.2656 50.4375 45.0938
  287
+Q46.9688 40.9219 40.5781 39.3125" id="BitstreamVeraSans-Roman-33"/>
  288
+       <path d="
  289
+M31.7812 66.4062
  290
+Q24.1719 66.4062 20.3281 58.9062
  291
+Q16.5 51.4219 16.5 36.375
  292
+Q16.5 21.3906 20.3281 13.8906
  293
+Q24.1719 6.39062 31.7812 6.39062
  294
+Q39.4531 6.39062 43.2812 13.8906
  295
+Q47.125 21.3906 47.125 36.375
  296
+Q47.125 51.4219 43.2812 58.9062
  297
+Q39.4531 66.4062 31.7812 66.4062
  298
+M31.7812 74.2188
  299
+Q44.0469 74.2188 50.5156 64.5156
  300
+Q56.9844 54.8281 56.9844 36.375
  301
+Q56.9844 17.9688 50.5156 8.26562
  302
+Q44.0469 -1.42188 31.7812 -1.42188
  303
+Q19.5312 -1.42188 13.0625 8.26562
  304
+Q6.59375 17.9688 6.59375 36.375
  305
+Q6.59375 54.8281 13.0625 64.5156
  306
+Q19.5312 74.2188 31.7812 74.2188" id="BitstreamVeraSans-Roman-30"/>
  307
+      </defs>
  308
+      <g transform="translate(44.7425 393.1678125)scale(0.12 -0.12)">
  309
+       <use xlink:href="#BitstreamVeraSans-Roman-2212"/>
  310
+       <use x="83.7890625" xlink:href="#BitstreamVeraSans-Roman-33"/>
  311
+       <use x="147.412109375" xlink:href="#BitstreamVeraSans-Roman-30"/>
  312
+      </g>
  313
+     </g>
  314
+    </g>
  315
+    <g id="ytick_2">
  316
+     <g id="line2d_23">
  317
+      <g>
  318
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="72.0" xlink:href="#mc8fcea1516" y="331.2"/>
  319
+      </g>
  320
+     </g>
  321
+     <g id="line2d_24">
  322
+      <g>
  323
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="518.4" xlink:href="#m0d5b0a6425" y="331.2"/>
  324
+      </g>
  325
+     </g>
  326
+     <g id="text_4">
  327
+      <!-- −20 -->
  328
+      <g transform="translate(44.7425 335.5678125)scale(0.12 -0.12)">
  329
+       <use xlink:href="#BitstreamVeraSans-Roman-2212"/>
  330
+       <use x="83.7890625" xlink:href="#BitstreamVeraSans-Roman-32"/>
  331
+       <use x="147.412109375" xlink:href="#BitstreamVeraSans-Roman-30"/>
  332
+      </g>
  333
+     </g>
  334
+    </g>
  335
+    <g id="ytick_3">
  336
+     <g id="line2d_25">
  337
+      <g>
  338
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="72.0" xlink:href="#mc8fcea1516" y="273.6"/>
  339
+      </g>
  340
+     </g>
  341
+     <g id="line2d_26">
  342
+      <g>
  343
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="518.4" xlink:href="#m0d5b0a6425" y="273.6"/>
  344
+      </g>
  345
+     </g>
  346
+     <g id="text_5">
  347
+      <!-- −10 -->
  348
+      <g transform="translate(44.7425 277.9678125)scale(0.12 -0.12)">
  349
+       <use xlink:href="#BitstreamVeraSans-Roman-2212"/>
  350
+       <use x="83.7890625" xlink:href="#BitstreamVeraSans-Roman-31"/>
  351
+       <use x="147.412109375" xlink:href="#BitstreamVeraSans-Roman-30"/>
  352
+      </g>
  353
+     </g>
  354
+    </g>
  355
+    <g id="ytick_4">
  356
+     <g id="line2d_27">
  357
+      <g>
  358
+       <use style="stroke:#000000;stroke-linecap:butt;stroke-width:0.5;" x="72.0" xlink:href="#mc8fcea1516" y="216.0"/>
  359
+      </g>
  360
+     </g>
  361
+     <g id="line2d_28">
  362
+      <g>