lessPython 0.1.0__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.
- lessPy/ANOVA.py +680 -0
- lessPy/Chart.py +1055 -0
- lessPy/Correlation.py +236 -0
- lessPy/Flows.py +116 -0
- lessPy/Logit.py +615 -0
- lessPy/Prop_test.py +267 -0
- lessPy/Regression.py +1491 -0
- lessPy/VariableLabels.py +119 -0
- lessPy/X.py +426 -0
- lessPy/XY.py +2007 -0
- lessPy/__init__.py +60 -0
- lessPy/anova_rmd.py +227 -0
- lessPy/bc_plotly.py +575 -0
- lessPy/bubble_plotly.py +470 -0
- lessPy/corCFA.py +316 -0
- lessPy/corEFA.py +220 -0
- lessPy/corPrint.py +45 -0
- lessPy/corProp.py +73 -0
- lessPy/corRead.py +48 -0
- lessPy/corReflect.py +72 -0
- lessPy/corReorder.py +161 -0
- lessPy/corScree.py +87 -0
- lessPy/data/Anova_1way.csv +25 -0
- lessPy/data/Anova_2way.csv +49 -0
- lessPy/data/Anova_rb.csv +8 -0
- lessPy/data/Anova_rbf.csv +49 -0
- lessPy/data/Anova_sp.csv +57 -0
- lessPy/data/BodyMeas.csv +341 -0
- lessPy/data/Cars93.csv +94 -0
- lessPy/data/Employee.csv +38 -0
- lessPy/data/Employee_lbl.csv +9 -0
- lessPy/data/FreqTable99.csv +5 -0
- lessPy/data/Jackets.csv +1026 -0
- lessPy/data/Learn.csv +35 -0
- lessPy/data/Mach4.csv +352 -0
- lessPy/data/Mach4_lbl.csv +21 -0
- lessPy/data/Reading.csv +101 -0
- lessPy/data/StockPrice.csv +1489 -0
- lessPy/data/WeightLoss.csv +11 -0
- lessPy/datasets.py +46 -0
- lessPy/date_infer.py +112 -0
- lessPy/details.py +314 -0
- lessPy/dn_plotly.py +495 -0
- lessPy/dot_plotly.py +385 -0
- lessPy/freq_poly_plotly.py +324 -0
- lessPy/getColors.py +399 -0
- lessPy/hier_plotly.py +352 -0
- lessPy/hs_plotly.py +395 -0
- lessPy/logit_rmd.py +410 -0
- lessPy/order_by.py +94 -0
- lessPy/pie_plotly.py +292 -0
- lessPy/pivot.py +158 -0
- lessPy/plotly_utils.py +787 -0
- lessPy/plt_add.py +129 -0
- lessPy/plt_contour.py +192 -0
- lessPy/plt_contour_facet.py +194 -0
- lessPy/plt_forecast.py +677 -0
- lessPy/plt_mat_plotly.py +201 -0
- lessPy/plt_plotly.py +216 -0
- lessPy/plt_smooth.py +170 -0
- lessPy/plt_time.py +143 -0
- lessPy/prob_norm.py +111 -0
- lessPy/prob_tcut.py +131 -0
- lessPy/prob_znorm.py +110 -0
- lessPy/radar_plotly.py +201 -0
- lessPy/reg_rmd.py +754 -0
- lessPy/rename.py +33 -0
- lessPy/reshape.py +95 -0
- lessPy/showColors.py +130 -0
- lessPy/simCImean.py +165 -0
- lessPy/simCLT.py +265 -0
- lessPy/simFlips.py +104 -0
- lessPy/simMeans.py +146 -0
- lessPy/stats_out.py +189 -0
- lessPy/ttest.py +641 -0
- lessPy/utils.py +235 -0
- lessPy/vbs_plotly.py +545 -0
- lesspython-0.1.0.dist-info/METADATA +93 -0
- lesspython-0.1.0.dist-info/RECORD +82 -0
- lesspython-0.1.0.dist-info/WHEEL +5 -0
- lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
- lesspython-0.1.0.dist-info/top_level.txt +1 -0
lessPy/plt_time.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# plt_time.py — analog of plt.time.R
|
|
2
|
+
#
|
|
3
|
+
# ts_unit/ts_agg for XY(): aggregate a date-x time series to a
|
|
4
|
+
# coarser time unit, sum or mean per period. Ports .plt.time():
|
|
5
|
+
# infer the existing unit from the date spacing, refuse a
|
|
6
|
+
# requested unit finer than the data, drop the trailing
|
|
7
|
+
# incomplete period, aggregate per period — per by= level when
|
|
8
|
+
# present — with the date placed at the period start for
|
|
9
|
+
# months/quarters/years (weeks and days keep the last observed
|
|
10
|
+
# date of the period, as xts::period.apply indexes), and note
|
|
11
|
+
# gaps in the final series. pandas periods replace the xts/zoo
|
|
12
|
+
# machinery; the unit inference is the median-gap rule (the
|
|
13
|
+
# calendar-structure refinements of .plt.time() for series with
|
|
14
|
+
# missing periods are not ported).
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from .utils import category_order
|
|
20
|
+
|
|
21
|
+
_UNIT_ORDER = ("days", "weeks", "months", "quarters", "years")
|
|
22
|
+
_PERIOD_FREQ = {"weeks": "W-SUN", "months": "M",
|
|
23
|
+
"quarters": "Q", "years": "Y"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _infer_unit(dates):
|
|
27
|
+
"""Existing time unit from the median day gap, "unknown"
|
|
28
|
+
when irregular. R analog: the tu_exist logic of .plt.time()"""
|
|
29
|
+
d = np.sort(pd.unique(pd.Series(dates).dropna()))
|
|
30
|
+
if len(d) <= 1:
|
|
31
|
+
return "days"
|
|
32
|
+
gaps = np.diff(d).astype("timedelta64[D]").astype(int)
|
|
33
|
+
gaps = gaps[gaps > 0]
|
|
34
|
+
if len(gaps) == 0:
|
|
35
|
+
return "days"
|
|
36
|
+
med = float(np.median(gaps))
|
|
37
|
+
if med <= 1.5:
|
|
38
|
+
return "days"
|
|
39
|
+
if 6 <= med <= 8: # weekly, unless biweekly
|
|
40
|
+
return ("unknown" if (gaps == 14).mean() >= 0.40
|
|
41
|
+
else "weeks")
|
|
42
|
+
if 28 <= med <= 31:
|
|
43
|
+
return "months"
|
|
44
|
+
if 90 <= med <= 92:
|
|
45
|
+
return "quarters"
|
|
46
|
+
if 365 <= med <= 366:
|
|
47
|
+
return "years"
|
|
48
|
+
return "unknown"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _agg_one(dser, yser, ts_unit, aggfun):
|
|
52
|
+
"""Aggregate one series: truncate the trailing incomplete
|
|
53
|
+
period, then one value per period. R analogs: ts_truncate()
|
|
54
|
+
and the xts::period.apply block of .plt.time()"""
|
|
55
|
+
if ts_unit == "days": # collapse duplicate dates
|
|
56
|
+
per = dser
|
|
57
|
+
else:
|
|
58
|
+
per = dser.dt.to_period(_PERIOD_FREQ[ts_unit])
|
|
59
|
+
# trailing partial period: last date short of period end
|
|
60
|
+
last_per = per.iloc[int(np.argmax(dser.to_numpy()))]
|
|
61
|
+
if dser.max() < last_per.end_time.normalize():
|
|
62
|
+
keep = (per != last_per).to_numpy()
|
|
63
|
+
dser, yser, per = dser[keep], yser[keep], per[keep]
|
|
64
|
+
if len(dser) == 0:
|
|
65
|
+
return (np.array([], dtype="datetime64[ns]"),
|
|
66
|
+
np.array([]))
|
|
67
|
+
y_out = yser.groupby(per.to_numpy()).agg(
|
|
68
|
+
lambda s: aggfun(s.to_numpy())) # NaN propagates, as R
|
|
69
|
+
if ts_unit in ("months", "quarters", "years"):
|
|
70
|
+
x_out = y_out.index.to_timestamp(how="start")
|
|
71
|
+
else: # last observed date
|
|
72
|
+
x_out = dser.groupby(per.to_numpy()).max().to_numpy()
|
|
73
|
+
return np.asarray(x_out), y_out.to_numpy()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def plt_time(x_ser, y_ser, by_ser, ts_unit, ts_agg, quiet=True):
|
|
77
|
+
"""Aggregate the time series to ts_unit by ts_agg. Returns
|
|
78
|
+
(x_ser, y_ser, by_ser, ts_unit). R analog: .plt.time()"""
|
|
79
|
+
hold7 = ts_unit == "days7"
|
|
80
|
+
if hold7:
|
|
81
|
+
ts_unit = "days"
|
|
82
|
+
if ts_unit not in _UNIT_ORDER:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"ts_unit must be one of days, days7, weeks, months, "
|
|
85
|
+
"quarters, years")
|
|
86
|
+
|
|
87
|
+
unit_exist = _infer_unit(
|
|
88
|
+
x_ser.to_numpy() if by_ser is None
|
|
89
|
+
else x_ser[by_ser == category_order(by_ser)[0]]
|
|
90
|
+
.to_numpy())
|
|
91
|
+
if (unit_exist != "unknown"
|
|
92
|
+
and _UNIT_ORDER.index(ts_unit)
|
|
93
|
+
< _UNIT_ORDER.index(unit_exist)):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Resolution of data is {unit_exist}; requested "
|
|
96
|
+
f"{ts_unit} data is not available")
|
|
97
|
+
|
|
98
|
+
aggfun = np.sum if ts_agg == "sum" else np.mean
|
|
99
|
+
if by_ser is None:
|
|
100
|
+
xo, yo = _agg_one(x_ser, y_ser, ts_unit, aggfun)
|
|
101
|
+
x_out = pd.Series(xo, name=x_ser.name)
|
|
102
|
+
y_out = pd.Series(yo, name=y_ser.name)
|
|
103
|
+
by_out = None
|
|
104
|
+
else:
|
|
105
|
+
levels = category_order(by_ser)
|
|
106
|
+
xs, ys, bs = [], [], []
|
|
107
|
+
for lvl in levels:
|
|
108
|
+
m = (by_ser == lvl).to_numpy()
|
|
109
|
+
xl, yl = _agg_one(x_ser[m], y_ser[m], ts_unit,
|
|
110
|
+
aggfun)
|
|
111
|
+
xs.append(xl)
|
|
112
|
+
ys.append(yl)
|
|
113
|
+
bs.extend([lvl] * len(xl))
|
|
114
|
+
x_out = pd.Series(np.concatenate(xs), name=x_ser.name)
|
|
115
|
+
y_out = pd.Series(np.concatenate(ys), name=y_ser.name)
|
|
116
|
+
by_out = pd.Series(
|
|
117
|
+
pd.Categorical(bs, categories=levels),
|
|
118
|
+
name=by_ser.name)
|
|
119
|
+
|
|
120
|
+
# gaps in the aggregated series (first by level, as R)
|
|
121
|
+
d = np.sort(x_out[by_out == levels[0]].to_numpy()
|
|
122
|
+
if by_out is not None else x_out.to_numpy())
|
|
123
|
+
gaps = False
|
|
124
|
+
if len(d) > 1:
|
|
125
|
+
dd = pd.DatetimeIndex(d)
|
|
126
|
+
if ts_unit == "days":
|
|
127
|
+
gaps = (np.diff(d).astype("timedelta64[D]")
|
|
128
|
+
.astype(int) > 1).any()
|
|
129
|
+
elif ts_unit == "weeks":
|
|
130
|
+
gaps = (np.diff(d).astype("timedelta64[D]")
|
|
131
|
+
.astype(int) > 7).any()
|
|
132
|
+
elif ts_unit == "months":
|
|
133
|
+
gaps = (np.diff(dd.year * 12 + dd.month) > 1).any()
|
|
134
|
+
elif ts_unit == "quarters":
|
|
135
|
+
gaps = (np.diff(dd.year * 4 + dd.quarter) > 1).any()
|
|
136
|
+
elif ts_unit == "years":
|
|
137
|
+
gaps = (np.diff(dd.year) > 1).any()
|
|
138
|
+
if gaps and not quiet:
|
|
139
|
+
print("There are gaps in the dates, so that there are "
|
|
140
|
+
"not regular\nintervals between all the dates.")
|
|
141
|
+
|
|
142
|
+
return (x_out, y_out, by_out,
|
|
143
|
+
"days7" if hold7 else ts_unit)
|
lessPy/prob_norm.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# prob_norm.py — analog of prob_norm.R.
|
|
2
|
+
#
|
|
3
|
+
# prob_norm(): plot a normal density curve, shade the interval
|
|
4
|
+
# from lo to hi, and return the probability P(lo < Y < hi). An
|
|
5
|
+
# open bound (lo or hi = None) extends to the tail. Prints the
|
|
6
|
+
# probability and returns a results object with the plotly figure
|
|
7
|
+
# in .plots.
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .utils import fmt
|
|
12
|
+
|
|
13
|
+
_FILL_NRM = "#E8E8E8" # grey91
|
|
14
|
+
_FILL_INT = "#9FB6CD" # slategray3
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProbNormResults:
|
|
18
|
+
"""Numeric results of prob_norm(): the interval probability,
|
|
19
|
+
the bounds lo/hi, mu/sigma, and the plotly figure in .plots
|
|
20
|
+
("norm")."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, **kw):
|
|
23
|
+
self.__dict__.update(kw)
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"<lessPy prob_norm: P={self.prob:.4g}>"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def prob_norm(lo=None, hi=None, mu=0, sigma=1, nrm_color="black",
|
|
30
|
+
fill_nrm=_FILL_NRM, fill_int=_FILL_INT, y_axis=False,
|
|
31
|
+
z=True):
|
|
32
|
+
"""Probability of a normal interval: shade the area under the
|
|
33
|
+
N(mu, sigma) curve from lo to hi and return P(lo < Y < hi). A
|
|
34
|
+
None bound extends to the tail. z adds a second axis of z
|
|
35
|
+
scores (suppressed for the standard normal). Prints the
|
|
36
|
+
probability and returns a ProbNormResults with .plots.
|
|
37
|
+
R analog: prob_norm()"""
|
|
38
|
+
from scipy.stats import norm
|
|
39
|
+
if lo is not None and hi is not None and lo > hi:
|
|
40
|
+
raise ValueError(f"lo ({lo}) cannot be larger than hi "
|
|
41
|
+
f"({hi})")
|
|
42
|
+
if sigma <= 0:
|
|
43
|
+
raise ValueError("sigma must be larger than zero")
|
|
44
|
+
|
|
45
|
+
if mu == 0 and sigma == 1:
|
|
46
|
+
z = False
|
|
47
|
+
|
|
48
|
+
lo_lbl = "..." if lo is None else str(lo)
|
|
49
|
+
hi_lbl = "..." if hi is None else str(hi)
|
|
50
|
+
lo_v = mu - sigma * 10 if lo is None else lo
|
|
51
|
+
hi_v = mu + sigma * 10 if hi is None else hi
|
|
52
|
+
|
|
53
|
+
prob = norm.cdf(hi_v, mu, sigma) - norm.cdf(lo_v, mu, sigma)
|
|
54
|
+
|
|
55
|
+
fig = _plot(lo_v, hi_v, lo_lbl, hi_lbl, mu, sigma, prob,
|
|
56
|
+
nrm_color, fill_nrm, fill_int, y_axis, z)
|
|
57
|
+
|
|
58
|
+
print(f"Probability: {prob}")
|
|
59
|
+
|
|
60
|
+
return ProbNormResults(prob=prob, lo=lo_v, hi=hi_v, mu=mu,
|
|
61
|
+
sigma=sigma, plots={"norm": fig})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _plot(lo, hi, lo_lbl, hi_lbl, mu, sigma, prob, nrm_color,
|
|
65
|
+
fill_nrm, fill_int, y_axis, z):
|
|
66
|
+
import plotly.graph_objects as go
|
|
67
|
+
from scipy.stats import norm
|
|
68
|
+
min_x, max_x = mu - 4 * sigma, mu + 4 * sigma
|
|
69
|
+
cuts = [mu + k * sigma for k in range(-4, 5)]
|
|
70
|
+
x = np.linspace(min_x, max_x, 200)
|
|
71
|
+
dnrm = norm.pdf(x, mu, sigma)
|
|
72
|
+
|
|
73
|
+
fig = go.Figure()
|
|
74
|
+
# full curve, filled
|
|
75
|
+
fig.add_trace(go.Scatter(
|
|
76
|
+
x=np.concatenate(([min_x], x, [max_x])),
|
|
77
|
+
y=np.concatenate(([0.0], dnrm, [0.0])),
|
|
78
|
+
mode="lines", fill="toself", fillcolor=fill_nrm,
|
|
79
|
+
line=dict(color=nrm_color), hoverinfo="skip",
|
|
80
|
+
showlegend=False))
|
|
81
|
+
# shaded interval
|
|
82
|
+
m = (x > lo) & (x < hi)
|
|
83
|
+
xs = np.concatenate(([lo], x[m], [hi]))
|
|
84
|
+
ys = np.concatenate(([0.0], dnrm[m],
|
|
85
|
+
[0.0]))
|
|
86
|
+
fig.add_trace(go.Scatter(
|
|
87
|
+
x=xs, y=ys, mode="lines", fill="toself",
|
|
88
|
+
fillcolor=fill_int, line=dict(color=fill_int),
|
|
89
|
+
hoverinfo="skip", showlegend=False))
|
|
90
|
+
|
|
91
|
+
title = (f"Prob = {float(f'{prob:.4g}')} for Y from "
|
|
92
|
+
f"{lo_lbl} to {hi_lbl}<br>"
|
|
93
|
+
f"<sub>μ={mu} σ={sigma}</sub>")
|
|
94
|
+
fig.update_layout(
|
|
95
|
+
plot_bgcolor="white", paper_bgcolor="white",
|
|
96
|
+
margin=dict(t=50, r=20, b=60 if z else 40, l=40),
|
|
97
|
+
title=dict(text=title, x=0.5, xanchor="center",
|
|
98
|
+
font=dict(size=13)),
|
|
99
|
+
xaxis=dict(tickvals=cuts, showgrid=False,
|
|
100
|
+
linecolor="black", ticks="outside"),
|
|
101
|
+
yaxis=dict(visible=y_axis, showgrid=False,
|
|
102
|
+
title="Normal Density" if y_axis else None,
|
|
103
|
+
rangemode="tozero"),
|
|
104
|
+
showlegend=False)
|
|
105
|
+
if z: # second row: z scores
|
|
106
|
+
for k, cx in zip(range(-4, 5), cuts):
|
|
107
|
+
fig.add_annotation(x=cx, xref="x", y=-0.13,
|
|
108
|
+
yref="paper", text=str(k),
|
|
109
|
+
showarrow=False,
|
|
110
|
+
font=dict(size=11))
|
|
111
|
+
return fig
|
lessPy/prob_tcut.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# prob_tcut.py — analog of prob_tcut.R.
|
|
2
|
+
#
|
|
3
|
+
# prob_tcut(): plot a t density (df degrees of freedom) with the
|
|
4
|
+
# central 1-alpha region shaded and the two alpha/2 tails shaded,
|
|
5
|
+
# overlaid with a standard normal for comparison. Both the t and
|
|
6
|
+
# normal two-tailed critical cutoffs are drawn as vertical lines
|
|
7
|
+
# and labeled. Prints and returns the upper t cutoff,
|
|
8
|
+
# qt(1 - alpha/2, df) — the value R reports (labeled "Probability").
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .utils import fmt
|
|
13
|
+
|
|
14
|
+
_BG = "#F9F9FC" # rgb(249,249,252)
|
|
15
|
+
_TAIL = "#8B475D" # palevioletred4
|
|
16
|
+
_NRM = "#B3B3B3" # gray(.7)
|
|
17
|
+
_T = "#141414" # gray(.08)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProbTcutResults:
|
|
21
|
+
"""Result of prob_tcut(): the upper t cutoff, df, alpha, and the
|
|
22
|
+
plotly figure in .plots ("tcut")."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, **kw):
|
|
25
|
+
self.__dict__.update(kw)
|
|
26
|
+
|
|
27
|
+
def __repr__(self):
|
|
28
|
+
return f"<lessPy prob_tcut: df={self.df} cutoff={self.cutoff:.4g}>"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prob_tcut(df, alpha=0.05, digits_d=3, y_axis=False,
|
|
32
|
+
fill="aliceblue", color_tail=_TAIL, nrm_color=_NRM,
|
|
33
|
+
color_t=_T):
|
|
34
|
+
"""Two-tailed t cutoffs at alpha: shade the central 1-alpha
|
|
35
|
+
region of the t(df) curve and its alpha/2 tails, overlay a
|
|
36
|
+
standard normal, and mark both sets of critical values. Returns
|
|
37
|
+
a ProbTcutResults whose cutoff is the upper t critical value
|
|
38
|
+
qt(1 - alpha/2, df). R analog: prob_tcut()"""
|
|
39
|
+
from scipy.stats import t as t_dist, norm
|
|
40
|
+
if df < 2:
|
|
41
|
+
raise ValueError("df must be 2 or larger")
|
|
42
|
+
|
|
43
|
+
tail = alpha / 2
|
|
44
|
+
t_lo = t_dist.ppf(tail, df)
|
|
45
|
+
t_hi = t_dist.ppf(1 - tail, df)
|
|
46
|
+
cutoff = t_hi # value R returns
|
|
47
|
+
n_lo = norm.ppf(tail)
|
|
48
|
+
n_hi = norm.ppf(1 - tail)
|
|
49
|
+
|
|
50
|
+
fig = _plot(df, alpha, t_lo, t_hi, n_lo, n_hi, digits_d, y_axis,
|
|
51
|
+
fill, color_tail, nrm_color, color_t)
|
|
52
|
+
|
|
53
|
+
print(f"Probability: {cutoff}")
|
|
54
|
+
return ProbTcutResults(cutoff=cutoff, df=df, alpha=alpha,
|
|
55
|
+
plots={"tcut": fig})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _cut_line(fig, x, top, color):
|
|
59
|
+
fig.add_shape(type="line", x0=x, x1=x, y0=0, y1=top,
|
|
60
|
+
line=dict(color=color))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _cut_lbl(fig, x, y, value, digits_d, color):
|
|
64
|
+
fig.add_annotation(x=x, y=y, text=fmt(value, digits_d),
|
|
65
|
+
showarrow=False, font=dict(size=12,
|
|
66
|
+
color=color))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _plot(df, alpha, t_lo, t_hi, n_lo, n_hi, digits_d, y_axis,
|
|
70
|
+
fill, color_tail, nrm_color, color_t):
|
|
71
|
+
import plotly.graph_objects as go
|
|
72
|
+
from scipy.stats import t as t_dist, norm
|
|
73
|
+
xmin, xmax = -5.5, 5.5
|
|
74
|
+
x = np.linspace(xmin, xmax, 400)
|
|
75
|
+
yt = t_dist.pdf(x, df)
|
|
76
|
+
|
|
77
|
+
fig = go.Figure()
|
|
78
|
+
# central 1-alpha region of the t curve (aliceblue)
|
|
79
|
+
fig.add_trace(go.Scatter(
|
|
80
|
+
x=np.concatenate(([xmin], x, [xmax])),
|
|
81
|
+
y=np.concatenate(([0.0], yt, [0.0])),
|
|
82
|
+
mode="lines", fill="toself", fillcolor=fill,
|
|
83
|
+
line=dict(color="rgba(0,0,0,0)"), hoverinfo="skip",
|
|
84
|
+
showlegend=False))
|
|
85
|
+
# alpha/2 tails (palevioletred4)
|
|
86
|
+
for lo, hi in ((xmin, t_lo), (t_hi, xmax)):
|
|
87
|
+
xs = np.linspace(lo, hi, 100)
|
|
88
|
+
ys = t_dist.pdf(xs, df)
|
|
89
|
+
fig.add_trace(go.Scatter(
|
|
90
|
+
x=np.concatenate(([lo], xs, [hi])),
|
|
91
|
+
y=np.concatenate(([0.0], ys, [0.0])),
|
|
92
|
+
mode="lines", fill="toself", fillcolor=color_tail,
|
|
93
|
+
line=dict(color=nrm_color, width=1), hoverinfo="skip",
|
|
94
|
+
showlegend=False))
|
|
95
|
+
# t curve on top
|
|
96
|
+
fig.add_trace(go.Scatter(
|
|
97
|
+
x=x, y=yt, mode="lines", line=dict(color=color_t, width=3),
|
|
98
|
+
name=f"t, df={df}", hoverinfo="skip"))
|
|
99
|
+
# standard normal overlay
|
|
100
|
+
fig.add_trace(go.Scatter(
|
|
101
|
+
x=x, y=norm.pdf(x), mode="lines",
|
|
102
|
+
line=dict(color=nrm_color, width=2), name="Normal",
|
|
103
|
+
hoverinfo="skip"))
|
|
104
|
+
|
|
105
|
+
# t cutoffs: line to .19, label at .20
|
|
106
|
+
for c in (t_lo, t_hi):
|
|
107
|
+
_cut_line(fig, c, 0.19, color_tail)
|
|
108
|
+
_cut_lbl(fig, c, 0.205, c, digits_d, color_t)
|
|
109
|
+
# normal cutoffs: line to .25, label at .26
|
|
110
|
+
for c in (n_lo, n_hi):
|
|
111
|
+
_cut_line(fig, c, 0.25, nrm_color)
|
|
112
|
+
_cut_lbl(fig, c, 0.265, c, digits_d, nrm_color)
|
|
113
|
+
# central confidence label, e.g. "95%"
|
|
114
|
+
fig.add_annotation(x=0, y=0.175,
|
|
115
|
+
text=f"{round(100 * (1 - alpha))}%",
|
|
116
|
+
showarrow=False,
|
|
117
|
+
font=dict(size=17, color=nrm_color))
|
|
118
|
+
|
|
119
|
+
fig.update_layout(
|
|
120
|
+
plot_bgcolor=_BG, paper_bgcolor="white",
|
|
121
|
+
margin=dict(t=20, r=20, b=50, l=40),
|
|
122
|
+
xaxis=dict(title="Standard Errors from Zero", range=[xmin,
|
|
123
|
+
xmax], showgrid=False, zeroline=False,
|
|
124
|
+
linecolor="black", ticks="outside"),
|
|
125
|
+
yaxis=dict(visible=y_axis, range=[0, 0.42], showgrid=False,
|
|
126
|
+
zeroline=False,
|
|
127
|
+
title="Density" if y_axis else None),
|
|
128
|
+
legend=dict(x=0.99, y=0.99, xanchor="right", yanchor="top",
|
|
129
|
+
bordercolor="black", borderwidth=0.5,
|
|
130
|
+
font=dict(size=10)))
|
|
131
|
+
return fig
|
lessPy/prob_znorm.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# prob_znorm.py — analog of prob_znorm.R.
|
|
2
|
+
#
|
|
3
|
+
# prob_znorm(): plot a normal density curve marked at each integer
|
|
4
|
+
# standard deviation, with the central +/-1, +/-2, and +/-3 sigma
|
|
5
|
+
# regions shaded by three nested translucent bands. Because the
|
|
6
|
+
# bands are translucent and stacked, the center (covered by all
|
|
7
|
+
# three) is darkest, then two layers, then one — the empirical-rule
|
|
8
|
+
# graphic. Unlike prob_norm() this returns no probability; it is a
|
|
9
|
+
# display, so the results object holds only the plotly figure in
|
|
10
|
+
# .plots ("znorm").
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProbZnormResults:
|
|
16
|
+
"""Result of prob_znorm(): mu/sigma and the plotly figure in
|
|
17
|
+
.plots ("znorm"). No probability is computed."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, **kw):
|
|
20
|
+
self.__dict__.update(kw)
|
|
21
|
+
|
|
22
|
+
def __repr__(self):
|
|
23
|
+
return f"<lessPy prob_znorm: mu={self.mu} sigma={self.sigma}>"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def prob_znorm(mu=0, sigma=1, color_border="gray10", r=0.10,
|
|
27
|
+
g=0.34, b=0.94, a=0.20, main="", y_axis=False,
|
|
28
|
+
z=True):
|
|
29
|
+
"""Normal curve with the central 1/2/3-sigma regions shown as
|
|
30
|
+
three nested translucent bands (the empirical-rule graphic).
|
|
31
|
+
color_border is the curve/outline color; r/g/b/a (each in
|
|
32
|
+
[0, 1]) set the translucent fill. z adds a second axis of z
|
|
33
|
+
scores (suppressed for the standard normal). Returns a
|
|
34
|
+
ProbZnormResults with .plots. R analog: prob_znorm()"""
|
|
35
|
+
if not all(0 <= v <= 1 for v in (r, g, b, a)):
|
|
36
|
+
raise ValueError("r, g, b and a must each be between 0 and "
|
|
37
|
+
"1, inclusive")
|
|
38
|
+
if sigma <= 0:
|
|
39
|
+
raise ValueError("sigma must be larger than zero")
|
|
40
|
+
|
|
41
|
+
if mu == 0 and sigma == 1:
|
|
42
|
+
z = False
|
|
43
|
+
|
|
44
|
+
border = _gray_to_hex(color_border)
|
|
45
|
+
fill = f"rgba({round(r * 255)},{round(g * 255)}," \
|
|
46
|
+
f"{round(b * 255)},{a})"
|
|
47
|
+
|
|
48
|
+
fig = _plot(mu, sigma, border, fill, main, y_axis, z)
|
|
49
|
+
return ProbZnormResults(mu=mu, sigma=sigma, plots={"znorm": fig})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _gray_to_hex(color):
|
|
53
|
+
"""plotly rejects R's grayNN/greyNN names; convert those to hex,
|
|
54
|
+
pass any other color string through unchanged."""
|
|
55
|
+
lc = color.lower()
|
|
56
|
+
for pre in ("gray", "grey"):
|
|
57
|
+
if lc.startswith(pre) and lc[len(pre):].isdigit():
|
|
58
|
+
v = round(int(lc[len(pre):]) / 100 * 255)
|
|
59
|
+
return f"#{v:02X}{v:02X}{v:02X}"
|
|
60
|
+
return color
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _plot(mu, sigma, border, fill, main, y_axis, z):
|
|
64
|
+
import plotly.graph_objects as go
|
|
65
|
+
from scipy.stats import norm
|
|
66
|
+
xmin, xmax = mu - 4 * sigma, mu + 4 * sigma
|
|
67
|
+
cuts = [mu + k * sigma for k in range(-4, 5)]
|
|
68
|
+
x = np.linspace(xmin, xmax, 200)
|
|
69
|
+
y = norm.pdf(x, mu, sigma)
|
|
70
|
+
|
|
71
|
+
fig = go.Figure()
|
|
72
|
+
# normal curve
|
|
73
|
+
fig.add_trace(go.Scatter(
|
|
74
|
+
x=x, y=y, mode="lines", line=dict(color=border, width=2),
|
|
75
|
+
hoverinfo="skip", showlegend=False))
|
|
76
|
+
# dotted vertical segment at the mean, up to the peak
|
|
77
|
+
fig.add_shape(type="line", x0=mu, x1=mu, y0=0,
|
|
78
|
+
y1=norm.pdf(mu, mu, sigma),
|
|
79
|
+
line=dict(color=border, dash="dot"))
|
|
80
|
+
# nested translucent bands: +/-3, +/-2, +/-1 sigma. Drawn in
|
|
81
|
+
# this order so the overlaps stack — the center is darkest.
|
|
82
|
+
for k in (3, 2, 1):
|
|
83
|
+
lo, hi = mu - k * sigma, mu + k * sigma
|
|
84
|
+
m = (x > lo) & (x < hi)
|
|
85
|
+
fig.add_trace(go.Scatter(
|
|
86
|
+
x=np.concatenate(([lo], x[m], [hi])),
|
|
87
|
+
y=np.concatenate(([0.0], y[m], [0.0])),
|
|
88
|
+
mode="lines", fill="toself", fillcolor=fill,
|
|
89
|
+
line=dict(color=border, dash="dot", width=1),
|
|
90
|
+
hoverinfo="skip", showlegend=False))
|
|
91
|
+
|
|
92
|
+
fig.update_layout(
|
|
93
|
+
plot_bgcolor="white", paper_bgcolor="white",
|
|
94
|
+
margin=dict(t=50 if main else 30, r=20,
|
|
95
|
+
b=60 if z else 40, l=40),
|
|
96
|
+
title=dict(text=main, x=0.5, xanchor="center",
|
|
97
|
+
font=dict(size=13)),
|
|
98
|
+
xaxis=dict(tickvals=cuts, showgrid=False,
|
|
99
|
+
linecolor="black", ticks="outside"),
|
|
100
|
+
yaxis=dict(visible=y_axis, showgrid=False,
|
|
101
|
+
title="Normal Density" if y_axis else None,
|
|
102
|
+
rangemode="tozero"),
|
|
103
|
+
showlegend=False)
|
|
104
|
+
if z: # second row: z scores
|
|
105
|
+
for k, cx in zip(range(-4, 5), cuts):
|
|
106
|
+
fig.add_annotation(x=cx, xref="x", y=-0.13,
|
|
107
|
+
yref="paper", text=str(k),
|
|
108
|
+
showarrow=False,
|
|
109
|
+
font=dict(size=11))
|
|
110
|
+
return fig
|