Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ed0ce1a

Browse files
committed
Merge pull request #3753 from iosonofabio/logit
Logit scale
2 parents 7fcc140 + c640f77 commit ed0ce1a

File tree

9 files changed

+341
-13
lines changed

9 files changed

+341
-13
lines changed

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
('color', 'Color'),
127127
('text_labels_and_annotations', 'Text, labels, and annotations'),
128128
('ticks_and_spines', 'Ticks and spines'),
129+
('scales', 'Axis scales'),
129130
('subplots_axes_and_figures', 'Subplots, axes, and figures'),
130131
('style_sheets', 'Style sheets'),
131132
('specialty_plots', 'Specialty plots'),

doc/pyplots/pyplot_scales.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
4+
# make up some data in the interval ]0, 1[
5+
y = np.random.normal(loc=0.5, scale=0.4, size=1000)
6+
y = y[(y > 0) & (y < 1)]
7+
y.sort()
8+
x = np.arange(len(y))
9+
10+
# plot with various axes scales
11+
plt.figure(1)
12+
13+
# linear
14+
plt.subplot(221)
15+
plt.plot(x, y)
16+
plt.yscale('linear')
17+
plt.title('linear')
18+
plt.grid(True)
19+
20+
21+
# log
22+
plt.subplot(222)
23+
plt.plot(x, y)
24+
plt.yscale('log')
25+
plt.title('log')
26+
plt.grid(True)
27+
28+
29+
# symmetric log
30+
plt.subplot(223)
31+
plt.plot(x, y - y.mean())
32+
plt.yscale('symlog', linthreshy=0.05)
33+
plt.title('symlog')
34+
plt.grid(True)
35+
36+
# logit
37+
plt.subplot(223)
38+
plt.plot(x, y)
39+
plt.yscale('logit')
40+
plt.title('logit')
41+
plt.grid(True)
42+
43+
plt.show()

doc/users/pyplot_tutorial.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,22 @@ variety of other coordinate systems one can choose -- see
280280
:ref:`annotations-tutorial` and :ref:`plotting-guide-annotation` for
281281
details. More examples can be found in
282282
:ref:`pylab_examples-annotation_demo`.
283+
284+
285+
Logarithmic and other nonlinear axis
286+
====================================
287+
288+
:mod:`matplotlib.pyplot` supports not only linear axis scales, but also
289+
logarithmic and logit scales. This is commonly used if data spans many orders
290+
of magnitude. Changing the scale of an axis is easy:
291+
292+
plt.xscale('log')
293+
294+
An example of four plots with the same data and different scales for the y axis
295+
is shown below.
296+
297+
.. plot:: pyplots/pyplot_scales.py
298+
:include-source:
299+
300+
It is also possible to add your own scale, see :ref:`adding-new-scales` for
301+
details.

doc/users/whats_new/updated_scale.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Logit Scale
2+
-----------
3+
Added support for the 'logit' axis scale, a nonlinear transformation
4+
`x -> log10(x / (1-x))` for data between 0 and 1 excluded.

examples/scales/scales.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
Illustrate the scale transformations applied to axes, e.g. log, symlog, logit.
3+
"""
4+
import numpy as np
5+
import matplotlib.pyplot as plt
6+
7+
# make up some data in the interval ]0, 1[
8+
y = np.random.normal(loc=0.5, scale=0.4, size=1000)
9+
y = y[(y > 0) & (y < 1)]
10+
y.sort()
11+
x = np.arange(len(y))
12+
13+
# plot with various axes scales
14+
fig, axs = plt.subplots(2, 2)
15+
16+
# linear
17+
ax = axs[0, 0]
18+
ax.plot(x, y)
19+
ax.set_yscale('linear')
20+
ax.set_title('linear')
21+
ax.grid(True)
22+
23+
24+
# log
25+
ax = axs[0, 1]
26+
ax.plot(x, y)
27+
ax.set_yscale('log')
28+
ax.set_title('log')
29+
ax.grid(True)
30+
31+
32+
# symmetric log
33+
ax = axs[1, 0]
34+
ax.plot(x, y - y.mean())
35+
ax.set_yscale('symlog', linthreshy=0.05)
36+
ax.set_title('symlog')
37+
ax.grid(True)
38+
39+
# logit
40+
ax = axs[1, 1]
41+
ax.plot(x, y)
42+
ax.set_yscale('logit')
43+
ax.set_title('logit')
44+
ax.grid(True)
45+
46+
47+
plt.show()

lib/matplotlib/scale.py

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
from matplotlib.cbook import dedent
1010
from matplotlib.ticker import (NullFormatter, ScalarFormatter,
11-
LogFormatterMathtext)
11+
LogFormatterMathtext, LogitFormatter)
1212
from matplotlib.ticker import (NullLocator, LogLocator, AutoLocator,
13-
SymmetricalLogLocator)
13+
SymmetricalLogLocator, LogitLocator)
1414
from matplotlib.transforms import Transform, IdentityTransform
1515
from matplotlib import docstring
1616

@@ -86,8 +86,8 @@ def get_transform(self):
8686

8787
def _mask_non_positives(a):
8888
"""
89-
Return a Numpy masked array where all non-positive values are
90-
replaced with NaNs. If there are no non-positive values, the
89+
Return a Numpy array where all non-positive values are
90+
replaced with NaNs. If there are no non-positive values, the
9191
original array is returned.
9292
"""
9393
mask = a <= 0.0
@@ -97,6 +97,7 @@ def _mask_non_positives(a):
9797

9898

9999
def _clip_non_positives(a):
100+
a = np.array(a, float)
100101
a[a <= 0.0] = 1e-300
101102
return a
102103

@@ -120,8 +121,6 @@ class Log10Transform(LogTransformBase):
120121

121122
def transform_non_affine(self, a):
122123
a = self._handle_nonpos(a * 10.0)
123-
if isinstance(a, ma.MaskedArray):
124-
return ma.log10(a)
125124
return np.log10(a)
126125

127126
def inverted(self):
@@ -147,8 +146,6 @@ class Log2Transform(LogTransformBase):
147146

148147
def transform_non_affine(self, a):
149148
a = self._handle_nonpos(a * 2.0)
150-
if isinstance(a, ma.MaskedArray):
151-
return ma.log(a) / np.log(2)
152149
return np.log2(a)
153150

154151
def inverted(self):
@@ -174,8 +171,6 @@ class NaturalLogTransform(LogTransformBase):
174171

175172
def transform_non_affine(self, a):
176173
a = self._handle_nonpos(a * np.e)
177-
if isinstance(a, ma.MaskedArray):
178-
return ma.log(a)
179174
return np.log(a)
180175

181176
def inverted(self):
@@ -212,8 +207,6 @@ def __init__(self, base, nonpos):
212207

213208
def transform_non_affine(self, a):
214209
a = self._handle_nonpos(a * self.base)
215-
if isinstance(a, ma.MaskedArray):
216-
return ma.log(a) / np.log(self.base)
217210
return np.log(a) / np.log(self.base)
218211

219212
def inverted(self):
@@ -478,10 +471,112 @@ def get_transform(self):
478471
return self._transform
479472

480473

474+
def _mask_non_logit(a):
475+
"""
476+
Return a Numpy array where all values outside ]0, 1[ are
477+
replaced with NaNs. If all values are inside ]0, 1[, the original
478+
array is returned.
479+
"""
480+
mask = (a <= 0.0) | (a >= 1.0)
481+
if mask.any():
482+
return np.where(mask, np.nan, a)
483+
return a
484+
485+
486+
def _clip_non_logit(a):
487+
a = np.array(a, float)
488+
a[a <= 0.0] = 1e-300
489+
a[a >= 1.0] = 1 - 1e-300
490+
return a
491+
492+
493+
class LogitTransform(Transform):
494+
input_dims = 1
495+
output_dims = 1
496+
is_separable = True
497+
has_inverse = True
498+
499+
def __init__(self, nonpos):
500+
Transform.__init__(self)
501+
if nonpos == 'mask':
502+
self._handle_nonpos = _mask_non_logit
503+
else:
504+
self._handle_nonpos = _clip_non_logit
505+
self._nonpos = nonpos
506+
507+
def transform_non_affine(self, a):
508+
"""logit transform (base 10), masked or clipped"""
509+
a = self._handle_nonpos(a)
510+
return np.log10(1.0 * a / (1.0 - a))
511+
512+
def inverted(self):
513+
return LogisticTransform(self._nonpos)
514+
515+
516+
class LogisticTransform(Transform):
517+
input_dims = 1
518+
output_dims = 1
519+
is_separable = True
520+
has_inverse = True
521+
522+
def __init__(self, nonpos='mask'):
523+
Transform.__init__(self)
524+
self._nonpos = nonpos
525+
526+
def transform_non_affine(self, a):
527+
"""logistic transform (base 10)"""
528+
return 1.0 / (1 + 10**(-a))
529+
530+
def inverted(self):
531+
return LogitTransform(self._nonpos)
532+
533+
534+
class LogitScale(ScaleBase):
535+
"""
536+
Logit scale for data between zero and one, both excluded.
537+
538+
This scale is similar to a log scale close to zero and to one, and almost
539+
linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
540+
"""
541+
name = 'logit'
542+
543+
def __init__(self, axis, nonpos='mask'):
544+
"""
545+
*nonpos*: ['mask' | 'clip' ]
546+
values beyond ]0, 1[ can be masked as invalid, or clipped to a number
547+
very close to 0 or 1
548+
"""
549+
if nonpos not in ['mask', 'clip']:
550+
raise ValueError("nonposx, nonposy kwarg must be 'mask' or 'clip'")
551+
552+
self._transform = LogitTransform(nonpos)
553+
554+
def get_transform(self):
555+
"""
556+
Return a :class:`LogitTransform` instance.
557+
"""
558+
return self._transform
559+
560+
def set_default_locators_and_formatters(self, axis):
561+
# ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
562+
axis.set_major_locator(LogitLocator())
563+
axis.set_major_formatter(LogitFormatter())
564+
axis.set_minor_locator(LogitLocator(minor=True))
565+
axis.set_minor_formatter(LogitFormatter())
566+
567+
def limit_range_for_scale(self, vmin, vmax, minpos):
568+
"""
569+
Limit the domain to values between 0 and 1 (excluded).
570+
"""
571+
return (vmin <= 0 and minpos or vmin,
572+
vmax >= 1 and (1 - minpos) or vmax)
573+
574+
481575
_scale_mapping = {
482576
'linear': LinearScale,
483577
'log': LogScale,
484-
'symlog': SymmetricalLogScale
578+
'symlog': SymmetricalLogScale,
579+
'logit': LogitScale,
485580
}
486581

487582

lib/matplotlib/tests/test_scale.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ def test_log_scales():
1414
ax.axhline(24.1)
1515

1616

17+
@image_comparison(baseline_images=['logit_scales'], remove_text=True,
18+
extensions=['png'])
19+
def test_logit_scales():
20+
ax = plt.subplot(111, xscale='logit')
21+
22+
# Typical extinction curve for logit
23+
x = np.array([0.001, 0.003, 0.01, 0.03, 0.1, 0.2, 0.3, 0.4, 0.5,
24+
0.6, 0.7, 0.8, 0.9, 0.97, 0.99, 0.997, 0.999])
25+
y = 1.0 / x
26+
27+
ax.plot(x, y)
28+
ax.grid(True)
29+
30+
1731
@cleanup
1832
def test_log_scatter():
1933
"""Issue #1799"""

0 commit comments

Comments
 (0)