Skip to content

Commit 55673e0

Browse files
committed
split up transforms and formatters to their own files
1 parent 53a4bb6 commit 55673e0

File tree

14 files changed

+395
-427
lines changed

14 files changed

+395
-427
lines changed

probscale/formatters.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import numpy as numpy
2+
from matplotlib.ticker import Formatter
3+
4+
5+
class _FormatterMixin(Formatter):
6+
@classmethod
7+
def _sig_figs(cls, x, n, expthresh=5, forceint=False):
8+
""" Formats a number with the correct number of sig figs.
9+
10+
Parameters
11+
----------
12+
x : int or float
13+
The number you want to format.
14+
n : int
15+
The number of significan figures it should have.
16+
expthresh : int, optional (default = 5)
17+
The absolute value of the order of magnitude at which numbers
18+
are formatted in exponential notation.
19+
forceint : bool, optional (default is False)
20+
If true, simply returns int(x)
21+
22+
Returns
23+
-------
24+
formatted : str
25+
The formatted number as a string
26+
27+
Examples
28+
--------
29+
>>> print(_sig_figs(1247.15, 3))
30+
1250
31+
>>> print(_sig_figs(1247.15, 7))
32+
1247.150
33+
34+
"""
35+
36+
# return a string value unaltered
37+
if isinstance(x, str) or x == 0.0:
38+
out = str(x)
39+
40+
# check on the number provided
41+
elif x is not None and not numpy.isinf(x) and not numpy.isnan(x):
42+
43+
# check on the _sig_figs
44+
if n < 1:
45+
raise ValueError("number of sig figs (n) must be greater than zero")
46+
47+
elif forceint:
48+
out = '{:,.0f}'.format(x)
49+
50+
# logic to do all of the rounding
51+
else:
52+
order = numpy.floor(numpy.log10(numpy.abs(x)))
53+
54+
if (-1.0 * expthresh <= order <= expthresh):
55+
decimal_places = int(n - 1 - order)
56+
57+
if decimal_places <= 0:
58+
out = '{0:,.0f}'.format(round(x, decimal_places))
59+
60+
else:
61+
fmt = '{0:,.%df}' % decimal_places
62+
out = fmt.format(x)
63+
64+
else:
65+
decimal_places = n - 1
66+
fmt = '{0:.%de}' % decimal_places
67+
out = fmt.format(x)
68+
69+
# with NAs and INFs, just return 'NA'
70+
else:
71+
out = 'NA'
72+
73+
return out
74+
75+
def __call__(self, x, pos=None):
76+
if x < (10 / self.factor):
77+
out = self._sig_figs(x, 1)
78+
elif x <= (99 / self.factor):
79+
out = self._sig_figs(x, 2)
80+
else:
81+
order = numpy.ceil(numpy.round(numpy.abs(numpy.log10(self.top - x)), 6))
82+
out = self._sig_figs(x, order + self.offset)
83+
84+
return '{}'.format(out)
85+
86+
87+
class PctFormatter(_FormatterMixin):
88+
factor = 1.0
89+
offset = 2
90+
top = 100
91+
92+
93+
class ProbFormatter(_FormatterMixin):
94+
factor = 100.0
95+
offset = 0
96+
top = 1

probscale/probscale.py

Lines changed: 19 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,18 @@
1-
import numpy as np
2-
import matplotlib
3-
from matplotlib.transforms import Transform
1+
import numpy
42
from matplotlib.scale import ScaleBase
53
from matplotlib.ticker import (
64
FixedLocator,
75
NullLocator,
8-
Formatter,
96
NullFormatter,
107
FuncFormatter,
118
)
129

13-
14-
def _mask_non_positives(a):
15-
"""
16-
Return a Numpy array where all values outside ]0, 1[ are
17-
replaced with NaNs. If all values are inside ]0, 1[, the original
18-
array is returned.
19-
"""
20-
mask = (a <= 0.0) | (a >= 1.0)
21-
if mask.any():
22-
return np.where(mask, np.nan, a)
23-
return a
24-
25-
26-
def _clip_non_positives(a):
27-
a = np.array(a, float)
28-
a[a <= 0.0] = 1e-300
29-
a[a >= 1.0] = 1 - 1e-300
30-
return a
10+
from .transforms import ProbTransform
11+
from .formatters import PctFormatter, ProbFormatter
3112

3213

3314
class _minimal_norm(object):
34-
_A = -(8 * (np.pi - 3.0) / (3.0 * np.pi * (np.pi - 4.0)))
15+
_A = -(8 * (numpy.pi - 3.0) / (3.0 * numpy.pi * (numpy.pi - 4.0)))
3516

3617
@classmethod
3718
def _approx_erf(cls, x):
@@ -41,8 +22,8 @@ def _approx_erf(cls, x):
4122
4223
"""
4324

44-
guts = -x**2 * (4.0 / np.pi + cls._A * x**2) / (1.0 + cls._A * x**2)
45-
return np.sign(x) * np.sqrt(1.0 - np.exp(guts))
25+
guts = -x**2 * (4.0 / numpy.pi + cls._A * x**2) / (1.0 + cls._A * x**2)
26+
return numpy.sign(x) * numpy.sqrt(1.0 - numpy.exp(guts))
4627

4728
@classmethod
4829
def _approx_inv_erf(cls, z):
@@ -52,9 +33,9 @@ def _approx_inv_erf(cls, z):
5233
5334
"""
5435

55-
_b = (2 / np.pi / cls._A) + (0.5 * np.log(1 - z**2))
56-
_c = np.log(1 - z**2) / cls._A
57-
return np.sign(z) * np.sqrt(np.sqrt(_b**2 - _c) - _b)
36+
_b = (2 / numpy.pi / cls._A) + (0.5 * numpy.log(1 - z**2))
37+
_c = numpy.log(1 - z**2) / cls._A
38+
return numpy.sign(z) * numpy.sqrt(numpy.sqrt(_b**2 - _c) - _b)
5839

5940
@classmethod
6041
def ppf(cls, q):
@@ -63,7 +44,7 @@ def ppf(cls, q):
6344
Wikipedia: https://goo.gl/Rtxjme
6445
6546
"""
66-
return np.sqrt(2) * cls._approx_inv_erf(2*q - 1)
47+
return numpy.sqrt(2) * cls._approx_inv_erf(2*q - 1)
6748

6849
@classmethod
6950
def cdf(cls, x):
@@ -72,151 +53,7 @@ def cdf(cls, x):
7253
Wikipedia: https://goo.gl/ciUNLx
7354
7455
"""
75-
return 0.5 * (1 + cls._approx_erf(x/np.sqrt(2)))
76-
77-
78-
class _FormatterMixin(Formatter):
79-
@classmethod
80-
def _sig_figs(cls, x, n, expthresh=5, forceint=False):
81-
""" Formats a number with the correct number of sig figs.
82-
83-
Parameters
84-
----------
85-
x : int or float
86-
The number you want to format.
87-
n : int
88-
The number of significan figures it should have.
89-
expthresh : int, optional (default = 5)
90-
The absolute value of the order of magnitude at which numbers
91-
are formatted in exponential notation.
92-
forceint : bool, optional (default is False)
93-
If true, simply returns int(x)
94-
95-
Returns
96-
-------
97-
formatted : str
98-
The formatted number as a string
99-
100-
Examples
101-
--------
102-
>>> print(_sig_figs(1247.15, 3))
103-
1250
104-
>>> print(_sig_figs(1247.15, 7))
105-
1247.150
106-
107-
"""
108-
109-
# return a string value unaltered
110-
if isinstance(x, str) or x == 0.0:
111-
out = str(x)
112-
113-
# check on the number provided
114-
elif x is not None and not np.isinf(x) and not np.isnan(x):
115-
116-
# check on the _sig_figs
117-
if n < 1:
118-
raise ValueError("number of sig figs (n) must be greater than zero")
119-
120-
elif forceint:
121-
out = '{:,.0f}'.format(x)
122-
123-
# logic to do all of the rounding
124-
else:
125-
order = np.floor(np.log10(np.abs(x)))
126-
127-
if (-1.0 * expthresh <= order <= expthresh):
128-
decimal_places = int(n - 1 - order)
129-
130-
if decimal_places <= 0:
131-
out = '{0:,.0f}'.format(round(x, decimal_places))
132-
133-
else:
134-
fmt = '{0:,.%df}' % decimal_places
135-
out = fmt.format(x)
136-
137-
else:
138-
decimal_places = n - 1
139-
fmt = '{0:.%de}' % decimal_places
140-
out = fmt.format(x)
141-
142-
# with NAs and INFs, just return 'NA'
143-
else:
144-
out = 'NA'
145-
146-
return out
147-
148-
def __call__(self, x, pos=None):
149-
if x < (10 / self.factor):
150-
out = self._sig_figs(x, 1)
151-
elif x <= (99 / self.factor):
152-
out = self._sig_figs(x, 2)
153-
else:
154-
order = np.ceil(np.round(np.abs(np.log10(self.top - x)), 6))
155-
out = self._sig_figs(x, order + self.offset)
156-
157-
return '{}'.format(out)
158-
159-
160-
class PctFormatter(_FormatterMixin):
161-
factor = 1.0
162-
offset = 2
163-
top = 100
164-
165-
166-
class ProbFormatter(_FormatterMixin):
167-
factor = 100.0
168-
offset = 0
169-
top = 1
170-
171-
172-
class _ProbTransformMixin(Transform):
173-
input_dims = 1
174-
output_dims = 1
175-
is_separable = True
176-
has_inverse = True
177-
178-
def __init__(self, dist, as_pct=True, nonpos='mask'):
179-
Transform.__init__(self)
180-
self.dist = dist
181-
if as_pct:
182-
self.factor = 100.0
183-
else:
184-
self.factor = 1.0
185-
186-
if nonpos == 'mask':
187-
self._handle_nonpos = _mask_non_positives
188-
elif nonpos == 'clip':
189-
self._handle_nonpos = _clip_non_positives
190-
else:
191-
raise ValueError("`nonpos` muse be either 'mask' or 'clip'")
192-
193-
194-
class ProbTransform(_ProbTransformMixin):
195-
def transform_non_affine(self, prob):
196-
q = self.dist.ppf(prob / self.factor)
197-
return q
198-
199-
def inverted(self):
200-
return QuantileTransform(self.dist, as_pct=self.as_pct, nonpos=self.nonpos)
201-
202-
203-
class QuantileTransform(_ProbTransformMixin):
204-
def transform_non_affine(self, q):
205-
prob = self.dist.cdf(q) * self.factor
206-
return prob
207-
208-
class InvertedProbTransform(_ProbTransformMixin):
209-
def transform_non_affine(self, a):
210-
return self.dist.cdf(a) * self.factor
211-
212-
213-
class InvertedProbTransform(_ProbTransformMixin):
214-
def transform_non_affine(self, q):
215-
prob = self.dist.cdf(q) * self.factor
216-
return prob
217-
218-
def inverted(self):
219-
return ProbTransform(self.dist, as_pct=self.as_pct, nonpos=self.nonpos)
56+
return 0.5 * (1 + cls._approx_erf(x/numpy.sqrt(2)))
22057

22158

22259
class ProbScale(ScaleBase):
@@ -237,7 +74,7 @@ class ProbScale(ScaleBase):
23774
>>> from matplotlib import pyplot
23875
>>> import probscale
23976
>>> fig, ax = pyplot.subplots()
240-
>>> ax.set_scale('prob')
77+
>>> ax.set_xscale('prob')
24178
24279
"""
24380

@@ -259,21 +96,21 @@ def _get_probs(cls, nobs, as_pct):
25996
else:
26097
factor = 100.0
26198

262-
order = int(np.floor(np.log10(nobs)))
263-
base_probs = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90])
99+
order = int(numpy.floor(numpy.log10(nobs)))
100+
base_probs = numpy.array([10, 20, 30, 40, 50, 60, 70, 80, 90])
264101

265102
axis_probs = base_probs.copy()
266103
for n in range(order):
267104
if n <= 2:
268-
lower_fringe = np.array([1, 2, 5])
269-
upper_fringe = np.array([5, 8, 9])
105+
lower_fringe = numpy.array([1, 2, 5])
106+
upper_fringe = numpy.array([5, 8, 9])
270107
else:
271-
lower_fringe = np.array([1])
272-
upper_fringe = np.array([9])
108+
lower_fringe = numpy.array([1])
109+
upper_fringe = numpy.array([9])
273110

274111
new_lower = lower_fringe / 10**(n)
275112
new_upper = upper_fringe / 10**(n) + axis_probs.max()
276-
axis_probs = np.hstack([new_lower, axis_probs, new_upper])
113+
axis_probs = numpy.hstack([new_lower, axis_probs, new_upper])
277114
locs = axis_probs / factor
278115
return locs
279116

0 commit comments

Comments
 (0)