lessPython 0.2.0__py3-none-any.whl → 0.3.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 +5 -5
- lessPy/Chart.py +386 -88
- lessPy/Correlation.py +2 -2
- lessPy/Logit.py +2 -2
- lessPy/Regression.py +6 -4
- lessPy/X.py +279 -42
- lessPy/XY.py +814 -158
- lessPy/__init__.py +12 -2
- lessPy/_themes.py +5007 -0
- lessPy/bc_items_plotly.py +31 -18
- lessPy/bc_plotly.py +23 -7
- lessPy/bubble_plotly.py +17 -2
- lessPy/corCFA.py +3 -3
- lessPy/corProp.py +2 -2
- lessPy/corReflect.py +2 -2
- lessPy/corReorder.py +2 -2
- lessPy/corScree.py +3 -3
- lessPy/data/StockPrice.csv +1401 -1386
- lessPy/details.py +17 -4
- lessPy/dn_plotly.py +65 -45
- lessPy/freq_poly_plotly.py +12 -5
- lessPy/getColors.py +247 -20
- lessPy/hier_plotly.py +36 -9
- lessPy/hs_plotly.py +16 -4
- lessPy/plotly_utils.py +78 -9
- lessPy/plt_mat_plotly.py +74 -2
- lessPy/plt_time.py +10 -3
- lessPy/prob_norm.py +3 -3
- lessPy/prob_tcut.py +3 -3
- lessPy/prob_znorm.py +3 -1
- lessPy/rcolors.py +461 -0
- lessPy/simCImean.py +3 -3
- lessPy/simCLT.py +3 -3
- lessPy/simFlips.py +3 -3
- lessPy/simMeans.py +3 -3
- lessPy/stats_out.py +257 -11
- lessPy/style.py +160 -0
- lessPy/sunflower_plotly.py +140 -0
- lessPy/ttest.py +5 -5
- lessPy/utils.py +43 -6
- lessPy/vbs_plotly.py +16 -5
- lessPy/x_console.py +763 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/METADATA +6 -2
- lesspython-0.3.0.dist-info/RECORD +89 -0
- lesspython-0.2.0.dist-info/RECORD +0 -84
- {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/WHEEL +0 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {lesspython-0.2.0.dist-info → lesspython-0.3.0.dist-info}/top_level.txt +0 -0
lessPy/x_console.py
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
# x_console.py — the console report of X(), ports of lessR's printers
|
|
2
|
+
#
|
|
3
|
+
# R analogs: .ss.numeric() (brief, one variable), .bx.stats() (the
|
|
4
|
+
# outlier listing and the VBS statistics), .hst.stats() (the bin
|
|
5
|
+
# table and the histogram suggestions), dn.main.R (bandwidth,
|
|
6
|
+
# Shapiro-Wilk, the density suggestions), the grouped table that
|
|
7
|
+
# X.R prints through .ss.pivot(), and .param.VBS() (suggestions,
|
|
8
|
+
# duplicated values, parameter values) with .vbs_summary_table().
|
|
9
|
+
#
|
|
10
|
+
# Each function returns a list of lines, a component of R's
|
|
11
|
+
# print.out_all(); report() joins the components with one blank
|
|
12
|
+
# line between them, as print.out_all() does. Trailing blanks are
|
|
13
|
+
# dropped (R's cat(x, "\n") leaves one at the end of every line).
|
|
14
|
+
#
|
|
15
|
+
# The suggestions are written in Python syntax, naming the module
|
|
16
|
+
# and the DataFrame as the caller's own code names them.
|
|
17
|
+
|
|
18
|
+
import inspect
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from .stats_out import _fmtc, _fmti, _fmtr, _r_chr, _stat_by_levels
|
|
25
|
+
from .utils import get_option
|
|
26
|
+
from .vbs_plotly import _adj_fences, _medcouple, five_num
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ----- R number formatting --------------------------------------
|
|
30
|
+
|
|
31
|
+
def r_format1(v):
|
|
32
|
+
"""R format() of one number: 7 significant digits, trailing
|
|
33
|
+
zeros dropped, fixed notation."""
|
|
34
|
+
s = f"{float(v):.7g}"
|
|
35
|
+
if "e" in s:
|
|
36
|
+
s = np.format_float_positional(float(s), trim="-")
|
|
37
|
+
return s
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def r_chr(v):
|
|
41
|
+
"""R as.character() of one number: 15 significant digits."""
|
|
42
|
+
s = f"{float(v):.15g}"
|
|
43
|
+
if "e" in s:
|
|
44
|
+
s = np.format_float_positional(float(s), trim="-")
|
|
45
|
+
return s
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _n_dec(s):
|
|
49
|
+
return len(s) - s.index(".") - 1 if "." in s else 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def max_dd(x):
|
|
53
|
+
"""Decimal digits of the most precise of the first 200 values.
|
|
54
|
+
R analog: .max.dd()"""
|
|
55
|
+
m = 0
|
|
56
|
+
for v in list(np.asarray(x, dtype=float))[:200]:
|
|
57
|
+
if np.isfinite(v):
|
|
58
|
+
m = max(m, _n_dec(r_format1(v)))
|
|
59
|
+
return m
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def r_format_vec(vals):
|
|
63
|
+
"""R format() of a numeric vector: the decimals that show each
|
|
64
|
+
value to 7 significant digits, the most of them for all, then
|
|
65
|
+
one common width, right-justified."""
|
|
66
|
+
v = np.asarray(vals, dtype=float)
|
|
67
|
+
fin = v[np.isfinite(v)]
|
|
68
|
+
rgt = max((_n_dec(r_format1(z)) for z in fin), default=0)
|
|
69
|
+
out = [f"{z:.{rgt}f}" if np.isfinite(z) else "NA" for z in v]
|
|
70
|
+
w = max((len(s) for s in out), default=0)
|
|
71
|
+
return [s.rjust(w) for s in out]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def decdig(vals):
|
|
75
|
+
"""Round a column of statistics as lessR's pivot() does before
|
|
76
|
+
it prints: integers kept, otherwise 3 decimals, or one past the
|
|
77
|
+
leading zeros of values below 1. R analog: .decdig()"""
|
|
78
|
+
v = np.asarray(vals, dtype=float)
|
|
79
|
+
fin = v[np.isfinite(v)]
|
|
80
|
+
if len(fin) == 0 or all(float(z).is_integer() for z in fin):
|
|
81
|
+
return v
|
|
82
|
+
lead0 = _lead0(fin)
|
|
83
|
+
locs = [r_format1(z).find(".") + 1 or len(r_format1(z)) + 1
|
|
84
|
+
for z in fin]
|
|
85
|
+
dgs = lead0 + 1 if lead0 > 0 else 3
|
|
86
|
+
if min(locs) > 2:
|
|
87
|
+
dgs = 3
|
|
88
|
+
return np.round(v, dgs)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _lead0(vals):
|
|
92
|
+
"""Leading zeros after the decimal point of values below 1.
|
|
93
|
+
R analog: .lead0()"""
|
|
94
|
+
n_max = 0
|
|
95
|
+
for v in vals:
|
|
96
|
+
s = r_format1(v)
|
|
97
|
+
if "." in s and v < 1:
|
|
98
|
+
n = len(s.split(".")[1]) - len(s.split(".")[1].lstrip("0"))
|
|
99
|
+
n_max = max(n_max, n)
|
|
100
|
+
return n_max
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def title(x_name, x_lbl=None):
|
|
104
|
+
"""--- x --- heading, with the variable label. R .title2()"""
|
|
105
|
+
t = x_name if x_lbl is None else f"{x_name}: {x_lbl}"
|
|
106
|
+
return f"--- {t} ---"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ----- the caller's names, for runnable suggestions ---------------
|
|
110
|
+
|
|
111
|
+
def call_names(data):
|
|
112
|
+
"""The prefix that reaches lessPy in the caller's namespace
|
|
113
|
+
("lp." for import lessPy as lp, "" for from lessPy import X)
|
|
114
|
+
and the caller's name of the DataFrame (default "d")."""
|
|
115
|
+
mod = sys.modules.get("lessPy")
|
|
116
|
+
pre, dname = "lp.", "d"
|
|
117
|
+
f = inspect.currentframe()
|
|
118
|
+
try:
|
|
119
|
+
while f is not None and str(
|
|
120
|
+
f.f_globals.get("__name__", "")).startswith("lessPy"):
|
|
121
|
+
f = f.f_back
|
|
122
|
+
if f is not None:
|
|
123
|
+
ns = dict(f.f_globals)
|
|
124
|
+
ns.update(f.f_locals)
|
|
125
|
+
mods = [k for k, v in ns.items()
|
|
126
|
+
if v is mod and not k.startswith("_")]
|
|
127
|
+
if mods:
|
|
128
|
+
pre = mods[0] + "."
|
|
129
|
+
elif "X" in ns:
|
|
130
|
+
pre = ""
|
|
131
|
+
names = [k for k, v in ns.items()
|
|
132
|
+
if v is data and not k.startswith("_")]
|
|
133
|
+
if names:
|
|
134
|
+
dname = names[0]
|
|
135
|
+
finally:
|
|
136
|
+
del f
|
|
137
|
+
return pre, dname
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ----- components ------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def ss_numeric(xv, x_name, x_lbl=None, digits_d=None):
|
|
143
|
+
"""n, miss, mean, sd, min, mdn, max of one variable, missing
|
|
144
|
+
values included in the count. R analog: .ss.numeric(x,
|
|
145
|
+
brief=TRUE) with one line"""
|
|
146
|
+
v = np.asarray(xv, dtype=float)
|
|
147
|
+
miss = int(np.isnan(v).sum())
|
|
148
|
+
v = v[~np.isnan(v)]
|
|
149
|
+
n = len(v)
|
|
150
|
+
d = digits_d
|
|
151
|
+
if d is None:
|
|
152
|
+
d = max_dd(v) + 1
|
|
153
|
+
if d == 1:
|
|
154
|
+
d = 2
|
|
155
|
+
max_lv = 3
|
|
156
|
+
max_n = len(str(n))
|
|
157
|
+
max_nm = len(str(miss))
|
|
158
|
+
m = v.mean() if n > 0 else np.nan
|
|
159
|
+
s = v.std(ddof=1) if n > 1 else np.nan
|
|
160
|
+
if np.isnan(s):
|
|
161
|
+
max_ln = len(_r_chr(m, d)) + d if n > 0 else 0
|
|
162
|
+
else:
|
|
163
|
+
max_ln = max(len(_r_chr(m, d)), len(_r_chr(s, d))) + d
|
|
164
|
+
for lim, add in ((5, 1), (10, 1), (4, 2), (8, 1)):
|
|
165
|
+
if max_ln < lim:
|
|
166
|
+
max_ln += add
|
|
167
|
+
lines = [title(x_name, x_lbl), ""]
|
|
168
|
+
lines.append(" ".join([
|
|
169
|
+
_fmtc("n", len(str(max_n)) + 3 + max_lv - 2),
|
|
170
|
+
_fmtc("miss", len(str(max_nm)) + 5),
|
|
171
|
+
_fmtc("mean", max_ln), _fmtc("sd", max_ln),
|
|
172
|
+
_fmtc("min", max_ln), _fmtc("mdn", max_ln),
|
|
173
|
+
_fmtc("max", max_ln)]))
|
|
174
|
+
row = [_fmtc("", max_lv), _fmti(n, max_n + 1),
|
|
175
|
+
_fmti(miss, max_nm + 5)]
|
|
176
|
+
if n == 1:
|
|
177
|
+
row.append(_fmtr(m, d, max_ln))
|
|
178
|
+
elif n > 1:
|
|
179
|
+
row += [_fmtr(z, d, max_ln) for z in
|
|
180
|
+
(m, s, v.min(), np.median(v), v.max())]
|
|
181
|
+
lines.append(" ".join(row))
|
|
182
|
+
lines.append("")
|
|
183
|
+
return lines
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _box_stats(x, k_iqr=1.5, box_adj=False, a=-4, b=3):
|
|
187
|
+
"""Whiskers and outliers on Tukey hinges, as boxplot.stats(), or
|
|
188
|
+
medcouple-adjusted as robustbase::adjboxStats()"""
|
|
189
|
+
mn, q1, md, q3, mx = five_num(x)
|
|
190
|
+
iqr = q3 - q1
|
|
191
|
+
mcv = _medcouple(x) if box_adj else 0.0
|
|
192
|
+
(lo_f, hi_f), _ = _adj_fences(q1, q3, iqr, k_iqr, mcv, a, b)
|
|
193
|
+
out = (x < lo_f) | (x > hi_f)
|
|
194
|
+
inside = x[~out]
|
|
195
|
+
return (inside.min(), q1, md, q3, inside.max()), x[out]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def outliers(x, digits_d=2, ids=None, k_iqr=1.5, box_adj=False,
|
|
199
|
+
a=-4, b=3):
|
|
200
|
+
"""The boxplot outliers, small and large, with their IDs.
|
|
201
|
+
R analog: .bx.stats()$txotl"""
|
|
202
|
+
x = np.asarray(x, dtype=float)
|
|
203
|
+
x = x[np.isfinite(x)]
|
|
204
|
+
if digits_d is None:
|
|
205
|
+
digits_d = max(max_dd(x) + 1, 2)
|
|
206
|
+
st, out = _box_stats(x, k_iqr, box_adj, a, b)
|
|
207
|
+
if len(out) == 0:
|
|
208
|
+
return ["No (Box plot) outliers"]
|
|
209
|
+
ids = (np.asarray(ids).astype(str) if ids is not None
|
|
210
|
+
else np.array([""] * len(x)))
|
|
211
|
+
d1 = digits_d - 1
|
|
212
|
+
|
|
213
|
+
def side(mask, decreasing):
|
|
214
|
+
xv, iv = x[mask], ids[mask]
|
|
215
|
+
o = np.argsort(-xv if decreasing else xv, kind="stable")
|
|
216
|
+
return ([f"{z:.{d1}f}" for z in xv[o]], [str(i) for i in iv[o]])
|
|
217
|
+
|
|
218
|
+
lo_mask, hi_mask = x < st[0], x > st[4]
|
|
219
|
+
if lo_mask.any():
|
|
220
|
+
xc_lo, id_lo = side(lo_mask, False)
|
|
221
|
+
lo_len = len(xc_lo)
|
|
222
|
+
max_lo = max(len(s) for s in xc_lo)
|
|
223
|
+
max_id_lo = max(len(s) for s in id_lo)
|
|
224
|
+
else:
|
|
225
|
+
xc_lo, id_lo, max_lo, max_id_lo, lo_len = [""], [" "], 0, 5, 0
|
|
226
|
+
if hi_mask.any():
|
|
227
|
+
xc_hi, id_hi = side(hi_mask, True)
|
|
228
|
+
hi_len = len(xc_hi)
|
|
229
|
+
max_hi = max(len(s) for s in xc_hi)
|
|
230
|
+
max_id_hi = max(len(s) for s in id_hi)
|
|
231
|
+
else:
|
|
232
|
+
xc_hi, id_hi, max_hi, max_id_hi, hi_len = [""], [" "], 0, 5, 0
|
|
233
|
+
|
|
234
|
+
max_list = 18
|
|
235
|
+
n_lines = min(max(lo_len, hi_len), max_list)
|
|
236
|
+
for i in range(1, n_lines + 1): # R grows the shorter side
|
|
237
|
+
if i > lo_len:
|
|
238
|
+
xc_lo.append("")
|
|
239
|
+
id_lo.append("")
|
|
240
|
+
if i > hi_len:
|
|
241
|
+
xc_hi.append("")
|
|
242
|
+
id_hi.append("")
|
|
243
|
+
|
|
244
|
+
def left(s, w):
|
|
245
|
+
return str(s).ljust(max(w, 0))
|
|
246
|
+
|
|
247
|
+
adj = len("Small") - max_id_lo
|
|
248
|
+
buf = 3 - adj if adj > 0 else 3
|
|
249
|
+
lines = [" ", f"--- Outliers --- from the boxplot: {len(out)}",
|
|
250
|
+
"",
|
|
251
|
+
" ".join([left("Small", max_id_lo), _fmtc("", max_lo),
|
|
252
|
+
_fmtc("", max(buf, 0)), left("Large", max_id_hi)]),
|
|
253
|
+
" ".join([left("-----", max_id_lo), _fmtc("", max_lo),
|
|
254
|
+
_fmtc("", max(buf, 0)), left("-----", max_id_hi)])]
|
|
255
|
+
for i in range(n_lines):
|
|
256
|
+
lines.append(" ".join([
|
|
257
|
+
left(id_lo[i], max_id_lo), _fmtc(xc_lo[i], max_lo), " ",
|
|
258
|
+
left(id_hi[i], max_id_hi), _fmtc(xc_hi[i], max_hi)]))
|
|
259
|
+
if max(lo_len, hi_len) > n_lines:
|
|
260
|
+
lines += ["", f"+ {len(out) - max_list} more outliers"]
|
|
261
|
+
return lines
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def box_summary(x, x_name, x_lbl=None, digits_d=None, n_total=None,
|
|
265
|
+
k_iqr=1.5, box_adj=False, a=-4, b=3):
|
|
266
|
+
"""The statistics of the VBS display, whiskers included.
|
|
267
|
+
R analog: .bx.stats()$txstat"""
|
|
268
|
+
x = np.asarray(x, dtype=float)
|
|
269
|
+
miss = int(np.isnan(x).sum())
|
|
270
|
+
v = x[~np.isnan(x)]
|
|
271
|
+
d = digits_d
|
|
272
|
+
if d is None:
|
|
273
|
+
d = max(max_dd(v) + 1, 2)
|
|
274
|
+
st, _ = _box_stats(v, k_iqr, box_adj, a, b)
|
|
275
|
+
q1, q3 = np.percentile(v, [25, 75])
|
|
276
|
+
|
|
277
|
+
def f(z):
|
|
278
|
+
return f"{z:.{d}f}"
|
|
279
|
+
|
|
280
|
+
return [title(x_name, x_lbl),
|
|
281
|
+
f"Present: {len(v)}",
|
|
282
|
+
f"Missing: {miss}",
|
|
283
|
+
f"Total : {len(x)}",
|
|
284
|
+
"",
|
|
285
|
+
f"Mean : {f(v.mean())}",
|
|
286
|
+
f"Stnd Dev : {f(v.std(ddof=1))}",
|
|
287
|
+
f"IQR : {f(q3 - q1)}",
|
|
288
|
+
f"Skew : {f(_medcouple(v))} [medcouple, -1 to 1]",
|
|
289
|
+
"",
|
|
290
|
+
f"Minimum : {f(v.min())}",
|
|
291
|
+
f"Lower Whisker: {f(st[0])}",
|
|
292
|
+
f"1st Quartile : {f(q1)}",
|
|
293
|
+
f"Median : {f(np.median(v))}",
|
|
294
|
+
f"3rd Quartile : {f(q3)}",
|
|
295
|
+
f"Upper Whisker: {f(st[4])}",
|
|
296
|
+
f"Maximum : {f(v.max())}"]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def bin_table(edges, counts, n, mx_dd):
|
|
300
|
+
"""Bin width, number of bins, and the frequency distribution.
|
|
301
|
+
R analog: .hst.stats()$tx"""
|
|
302
|
+
br = np.asarray(edges, dtype=float)
|
|
303
|
+
cnt = np.asarray(counts, dtype=float)
|
|
304
|
+
mids = (br[:-1] + br[1:]) / 2
|
|
305
|
+
bw = float(f"{br[1] - br[0]:.10g}")
|
|
306
|
+
max_dg = max((len(r_chr(b)) for b in br if len(r_chr(b)) < 17),
|
|
307
|
+
default=0)
|
|
308
|
+
max_dg_mid = max((len(r_chr(m)) for m in mids
|
|
309
|
+
if len(r_chr(m)) < 19), default=0)
|
|
310
|
+
x_br = [s.rjust(max_dg) for s in r_format_vec(br)]
|
|
311
|
+
x_mid = [s.rjust(max_dg_mid) for s in r_format_vec(mids)]
|
|
312
|
+
if _lead0(mids) > mx_dd:
|
|
313
|
+
x_mid = [f"{m:.{mx_dd}f}" for m in mids]
|
|
314
|
+
bn = [f"{x_br[i]} > {x_br[i + 1]}" for i in range(len(br) - 1)]
|
|
315
|
+
prop = cnt / n
|
|
316
|
+
cols = [
|
|
317
|
+
("Bin", bn), ("Midpnt", x_mid),
|
|
318
|
+
("Count", [f"{c:.0f}" for c in cnt]),
|
|
319
|
+
(" Prop", [f"{p:.2f}" for p in prop]),
|
|
320
|
+
("Cumul.c", [f"{c:.0f}" for c in np.cumsum(cnt)]),
|
|
321
|
+
("Cumul.p", [f"{p:.2f}" for p in np.cumsum(prop)])]
|
|
322
|
+
widths = [max(len(nm) + 1, max(len(s) for s in vals)) + 1
|
|
323
|
+
for nm, vals in cols]
|
|
324
|
+
lines = ["", f"Bin Width: {r_chr(bw)}",
|
|
325
|
+
f"Number of Bins: {len(br) - 1}", "",
|
|
326
|
+
"".join(_fmtc(nm, w) for (nm, _), w in zip(cols, widths)),
|
|
327
|
+
"-" * sum(widths)]
|
|
328
|
+
for i in range(len(bn)):
|
|
329
|
+
lines.append("".join(_fmtc(vals[i], w)
|
|
330
|
+
for (_, vals), w in zip(cols, widths)))
|
|
331
|
+
lines.append("")
|
|
332
|
+
return lines
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _df_lines(cols, sep=" ", lead=" "):
|
|
336
|
+
"""Columns (name, strings) right-justified to their widths,
|
|
337
|
+
as R prints a data frame without row names."""
|
|
338
|
+
widths = [max(len(nm), max((len(s) for s in vals), default=0))
|
|
339
|
+
for nm, vals in cols]
|
|
340
|
+
n_rows = len(cols[0][1]) if cols else 0
|
|
341
|
+
out = [lead + sep.join(nm.rjust(w)
|
|
342
|
+
for (nm, _), w in zip(cols, widths))]
|
|
343
|
+
for i in range(n_rows):
|
|
344
|
+
out.append(lead + sep.join(vals[i].rjust(w)
|
|
345
|
+
for (_, vals), w in zip(cols, widths)))
|
|
346
|
+
return out
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _group_stats(x, g, order=None):
|
|
350
|
+
"""Per-group statistics, groups in the given order (the levels of
|
|
351
|
+
a categorical, as R's factor levels) or sorted."""
|
|
352
|
+
s = pd.Series(np.asarray(x, dtype=float))
|
|
353
|
+
gs = pd.Series(np.asarray(g)).astype(str)
|
|
354
|
+
lvls = ([str(v) for v in order] if order is not None
|
|
355
|
+
else sorted(gs.unique()))
|
|
356
|
+
rows = []
|
|
357
|
+
for lv in lvls:
|
|
358
|
+
z = s[(gs == lv).to_numpy()]
|
|
359
|
+
if len(z) == 0:
|
|
360
|
+
continue
|
|
361
|
+
zz = z.dropna()
|
|
362
|
+
q1, q3 = (np.percentile(zz, [25, 75]) if len(zz)
|
|
363
|
+
else (np.nan, np.nan))
|
|
364
|
+
rows.append(dict(lv=lv, n=len(zz), na=int(z.isna().sum()),
|
|
365
|
+
Mean=zz.mean(), Median=zz.median(),
|
|
366
|
+
SD=zz.std(ddof=1), IQR=q3 - q1,
|
|
367
|
+
Min=zz.min(), Max=zz.max()))
|
|
368
|
+
return rows
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def by_table(x, by, x_name, by_name, pre="lp.", dname="d",
|
|
372
|
+
order=None):
|
|
373
|
+
"""Statistics of x for each group of by, then the pointer to
|
|
374
|
+
the test of the mean difference for two groups. R analog: the
|
|
375
|
+
grouped-stats block of X.R through .ss.pivot()"""
|
|
376
|
+
rows = _group_stats(x, by, order)
|
|
377
|
+
cols = [(by_name, [r["lv"] for r in rows]),
|
|
378
|
+
("n", [str(r["n"]) for r in rows]),
|
|
379
|
+
("na", [str(r["na"]) for r in rows])]
|
|
380
|
+
for k in ("Mean", "Median", "SD", "IQR", "Min", "Max"):
|
|
381
|
+
cols.append((k, r_format_vec(decdig([r[k] for r in rows]))))
|
|
382
|
+
lines = [f"{x_name} by {by_name}", ""] + _df_lines(cols)
|
|
383
|
+
if len(rows) == 2:
|
|
384
|
+
lines += ["", "For inferential analysis of the mean difference:",
|
|
385
|
+
f'> {pre}ttest("{x_name} ~ {by_name}", data={dname})',
|
|
386
|
+
""]
|
|
387
|
+
else:
|
|
388
|
+
lines += [""]
|
|
389
|
+
return lines
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def facet_table(x, cells, x_name, cells_name, digits_d=2):
|
|
393
|
+
"""Statistics of x in each panel, missing values counted per
|
|
394
|
+
panel. R analog: .ss.numeric(x, by=cells, brief=TRUE) as
|
|
395
|
+
.bar.lattice() calls it, at getOption("digits_d"): lessR's
|
|
396
|
+
default 2 for facet alone, the one-variable digits (.max.dd + 1)
|
|
397
|
+
left by the earlier summary when by= divides the panels"""
|
|
398
|
+
return _stat_by_levels(pd.Series(np.asarray(x, dtype=float)),
|
|
399
|
+
pd.Series(np.asarray(cells)).astype(str),
|
|
400
|
+
x_name, cells_name, digits_d)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def vbs_summary(x, g, label, digits_d, order=None):
|
|
404
|
+
"""n, Mean, Median, SD, IQR, Min, Max of x, per group of g or
|
|
405
|
+
for all of x. R analog: .vbs_summary_table() with .df_char()"""
|
|
406
|
+
if g is None:
|
|
407
|
+
rows = _group_stats(x, np.repeat(label, len(x)))
|
|
408
|
+
else:
|
|
409
|
+
rows = _group_stats(x, g, order)
|
|
410
|
+
cols = [(label, [r["lv"] for r in rows]),
|
|
411
|
+
("n", [str(r["n"]) for r in rows])]
|
|
412
|
+
for k in ("Mean", "Median", "SD", "IQR", "Min", "Max"):
|
|
413
|
+
cols.append((k, [f"{r[k]:.{digits_d}f}" for r in rows]))
|
|
414
|
+
return _df_lines(cols, sep=" ", lead="")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def dup_values(x, grp=None, grp_order=None):
|
|
418
|
+
"""The most repeated value(s) of x, overall or per group.
|
|
419
|
+
R analog: .get.dup() of .param.VBS()"""
|
|
420
|
+
x = np.asarray(x, dtype=float)
|
|
421
|
+
ux = np.unique(x)
|
|
422
|
+
if len(ux) >= 1001:
|
|
423
|
+
return []
|
|
424
|
+
if grp is None:
|
|
425
|
+
cnt = pd.Series(x).value_counts()
|
|
426
|
+
mx_c = 0 if len(ux) == len(x) else int(cnt.max())
|
|
427
|
+
if mx_c <= 1:
|
|
428
|
+
return ["Number of duplicated values: 0"]
|
|
429
|
+
lvls, cols = [""], [cnt]
|
|
430
|
+
else:
|
|
431
|
+
g = np.asarray(grp).astype(str)
|
|
432
|
+
lvls = [str(lv) for lv in grp_order]
|
|
433
|
+
cols = [pd.Series(x[g == lv]).value_counts() for lv in lvls]
|
|
434
|
+
max_lvl = max((len(lv) for lv in lvls), default=0)
|
|
435
|
+
buf = max_lvl - 4 if max_lvl - 5 >= 0 else 0
|
|
436
|
+
# R's for (i in 1:buf) runs twice when buf is 0
|
|
437
|
+
h1 = "Level" + " " * (buf if buf > 0 else 2)
|
|
438
|
+
h0 = h1.replace("Level", " ")
|
|
439
|
+
lines = ["", f"{h0} Max Dupli-", f"{h1} cations Values", "-" * 30]
|
|
440
|
+
for lv, cnt in zip(lvls, cols):
|
|
441
|
+
mx = int(cnt.max()) if len(cnt) else 0
|
|
442
|
+
lvl_c = "" if grp is None else lv.ljust(max(max_lvl, 6))
|
|
443
|
+
if mx > 1:
|
|
444
|
+
vals = sorted(cnt[cnt == mx].index)
|
|
445
|
+
more = " ..." if len(vals) > 8 else ""
|
|
446
|
+
vals = " ".join(r_chr(v) for v in vals[:8])
|
|
447
|
+
lines.append(f"{lvl_c} {mx:3d} {vals}{more}")
|
|
448
|
+
else:
|
|
449
|
+
lines.append(f"{lvl_c} 0")
|
|
450
|
+
return lines
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def vbs_params(vbs_plot, prm, show_bw=True):
|
|
454
|
+
"""Point size, outlier size, jitter, and bandwidth as plotted.
|
|
455
|
+
R analog: .get.param() of .param.VBS()"""
|
|
456
|
+
if "s" not in vbs_plot and "v" not in vbs_plot:
|
|
457
|
+
return []
|
|
458
|
+
lines = ["", "---------- Parameter values (can be manually set)", ""]
|
|
459
|
+
if "s" in vbs_plot:
|
|
460
|
+
lines += [
|
|
461
|
+
f"pt_size: {prm['pt_size']:.2f} size of plotted points",
|
|
462
|
+
f"out_size: {prm['out_size']:.2f} size of plotted "
|
|
463
|
+
"outlier points",
|
|
464
|
+
f"jitter_y: {prm['jitter_y']:.2f} random vertical movement "
|
|
465
|
+
"of points",
|
|
466
|
+
f"jitter_x: {prm['jitter_x']:.2f} random horizontal "
|
|
467
|
+
"movement of points"]
|
|
468
|
+
if "v" in vbs_plot and show_bw:
|
|
469
|
+
lines.append(f"bw: {prm['bw']:.2f} set bandwidth higher "
|
|
470
|
+
"for smoother edges")
|
|
471
|
+
return lines
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ----- suggestions ------------------------------------------------
|
|
475
|
+
|
|
476
|
+
def _x_call(pre, x_name, dname, args):
|
|
477
|
+
return f'{pre}X("{x_name}", data={dname}{args})'
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def suggest_hist(x_name, pre, dname, given):
|
|
481
|
+
if not get_option("suggest", True):
|
|
482
|
+
return []
|
|
483
|
+
lines = [">>> Suggestions"]
|
|
484
|
+
for p, what in (("bin_width", "set the width of each bin"),
|
|
485
|
+
("bin_start", "set the start of the first bin"),
|
|
486
|
+
("bin_end", "set the end of the last bin")):
|
|
487
|
+
if p not in given:
|
|
488
|
+
lines.append(f"{p}: {what}")
|
|
489
|
+
lines.append(_x_call(pre, x_name, dname, ', form="density"')
|
|
490
|
+
+ " # smoothed curve + histogram")
|
|
491
|
+
lines.append(_x_call(pre, x_name, dname, ', form="vbs"')
|
|
492
|
+
+ " # Violin/Box/Scatterplot (VBS) plot")
|
|
493
|
+
return lines
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def suggest_density(x_name, pre, dname):
|
|
497
|
+
if not get_option("suggest", True):
|
|
498
|
+
return []
|
|
499
|
+
return [">>> Suggestions",
|
|
500
|
+
"bin_width: set the width of each bin",
|
|
501
|
+
_x_call(pre, x_name, dname, ', form="histogram"')
|
|
502
|
+
+ " # histogram only",
|
|
503
|
+
_x_call(pre, x_name, dname, ', form="vbs"')
|
|
504
|
+
+ " # Violin/Box/Scatterplot (VBS) plot"]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def suggest_freq_poly(x_name, pre, dname, given):
|
|
508
|
+
if not get_option("suggest", True):
|
|
509
|
+
return []
|
|
510
|
+
lines = [">>> Suggestions"]
|
|
511
|
+
if "bin_width" not in given:
|
|
512
|
+
lines.append("bin_width: set the width of each bin")
|
|
513
|
+
lines.append(_x_call(pre, x_name, dname, ', form="histogram"')
|
|
514
|
+
+ " # histogram")
|
|
515
|
+
lines.append(_x_call(pre, x_name, dname,
|
|
516
|
+
', form="freq_poly", area_fill="off"')
|
|
517
|
+
+ " # polygon outline only")
|
|
518
|
+
return lines
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def suggest_vbs(x_name, pre, dname, given, n_by=0, by_name=None,
|
|
522
|
+
n_facet=0, facet_name=None):
|
|
523
|
+
"""R analog: the suggestions of .param.VBS()"""
|
|
524
|
+
if not get_option("suggest", True):
|
|
525
|
+
return []
|
|
526
|
+
lines = [">>> Suggestions"]
|
|
527
|
+
t = ["", "", ""]
|
|
528
|
+
c = ["", "", ""]
|
|
529
|
+
if "out_cut" not in given:
|
|
530
|
+
t[0], c[0] = ", out_cut=2", "Label two outliers ..."
|
|
531
|
+
if "fences" not in given:
|
|
532
|
+
t[1] = ", fences=True"
|
|
533
|
+
if c[0] == "":
|
|
534
|
+
c[1] = "Show inner fences"
|
|
535
|
+
if "vbs_mean" not in given:
|
|
536
|
+
t[2] = ", vbs_mean=True"
|
|
537
|
+
if c[1] == "":
|
|
538
|
+
c[2] = " Show mean"
|
|
539
|
+
if any(t):
|
|
540
|
+
lines.append(_x_call(pre, x_name, dname,
|
|
541
|
+
"".join(t) + ', form="vbs"')
|
|
542
|
+
+ " # " + "".join(c))
|
|
543
|
+
if "box_adj" not in given:
|
|
544
|
+
lines.append(_x_call(pre, x_name, dname,
|
|
545
|
+
', box_adj=True, form="vbs"')
|
|
546
|
+
+ " # Adjust whiskers for asymmetry")
|
|
547
|
+
nm = (facet_name if n_facet == 2 else by_name if n_by == 2
|
|
548
|
+
else None)
|
|
549
|
+
if nm is not None:
|
|
550
|
+
lines.append(f'{pre}ttest("{x_name} ~ {nm}", data={dname})')
|
|
551
|
+
nm = (facet_name if n_facet > 2 else by_name if n_by > 2
|
|
552
|
+
else None)
|
|
553
|
+
if nm is not None:
|
|
554
|
+
lines.append(f'{pre}ANOVA("{x_name} ~ {nm}", data={dname})')
|
|
555
|
+
return lines
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def report(*components):
|
|
559
|
+
"""Join the components with one blank line between them, as
|
|
560
|
+
print.out_all() does, skipping empty ones."""
|
|
561
|
+
lines = []
|
|
562
|
+
for comp in components:
|
|
563
|
+
if not comp:
|
|
564
|
+
continue
|
|
565
|
+
if lines:
|
|
566
|
+
lines.append("")
|
|
567
|
+
lines += comp
|
|
568
|
+
return "\n".join(s.rstrip() for s in lines)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
# ===== XY() ======================================================
|
|
572
|
+
# R analogs: .cr.main(brief=TRUE) (the correlation), the fit text and
|
|
573
|
+
# suggestions of .plt.txt(), .plt.MD(), and the run analysis.
|
|
574
|
+
|
|
575
|
+
def xy_digits(yv, digits_d=None):
|
|
576
|
+
""".plt.txt(): one past the decimals of y, at least 3"""
|
|
577
|
+
if digits_d is None:
|
|
578
|
+
digits_d = max_dd(yv) + 1
|
|
579
|
+
return max(digits_d, 3)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def cor_block(xv, yv, x_name, y_name, x_lbl=None, y_lbl=None):
|
|
583
|
+
"""Pearson correlation, its t test and 95% confidence interval.
|
|
584
|
+
R analog: .cr.main(brief=TRUE) through cor.test()"""
|
|
585
|
+
from scipy import stats as sps
|
|
586
|
+
x = np.asarray(xv, dtype=float)
|
|
587
|
+
y = np.asarray(yv, dtype=float)
|
|
588
|
+
ok = np.isfinite(x) & np.isfinite(y)
|
|
589
|
+
x, y = x[ok], y[ok]
|
|
590
|
+
n = len(x)
|
|
591
|
+
lines = ["", ">>> Pearson's product-moment correlation"]
|
|
592
|
+
if x_lbl is not None or y_lbl is not None:
|
|
593
|
+
lines += ["", x_name if x_lbl is None else f"{x_name}: {x_lbl}",
|
|
594
|
+
y_name if y_lbl is None else f"{y_name}: {y_lbl}"]
|
|
595
|
+
lines += ["", "Number of paired values with neither missing, "
|
|
596
|
+
f"n = {n}"]
|
|
597
|
+
if n < 4:
|
|
598
|
+
return lines
|
|
599
|
+
r = float(np.corrcoef(x, y)[0, 1])
|
|
600
|
+
df = n - 2
|
|
601
|
+
t = r * np.sqrt(df / max(1 - r * r, 1e-300))
|
|
602
|
+
p = 2 * sps.t.sf(abs(t), df)
|
|
603
|
+
z, se = np.arctanh(r), 1 / np.sqrt(n - 3)
|
|
604
|
+
q = sps.norm.ppf(0.975)
|
|
605
|
+
lb, ub = np.tanh(z - q * se), np.tanh(z + q * se)
|
|
606
|
+
|
|
607
|
+
def f3(v):
|
|
608
|
+
return f"{round(float(v), 3):.3f}"
|
|
609
|
+
|
|
610
|
+
lines += [f"Sample Correlation of {x_name} and {y_name}: r = {r:.3f}",
|
|
611
|
+
"",
|
|
612
|
+
f"Hypothesis Test of 0 Correlation: t = {f3(t)}, "
|
|
613
|
+
f"df = {df}, p-value = {f3(p)}",
|
|
614
|
+
f"95% Confidence Interval for Correlation: {f3(lb)} to "
|
|
615
|
+
f"{f3(ub)}", ""]
|
|
616
|
+
return lines
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _lin_fit(xs, ys):
|
|
620
|
+
b1, b0 = np.polyfit(xs, ys, 1)
|
|
621
|
+
e = ys - (b0 + b1 * xs)
|
|
622
|
+
tss = ((ys - ys.mean()) ** 2).sum()
|
|
623
|
+
rsq = 1 - (e @ e) / tss if tss > 0 else np.nan
|
|
624
|
+
return b0, b1, (e @ e) / (len(ys) - 2), rsq
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# the linearizing transform of y for each curved fit, as .plt.fit()
|
|
628
|
+
_FIT_OPS = {"quad": ("sqrt()", "square"),
|
|
629
|
+
"power": ("the root of the\n reciprocal of the power",
|
|
630
|
+
"of the power"),
|
|
631
|
+
"exp": ("log()", "exp()"),
|
|
632
|
+
"log": ("exp()", "log()")}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def fit_text(fit_stats, fit, fit_power, y_name, digits_d,
|
|
636
|
+
by_name=None, var_label=False):
|
|
637
|
+
"""The fit line(s) and their MSE. lm and the curved fits report
|
|
638
|
+
the line of the (linearized) regression; loess its MSE alone.
|
|
639
|
+
R analog: the out_reg text of .plt.txt()"""
|
|
640
|
+
d = digits_d
|
|
641
|
+
p = 2 if fit == "quad" else fit_power
|
|
642
|
+
lines = []
|
|
643
|
+
for i, fs in enumerate(fit_stats):
|
|
644
|
+
nm, ys, f = fs[0], np.asarray(fs[2], float), np.asarray(fs[3], float)
|
|
645
|
+
xs = np.asarray(fs[4], float) if len(fs) > 4 else None
|
|
646
|
+
msg = ""
|
|
647
|
+
if fit in _FIT_OPS and (i == 0 or nm is not None):
|
|
648
|
+
msg = ("Regressed linearized data of transformed data "
|
|
649
|
+
f"values of {y_name} with {_FIT_OPS[fit][0]}")
|
|
650
|
+
# R: paste(msg, by.name, ": ", by.cat, " ") or by.cat ("")
|
|
651
|
+
head = (" " if nm is None
|
|
652
|
+
else f"Variable: {nm} " if var_label
|
|
653
|
+
else f"{by_name}: {nm} ")
|
|
654
|
+
if msg:
|
|
655
|
+
lines += msg.split("\n")
|
|
656
|
+
if fit == "loess" or xs is None:
|
|
657
|
+
mse = ((ys - f) ** 2).sum() / (len(ys) - 2)
|
|
658
|
+
md = "Loess" if fit == "loess" else "Linear"
|
|
659
|
+
lines += [f"{head} {md} Model MSE = {mse:,.{d}f}", ""]
|
|
660
|
+
continue
|
|
661
|
+
yt = ys
|
|
662
|
+
if fit in ("quad", "power"):
|
|
663
|
+
yt = ys ** (1 / p)
|
|
664
|
+
elif fit == "exp":
|
|
665
|
+
yt = np.log(ys if p == 1 else ys ** (1 / p))
|
|
666
|
+
elif fit == "log":
|
|
667
|
+
yt = np.exp(ys if p == 1 else ys ** (1 / p))
|
|
668
|
+
ok = np.isfinite(yt)
|
|
669
|
+
b0, b1, mse, rsq = _lin_fit(xs[ok], yt[ok])
|
|
670
|
+
lines += [f"{head}Line: b0 = {b0:.{d}f} b1 = {b1:.{d}f}",
|
|
671
|
+
f" Linear Model MSE = {mse:,.{d}f} Rsq = {rsq:.3f}",
|
|
672
|
+
""]
|
|
673
|
+
if fit in _FIT_OPS:
|
|
674
|
+
mse_nl = ((ys - f) ** 2).sum() / (len(ys) - 2)
|
|
675
|
+
lines[-1:] = ["", "Fit to the data with back transform "
|
|
676
|
+
f"{_FIT_OPS[fit][1]} of linear regression model",
|
|
677
|
+
f"Model MSE = {mse_nl:,.{d}f}", "", ""]
|
|
678
|
+
return lines
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _xy_call(pre, x, y, dname, extra=""):
|
|
682
|
+
def q(v):
|
|
683
|
+
if isinstance(v, (list, tuple)):
|
|
684
|
+
return "[" + ", ".join(f'"{s}"' for s in v) + "]"
|
|
685
|
+
return f'"{v}"'
|
|
686
|
+
return f"{pre}XY({q(x)}, {q(y)}, data={dname}{extra}"
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def suggest_scatter(pre, x, y, dname, given, by=None, rng=None):
|
|
690
|
+
"""The scatterplot suggestions, R's alternates drawn at random as
|
|
691
|
+
.plt.txt() does with runif(). R analog: .plt.txt() contcont"""
|
|
692
|
+
if not get_option("suggest", True):
|
|
693
|
+
return []
|
|
694
|
+
rng = np.random.default_rng() if rng is None else rng
|
|
695
|
+
fc = _xy_call(pre, x, y, dname,
|
|
696
|
+
f', by="{by}"' if by is not None else "")
|
|
697
|
+
lines = [">>> Suggestions or enter: style(suggest=False)"]
|
|
698
|
+
if "enhance" not in given:
|
|
699
|
+
lines.append(f"{fc}, enhance=True) # many options")
|
|
700
|
+
if rng.random() > 0.5:
|
|
701
|
+
if "fill" not in given:
|
|
702
|
+
lines.append(f'{fc}, fill="skyblue") # interior fill color '
|
|
703
|
+
"of points")
|
|
704
|
+
elif "color" not in given:
|
|
705
|
+
lines.append(f'{fc}, color="red") # exterior edge color of '
|
|
706
|
+
"points")
|
|
707
|
+
if "fit" not in given:
|
|
708
|
+
lines.append(f'{fc}, fit="lm", fit_se=[.90, .99]) # fit line, '
|
|
709
|
+
"stnd errors")
|
|
710
|
+
if rng.random() > 0.5:
|
|
711
|
+
if "out_cut" not in given:
|
|
712
|
+
lines.append(f"{fc}, out_cut=.10) # label top 10% from "
|
|
713
|
+
"center as outliers")
|
|
714
|
+
elif "MD_cut" not in given:
|
|
715
|
+
lines.append(f"{fc}, MD_cut=6) # Mahalanobis distance from "
|
|
716
|
+
"center > 6 is an outlier")
|
|
717
|
+
if by is not None and "pt_shape" not in given:
|
|
718
|
+
lines.append(f'{fc}, pt_shape="diamond") # diamond for points')
|
|
719
|
+
return lines
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def suggest_series(pre, x, y, dname, given, extra="", run=False,
|
|
723
|
+
pt_size=1):
|
|
724
|
+
"""Time-series and run-chart suggestions. R analog: .plt.txt()
|
|
725
|
+
date.var and run blocks"""
|
|
726
|
+
if not get_option("suggest", True):
|
|
727
|
+
return []
|
|
728
|
+
lines = [">>> Suggestions or enter: style(suggest=False)"]
|
|
729
|
+
fc = _xy_call(pre, x, y, dname)
|
|
730
|
+
full = _xy_call(pre, x, y, dname, extra)
|
|
731
|
+
if not run:
|
|
732
|
+
if "ts_ahead" not in given:
|
|
733
|
+
lines.append(f"{fc}, ts_ahead=4) # exponential smoothing "
|
|
734
|
+
"forecast 4 time units")
|
|
735
|
+
if "ts_unit" not in given:
|
|
736
|
+
lines.append(f'{fc}, ts_unit="years") # aggregate time by '
|
|
737
|
+
"yearly sum")
|
|
738
|
+
if "ts_agg" not in given:
|
|
739
|
+
lines.append(f'{fc}, ts_unit="years", ts_agg="mean") '
|
|
740
|
+
"# aggregate by yearly mean")
|
|
741
|
+
if "ts_ahead" in given and "ts_seasons" not in given:
|
|
742
|
+
lines.append(f"{fc}, ts_ahead=4, ts_seasons=False) # turn "
|
|
743
|
+
"off exponential smoothing seasonal effect")
|
|
744
|
+
if "pt_size" not in given and pt_size > 0:
|
|
745
|
+
lines.append(f"{full}, pt_size=0) # just line segments, "
|
|
746
|
+
"no points")
|
|
747
|
+
if "line_width" not in given:
|
|
748
|
+
if pt_size > 0:
|
|
749
|
+
lines.append(f"{full}, line_width=0) # just points, no "
|
|
750
|
+
"line segments")
|
|
751
|
+
else:
|
|
752
|
+
lines.append(f'{full}, line_width=0, ts_area_fill="on") '
|
|
753
|
+
"# just area")
|
|
754
|
+
if "ts_area_fill" not in given and "ts_stack" not in given:
|
|
755
|
+
lines.append(f'{full}, ts_area_fill="on") # default color fill')
|
|
756
|
+
return lines
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def run_summary(yv, digits_d):
|
|
760
|
+
"""The summary of y above a run analysis, untitled.
|
|
761
|
+
R analog: .ss.numeric(y, x.name="*NONE*", brief=TRUE)"""
|
|
762
|
+
lines = ss_numeric(yv, "", None, digits_d)
|
|
763
|
+
return [" "] + lines[2:]
|