py-flexplot 0.8.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_flexplot-0.8.2.dist-info/METADATA +272 -0
- py_flexplot-0.8.2.dist-info/RECORD +16 -0
- py_flexplot-0.8.2.dist-info/WHEEL +5 -0
- py_flexplot-0.8.2.dist-info/licenses/LICENSE +21 -0
- py_flexplot-0.8.2.dist-info/top_level.txt +1 -0
- pyflexplot/__init__.py +60 -0
- pyflexplot/bluepill.py +461 -0
- pyflexplot/core.py +3410 -0
- pyflexplot/descriptives.py +287 -0
- pyflexplot/ebbr.py +148 -0
- pyflexplot/flex_nn.py +583 -0
- pyflexplot/ml.py +175 -0
- pyflexplot/quality.py +372 -0
- pyflexplot/sem.py +195 -0
- pyflexplot/stats.py +803 -0
- pyflexplot/uncertainty.py +185 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
descriptives: Descriptive-statistics visualizations.
|
|
3
|
+
|
|
4
|
+
A port of R's ``fifer::meansplot()``: takes a numeric ``y ~ group`` formula
|
|
5
|
+
and shows the mean (with an error bar) per group, optionally connecting
|
|
6
|
+
the means with a line.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from pyflexplot.descriptives import meansplot
|
|
11
|
+
|
|
12
|
+
p = meansplot("weight ~ diet", data=df)
|
|
13
|
+
p = meansplot("weight ~ diet", data=df, error="sd") # SD instead of SE
|
|
14
|
+
p = meansplot("weight ~ diet", data=df, error="ci") # 95% CI on the mean
|
|
15
|
+
p = meansplot("weight ~ diet", data=df, connect=True) # line connecting means
|
|
16
|
+
|
|
17
|
+
This module is intentionally separate from ``core.py`` so the descriptive-
|
|
18
|
+
stats surface doesn't bloat the formula-dispatch logic in ``flexplot()``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from plotnine import (
|
|
27
|
+
aes,
|
|
28
|
+
geom_errorbar,
|
|
29
|
+
geom_line,
|
|
30
|
+
geom_point,
|
|
31
|
+
geom_tile,
|
|
32
|
+
ggplot,
|
|
33
|
+
labs,
|
|
34
|
+
scale_fill_gradient,
|
|
35
|
+
scale_color_gradient,
|
|
36
|
+
theme_bw,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from .core import parse_flexplot_formula, _validate_data_for_plot
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_VALID_ERROR = {"se", "sd", "ci", "range", "iqr", "no"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def meansplot(
|
|
46
|
+
formula: str,
|
|
47
|
+
data: pd.DataFrame,
|
|
48
|
+
error: str = "se",
|
|
49
|
+
level: float = 0.95,
|
|
50
|
+
connect: bool = True,
|
|
51
|
+
):
|
|
52
|
+
"""Plot the mean of y per level of x with an error bar.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
formula : str
|
|
57
|
+
Formula of the form ``y ~ group``. ``group`` may be a single
|
|
58
|
+
categorical variable (string/object) or a numeric variable with
|
|
59
|
+
few enough unique values to be treated as discrete.
|
|
60
|
+
data : pd.DataFrame
|
|
61
|
+
The dataset.
|
|
62
|
+
error : {"se", "sd", "ci", "range", "iqr", "no"}, default "se"
|
|
63
|
+
Kind of error bar to draw around each mean:
|
|
64
|
+
- ``"se"``: standard error of the mean (default).
|
|
65
|
+
- ``"sd"``: standard deviation.
|
|
66
|
+
- ``"ci"``: ``level`` confidence interval on the mean.
|
|
67
|
+
- ``"range"``: min-max range.
|
|
68
|
+
- ``"iqr"``: Q1-Q3 IQR.
|
|
69
|
+
- ``"no"``: no error bar.
|
|
70
|
+
level : float, default 0.95
|
|
71
|
+
Coverage probability for ``error="ci"``. Ignored otherwise.
|
|
72
|
+
connect : bool, default True
|
|
73
|
+
If ``True``, draw a line connecting the per-group means (useful
|
|
74
|
+
for ordinal predictors where the trend matters).
|
|
75
|
+
"""
|
|
76
|
+
if error not in _VALID_ERROR:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"error must be one of {sorted(_VALID_ERROR)}; got {error!r}."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
variables = parse_flexplot_formula(formula)
|
|
82
|
+
_validate_data_for_plot(formula, data, variables)
|
|
83
|
+
y = variables["y"]
|
|
84
|
+
x = variables["x"]
|
|
85
|
+
if variables.get("color"):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"meansplot does not support a `color` term; got formula "
|
|
88
|
+
f"{formula!r} with color={variables['color']!r}."
|
|
89
|
+
)
|
|
90
|
+
if variables.get("given"):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"meansplot does not support `given` terms (faceting); got "
|
|
93
|
+
f"formula {formula!r} with given={variables['given']!r}."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if not pd.api.types.is_numeric_dtype(data[y]):
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"meansplot requires a numeric y; got {y!r} with dtype "
|
|
99
|
+
f"{data[y].dtype}."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Group by x and compute summary statistics.
|
|
103
|
+
grouped = data.groupby(x, observed=True, sort=True)[y]
|
|
104
|
+
summary = grouped.agg(["count", "mean", "std"]).reset_index()
|
|
105
|
+
|
|
106
|
+
if error == "se":
|
|
107
|
+
summary["__lower"] = summary["mean"] - summary["std"] / np.sqrt(summary["count"])
|
|
108
|
+
summary["__upper"] = summary["mean"] + summary["std"] / np.sqrt(summary["count"])
|
|
109
|
+
elif error == "sd":
|
|
110
|
+
summary["__lower"] = summary["mean"] - summary["std"]
|
|
111
|
+
summary["__upper"] = summary["mean"] + summary["std"]
|
|
112
|
+
elif error == "ci":
|
|
113
|
+
from scipy import stats as _scipy_stats
|
|
114
|
+
# 95% CI on the mean using a t-distribution (n-1 df).
|
|
115
|
+
se = summary["std"] / np.sqrt(summary["count"])
|
|
116
|
+
df = summary["count"] - 1
|
|
117
|
+
t_crit = _scipy_stats.t.ppf(0.5 + level / 2, df)
|
|
118
|
+
summary["__lower"] = summary["mean"] - t_crit * se
|
|
119
|
+
summary["__upper"] = summary["mean"] + t_crit * se
|
|
120
|
+
elif error == "range":
|
|
121
|
+
summary["__lower"] = grouped.min().to_numpy()
|
|
122
|
+
summary["__upper"] = grouped.max().to_numpy()
|
|
123
|
+
elif error == "iqr":
|
|
124
|
+
summary["__lower"] = grouped.quantile(0.25).to_numpy()
|
|
125
|
+
summary["__upper"] = grouped.quantile(0.75).to_numpy()
|
|
126
|
+
elif error == "no":
|
|
127
|
+
# No error bars; we'll skip the geom_errorbar layer below.
|
|
128
|
+
summary["__lower"] = summary["mean"]
|
|
129
|
+
summary["__upper"] = summary["mean"]
|
|
130
|
+
|
|
131
|
+
# If x is numeric with few unique values, coerce to categorical so
|
|
132
|
+
# plotnine treats it as discrete levels (matches R's behavior).
|
|
133
|
+
plot_df = summary.copy()
|
|
134
|
+
if pd.api.types.is_numeric_dtype(plot_df[x]):
|
|
135
|
+
plot_df[x] = plot_df[x].astype(str)
|
|
136
|
+
|
|
137
|
+
p = (
|
|
138
|
+
ggplot(plot_df, aes(x=x, y="mean"))
|
|
139
|
+
+ geom_point(size=3, color="black")
|
|
140
|
+
+ labs(
|
|
141
|
+
x=x,
|
|
142
|
+
y=f"mean({y})",
|
|
143
|
+
title=f"Means plot: {y} by {x}",
|
|
144
|
+
)
|
|
145
|
+
+ theme_bw()
|
|
146
|
+
)
|
|
147
|
+
if error != "no":
|
|
148
|
+
p += geom_errorbar(
|
|
149
|
+
aes(ymin="__lower", ymax="__upper"),
|
|
150
|
+
width=0.2,
|
|
151
|
+
color="black",
|
|
152
|
+
)
|
|
153
|
+
if connect:
|
|
154
|
+
p += geom_line(
|
|
155
|
+
mapping=aes(x=x, y="mean", group=1),
|
|
156
|
+
color="gray",
|
|
157
|
+
linetype="dashed",
|
|
158
|
+
)
|
|
159
|
+
return p
|
|
160
|
+
|
|
161
|
+
_VALID_SCATTER3D_TYPE = {"points", "tile"}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def scatter3D(
|
|
165
|
+
formula: str,
|
|
166
|
+
data: pd.DataFrame,
|
|
167
|
+
type: str = "points",
|
|
168
|
+
bins: int = 20,
|
|
169
|
+
):
|
|
170
|
+
"""Visualize the relationship between a numeric outcome and two continuous predictors.
|
|
171
|
+
|
|
172
|
+
A 2D rendering of R-flexplot's ``scatter3D()``: plots ``x`` on the
|
|
173
|
+
horizontal axis and ``z`` on the vertical axis, with the outcome
|
|
174
|
+
``y`` mapped to either point color (``type='points'``, the default)
|
|
175
|
+
or tile fill (``type='tile'``).
|
|
176
|
+
|
|
177
|
+
This is a *projection* of the 3D relationship ``y ~ x + z`` — not a
|
|
178
|
+
true 3D scatter, since plotnine renders to 2D. R-flexplot's
|
|
179
|
+
``scatter3D()`` uses the rgl package for true 3D; that backend is
|
|
180
|
+
out of scope for the Python port. This 2D projection still surfaces
|
|
181
|
+
the relationship structure and is the closest faithful rendering
|
|
182
|
+
without adding a 3D plotting dependency.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
formula : str
|
|
187
|
+
Formula of the form ``y ~ x + z``. ``y``, ``x``, and ``z`` must
|
|
188
|
+
all be numeric. The parser uses ``x`` as the horizontal axis,
|
|
189
|
+
``z`` as the vertical axis, and ``y`` as the color / fill.
|
|
190
|
+
data : pd.DataFrame
|
|
191
|
+
The dataset.
|
|
192
|
+
type : {"points", "tile"}, default "points"
|
|
193
|
+
- ``"points"``: scatter of (x, z), color = y. Best for raw
|
|
194
|
+
inspection of the (x, z) -> y relationship.
|
|
195
|
+
- ``"tile"``: aggregate y into a (bins x bins) grid and draw
|
|
196
|
+
a heatmap. Best for dense data where point overlap obscures
|
|
197
|
+
structure.
|
|
198
|
+
bins : int, default 20
|
|
199
|
+
Number of bins per axis when ``type='tile'``. Ignored when
|
|
200
|
+
``type='points'``.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
plotnine.ggplot
|
|
205
|
+
"""
|
|
206
|
+
if type not in _VALID_SCATTER3D_TYPE:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"type must be one of {sorted(_VALID_SCATTER3D_TYPE)}; got {type!r}."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
variables = parse_flexplot_formula(formula)
|
|
212
|
+
_validate_data_for_plot(formula, data, variables)
|
|
213
|
+
y = variables["y"]
|
|
214
|
+
x = variables["x"]
|
|
215
|
+
# parse_flexplot_formula returns the first atom as x and the second
|
|
216
|
+
# (when present) as color. For y ~ x + z, z is exposed as the
|
|
217
|
+
# "color" term in the parser; we treat it as the vertical axis.
|
|
218
|
+
z = variables.get("color")
|
|
219
|
+
if z is None:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"scatter3D requires a formula with two predictors: y ~ x + z. "
|
|
222
|
+
f"Got {formula!r}."
|
|
223
|
+
)
|
|
224
|
+
if variables.get("given"):
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"scatter3D does not support `given` terms (faceting); got "
|
|
227
|
+
f"formula {formula!r} with given={variables['given']!r}."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
for col in (y, x, z):
|
|
231
|
+
if not pd.api.types.is_numeric_dtype(data[col]):
|
|
232
|
+
raise ValueError(
|
|
233
|
+
f"scatter3D requires numeric columns for y, x, and z; "
|
|
234
|
+
f"got column {col!r} with dtype {data[col].dtype}."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if type == "points":
|
|
238
|
+
p = (
|
|
239
|
+
ggplot(data, aes(x=x, y=z, color=y))
|
|
240
|
+
+ geom_point(alpha=0.6)
|
|
241
|
+
+ scale_color_gradient(low="#fee8c8", high="#7f0000", name=y)
|
|
242
|
+
+ labs(
|
|
243
|
+
x=x,
|
|
244
|
+
y=z,
|
|
245
|
+
color=y,
|
|
246
|
+
title=f"scatter3D: {y} by {x} + {z}",
|
|
247
|
+
)
|
|
248
|
+
+ theme_bw()
|
|
249
|
+
)
|
|
250
|
+
return p
|
|
251
|
+
|
|
252
|
+
# type == "tile": bin (x, z) into a grid; compute the mean of y per bin.
|
|
253
|
+
work = data[[x, z, y]].copy()
|
|
254
|
+
work["__x_bin"] = pd.cut(work[x], bins=bins, labels=False, include_lowest=True)
|
|
255
|
+
work["__z_bin"] = pd.cut(work[z], bins=bins, labels=False, include_lowest=True)
|
|
256
|
+
agg = (
|
|
257
|
+
work.groupby(["__x_bin", "__z_bin"], observed=True)[y]
|
|
258
|
+
.mean()
|
|
259
|
+
.reset_index()
|
|
260
|
+
)
|
|
261
|
+
# Recover bin centers for the aesthetic mapping.
|
|
262
|
+
x_cuts = pd.cut(data[x], bins=bins, include_lowest=True)
|
|
263
|
+
z_cuts = pd.cut(data[z], bins=bins, include_lowest=True)
|
|
264
|
+
x_bin_centers = {
|
|
265
|
+
i: float((interval.left + interval.right) / 2.0)
|
|
266
|
+
for i, interval in enumerate(x_cuts.cat.categories)
|
|
267
|
+
}
|
|
268
|
+
z_bin_centers = {
|
|
269
|
+
i: float((interval.left + interval.right) / 2.0)
|
|
270
|
+
for i, interval in enumerate(z_cuts.cat.categories)
|
|
271
|
+
}
|
|
272
|
+
agg["__x_center"] = agg["__x_bin"].map(x_bin_centers)
|
|
273
|
+
agg["__z_center"] = agg["__z_bin"].map(z_bin_centers)
|
|
274
|
+
|
|
275
|
+
p = (
|
|
276
|
+
ggplot(agg, aes(x="__x_center", y="__z_center", fill=y))
|
|
277
|
+
+ geom_tile()
|
|
278
|
+
+ scale_fill_gradient(low="#fee8c8", high="#7f0000", name=f"mean({y})")
|
|
279
|
+
+ labs(
|
|
280
|
+
x=x,
|
|
281
|
+
y=z,
|
|
282
|
+
fill=f"mean({y})",
|
|
283
|
+
title=f"scatter3D tile: {y} by {x} + {z}",
|
|
284
|
+
)
|
|
285
|
+
+ theme_bw()
|
|
286
|
+
)
|
|
287
|
+
return p
|
pyflexplot/ebbr.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Empirical Bayes binomial estimation (ported from ebbr)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from scipy.optimize import minimize
|
|
6
|
+
from scipy.special import betaln, gammaln
|
|
7
|
+
from scipy.stats import beta as beta_dist
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class BetaPrior:
|
|
13
|
+
alpha: float
|
|
14
|
+
beta: float
|
|
15
|
+
n_obs: int
|
|
16
|
+
method: str = "mle"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def mean(self) -> float:
|
|
20
|
+
return self.alpha / (self.alpha + self.beta)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _beta_binomial_loglik(params, successes, totals):
|
|
24
|
+
alpha, beta = params
|
|
25
|
+
if alpha <= 0 or beta <= 0:
|
|
26
|
+
return np.inf
|
|
27
|
+
log_coeff = gammaln(totals + 1) - gammaln(successes + 1) - gammaln(
|
|
28
|
+
totals - successes + 1
|
|
29
|
+
)
|
|
30
|
+
ll = (
|
|
31
|
+
log_coeff
|
|
32
|
+
+ betaln(successes + alpha, totals - successes + beta)
|
|
33
|
+
- betaln(alpha, beta)
|
|
34
|
+
)
|
|
35
|
+
return -np.sum(ll)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fit_beta_prior(successes, totals, method="mle"):
|
|
39
|
+
"""
|
|
40
|
+
Fit a Beta prior to observed binomial counts via MLE or method of moments.
|
|
41
|
+
|
|
42
|
+
Validates that ``0 <= successes <= totals`` and ``totals > 0``. Guards
|
|
43
|
+
against zero variance in the observed rates and verifies that the
|
|
44
|
+
optimizer converged to finite parameters.
|
|
45
|
+
"""
|
|
46
|
+
successes = np.asarray(successes, dtype=float)
|
|
47
|
+
totals = np.asarray(totals, dtype=float)
|
|
48
|
+
|
|
49
|
+
if successes.ndim != 1 or totals.ndim != 1:
|
|
50
|
+
raise ValueError("successes and totals must be one-dimensional arrays/Series")
|
|
51
|
+
if len(successes) != len(totals):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"successes and totals must have the same length: "
|
|
54
|
+
f"{len(successes)} vs {len(totals)}"
|
|
55
|
+
)
|
|
56
|
+
if len(successes) == 0:
|
|
57
|
+
raise ValueError("successes and totals must not be empty")
|
|
58
|
+
|
|
59
|
+
if np.any((successes < 0) | (successes > totals)):
|
|
60
|
+
bad = np.where((successes < 0) | (successes > totals))[0]
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"successes must satisfy 0 <= successes <= totals. Bad indices: {bad[:10].tolist()}"
|
|
63
|
+
)
|
|
64
|
+
if np.any(totals <= 0):
|
|
65
|
+
bad = np.where(totals <= 0)[0]
|
|
66
|
+
raise ValueError(f"totals must be positive. Bad indices: {bad[:10].tolist()}")
|
|
67
|
+
if np.any(np.isnan(successes)) or np.any(np.isnan(totals)):
|
|
68
|
+
raise ValueError("successes and totals must not contain NaN")
|
|
69
|
+
|
|
70
|
+
rates = successes / totals
|
|
71
|
+
m = rates.mean()
|
|
72
|
+
v = rates.var()
|
|
73
|
+
|
|
74
|
+
# Guard zero variance: all rates identical => no information to estimate a
|
|
75
|
+
# beta prior. Return a weakly informative prior rather than NaN/Inf seeds.
|
|
76
|
+
if v == 0:
|
|
77
|
+
if method == "moments":
|
|
78
|
+
return BetaPrior(1.0, 1.0, len(successes), "moments")
|
|
79
|
+
return BetaPrior(1.0, 1.0, len(successes), "mle")
|
|
80
|
+
|
|
81
|
+
common = m * (1 - m) / v - 1
|
|
82
|
+
alpha0 = m * common
|
|
83
|
+
beta0 = (1 - m) * common
|
|
84
|
+
|
|
85
|
+
# Clip method-of-moments seeds to avoid negative or extreme initial values.
|
|
86
|
+
alpha0 = max(1e-6, min(alpha0, 1e6))
|
|
87
|
+
beta0 = max(1e-6, min(beta0, 1e6))
|
|
88
|
+
|
|
89
|
+
if method == "moments":
|
|
90
|
+
return BetaPrior(alpha0, beta0, len(successes), "moments")
|
|
91
|
+
|
|
92
|
+
res = minimize(
|
|
93
|
+
_beta_binomial_loglik,
|
|
94
|
+
x0=[alpha0, beta0],
|
|
95
|
+
args=(successes, totals),
|
|
96
|
+
bounds=((1e-6, None), (1e-6, None)),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if not res.success:
|
|
100
|
+
raise RuntimeError(
|
|
101
|
+
f"Beta prior optimization did not converge: {res.message}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
alpha_hat, beta_hat = res.x
|
|
105
|
+
if not (np.isfinite(alpha_hat) and np.isfinite(beta_hat) and alpha_hat > 0 and beta_hat > 0):
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
f"Beta prior optimization returned invalid parameters: alpha={alpha_hat}, beta={beta_hat}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return BetaPrior(alpha_hat, beta_hat, len(successes), "mle")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def add_ebb_estimate(df, success_col, total_col, prior=None):
|
|
114
|
+
"""
|
|
115
|
+
Add empirical-Bayes beta-binomial shrinkage estimates to a DataFrame.
|
|
116
|
+
"""
|
|
117
|
+
if not isinstance(df, pd.DataFrame):
|
|
118
|
+
raise TypeError(f"df must be a pandas DataFrame, got {type(df).__name__}")
|
|
119
|
+
if success_col not in df.columns:
|
|
120
|
+
raise ValueError(f"success_col {success_col!r} not found in DataFrame")
|
|
121
|
+
if total_col not in df.columns:
|
|
122
|
+
raise ValueError(f"total_col {total_col!r} not found in DataFrame")
|
|
123
|
+
|
|
124
|
+
successes = pd.to_numeric(df[success_col], errors="coerce")
|
|
125
|
+
totals = pd.to_numeric(df[total_col], errors="coerce")
|
|
126
|
+
|
|
127
|
+
if successes.isna().any() or totals.isna().any():
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"{success_col!r} and {total_col!r} must be numeric and non-missing"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if prior is None:
|
|
133
|
+
prior = fit_beta_prior(successes.values, totals.values)
|
|
134
|
+
|
|
135
|
+
# Use scalar arithmetic on the underlying arrays to avoid index-alignment
|
|
136
|
+
# surprises when df has a non-default index.
|
|
137
|
+
alpha1 = prior.alpha + successes.values
|
|
138
|
+
beta1 = prior.beta + totals.values - successes.values
|
|
139
|
+
|
|
140
|
+
fitted = alpha1 / (alpha1 + beta1)
|
|
141
|
+
low = beta_dist.ppf(0.025, alpha1, beta1)
|
|
142
|
+
high = beta_dist.ppf(0.975, alpha1, beta1)
|
|
143
|
+
|
|
144
|
+
out = df.copy()
|
|
145
|
+
out["ebb_fitted"] = fitted
|
|
146
|
+
out["ebb_low"] = low
|
|
147
|
+
out["ebb_high"] = high
|
|
148
|
+
return out
|