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.
Files changed (82) hide show
  1. lessPy/ANOVA.py +680 -0
  2. lessPy/Chart.py +1055 -0
  3. lessPy/Correlation.py +236 -0
  4. lessPy/Flows.py +116 -0
  5. lessPy/Logit.py +615 -0
  6. lessPy/Prop_test.py +267 -0
  7. lessPy/Regression.py +1491 -0
  8. lessPy/VariableLabels.py +119 -0
  9. lessPy/X.py +426 -0
  10. lessPy/XY.py +2007 -0
  11. lessPy/__init__.py +60 -0
  12. lessPy/anova_rmd.py +227 -0
  13. lessPy/bc_plotly.py +575 -0
  14. lessPy/bubble_plotly.py +470 -0
  15. lessPy/corCFA.py +316 -0
  16. lessPy/corEFA.py +220 -0
  17. lessPy/corPrint.py +45 -0
  18. lessPy/corProp.py +73 -0
  19. lessPy/corRead.py +48 -0
  20. lessPy/corReflect.py +72 -0
  21. lessPy/corReorder.py +161 -0
  22. lessPy/corScree.py +87 -0
  23. lessPy/data/Anova_1way.csv +25 -0
  24. lessPy/data/Anova_2way.csv +49 -0
  25. lessPy/data/Anova_rb.csv +8 -0
  26. lessPy/data/Anova_rbf.csv +49 -0
  27. lessPy/data/Anova_sp.csv +57 -0
  28. lessPy/data/BodyMeas.csv +341 -0
  29. lessPy/data/Cars93.csv +94 -0
  30. lessPy/data/Employee.csv +38 -0
  31. lessPy/data/Employee_lbl.csv +9 -0
  32. lessPy/data/FreqTable99.csv +5 -0
  33. lessPy/data/Jackets.csv +1026 -0
  34. lessPy/data/Learn.csv +35 -0
  35. lessPy/data/Mach4.csv +352 -0
  36. lessPy/data/Mach4_lbl.csv +21 -0
  37. lessPy/data/Reading.csv +101 -0
  38. lessPy/data/StockPrice.csv +1489 -0
  39. lessPy/data/WeightLoss.csv +11 -0
  40. lessPy/datasets.py +46 -0
  41. lessPy/date_infer.py +112 -0
  42. lessPy/details.py +314 -0
  43. lessPy/dn_plotly.py +495 -0
  44. lessPy/dot_plotly.py +385 -0
  45. lessPy/freq_poly_plotly.py +324 -0
  46. lessPy/getColors.py +399 -0
  47. lessPy/hier_plotly.py +352 -0
  48. lessPy/hs_plotly.py +395 -0
  49. lessPy/logit_rmd.py +410 -0
  50. lessPy/order_by.py +94 -0
  51. lessPy/pie_plotly.py +292 -0
  52. lessPy/pivot.py +158 -0
  53. lessPy/plotly_utils.py +787 -0
  54. lessPy/plt_add.py +129 -0
  55. lessPy/plt_contour.py +192 -0
  56. lessPy/plt_contour_facet.py +194 -0
  57. lessPy/plt_forecast.py +677 -0
  58. lessPy/plt_mat_plotly.py +201 -0
  59. lessPy/plt_plotly.py +216 -0
  60. lessPy/plt_smooth.py +170 -0
  61. lessPy/plt_time.py +143 -0
  62. lessPy/prob_norm.py +111 -0
  63. lessPy/prob_tcut.py +131 -0
  64. lessPy/prob_znorm.py +110 -0
  65. lessPy/radar_plotly.py +201 -0
  66. lessPy/reg_rmd.py +754 -0
  67. lessPy/rename.py +33 -0
  68. lessPy/reshape.py +95 -0
  69. lessPy/showColors.py +130 -0
  70. lessPy/simCImean.py +165 -0
  71. lessPy/simCLT.py +265 -0
  72. lessPy/simFlips.py +104 -0
  73. lessPy/simMeans.py +146 -0
  74. lessPy/stats_out.py +189 -0
  75. lessPy/ttest.py +641 -0
  76. lessPy/utils.py +235 -0
  77. lessPy/vbs_plotly.py +545 -0
  78. lesspython-0.1.0.dist-info/METADATA +93 -0
  79. lesspython-0.1.0.dist-info/RECORD +82 -0
  80. lesspython-0.1.0.dist-info/WHEEL +5 -0
  81. lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
  82. 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
+ ```