-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Logit scale #3753
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() |
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. |
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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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 | ||
|
@@ -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 | ||
|
||
|
@@ -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): | ||
|
@@ -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): | ||
|
@@ -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): | ||
|
@@ -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): | ||
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you could achieve what you want in one line with |
||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
} | ||
|
||
|
||
|
There was a problem hiding this comment.
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.