Skip to content

Commit f309b1d

Browse files
committed
Merge pull request #11 from phobson/beef-it-up
Make the classes and functions more robust
2 parents 1479f53 + 3b33a48 commit f309b1d

File tree

15 files changed

+286
-173
lines changed

15 files changed

+286
-173
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Byte-compiled / optimized / DLL files
22
__pycache__/
33
*.py[cod]
4-
result_images
54

65
# C extensions
76
*.so
@@ -43,6 +42,8 @@ htmlcov/
4342
nosetests.xml
4443
coverage.xml
4544
*,cover
45+
.noseids
46+
result_images
4647

4748
# Translations
4849
*.mo
@@ -59,3 +60,8 @@ docs/tutorial/*_files/
5960

6061
# PyBuilder
6162
target/
63+
64+
# VS
65+
.vs
66+
*.pyproj
67+
*.sln

.travis.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ matrix:
55
- python: 2.7
66
env:
77
- COVERAGE=false
8-
- python: 3.3
9-
env:
10-
- COVERAGE=false
118
- python: 3.4
129
env:
1310
- COVERAGE=false
@@ -24,7 +21,6 @@ before_install:
2421
- ./miniconda.sh -b -p $HOME/miniconda
2522
- export PATH="$HOME/miniconda/bin:$PATH"
2623
- conda update --yes conda
27-
- cp probscale/tests/matplotlibrc .
2824

2925
install:
3026

@@ -33,13 +29,12 @@ install:
3329

3430
- conda create --yes -n test python=$TRAVIS_PYTHON_VERSION
3531
- source activate test
36-
#- conda config --add channels phobson
37-
- conda install --yes numpy nose scipy matplotlib=1.4 coverage docopt requests pyyaml
32+
- conda install --yes numpy nose scipy matplotlib coverage docopt requests pyyaml
3833
- pip install coveralls
3934
- pip install .
4035

4136
script:
42-
- nosetests --with-coverage --cover-package=probscale --verbose
37+
- python check_probscale.py --with-coverage --cover-package=probscale --verbose
4338

4439
after_success:
4540
- if [ ${COVERAGE} = true ]; then coveralls; fi

check_probscale.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import matplotlib
2+
matplotlib.use('agg')
3+
4+
import nose
5+
nose.main()

docs/tutorial/getting_started.ipynb

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
"collapsed": true
3232
},
3333
"outputs": [],
34+
"source": [
35+
"%matplotlib inline"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": null,
41+
"metadata": {
42+
"collapsed": false
43+
},
44+
"outputs": [],
3445
"source": [
3546
"import warnings\n",
3647
"warnings.simplefilter('ignore')\n",
@@ -41,8 +52,7 @@
4152
"import seaborn\n",
4253
"\n",
4354
"clear_bkgd = {'axes.facecolor':'none', 'figure.facecolor':'none'}\n",
44-
"seaborn.set(style='ticks', context='notebook', rc=clear_bkgd)\n",
45-
"%matplotlib inline"
55+
"seaborn.set(style='ticks', context='notebook', rc=clear_bkgd)"
4656
]
4757
},
4858
{
@@ -94,7 +104,7 @@
94104
"ax2.set_xlabel(\"Base 2\")\n",
95105
"ax2.set_yticks([])\n",
96106
"\n",
97-
"seaborn.despine(fig=fig, left=True)\n"
107+
"seaborn.despine(fig=fig, left=True)"
98108
]
99109
},
100110
{
@@ -133,14 +143,15 @@
133143
"cell_type": "markdown",
134144
"metadata": {},
135145
"source": [
136-
"It works after importing. Probability scales default to the standard normal distribution. Not that the formatting is a percentage-based probability."
146+
"To access probability scales, simply import :py:mod:`probscale`."
137147
]
138148
},
139149
{
140150
"cell_type": "code",
141151
"execution_count": null,
142152
"metadata": {
143-
"collapsed": false
153+
"collapsed": false,
154+
"scrolled": false
144155
},
145156
"outputs": [],
146157
"source": [
@@ -156,9 +167,11 @@
156167
"cell_type": "markdown",
157168
"metadata": {},
158169
"source": [
170+
"Probability scales default to the standard normal distribution (ote that the formatting is a percentage-based probability)\n",
171+
"\n",
159172
"You can even use different probability distributions, though it can be tricky. You have to pass a frozen distribution to the `dist` kwarg in `ax.set_[x|y]scale`.\n",
160173
"\n",
161-
"Here's a standard normal scale right next to a beta scale ($\\alpha = 1$ and $\\beta = 1$) for comparison"
174+
"Here's a standard normal scale right next to two different beta scales for comparison"
162175
]
163176
},
164177
{
@@ -171,9 +184,7 @@
171184
"outputs": [],
172185
"source": [
173186
"import paramnormal\n",
174-
"\n",
175-
"beta1 = paramnormal.beta(α=3, β=2)\n",
176-
"beta2 = paramnormal.beta(α=2, β=7)\n",
187+
"seaborn.set(style='ticks', context='notebook', rc=clear_bkgd)\n",
177188
"\n",
178189
"fig, (ax1, ax2, ax3, ax4) = pyplot.subplots(figsize=(8, 5), nrows=4)\n",
179190
"\n",
@@ -182,11 +193,13 @@
182193
"ax1.set_xlabel('Normal probability scale')\n",
183194
"ax1.set_yticks([])\n",
184195
"\n",
196+
"beta1 = paramnormal.beta(α=3, β=2)\n",
185197
"ax2.set_xscale('prob', dist=beta1)\n",
186198
"ax2.set_xlim(left=2, right=98)\n",
187199
"ax2.set_xlabel('Beta probability scale (α=3, β=2)')\n",
188200
"ax2.set_yticks([])\n",
189201
"\n",
202+
"beta2 = paramnormal.beta(α=2, β=7)\n",
190203
"ax3.set_xscale('prob', dist=beta2)\n",
191204
"ax3.set_xlim(left=2, right=98)\n",
192205
"ax3.set_xlabel('Beta probability scale (α=2, β=7)')\n",
@@ -197,7 +210,7 @@
197210
"ax4.set_xlabel('Linear scale (for reference)')\n",
198211
"ax4.set_yticks([])\n",
199212
"\n",
200-
"seaborn.despine(fig=fig, left=True)\n"
213+
"seaborn.despine(fig=fig, left=True)"
201214
]
202215
},
203216
{
@@ -292,10 +305,17 @@
292305
"metadata": {},
293306
"source": [
294307
"### Percentile and Quanitile plots\n",
295-
"For convenience, you can do percetile and quantile plots with the same function.\n",
296-
"\n",
308+
"For convenience, you can do percetile and quantile plots with the same function."
309+
]
310+
},
311+
{
312+
"cell_type": "raw",
313+
"metadata": {},
314+
"source": [
297315
".. note::\n",
298-
" The percentile and probability axes are plotted against the same values. The difference is only that \"percentiles\" are plotted on a linear scale."
316+
" The percentile and probability axes are plotted against the\n",
317+
" same values. The difference is only that \"percentiles\" \n",
318+
" are plotted on a linear scale."
299319
]
300320
},
301321
{
@@ -334,4 +354,4 @@
334354
},
335355
"nbformat": 4,
336356
"nbformat_minor": 0
337-
}
357+
}

probscale/probscale.py

Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,29 @@
77
NullLocator,
88
Formatter,
99
NullFormatter,
10-
FuncFormatter
10+
FuncFormatter,
1111
)
1212

1313

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
31+
32+
1433
class _minimal_norm(object):
1534
_A = -(8 * (np.pi - 3.0) / (3.0 * np.pi * (np.pi - 4.0)))
1635

@@ -44,7 +63,6 @@ def ppf(cls, q):
4463
Wikipedia: https://goo.gl/Rtxjme
4564
4665
"""
47-
4866
return np.sqrt(2) * cls._approx_inv_erf(2*q - 1)
4967

5068
@classmethod
@@ -57,7 +75,7 @@ def cdf(cls, x):
5775
return 0.5 * (1 + cls._approx_erf(x/np.sqrt(2)))
5876

5977

60-
class ProbFormatter(Formatter):
78+
class _FormatterMixin(Formatter):
6179
@classmethod
6280
def _sig_figs(cls, x, n, expthresh=5, forceint=False):
6381
""" Formats a number with the correct number of sig figs.
@@ -128,49 +146,67 @@ def _sig_figs(cls, x, n, expthresh=5, forceint=False):
128146
return out
129147

130148
def __call__(self, x, pos=None):
131-
if x < 10:
149+
if x < (10 / self.factor):
132150
out = self._sig_figs(x, 1)
133-
elif x <= 99:
151+
elif x <= (99 / self.factor):
134152
out = self._sig_figs(x, 2)
135153
else:
136-
order = np.ceil(np.round(np.abs(np.log10(100 - x)), 6))
137-
out = self._sig_figs(x, order + 2)
154+
order = np.ceil(np.round(np.abs(np.log10(self.top - x)), 6))
155+
out = self._sig_figs(x, order + self.offset)
138156

139157
return '{}'.format(out)
140158

141159

142-
class ProbTransform(Transform):
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):
143173
input_dims = 1
144174
output_dims = 1
145175
is_separable = True
146176
has_inverse = True
147177

148-
def __init__(self, dist):
178+
def __init__(self, dist, as_pct=True, nonpos='mask'):
149179
Transform.__init__(self)
150180
self.dist = dist
181+
if as_pct:
182+
self.factor = 100.0
183+
else:
184+
self.factor = 1.0
151185

152-
def transform_non_affine(self, a):
153-
return self.dist.ppf(a / 100.)
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'")
154192

155-
def inverted(self):
156-
return InvertedProbTransform(self.dist)
157193

194+
class ProbTransform(_ProbTransformMixin):
195+
def transform_non_affine(self, prob):
196+
q = self.dist.ppf(prob / self.factor)
197+
return q
158198

159-
class InvertedProbTransform(Transform):
160-
input_dims = 1
161-
output_dims = 1
162-
is_separable = True
163-
has_inverse = True
199+
def inverted(self):
200+
return QuantileTransform(self.dist, as_pct=self.as_pct, nonpos=self.nonpos)
164201

165-
def __init__(self, dist):
166-
self.dist = dist
167-
Transform.__init__(self)
168202

169-
def transform_non_affine(self, a):
170-
return self.dist.cdf(a) * 100.
203+
class QuantileTransform(_ProbTransformMixin):
204+
def transform_non_affine(self, q):
205+
prob = self.dist.cdf(q) * self.factor
206+
return prob
171207

172208
def inverted(self):
173-
return ProbTransform(self.dist)
209+
return ProbTransform(self.dist, as_pct=self.as_pct, nonpos=self.nonpos)
174210

175211

176212
class ProbScale(ScaleBase):
@@ -199,13 +235,19 @@ class ProbScale(ScaleBase):
199235

200236
def __init__(self, axis, **kwargs):
201237
self.dist = kwargs.pop('dist', _minimal_norm)
202-
self._transform = ProbTransform(self.dist)
238+
self.as_pct = kwargs.pop('as_pct', True)
239+
self.nonpos = kwargs.pop('nonpos', 'mask')
240+
self._transform = ProbTransform(self.dist, as_pct=self.as_pct)
203241

204242
@classmethod
205-
def _get_probs(cls, nobs):
243+
def _get_probs(cls, nobs, as_pct):
206244
""" Returns the x-axis labels for a probability plot based on
207245
the number of observations (`nobs`).
208246
"""
247+
if as_pct:
248+
factor = 1.0
249+
else:
250+
factor = 100.0
209251

210252
order = int(np.floor(np.log10(nobs)))
211253
base_probs = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90])
@@ -219,19 +261,23 @@ def _get_probs(cls, nobs):
219261
lower_fringe = np.array([1])
220262
upper_fringe = np.array([9])
221263

222-
new_lower = lower_fringe/10**(n)
223-
new_upper = upper_fringe/10**(n) + axis_probs.max()
264+
new_lower = lower_fringe / 10**(n)
265+
new_upper = upper_fringe / 10**(n) + axis_probs.max()
224266
axis_probs = np.hstack([new_lower, axis_probs, new_upper])
225-
226-
return axis_probs
267+
locs = axis_probs / factor
268+
return locs
227269

228270
def set_default_locators_and_formatters(self, axis):
229271
"""
230272
Set the locators and formatters to specialized versions for
231273
log scaling.
232274
"""
233-
axis.set_major_locator(FixedLocator(self._get_probs(1e10)))
234-
axis.set_major_formatter(FuncFormatter(ProbFormatter()))
275+
276+
axis.set_major_locator(FixedLocator(self._get_probs(1e8, self.as_pct)))
277+
if self.as_pct:
278+
axis.set_major_formatter(FuncFormatter(PctFormatter()))
279+
else:
280+
axis.set_major_formatter(FuncFormatter(ProbFormatter()))
235281
axis.set_minor_locator(NullLocator())
236282
axis.set_minor_formatter(NullFormatter())
237283

0 commit comments

Comments
 (0)