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/vbs_plotly.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# vbs_plotly.py — the violin/box/scatterplot (VBS) composite
|
|
2
|
+
#
|
|
3
|
+
# A DESIGN, not a translation: lessR renders VBS only through
|
|
4
|
+
# lattice (plt.lattice.R con_cat panel + plt.panel.R), so there is
|
|
5
|
+
# no plotly source to port. The geometry and styling reproduce the
|
|
6
|
+
# lattice look: a mirrored-KDE violin, a narrow box with whiskers
|
|
7
|
+
# and outlier points layered by severity (firebrick4 inside the
|
|
8
|
+
# outer fences, firebrick2 beyond), and jittered strip points
|
|
9
|
+
# between the inner fences. Statistics follow R exactly:
|
|
10
|
+
# boxplot.stats on Tukey hinges (fivenum), fences at k and 2k
|
|
11
|
+
# hinge-IQRs. Auto point size ports the one-variable and grouped
|
|
12
|
+
# formulas of .param.VBS(). With by=, as in the lattice panel:
|
|
13
|
+
# one violin and box over all the data (box fill dropped to 0.25
|
|
14
|
+
# alpha so points show through), strip points colored per group
|
|
15
|
+
# with varying symbols (.plt.shapes "vary" order), outliers in
|
|
16
|
+
# their group's color rather than firebrick, and a horizontal
|
|
17
|
+
# legend at the top. With facet=, one full VBS per level in its
|
|
18
|
+
# own band on the shared value axis — statistics computed per
|
|
19
|
+
# panel, first level in the BOTTOM band (the lattice as.table
|
|
20
|
+
# default), a shaded strip label above each band, and per-panel
|
|
21
|
+
# box hues (.plt.fill) under the common slate violin fill.
|
|
22
|
+
# Deviations, by design:
|
|
23
|
+
# - jitter_y is the strip half-spread as a fraction of the
|
|
24
|
+
# violin half-width (lattice jitter factors do not map to
|
|
25
|
+
# plotly coordinates); jitter_x keeps R's jitter() semantics,
|
|
26
|
+
# amount = factor * range/50, applied to the data as in X.R
|
|
27
|
+
# - violin bandwidth defaults to band_width() — bw_nrd0
|
|
28
|
+
# widened 10% per iteration toward a single peak
|
|
29
|
+
# (~ .band.width, bw_iter); with facet=, each panel gets
|
|
30
|
+
# its own bandwidth
|
|
31
|
+
#
|
|
32
|
+
# box_adj draws the Hubert-Vandervieren adjusted boxplot:
|
|
33
|
+
# fences scaled by exp(a*mc)/exp(b*mc) with mc the medcouple
|
|
34
|
+
# (_medcouple ~ robustbase::mc). out_cut labels up to out_cut
|
|
35
|
+
# outliers (largest |x|) with their IDs above the strip,
|
|
36
|
+
# ~ plt.lattice.R 707-734.
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
import plotly.graph_objects as go
|
|
40
|
+
|
|
41
|
+
from .plotly_utils import (
|
|
42
|
+
as_plotly_color, axis_format, axis_num, get_tick_fmt,
|
|
43
|
+
make_trans,
|
|
44
|
+
plot_border, plotly_style, sym_at, to_hex, x_grid,
|
|
45
|
+
)
|
|
46
|
+
from .utils import band_width, fmt, get_option, kde, pretty
|
|
47
|
+
|
|
48
|
+
_BAND = 3.0 # y extent of one facet band (unit is [-1, 1])
|
|
49
|
+
_STRIP = (1.30, 1.80) # facet strip band, unit-local
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def five_num(x):
|
|
53
|
+
"""Tukey five-number summary: min, lower hinge, median, upper
|
|
54
|
+
hinge, max. R analog: fivenum()"""
|
|
55
|
+
xs = np.sort(x[np.isfinite(x)])
|
|
56
|
+
n = len(xs)
|
|
57
|
+
n4 = np.floor((n + 3) / 2) / 2
|
|
58
|
+
d = np.array([1, n4, (n + 1) / 2, n + 1 - n4, n])
|
|
59
|
+
return 0.5 * (xs[np.floor(d).astype(int) - 1]
|
|
60
|
+
+ xs[np.ceil(d).astype(int) - 1])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _medcouple(x):
|
|
64
|
+
"""Medcouple robust skew measure (Brys, Hubert & Struyf):
|
|
65
|
+
the median of the kernel h(xi, xj) over xi <= med <= xj,
|
|
66
|
+
with the signum kernel for ties at the median.
|
|
67
|
+
R analog: robustbase::mc(doScale=FALSE)"""
|
|
68
|
+
x = np.sort(np.asarray(x, dtype=float))[::-1] # decreasing
|
|
69
|
+
med = float(np.median(x))
|
|
70
|
+
zp = x[x >= med] # decreasing
|
|
71
|
+
zm = x[x <= med] # decreasing
|
|
72
|
+
num = zp[:, None] + zm[None, :] - 2 * med
|
|
73
|
+
den = zp[:, None] - zm[None, :]
|
|
74
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
75
|
+
h = num / den
|
|
76
|
+
p = int((x == med).sum())
|
|
77
|
+
if p > 0: # ties at the median:
|
|
78
|
+
i = np.arange(len(zp)) # h = sign(p - 1 - i - j)
|
|
79
|
+
j = np.arange(len(zm))
|
|
80
|
+
ti = i - (len(zp) - p) # tie-local index in zp
|
|
81
|
+
tj = j # ties lead zm
|
|
82
|
+
tie = ((ti[:, None] >= 0)
|
|
83
|
+
& (tj[None, :] <= p - 1))
|
|
84
|
+
sgn = np.sign(p - 1 - ti[:, None] - tj[None, :])
|
|
85
|
+
h = np.where(tie, sgn, h)
|
|
86
|
+
return float(np.median(h))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _adj_fences(q1, q3, iqr, k_iqr, mcv, a, b):
|
|
90
|
+
"""Inner and outer fences, Hubert-Vandervieren adjusted by
|
|
91
|
+
the medcouple when box_adj (mcv nonzero from _medcouple).
|
|
92
|
+
R analog: plt.lattice.R ~612-627 / robustbase::adjboxStats"""
|
|
93
|
+
if mcv >= 0:
|
|
94
|
+
e_lo, e_hi = np.exp(a * mcv), np.exp(b * mcv)
|
|
95
|
+
else:
|
|
96
|
+
e_lo, e_hi = np.exp(-b * mcv), np.exp(-a * mcv)
|
|
97
|
+
fnc_in = (q1 - k_iqr * e_lo * iqr, q3 + k_iqr * e_hi * iqr)
|
|
98
|
+
fnc_out = (q1 - 2 * k_iqr * e_lo * iqr,
|
|
99
|
+
q3 + 2 * k_iqr * e_hi * iqr)
|
|
100
|
+
return fnc_in, fnc_out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _auto_pt_size(x, n_ux, mx_c):
|
|
104
|
+
"""Point size (cex) from the one-variable rules of
|
|
105
|
+
.param.VBS(): shrinks with n, more with heavy replication or
|
|
106
|
+
a compressed IQR."""
|
|
107
|
+
n = len(x)
|
|
108
|
+
rep_prop = (n - n_ux) / n
|
|
109
|
+
reps = rep_prop > 0.15 and mx_c > 0.10 * n
|
|
110
|
+
if not reps:
|
|
111
|
+
cex = (1.096 - 0.134 * np.log(n) if n < 2535
|
|
112
|
+
else 0.226 - 0.023 * np.log(n))
|
|
113
|
+
else:
|
|
114
|
+
cex = 0.842 - 0.109 * np.log(mx_c)
|
|
115
|
+
if cex < 0.01:
|
|
116
|
+
cex = 0.015 if n < 25000 else 0.006
|
|
117
|
+
q75, q25 = np.percentile(x, [75, 25])
|
|
118
|
+
rng = x.max() - x.min()
|
|
119
|
+
rt = (q75 - q25) / rng if rng > 0 else 1
|
|
120
|
+
if rt < 0.18:
|
|
121
|
+
cex *= 0.147 + 4.490 * rt
|
|
122
|
+
return cex
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _seg(x0, x1, y0, y1, color, width, dash=None):
|
|
126
|
+
line = dict(color=color, width=width)
|
|
127
|
+
if dash:
|
|
128
|
+
line["dash"] = dash
|
|
129
|
+
return go.Scatter(x=[x0, x1], y=[y0, y1], mode="lines",
|
|
130
|
+
line=line, hoverinfo="skip",
|
|
131
|
+
showlegend=False)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _col_for(cols, i):
|
|
135
|
+
cols = (list(cols) if isinstance(cols, (list, tuple))
|
|
136
|
+
else [cols])
|
|
137
|
+
return cols[i % len(cols)]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def vbs_plotly(x, x_name=None, vbs_plot="vbs",
|
|
141
|
+
by=None, by_order=None, by_name=None,
|
|
142
|
+
facet=None, facet_order=None, facet_name=None,
|
|
143
|
+
facet2=None, facet2_order=None, facet2_name=None,
|
|
144
|
+
violin_fill=None, violin_color=None, bw=None,
|
|
145
|
+
box_fill=None, box_color=None,
|
|
146
|
+
pt_fill="black", pt_color="black", pt_trans=0.10,
|
|
147
|
+
pt_size=None, out_size=None, out_shape="circle",
|
|
148
|
+
shape=None,
|
|
149
|
+
vbs_ratio=1.1, vbs_mean=False, fences=False,
|
|
150
|
+
k_iqr=1.5, box_adj=False, a=-4, b=3, bw_iter=10,
|
|
151
|
+
out_cut=0, ids=None, ID_size=0.6,
|
|
152
|
+
jitter_x=None, jitter_y=None,
|
|
153
|
+
x_lab=None, main=None, digits_d=2,
|
|
154
|
+
axis_fmt="K", axis_x_pre="",
|
|
155
|
+
style_opts=None):
|
|
156
|
+
"""Violin/box/strip composite of one numeric vector, laid out
|
|
157
|
+
horizontally on a hidden y axis. by= colors the strip points
|
|
158
|
+
per group (pt_fill/pt_color then lists per level of
|
|
159
|
+
by_order); facet= draws one VBS band per level of
|
|
160
|
+
facet_order (box_fill then a list per level). facet2=
|
|
161
|
+
stacks one full band set per facet2 level, all sections on
|
|
162
|
+
the one shared x axis, each under its own Var = "level"
|
|
163
|
+
section strip — the hybrid stand-in for R's lattice
|
|
164
|
+
facet1 x facet2 grid, keeping the band design's full-width
|
|
165
|
+
comparability."""
|
|
166
|
+
if style_opts is None:
|
|
167
|
+
style_opts = plotly_style()
|
|
168
|
+
|
|
169
|
+
x = np.asarray(x, dtype=float)
|
|
170
|
+
ok = np.isfinite(x)
|
|
171
|
+
x = x[ok]
|
|
172
|
+
if ids is not None:
|
|
173
|
+
ids = np.asarray(ids)[ok]
|
|
174
|
+
if by is not None:
|
|
175
|
+
by = np.asarray(by)[ok]
|
|
176
|
+
if by_order is None:
|
|
177
|
+
by_order = sorted(set(by))
|
|
178
|
+
if facet is not None:
|
|
179
|
+
facet = np.asarray(facet)[ok]
|
|
180
|
+
if facet_order is None:
|
|
181
|
+
facet_order = sorted(set(facet))
|
|
182
|
+
if facet2 is not None:
|
|
183
|
+
facet2 = np.asarray(facet2)[ok]
|
|
184
|
+
if facet2_order is None:
|
|
185
|
+
facet2_order = sorted(set(facet2))
|
|
186
|
+
n = len(x)
|
|
187
|
+
n_grp = len(by_order) if by is not None else 1
|
|
188
|
+
n_fac = len(facet_order) if facet is not None else 1
|
|
189
|
+
if facet2 is not None:
|
|
190
|
+
n_fac *= len(facet2_order) # total bands, all sections
|
|
191
|
+
ux, cnt = np.unique(x, return_counts=True)
|
|
192
|
+
n_ux = len(ux)
|
|
193
|
+
# max duplication count; 0 when all values unique (X.R)
|
|
194
|
+
mx_c = (int(cnt.max())
|
|
195
|
+
if n_ux < 1001 and n_ux < n else 0)
|
|
196
|
+
|
|
197
|
+
violin = "v" in vbs_plot
|
|
198
|
+
box = "b" in vbs_plot
|
|
199
|
+
strip = "s" in vbs_plot
|
|
200
|
+
|
|
201
|
+
# grouped replication measures, .param.VBS() group branch;
|
|
202
|
+
# by and facet share the branch, as .param.VBS() does
|
|
203
|
+
grp = by if by is not None else facet
|
|
204
|
+
grp_ord = by_order if by is not None else facet_order
|
|
205
|
+
if grp is not None:
|
|
206
|
+
mx_gc = max((grp == nm).sum() for nm in grp_ord)
|
|
207
|
+
mc_w = max(int(np.unique(x[grp == nm],
|
|
208
|
+
return_counts=True)[1].max())
|
|
209
|
+
for nm in grp_ord if (grp == nm).any())
|
|
210
|
+
reps = (n - n_ux) / n > 0.15 and mc_w > 0.05 * n
|
|
211
|
+
|
|
212
|
+
# jitter_x breaks ties in the data itself, as in X.R
|
|
213
|
+
if jitter_x is None:
|
|
214
|
+
if grp is None:
|
|
215
|
+
jitter_x = 1.1 * (1 - np.exp(-0.03 * mx_c))
|
|
216
|
+
else:
|
|
217
|
+
jitter_x = (0.086 + 0.141 * np.log(mx_gc)
|
|
218
|
+
if reps else 0)
|
|
219
|
+
if strip and jitter_x > 0 and x.max() > x.min():
|
|
220
|
+
amt = jitter_x * (x.max() - x.min()) / 50
|
|
221
|
+
# seeded so the plot (and its stats/outlier split) is
|
|
222
|
+
# reproducible; a distinct seed from the strip y-jitter
|
|
223
|
+
# (default_rng(n) below) keeps the two offsets independent
|
|
224
|
+
x = x + np.random.default_rng(n + 1).uniform(-amt, amt, n)
|
|
225
|
+
|
|
226
|
+
# geometry: violin half-width, box half-height (plt.lattice,
|
|
227
|
+
# denom shrinks by 0.5 per facet level)
|
|
228
|
+
hv = vbs_ratio / 2
|
|
229
|
+
n_adj = n if n < 1000 else 3 * n
|
|
230
|
+
base = (4.10 - 0.000065 * n_adj if n_adj <= 25000
|
|
231
|
+
else 3.25 - 0.00003 * n_adj)
|
|
232
|
+
hb = (vbs_ratio / max(1.5, base - 0.5 * n_fac)) / 2
|
|
233
|
+
|
|
234
|
+
# point sizes: auto cex rules, same px scale as the scatter
|
|
235
|
+
if pt_size is not None:
|
|
236
|
+
cex = pt_size
|
|
237
|
+
elif grp is None:
|
|
238
|
+
cex = _auto_pt_size(x, n_ux, mx_c)
|
|
239
|
+
else: # .param.VBS() group-branch rules
|
|
240
|
+
n_lvl = len(grp_ord)
|
|
241
|
+
cex = (0.72 - 0.124 * np.log(mc_w) if reps
|
|
242
|
+
else 0.926 - 0.108 * np.log(mx_gc)
|
|
243
|
+
- 0.023 * n_lvl)
|
|
244
|
+
cex = max(cex, 0.025)
|
|
245
|
+
px = max(1.0, cex * 7.25)
|
|
246
|
+
out_cex = out_size if out_size is not None \
|
|
247
|
+
else 0.58 + 0.40 * cex
|
|
248
|
+
px_out = max(1.0, out_cex * 7.25)
|
|
249
|
+
|
|
250
|
+
if violin_fill is None:
|
|
251
|
+
violin_fill = get_option("violin_fill", "#7485975A")
|
|
252
|
+
if violin_color is None:
|
|
253
|
+
violin_color = get_option("violin_color", "gray15")
|
|
254
|
+
if box_fill is None:
|
|
255
|
+
box_fill = get_option("box_fill", "#419BD2")
|
|
256
|
+
if box_color is None:
|
|
257
|
+
box_color = get_option("box_color", "gray15")
|
|
258
|
+
box_color = to_hex(box_color)
|
|
259
|
+
|
|
260
|
+
fig = go.Figure()
|
|
261
|
+
val_fmt = get_tick_fmt(x, digits_d)
|
|
262
|
+
vpart = f"%{{x:{val_fmt}}}" if val_fmt else "%{x}"
|
|
263
|
+
rng_ = np.random.default_rng(n) # reproducible spread
|
|
264
|
+
jy = 0.5 if jitter_y is None else float(jitter_y)
|
|
265
|
+
|
|
266
|
+
# facet units, first level in the BOTTOM band (lattice);
|
|
267
|
+
# facet2 stacks one section of facet bands per level, bottom
|
|
268
|
+
# section first, each topped by a Var = "level" strip
|
|
269
|
+
_SEC_EXTRA = 1.8 # extra y units above each section
|
|
270
|
+
sec_strips = [] # (y0, y1, label) per facet2 section
|
|
271
|
+
if facet is None:
|
|
272
|
+
units = [dict(mask=np.ones(n, dtype=bool), yc=0.0,
|
|
273
|
+
strip=None, ci=0, hov="")]
|
|
274
|
+
elif facet2 is None:
|
|
275
|
+
units = [dict(mask=facet == lv, yc=ui * _BAND,
|
|
276
|
+
strip=str(lv), ci=ui,
|
|
277
|
+
hov=f"<br>{facet_name}: {lv}")
|
|
278
|
+
for ui, lv in enumerate(facet_order)]
|
|
279
|
+
else:
|
|
280
|
+
from .plotly_utils import facet_cell_label
|
|
281
|
+
k1 = len(facet_order)
|
|
282
|
+
stride = k1 * _BAND + _SEC_EXTRA
|
|
283
|
+
units = []
|
|
284
|
+
for i2, lv2 in enumerate(facet2_order):
|
|
285
|
+
y_base = i2 * stride
|
|
286
|
+
for i1, lv1 in enumerate(facet_order):
|
|
287
|
+
units.append(dict(
|
|
288
|
+
mask=(facet == lv1) & (facet2 == lv2),
|
|
289
|
+
yc=y_base + i1 * _BAND,
|
|
290
|
+
strip=str(lv1), ci=i1,
|
|
291
|
+
hov=(f"<br>{facet_name}: {lv1}"
|
|
292
|
+
f"<br>{facet2_name}: {lv2}")))
|
|
293
|
+
top = y_base + (k1 - 1) * _BAND
|
|
294
|
+
sec_strips.append((top + _STRIP[1] + 0.25,
|
|
295
|
+
top + _STRIP[1] + 0.95,
|
|
296
|
+
facet_cell_label(facet2_name,
|
|
297
|
+
lv2)))
|
|
298
|
+
|
|
299
|
+
fence_ext = [] # axis range extension
|
|
300
|
+
|
|
301
|
+
for ui, u in enumerate(units):
|
|
302
|
+
um = u["mask"]
|
|
303
|
+
xu = x[um]
|
|
304
|
+
yc = u["yc"]
|
|
305
|
+
if len(xu) == 0:
|
|
306
|
+
continue
|
|
307
|
+
lvl_part = u["hov"]
|
|
308
|
+
|
|
309
|
+
# box statistics on Tukey hinges; fences at k and 2k
|
|
310
|
+
# IQRs, medcouple-adjusted when box_adj (mc 0: exp = 1)
|
|
311
|
+
mn, q1, md, q3, mx = five_num(xu)
|
|
312
|
+
iqr = q3 - q1
|
|
313
|
+
mcv = _medcouple(xu) if box_adj else 0.0
|
|
314
|
+
fnc_in, fnc_out = _adj_fences(q1, q3, iqr, k_iqr,
|
|
315
|
+
mcv, a, b)
|
|
316
|
+
inside_u = (xu >= fnc_in[0]) & (xu <= fnc_in[1])
|
|
317
|
+
lo_whisk = xu[inside_u].min()
|
|
318
|
+
hi_whisk = xu[inside_u].max()
|
|
319
|
+
if fences:
|
|
320
|
+
fence_ext += [fnc_in[0], fnc_in[1]]
|
|
321
|
+
|
|
322
|
+
# violin: mirrored KDE, max width = vbs_ratio
|
|
323
|
+
if violin and len(xu) > 1:
|
|
324
|
+
h = (bw if bw is not None
|
|
325
|
+
else band_width(xu, bw_iter))
|
|
326
|
+
grid = np.linspace(xu.min() - 3 * h,
|
|
327
|
+
xu.max() + 3 * h, 512)
|
|
328
|
+
dens = kde(xu, grid, h)
|
|
329
|
+
half = dens / dens.max() * hv
|
|
330
|
+
fig.add_trace(go.Scatter(
|
|
331
|
+
x=np.concatenate([grid, grid[::-1]]),
|
|
332
|
+
y=np.concatenate([yc + half,
|
|
333
|
+
yc - half[::-1]]),
|
|
334
|
+
mode="lines",
|
|
335
|
+
line=dict(color=to_hex(violin_color), width=1),
|
|
336
|
+
fill="toself",
|
|
337
|
+
fillcolor=as_plotly_color(violin_fill),
|
|
338
|
+
hoverinfo="skip", showlegend=False,
|
|
339
|
+
))
|
|
340
|
+
|
|
341
|
+
# box, whiskers, median
|
|
342
|
+
if box and len(xu) > 1:
|
|
343
|
+
d = digits_d
|
|
344
|
+
bf = _col_for(box_fill, u["ci"])
|
|
345
|
+
fig.add_trace(go.Scatter(
|
|
346
|
+
x=[q1, q3, q3, q1, q1],
|
|
347
|
+
y=[yc - hb, yc - hb, yc + hb, yc + hb, yc - hb],
|
|
348
|
+
mode="lines",
|
|
349
|
+
line=dict(color=box_color, width=2),
|
|
350
|
+
fill="toself",
|
|
351
|
+
# translucent with groups so points show through
|
|
352
|
+
fillcolor=(make_trans(bf, 0.25) if n_grp > 1
|
|
353
|
+
else as_plotly_color(bf)),
|
|
354
|
+
text=(f"Median: {fmt(md, d)}<br>"
|
|
355
|
+
f"Hinges: {fmt(q1, d)}, {fmt(q3, d)}<br>"
|
|
356
|
+
f"Whiskers: {fmt(lo_whisk, d)}, "
|
|
357
|
+
f"{fmt(hi_whisk, d)}" + lvl_part),
|
|
358
|
+
hoverinfo="text", showlegend=False,
|
|
359
|
+
))
|
|
360
|
+
fig.add_trace(_seg(q1, lo_whisk, yc, yc,
|
|
361
|
+
box_color, 1))
|
|
362
|
+
fig.add_trace(_seg(q3, hi_whisk, yc, yc,
|
|
363
|
+
box_color, 1))
|
|
364
|
+
for w in (lo_whisk, hi_whisk): # whisker caps
|
|
365
|
+
fig.add_trace(_seg(w, w, yc - hb, yc + hb,
|
|
366
|
+
box_color, 1))
|
|
367
|
+
fig.add_trace(_seg(md, md, yc - hb, yc + hb,
|
|
368
|
+
box_color, 2))
|
|
369
|
+
if vbs_mean:
|
|
370
|
+
m = xu.mean()
|
|
371
|
+
fig.add_trace(_seg(
|
|
372
|
+
m, m, yc - hb, yc + hb,
|
|
373
|
+
to_hex(get_option("out_fill")), 2))
|
|
374
|
+
if fences:
|
|
375
|
+
for f in fnc_in:
|
|
376
|
+
fig.add_trace(_seg(f, f, yc - hb, yc + hb,
|
|
377
|
+
"darkred", 1.5))
|
|
378
|
+
|
|
379
|
+
# strip points, per by group within the unit
|
|
380
|
+
if by is None:
|
|
381
|
+
groups = [(None, np.ones(len(xu), dtype=bool))]
|
|
382
|
+
else:
|
|
383
|
+
byu = by[um]
|
|
384
|
+
groups = [(nm, byu == nm) for nm in by_order]
|
|
385
|
+
|
|
386
|
+
if strip and len(xu) > 0:
|
|
387
|
+
for i, (nm, g) in enumerate(groups):
|
|
388
|
+
# outliers render through their own traces below
|
|
389
|
+
pts = xu[inside_u & g]
|
|
390
|
+
if len(pts) == 0:
|
|
391
|
+
continue
|
|
392
|
+
yj = yc + rng_.uniform(-jy * hv, jy * hv,
|
|
393
|
+
len(pts))
|
|
394
|
+
# by groups differ by color only; shape= (scalar or
|
|
395
|
+
# per-group vector) overrides for every group
|
|
396
|
+
sym = "circle" if shape is None else sym_at(shape, i)
|
|
397
|
+
hover = f"{x_lab}: {vpart}"
|
|
398
|
+
if nm is not None:
|
|
399
|
+
hover += f"<br>{by_name}: {nm}"
|
|
400
|
+
hover += lvl_part
|
|
401
|
+
fig.add_trace(go.Scatter(
|
|
402
|
+
x=pts, y=yj, mode="markers",
|
|
403
|
+
name=None if nm is None else str(nm),
|
|
404
|
+
marker=dict(
|
|
405
|
+
symbol=sym, size=px,
|
|
406
|
+
color=make_trans(_col_for(pt_fill, i),
|
|
407
|
+
1 - pt_trans),
|
|
408
|
+
line=dict(
|
|
409
|
+
color=to_hex(_col_for(pt_color, i)),
|
|
410
|
+
width=0.5)),
|
|
411
|
+
hovertemplate=hover + "<extra></extra>",
|
|
412
|
+
showlegend=nm is not None and ui == 0,
|
|
413
|
+
))
|
|
414
|
+
if box or strip:
|
|
415
|
+
reg = (((xu >= fnc_out[0]) & (xu < fnc_in[0]))
|
|
416
|
+
| ((xu > fnc_in[1]) & (xu <= fnc_out[1])))
|
|
417
|
+
ext = (xu < fnc_out[0]) | (xu > fnc_out[1])
|
|
418
|
+
for i, (nm, g) in enumerate(groups):
|
|
419
|
+
if nm is None: # firebrick severity colors
|
|
420
|
+
sets = [(xu[reg], get_option("out_fill"),
|
|
421
|
+
get_option("out_color"), "outlier"),
|
|
422
|
+
(xu[ext], get_option("out2_fill"),
|
|
423
|
+
get_option("out2_color"),
|
|
424
|
+
"extreme outlier")]
|
|
425
|
+
else: # group colors, as in the panel
|
|
426
|
+
gf = _col_for(pt_fill, i)
|
|
427
|
+
gc = _col_for(pt_color, i)
|
|
428
|
+
sets = [(xu[reg & g], gf, gc, "outlier"),
|
|
429
|
+
(xu[ext & g], gf, gc,
|
|
430
|
+
"extreme outlier")]
|
|
431
|
+
for vals, o_fill, o_color, tag in sets:
|
|
432
|
+
if len(vals) == 0:
|
|
433
|
+
continue
|
|
434
|
+
hover = f"{x_lab}: {vpart} ({tag})"
|
|
435
|
+
if nm is not None:
|
|
436
|
+
hover += f"<br>{by_name}: {nm}"
|
|
437
|
+
hover += lvl_part
|
|
438
|
+
fig.add_trace(go.Scatter(
|
|
439
|
+
x=vals, y=np.full(len(vals), float(yc)),
|
|
440
|
+
mode="markers",
|
|
441
|
+
marker=dict(
|
|
442
|
+
symbol=out_shape, size=px_out,
|
|
443
|
+
color=to_hex(o_fill),
|
|
444
|
+
line=dict(color=to_hex(o_color),
|
|
445
|
+
width=1)),
|
|
446
|
+
hovertemplate=hover + "<extra></extra>",
|
|
447
|
+
showlegend=False,
|
|
448
|
+
))
|
|
449
|
+
|
|
450
|
+
# label up to out_cut outliers, the largest |x|,
|
|
451
|
+
# IDs rotated above the band (plt.lattice 707-734)
|
|
452
|
+
if out_cut > 0 and ids is not None:
|
|
453
|
+
idu = ids[um]
|
|
454
|
+
lo_i = np.where(xu < fnc_in[0])[0]
|
|
455
|
+
lo_i = lo_i[np.argsort(xu[lo_i],
|
|
456
|
+
kind="stable")][:out_cut]
|
|
457
|
+
hi_i = np.where(xu > fnc_in[1])[0]
|
|
458
|
+
hi_i = hi_i[np.argsort(-xu[hi_i],
|
|
459
|
+
kind="stable")][:out_cut]
|
|
460
|
+
cand = np.concatenate([lo_i, hi_i]).astype(int)
|
|
461
|
+
keep_i = cand[np.argsort(-np.abs(xu[cand]),
|
|
462
|
+
kind="stable")][:out_cut]
|
|
463
|
+
id_font = dict(
|
|
464
|
+
color=to_hex(get_option("ID_color",
|
|
465
|
+
"gray50")),
|
|
466
|
+
size=max(8, round(float(ID_size) * 14)))
|
|
467
|
+
for ii in keep_i:
|
|
468
|
+
fig.add_annotation(
|
|
469
|
+
x=float(xu[ii]), y=yc + 0.55,
|
|
470
|
+
text=str(idu[ii]), showarrow=False,
|
|
471
|
+
textangle=-90, yanchor="bottom",
|
|
472
|
+
font=id_font)
|
|
473
|
+
|
|
474
|
+
# ---- axes, grid, strips, layout --------------------------------
|
|
475
|
+
axT1 = pretty(float(min([x.min()] + fence_ext)),
|
|
476
|
+
float(max([x.max()] + fence_ext)))
|
|
477
|
+
axL1 = axis_format(axT1, digits_d, axis_fmt,
|
|
478
|
+
axis_x_pre)
|
|
479
|
+
ax_x = axis_num(x_lab, axT1, axL1)
|
|
480
|
+
|
|
481
|
+
shapes = x_grid(axT1) + plot_border()
|
|
482
|
+
annotations = []
|
|
483
|
+
if n_fac > 1: # shaded strip label per band
|
|
484
|
+
s_fill = as_plotly_color(get_option("strip_fill"))
|
|
485
|
+
s_line = to_hex(get_option("strip_color"))
|
|
486
|
+
s_text = to_hex(get_option("strip_text_color"))
|
|
487
|
+
s_font = round(14 * get_option("axis_size", 0.9))
|
|
488
|
+
strip_items = [(u["yc"] + _STRIP[0],
|
|
489
|
+
u["yc"] + _STRIP[1], u["strip"])
|
|
490
|
+
for u in units if u["strip"] is not None]
|
|
491
|
+
for y0, y1, txt in strip_items + sec_strips:
|
|
492
|
+
shapes.append({
|
|
493
|
+
"type": "rect", "xref": "paper", "yref": "y",
|
|
494
|
+
"x0": 0, "x1": 1, "y0": y0, "y1": y1,
|
|
495
|
+
"fillcolor": s_fill,
|
|
496
|
+
"line": {"color": s_line, "width": 1},
|
|
497
|
+
})
|
|
498
|
+
annotations.append(dict(
|
|
499
|
+
xref="paper", yref="y", x=0.5,
|
|
500
|
+
y=(y0 + y1) / 2, text=txt,
|
|
501
|
+
showarrow=False,
|
|
502
|
+
font=dict(size=s_font, color=s_text),
|
|
503
|
+
))
|
|
504
|
+
|
|
505
|
+
if sec_strips:
|
|
506
|
+
y_top = sec_strips[-1][1] + 0.05
|
|
507
|
+
elif n_fac > 1:
|
|
508
|
+
y_top = units[-1]["yc"] + _STRIP[1] + 0.05
|
|
509
|
+
else:
|
|
510
|
+
y_top = 1.05
|
|
511
|
+
fig.update_layout(
|
|
512
|
+
xaxis=ax_x,
|
|
513
|
+
yaxis=dict(visible=False, range=[-1.05, y_top],
|
|
514
|
+
fixedrange=True),
|
|
515
|
+
shapes=shapes,
|
|
516
|
+
annotations=annotations,
|
|
517
|
+
template=None,
|
|
518
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
519
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
520
|
+
height=(450 if n_fac == 1
|
|
521
|
+
else 120 + 190 * n_fac
|
|
522
|
+
+ 40 * len(sec_strips)),
|
|
523
|
+
)
|
|
524
|
+
if n_grp > 1: # lattice key: top, one row
|
|
525
|
+
leg_color = to_hex(style_opts["lab_color"])
|
|
526
|
+
fig.update_layout(legend=dict(
|
|
527
|
+
orientation="h",
|
|
528
|
+
x=0.5, xanchor="center", y=1.02, yanchor="bottom",
|
|
529
|
+
font=dict(size=round(14 * get_option("axis_size",
|
|
530
|
+
0.9)),
|
|
531
|
+
family="Arial", color=leg_color),
|
|
532
|
+
bgcolor=to_hex(style_opts["window_fill"]),
|
|
533
|
+
bordercolor=to_hex("gray80"), borderwidth=1,
|
|
534
|
+
itemsizing="constant",
|
|
535
|
+
))
|
|
536
|
+
if main:
|
|
537
|
+
title_size = round(16 * get_option("main_size", 1))
|
|
538
|
+
fig.update_layout(
|
|
539
|
+
title=dict(text=main, x=0.5, xanchor="center",
|
|
540
|
+
y=0.98, yanchor="top",
|
|
541
|
+
font=dict(size=title_size)),
|
|
542
|
+
margin=dict(t=round(title_size * 2.2
|
|
543
|
+
+ (26 if n_grp > 1 else 0))),
|
|
544
|
+
)
|
|
545
|
+
return fig
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lessPython
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python port of R's lessR: analytic-view visualizations, statistical models, and data utilities, rendered with plotly
|
|
5
|
+
Author-email: "David W. Gerbing" <gerbing@pdx.edu>
|
|
6
|
+
License-Expression: GPL-2.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/lessPython/
|
|
8
|
+
Project-URL: Source (lessR), https://cran.r-project.org/package=lessR
|
|
9
|
+
Keywords: visualization,statistics,plotly,regression,anova,factor-analysis,data-analysis,lessR
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: plotly>=5.20
|
|
25
|
+
Requires-Dist: pandas>=2.0
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: scipy>=1.10
|
|
28
|
+
Requires-Dist: statsmodels>=0.14
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: kaleido; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# lessPython
|
|
35
|
+
|
|
36
|
+
A Python port of [lessR](https://cran.r-project.org/package=lessR):
|
|
37
|
+
analytic-view visualizations, statistical models, correlation and
|
|
38
|
+
factor analysis, teaching simulations, and data utilities — the
|
|
39
|
+
graphics rendered with plotly.
|
|
40
|
+
|
|
41
|
+
Install as `lessPython`, import as `lessPy`:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install lessPython
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import lessPy as lp
|
|
49
|
+
|
|
50
|
+
d = lp.read_data("Employee")
|
|
51
|
+
lp.Chart("JobSat", data=d) # bar chart
|
|
52
|
+
lp.Chart("JobSat", by="Gender", data=d, form="pie")
|
|
53
|
+
lp.Regression("Salary ~ Years + Pre", data=d)
|
|
54
|
+
lp.simCLT(ns=10000, n=30, dist="uniform")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Variables are passed as **strings** naming columns of a pandas
|
|
58
|
+
`DataFrame` — Python has no equivalent of R's non-standard
|
|
59
|
+
evaluation, so `Chart("JobSat", data=d)`, not `Chart(JobSat)`.
|
|
60
|
+
|
|
61
|
+
## The API
|
|
62
|
+
|
|
63
|
+
The public API is **flat**: every function is reached from the
|
|
64
|
+
top-level package, either as `lp.<name>` or via
|
|
65
|
+
`from lessPy import <name>`. There are no sub-packages to import.
|
|
66
|
+
|
|
67
|
+
See **[docs/reference.md](docs/reference.md)** for the full list
|
|
68
|
+
of functions, grouped by category for navigation:
|
|
69
|
+
|
|
70
|
+
- **Plots** — `Chart`, `X`, `XY`, `Flows`
|
|
71
|
+
- **Models** — `Regression`, `Logit`, `ANOVA`, `ttest`,
|
|
72
|
+
`Correlation`, `Prop_test`
|
|
73
|
+
- **Correlation / factor analysis** — `corEFA`, `corCFA`,
|
|
74
|
+
`corScree`, `corReorder`, `corProp`, `corReflect`, `corRead`,
|
|
75
|
+
`corPrint`
|
|
76
|
+
- **Simulations** — `simCLT`, `simMeans`, `simFlips`, `simCImean`
|
|
77
|
+
- **Data** — `read_data`, `datasets`, `reshape_long`,
|
|
78
|
+
`reshape_wide`, `pivot`, `rename`
|
|
79
|
+
- **Color** — `getColors`, `showColors`
|
|
80
|
+
- **Dates** — `date_infer`, `format_date_labels`
|
|
81
|
+
|
|
82
|
+
The inferential functions print their analysis as in lessR and
|
|
83
|
+
also return a results object whose numeric fields and plotly
|
|
84
|
+
figures (in `.plots`) are available for programmatic use.
|
|
85
|
+
|
|
86
|
+
## Development install
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
conda activate lessPy
|
|
90
|
+
pip install -e ".[dev]"
|
|
91
|
+
pytest
|
|
92
|
+
python demo/demo_chart.py
|
|
93
|
+
```
|