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

Skip to content

Logit scale #3753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
('color', 'Color'),
('text_labels_and_annotations', 'Text, labels, and annotations'),
('ticks_and_spines', 'Ticks and spines'),
('scales', 'Axis scales'),
('subplots_axes_and_figures', 'Subplots, axes, and figures'),
('style_sheets', 'Style sheets'),
('specialty_plots', 'Specialty plots'),
Expand Down
43 changes: 43 additions & 0 deletions doc/pyplots/pyplot_scales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import numpy as np
import matplotlib.pyplot as plt

# make up some data in the interval ]0, 1[
y = np.random.normal(loc=0.5, scale=0.4, size=1000)
y = y[(y > 0) & (y < 1)]
y.sort()
x = np.arange(len(y))

# plot with various axes scales
plt.figure(1)

# linear
plt.subplot(221)
plt.plot(x, y)
plt.yscale('linear')
plt.title('linear')
plt.grid(True)


# log
plt.subplot(222)
plt.plot(x, y)
plt.yscale('log')
plt.title('log')
plt.grid(True)


# symmetric log
plt.subplot(223)
plt.plot(x, y - y.mean())
plt.yscale('symlog', linthreshy=0.05)
plt.title('symlog')
plt.grid(True)

# logit
plt.subplot(223)
plt.plot(x, y)
plt.yscale('logit')
plt.title('logit')
plt.grid(True)

plt.show()
19 changes: 19 additions & 0 deletions doc/users/pyplot_tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,22 @@ variety of other coordinate systems one can choose -- see
:ref:`annotations-tutorial` and :ref:`plotting-guide-annotation` for
details. More examples can be found in
:ref:`pylab_examples-annotation_demo`.


Logarithmic and other nonlinear axis
====================================

:mod:`matplotlib.pyplot` supports not only linear axis scales, but also
logarithmic and logit scales. This is commonly used if data spans many orders
of magnitude. Changing the scale of an axis is easy:

plt.xscale('log')

An example of four plots with the same data and different scales for the y axis
is shown below.

.. plot:: pyplots/pyplot_scales.py
:include-source:

It is also possible to add your own scale, see :ref:`adding-new-scales` for
details.
4 changes: 4 additions & 0 deletions doc/users/whats_new/updated_scale.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Logit Scale
-----------
Added support for the 'logit' axis scale, a nonlinear transformation
`x -> log10(x / (1-x))` for data between 0 and 1 excluded.
47 changes: 47 additions & 0 deletions examples/scales/scales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Illustrate the scale transformations applied to axes, e.g. log, symlog, logit.
"""
import numpy as np
import matplotlib.pyplot as plt

# make up some data in the interval ]0, 1[
y = np.random.normal(loc=0.5, scale=0.4, size=1000)
y = y[(y > 0) & (y < 1)]
y.sort()
x = np.arange(len(y))

# plot with various axes scales
fig, axs = plt.subplots(2, 2)

# linear
ax = axs[0, 0]
ax.plot(x, y)
ax.set_yscale('linear')
ax.set_title('linear')
ax.grid(True)


# log
ax = axs[0, 1]
ax.plot(x, y)
ax.set_yscale('log')
ax.set_title('log')
ax.grid(True)


# symmetric log
ax = axs[1, 0]
ax.plot(x, y - y.mean())
ax.set_yscale('symlog', linthreshy=0.05)
ax.set_title('symlog')
ax.grid(True)

# logit
ax = axs[1, 1]
ax.plot(x, y)
ax.set_yscale('logit')
ax.set_title('logit')
ax.grid(True)


plt.show()
121 changes: 108 additions & 13 deletions lib/matplotlib/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

from matplotlib.cbook import dedent
from matplotlib.ticker import (NullFormatter, ScalarFormatter,
LogFormatterMathtext)
LogFormatterMathtext, LogitFormatter)
from matplotlib.ticker import (NullLocator, LogLocator, AutoLocator,
SymmetricalLogLocator)
SymmetricalLogLocator, LogitLocator)
from matplotlib.transforms import Transform, IdentityTransform
from matplotlib import docstring

Expand Down Expand Up @@ -86,8 +86,8 @@ def get_transform(self):

def _mask_non_positives(a):
"""
Return a Numpy masked array where all non-positive values are
replaced with NaNs. If there are no non-positive values, the
Return a Numpy array where all non-positive values are
replaced with NaNs. If there are no non-positive values, the
original array is returned.
"""
mask = a <= 0.0
Expand All @@ -97,6 +97,7 @@ def _mask_non_positives(a):


def _clip_non_positives(a):
a = np.array(a, float)
a[a <= 0.0] = 1e-300
return a

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

def transform_non_affine(self, a):
a = self._handle_nonpos(a * 10.0)
if isinstance(a, ma.MaskedArray):
return ma.log10(a)
return np.log10(a)

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

def transform_non_affine(self, a):
a = self._handle_nonpos(a * 2.0)
if isinstance(a, ma.MaskedArray):
return ma.log(a) / np.log(2)
return np.log2(a)

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

def transform_non_affine(self, a):
a = self._handle_nonpos(a * np.e)
if isinstance(a, ma.MaskedArray):
return ma.log(a)
return np.log(a)

def inverted(self):
Expand Down Expand Up @@ -212,8 +207,6 @@ def __init__(self, base, nonpos):

def transform_non_affine(self, a):
a = self._handle_nonpos(a * self.base)
if isinstance(a, ma.MaskedArray):
return ma.log(a) / np.log(self.base)
return np.log(a) / np.log(self.base)

def inverted(self):
Expand Down Expand Up @@ -478,10 +471,112 @@ def get_transform(self):
return self._transform


def _mask_non_logit(a):
"""
Return a Numpy array where all values outside ]0, 1[ are
replaced with NaNs. If all values are inside ]0, 1[, the original
array is returned.
"""
mask = (a <= 0.0) | (a >= 1.0)
if mask.any():
return np.where(mask, np.nan, a)
return a


def _clip_non_logit(a):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should make a copy of the input array either here or where _handle_non_affine is called. Modifying user objects is bad practice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could achieve what you want in one line with return np.clip(a, 1e-300, 1 - 1e-300). Either way, though, you are assuming a is double precision. Is this a safe assumption?

a = np.array(a, float)
a[a <= 0.0] = 1e-300
a[a >= 1.0] = 1 - 1e-300
return a


class LogitTransform(Transform):
input_dims = 1
output_dims = 1
is_separable = True
has_inverse = True

def __init__(self, nonpos):
Transform.__init__(self)
if nonpos == 'mask':
self._handle_nonpos = _mask_non_logit
else:
self._handle_nonpos = _clip_non_logit
self._nonpos = nonpos

def transform_non_affine(self, a):
"""logit transform (base 10), masked or clipped"""
a = self._handle_nonpos(a)
return np.log10(1.0 * a / (1.0 - a))

def inverted(self):
return LogisticTransform(self._nonpos)


class LogisticTransform(Transform):
input_dims = 1
output_dims = 1
is_separable = True
has_inverse = True

def __init__(self, nonpos='mask'):
Transform.__init__(self)
self._nonpos = nonpos

def transform_non_affine(self, a):
"""logistic transform (base 10)"""
return 1.0 / (1 + 10**(-a))

def inverted(self):
return LogitTransform(self._nonpos)


class LogitScale(ScaleBase):
"""
Logit scale for data between zero and one, both excluded.

This scale is similar to a log scale close to zero and to one, and almost
linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
"""
name = 'logit'

def __init__(self, axis, nonpos='mask'):
"""
*nonpos*: ['mask' | 'clip' ]
values beyond ]0, 1[ can be masked as invalid, or clipped to a number
very close to 0 or 1
"""
if nonpos not in ['mask', 'clip']:
raise ValueError("nonposx, nonposy kwarg must be 'mask' or 'clip'")

self._transform = LogitTransform(nonpos)

def get_transform(self):
"""
Return a :class:`LogitTransform` instance.
"""
return self._transform

def set_default_locators_and_formatters(self, axis):
# ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
axis.set_major_locator(LogitLocator())
axis.set_major_formatter(LogitFormatter())
axis.set_minor_locator(LogitLocator(minor=True))
axis.set_minor_formatter(LogitFormatter())

def limit_range_for_scale(self, vmin, vmax, minpos):
"""
Limit the domain to values between 0 and 1 (excluded).
"""
return (vmin <= 0 and minpos or vmin,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it hard to parse this expression; and it needs a docstring at least to explain what minpos is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a short docstring, but it's the same like the analog function for LogTransforms. Again, I'm just being consistent with the codebase.

minpos is not documented in the LogTransform either, but it is documented in the base class. If it gets explained in LogitTransform, it will be in LogTransform too, is my opinion.

vmax >= 1 and (1 - minpos) or vmax)


_scale_mapping = {
'linear': LinearScale,
'log': LogScale,
'symlog': SymmetricalLogScale
'symlog': SymmetricalLogScale,
'logit': LogitScale,
}


Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions lib/matplotlib/tests/test_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ def test_log_scales():
ax.axhline(24.1)


@image_comparison(baseline_images=['logit_scales'], remove_text=True,
extensions=['png'])
def test_logit_scales():
ax = plt.subplot(111, xscale='logit')

# Typical extinction curve for logit
x = np.array([0.001, 0.003, 0.01, 0.03, 0.1, 0.2, 0.3, 0.4, 0.5,
0.6, 0.7, 0.8, 0.9, 0.97, 0.99, 0.997, 0.999])
y = 1.0 / x

ax.plot(x, y)
ax.grid(True)


@cleanup
def test_log_scatter():
"""Issue #1799"""
Expand Down
Loading