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/stats_out.py
CHANGED
|
@@ -32,11 +32,17 @@ def x_stats(xv, x_name, digits_d=2):
|
|
|
32
32
|
lines = [f"--- {x_name} ---",
|
|
33
33
|
f"n: {n} missing: {miss}"]
|
|
34
34
|
if n < 2:
|
|
35
|
+
record_stats(n=n, n_miss=miss)
|
|
35
36
|
return lines
|
|
36
37
|
q1, med, q3 = np.percentile(v, [25, 50, 75])
|
|
37
38
|
iqr = q3 - q1
|
|
38
39
|
out_v = np.sort(v[(v < q1 - 1.5 * iqr)
|
|
39
40
|
| (v > q3 + 1.5 * iqr)])
|
|
41
|
+
record_stats(n=n, n_miss=miss, mean=float(v.mean()),
|
|
42
|
+
sd=float(v.std(ddof=1)), min=float(v.min()),
|
|
43
|
+
q1=float(q1), median=float(med), q3=float(q3),
|
|
44
|
+
max=float(v.max()), iqr=float(iqr),
|
|
45
|
+
outliers=out_v.tolist())
|
|
40
46
|
lines += [
|
|
41
47
|
f"mean: {fmt(v.mean(), d)} sd: {fmt(v.std(ddof=1), d)}",
|
|
42
48
|
f"min: {fmt(v.min(), d)} 1st Qu: {fmt(q1, d)} "
|
|
@@ -94,29 +100,70 @@ def _cor_block(xg, yg, label, d):
|
|
|
94
100
|
|
|
95
101
|
|
|
96
102
|
def xy_stats(groups, x_name, y_name, fit_stats=None,
|
|
97
|
-
digits_d=2):
|
|
103
|
+
digits_d=2, by_name=None):
|
|
98
104
|
"""Correlation (t test, Fisher-z CI) for the relationship
|
|
99
105
|
XY() displays, overall or per by= group, plus fit-line MSE
|
|
100
106
|
when a fit line is drawn."""
|
|
101
107
|
lines = [f"--- {x_name} and {y_name} ---"]
|
|
108
|
+
cors = {}
|
|
102
109
|
for nm, xg, yg in groups:
|
|
103
110
|
label = "" if nm is None else f" ({nm})"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
xg, yg = np.asarray(xg, float), np.asarray(yg, float)
|
|
112
|
+
lines += _cor_block(xg, yg, label, digits_d)
|
|
113
|
+
cors[nm] = _cor_values(xg, yg)
|
|
114
|
+
fits = []
|
|
107
115
|
for fs in (fit_stats or []):
|
|
108
|
-
nm, fit, ys, f = fs
|
|
116
|
+
nm, fit, ys, f = fs[:4]
|
|
117
|
+
xs = fs[4] if len(fs) > 4 else None
|
|
109
118
|
label = "" if nm is None else f" ({nm})"
|
|
110
|
-
|
|
119
|
+
# the error variance of one predictor, n - 2 degrees of
|
|
120
|
+
# freedom, as R's .plt.fit() computes it for every fit
|
|
121
|
+
sse = float(((ys - f) ** 2).sum())
|
|
122
|
+
mse = sse / (len(ys) - 2) if len(ys) > 2 else np.nan
|
|
111
123
|
tss = float(((ys - ys.mean()) ** 2).sum())
|
|
112
124
|
rsq = 1 - float(((ys - f) ** 2).sum()) / tss \
|
|
113
125
|
if tss > 0 else np.nan
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
entry = dict(group=nm, fit=fit, mse=mse, rsq=rsq)
|
|
127
|
+
if fit == "lm" and xs is not None and len(xs) > 1:
|
|
128
|
+
# the line and its fit as R's .plt.fit() reports them
|
|
129
|
+
b1, b0 = np.polyfit(np.asarray(xs, float),
|
|
130
|
+
np.asarray(ys, float), 1)
|
|
131
|
+
entry.update(b0=float(b0), b1=float(b1))
|
|
132
|
+
head = (" " if nm is None
|
|
133
|
+
else f"{by_name}: {nm} " if by_name
|
|
134
|
+
else f"{nm}: ")
|
|
135
|
+
lines += [f"{head}Line: b0 = {b0:.3f} b1 = {b1:.3f}",
|
|
136
|
+
f" Linear Model MSE = {mse:,.3f} "
|
|
137
|
+
f"Rsq = {rsq:.3f}"]
|
|
138
|
+
else:
|
|
139
|
+
lines.append(
|
|
140
|
+
f'fit="{fit}"{label}: MSE: {fmt(mse, digits_d + 1)}'
|
|
141
|
+
f" R-squared: {fmt(rsq, 3)}")
|
|
142
|
+
fits.append(entry)
|
|
143
|
+
# one relationship reads as its own entries; groups by name
|
|
144
|
+
if list(cors) == [None]:
|
|
145
|
+
record_stats(**cors[None])
|
|
146
|
+
else:
|
|
147
|
+
record_stats(cor=cors)
|
|
148
|
+
if fits:
|
|
149
|
+
record_stats(fit=fits if len(fits) > 1 else fits[0])
|
|
117
150
|
return lines
|
|
118
151
|
|
|
119
152
|
|
|
153
|
+
def _cor_values(xg, yg):
|
|
154
|
+
"""The correlation of _cor_block as numbers."""
|
|
155
|
+
n = len(xg)
|
|
156
|
+
if n < 4:
|
|
157
|
+
return dict(n=n)
|
|
158
|
+
r = float(np.corrcoef(xg, yg)[0, 1])
|
|
159
|
+
tval = r * np.sqrt((n - 2) / max(1 - r * r, 1e-12))
|
|
160
|
+
p = 2 * sps.t.sf(abs(tval), n - 2)
|
|
161
|
+
zlo = np.arctanh(r) - 1.959964 / np.sqrt(n - 3)
|
|
162
|
+
zhi = np.arctanh(r) + 1.959964 / np.sqrt(n - 3)
|
|
163
|
+
return dict(n=n, r=r, t=float(tval), df=n - 2, p_value=float(p),
|
|
164
|
+
ci_lb=float(np.tanh(zlo)), ci_ub=float(np.tanh(zhi)))
|
|
165
|
+
|
|
166
|
+
|
|
120
167
|
def md_outliers(xv, yv, ids, MD_cut, out_cut):
|
|
121
168
|
"""Bivariate outliers by Mahalanobis distance from the
|
|
122
169
|
centroid: MD_cut an absolute threshold, out_cut a proportion
|
|
@@ -144,7 +191,7 @@ def md_outliers(xv, yv, ids, MD_cut, out_cut):
|
|
|
144
191
|
n_lines = min(len(out_idx) + 3, len(dst))
|
|
145
192
|
w_id = max(len(s) for s in ids)
|
|
146
193
|
w_md = max(len(fmt(d, 2)) for d in dst)
|
|
147
|
-
lines = [">>> Outlier analysis with Mahalanobis Distance",
|
|
194
|
+
lines = [">>> Outlier analysis with squared Mahalanobis Distance",
|
|
148
195
|
"",
|
|
149
196
|
f"{'MD':>{w_md}} {'ID':>{w_id + 1}}",
|
|
150
197
|
f"{'-----':>{w_md}} {'-----':>{w_id + 1}}"]
|
|
@@ -155,7 +202,7 @@ def md_outliers(xv, yv, ids, MD_cut, out_cut):
|
|
|
155
202
|
lines.append(f"{fmt(dst[j], 2):>{w_md}} "
|
|
156
203
|
f"{ids[j]:>{w_id + 1}}")
|
|
157
204
|
if n_lines < len(dst):
|
|
158
|
-
lines.append(f"{'...':>{w_md}} {'...':>{w_id +
|
|
205
|
+
lines.append(f"{'...':>{w_md - 1}} {'...':>{w_id + 2}}")
|
|
159
206
|
return out_idx, lines
|
|
160
207
|
|
|
161
208
|
|
|
@@ -542,3 +589,202 @@ def nested_stats(cols, names, y_ser, stat, y_name, digits_d=2):
|
|
|
542
589
|
title = (f"{STAT_LBL.get(stat, stat)} of {y_name} by "
|
|
543
590
|
+ ", ".join(names))
|
|
544
591
|
return [title, "-" * len(title), ""] + body.split("\n") + [""]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# ----- the statistics returned with a Chart() figure ----------------
|
|
595
|
+
|
|
596
|
+
class ChartStats:
|
|
597
|
+
"""The statistics behind a Chart(), attached to the returned figure
|
|
598
|
+
as fig.stats. Each entry reads as an attribute, fig.stats.freq, or
|
|
599
|
+
by name, fig.stats["freq"], named as R's Chart() names its returned
|
|
600
|
+
list where it has a counterpart: freq, prop, values, p_value,
|
|
601
|
+
n_miss. text holds the console report, kept when quiet=True
|
|
602
|
+
suppresses its display. Not a dict subclass, so that an entry such
|
|
603
|
+
as values is not shadowed by a dict method. R analog: the list
|
|
604
|
+
that Chart() returns invisibly"""
|
|
605
|
+
|
|
606
|
+
def __init__(self, *args, **kw):
|
|
607
|
+
for k, v in dict(*args, **kw).items():
|
|
608
|
+
setattr(self, k, v)
|
|
609
|
+
|
|
610
|
+
def __getitem__(self, name):
|
|
611
|
+
try:
|
|
612
|
+
return self.__dict__[name]
|
|
613
|
+
except KeyError:
|
|
614
|
+
raise KeyError(name) from None
|
|
615
|
+
|
|
616
|
+
def __setitem__(self, name, value):
|
|
617
|
+
self.__dict__[name] = value
|
|
618
|
+
|
|
619
|
+
def __contains__(self, name):
|
|
620
|
+
return name in self.__dict__
|
|
621
|
+
|
|
622
|
+
def __iter__(self):
|
|
623
|
+
return iter(self.__dict__)
|
|
624
|
+
|
|
625
|
+
def __len__(self):
|
|
626
|
+
return len(self.__dict__)
|
|
627
|
+
|
|
628
|
+
def keys(self):
|
|
629
|
+
return self.__dict__.keys()
|
|
630
|
+
|
|
631
|
+
def get(self, name, default=None):
|
|
632
|
+
return self.__dict__.get(name, default)
|
|
633
|
+
|
|
634
|
+
def update(self, *args, **kw):
|
|
635
|
+
for k, v in dict(*args, **kw).items():
|
|
636
|
+
self.__dict__[k] = v
|
|
637
|
+
|
|
638
|
+
def __repr__(self):
|
|
639
|
+
keys = ", ".join(k for k in self.__dict__ if k != "text")
|
|
640
|
+
return (self.get("text", "").rstrip() + "\n\n"
|
|
641
|
+
f"[ChartStats: {keys}]")
|
|
642
|
+
|
|
643
|
+
__str__ = __repr__
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _ordered_counts(s):
|
|
647
|
+
t = s.groupby(s, observed=True).size()
|
|
648
|
+
return t[t > 0]
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _chisq_1d(cnt):
|
|
652
|
+
if len(cnt) < 2:
|
|
653
|
+
return {}
|
|
654
|
+
chi, p = sps.chisquare(cnt.to_numpy())
|
|
655
|
+
return dict(chisq=float(chi), df=len(cnt) - 1, p_value=float(p))
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _xtab_stats(ct):
|
|
659
|
+
obs = ct.to_numpy()
|
|
660
|
+
out = dict(freq=ct, prop=ct / obs.sum(),
|
|
661
|
+
prop_col=ct / ct.sum(axis=0),
|
|
662
|
+
prop_row=ct.div(ct.sum(axis=1), axis=0))
|
|
663
|
+
chi, p, dof, _ = sps.chi2_contingency(obs, correction=False)
|
|
664
|
+
if np.isfinite(chi):
|
|
665
|
+
out.update(chisq=float(chi), df=int(dof), p_value=float(p),
|
|
666
|
+
cramer_v=float(np.sqrt(chi / ((min(obs.shape) - 1)
|
|
667
|
+
* obs.sum()))))
|
|
668
|
+
return out
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def chart_stats_data(x_ser, by_ser, y_ser, stat, facet_ser=None,
|
|
672
|
+
n_miss=None):
|
|
673
|
+
"""The numbers of the Chart() report as pandas objects: counts
|
|
674
|
+
with proportions and the chi-square test of equal probabilities;
|
|
675
|
+
a cross-tabulation with cell, column, and row proportions,
|
|
676
|
+
Cramer's V, and the test of independence; or the summary of y
|
|
677
|
+
over the levels of x and the plotted values, or the table of the
|
|
678
|
+
statistic over the grouping"""
|
|
679
|
+
st = ChartStats()
|
|
680
|
+
if y_ser is None:
|
|
681
|
+
grp = by_ser if by_ser is not None else facet_ser
|
|
682
|
+
if grp is None:
|
|
683
|
+
cnt = _ordered_counts(x_ser)
|
|
684
|
+
st.update(n_dim=1, freq=cnt, prop=cnt / cnt.sum(),
|
|
685
|
+
n_miss=n_miss, **_chisq_1d(cnt))
|
|
686
|
+
elif by_ser is not None and facet_ser is not None:
|
|
687
|
+
# one cross-tabulation per panel
|
|
688
|
+
panels = {}
|
|
689
|
+
for lv in _category_order_sorted(facet_ser):
|
|
690
|
+
m = facet_ser == lv
|
|
691
|
+
panels[lv] = ChartStats(
|
|
692
|
+
_xtab_stats(pd.crosstab(by_ser[m], x_ser[m])))
|
|
693
|
+
st.update(n_dim=3, panels=panels)
|
|
694
|
+
else:
|
|
695
|
+
st.update(n_dim=2, **_xtab_stats(pd.crosstab(grp, x_ser)))
|
|
696
|
+
return st
|
|
697
|
+
|
|
698
|
+
fun = STAT_FUN[stat]
|
|
699
|
+
if by_ser is None and facet_ser is None:
|
|
700
|
+
y = pd.to_numeric(y_ser)
|
|
701
|
+
g = y.groupby(x_ser, observed=True, sort=True)
|
|
702
|
+
summary = pd.DataFrame({
|
|
703
|
+
"n": g.count(), "miss": g.size() - g.count(),
|
|
704
|
+
"mean": g.mean(), "sd": g.std(ddof=1), "min": g.min(),
|
|
705
|
+
"mdn": g.median(), "max": g.max()})
|
|
706
|
+
st.update(n_dim=1, summary=summary, values=fun(g))
|
|
707
|
+
return st
|
|
708
|
+
grp = by_ser if by_ser is not None else facet_ser
|
|
709
|
+
df = pd.DataFrame({"g": grp, "x": x_ser, "y": pd.to_numeric(y_ser)})
|
|
710
|
+
tbl = (df.groupby(["g", "x"], observed=True)["y"].agg(fun)
|
|
711
|
+
.unstack("x"))
|
|
712
|
+
st.update(n_dim=2, summary=tbl)
|
|
713
|
+
return st
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
# ----- statistics of the current call, returned with its figure -----
|
|
718
|
+
|
|
719
|
+
import contextvars as _contextvars
|
|
720
|
+
|
|
721
|
+
CALL_STATS = _contextvars.ContextVar("lessPy_call_stats", default=None)
|
|
722
|
+
|
|
723
|
+
# the caller's output while a report is captured, for advisories
|
|
724
|
+
_REAL_OUT = _contextvars.ContextVar("lessPy_real_stdout", default=None)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def advise(msg):
|
|
728
|
+
"""An advisory, shown whether or not the report is quiet, as R's
|
|
729
|
+
message() is: written past a captured report to the caller's
|
|
730
|
+
output."""
|
|
731
|
+
out = _REAL_OUT.get()
|
|
732
|
+
if out is None:
|
|
733
|
+
print(msg)
|
|
734
|
+
else:
|
|
735
|
+
print(msg, file=out)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def record_stats(**kw):
|
|
739
|
+
"""Add entries to the statistics of the current Chart(), X(), or
|
|
740
|
+
XY() call; a no-op outside one."""
|
|
741
|
+
st = CALL_STATS.get()
|
|
742
|
+
if st is not None:
|
|
743
|
+
st.update(kw)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def attach_stats(func, capture=False):
|
|
747
|
+
"""Return the figure with the call's statistics as fig.stats. A
|
|
748
|
+
plotly Figure accepts no new attributes by assignment, so it is
|
|
749
|
+
set past plotly's validation; it survives update_layout() and
|
|
750
|
+
stays out of the figure's JSON, though go.Figure(fig) copies the
|
|
751
|
+
figure without it.
|
|
752
|
+
|
|
753
|
+
capture: the function prints its report from many places, so run
|
|
754
|
+
it with quiet=False and its output captured as stats.text, then
|
|
755
|
+
display that text unless the caller asked for quiet."""
|
|
756
|
+
import functools
|
|
757
|
+
import io
|
|
758
|
+
import sys
|
|
759
|
+
from contextlib import redirect_stdout
|
|
760
|
+
|
|
761
|
+
@functools.wraps(func)
|
|
762
|
+
def wrapper(*args, **kwargs):
|
|
763
|
+
token = CALL_STATS.set(ChartStats() if capture else None)
|
|
764
|
+
try:
|
|
765
|
+
if capture:
|
|
766
|
+
quiet = resolve_quiet(kwargs.get("quiet"))
|
|
767
|
+
kwargs["quiet"] = False
|
|
768
|
+
buf = io.StringIO()
|
|
769
|
+
real = _REAL_OUT.set(sys.stdout)
|
|
770
|
+
try:
|
|
771
|
+
with redirect_stdout(buf):
|
|
772
|
+
fig = func(*args, **kwargs)
|
|
773
|
+
finally:
|
|
774
|
+
_REAL_OUT.reset(real)
|
|
775
|
+
out = buf.getvalue()
|
|
776
|
+
if not quiet:
|
|
777
|
+
sys.stdout.write(out)
|
|
778
|
+
st = CALL_STATS.get()
|
|
779
|
+
st["text"] = out.rstrip("\n")
|
|
780
|
+
else:
|
|
781
|
+
fig = func(*args, **kwargs)
|
|
782
|
+
st = CALL_STATS.get()
|
|
783
|
+
finally:
|
|
784
|
+
CALL_STATS.reset(token)
|
|
785
|
+
if fig is not None and hasattr(fig, "layout"):
|
|
786
|
+
object.__setattr__(fig, "stats",
|
|
787
|
+
st if st is not None else ChartStats())
|
|
788
|
+
return fig
|
|
789
|
+
|
|
790
|
+
return wrapper
|
lessPy/style.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# style.py — analog of lessR's style(): the color theme, a sub_theme,
|
|
2
|
+
# and the individual appearance settings that every visualization
|
|
3
|
+
# reads, Chart(), X(), and XY() alike, until changed.
|
|
4
|
+
#
|
|
5
|
+
# The settings of each theme come from lessR itself (_themes.py,
|
|
6
|
+
# generated from style(theme[, sub_theme])), so a theme draws the same
|
|
7
|
+
# colors in both packages. Text sizes are lessPy's own, tuned for
|
|
8
|
+
# plotly in notebooks, and naming a theme leaves them as they were.
|
|
9
|
+
# R analog: style.R
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
|
|
13
|
+
from . import utils
|
|
14
|
+
from ._themes import THEMES
|
|
15
|
+
|
|
16
|
+
# the text sizes, which a theme does not set
|
|
17
|
+
_SIZE_KEYS = ("main_size", "lab_size", "lab_x_size", "lab_y_size",
|
|
18
|
+
"axis_size", "axis_x_size", "axis_y_size", "sub_size",
|
|
19
|
+
"add_size")
|
|
20
|
+
|
|
21
|
+
# lessR style() parameters with no lessPy counterpart: R Markdown output
|
|
22
|
+
# and console settings, and parameters that lessPy takes in the call
|
|
23
|
+
# of the visualization function instead
|
|
24
|
+
_R_ONLY = ("results", "explain", "interpret", "document", "code",
|
|
25
|
+
"width", "show", "lessR.use_plotly", "notes",
|
|
26
|
+
"brief", "offset", "rotate_x", "rotate_y", "labels",
|
|
27
|
+
"labels_size", "labels_digits", "labels_position")
|
|
28
|
+
|
|
29
|
+
# start-up settings that a theme does not reach: lessPy's own options,
|
|
30
|
+
# captured once at import, before any style() call
|
|
31
|
+
_BASELINE = copy.deepcopy(utils._OPTIONS)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _style_keys():
|
|
35
|
+
keys = set(THEMES["colors"]["none"]) | set(_SIZE_KEYS)
|
|
36
|
+
keys |= {"quiet", "suggest", "digits_d", "n_cat", "grid_col",
|
|
37
|
+
"legend_border"}
|
|
38
|
+
return keys
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _theme_settings(theme, sub_theme):
|
|
42
|
+
if theme not in THEMES:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"theme='{theme}' is not a theme. Themes: "
|
|
45
|
+
+ ", ".join(THEMES))
|
|
46
|
+
variant = "none" if sub_theme is None else sub_theme
|
|
47
|
+
if variant not in THEMES[theme]:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"sub_theme='{sub_theme}' is not a sub_theme. Sub-themes: "
|
|
50
|
+
"default, black, wsj")
|
|
51
|
+
return THEMES[theme][variant]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def style(theme=None, sub_theme=None, set=None, get=False, reset=True,
|
|
55
|
+
**settings):
|
|
56
|
+
"""Set the appearance of every subsequent visualization.
|
|
57
|
+
|
|
58
|
+
style("darkred") applies a color theme, resetting every other
|
|
59
|
+
setting to the theme's own; sub_theme="black" draws on a black
|
|
60
|
+
background, "default" on a gray panel with white grid lines, as in
|
|
61
|
+
lessR. Name any setting to change only that one, as in
|
|
62
|
+
style(panel_fill="gray95", axis_size=1.1), with or without a theme.
|
|
63
|
+
style() alone restores the default theme. get=True returns the
|
|
64
|
+
current settings as a dict, which set= restores. A visualization
|
|
65
|
+
function's own theme= applies a theme for that call only.
|
|
66
|
+
|
|
67
|
+
trans sets the transparency of both the bar and the point fills.
|
|
68
|
+
Settings that lessR's style() takes for R Markdown or the console,
|
|
69
|
+
or that lessPy takes in the visualization call (rotate_x, labels,
|
|
70
|
+
...), are not available here. R analog: style()"""
|
|
71
|
+
keys = _style_keys()
|
|
72
|
+
|
|
73
|
+
if set is not None:
|
|
74
|
+
for k, v in set.items():
|
|
75
|
+
utils._OPTIONS[k] = copy.deepcopy(v)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# transparency names both fills at once, as in lessR
|
|
79
|
+
for alias in ("trans", "transparency"):
|
|
80
|
+
if alias in settings:
|
|
81
|
+
t = settings.pop(alias)
|
|
82
|
+
settings.setdefault("trans_bar_fill", t)
|
|
83
|
+
settings.setdefault("trans_pt_fill", t)
|
|
84
|
+
|
|
85
|
+
bad = [k for k in settings if k not in keys]
|
|
86
|
+
if bad:
|
|
87
|
+
r_only = [k for k in bad if k in _R_ONLY]
|
|
88
|
+
other = [k for k in bad if k not in _R_ONLY]
|
|
89
|
+
msg = []
|
|
90
|
+
if r_only:
|
|
91
|
+
msg.append(
|
|
92
|
+
f"{', '.join(r_only)}: lessR style() settings with no "
|
|
93
|
+
"lessPy counterpart (R Markdown and console output, or "
|
|
94
|
+
"set in the call of the visualization function)")
|
|
95
|
+
if other:
|
|
96
|
+
msg.append(f"{', '.join(other)}: not a style() setting")
|
|
97
|
+
raise ValueError(".\n".join(msg))
|
|
98
|
+
|
|
99
|
+
nothing = (theme is None and sub_theme is None and not settings
|
|
100
|
+
and not get)
|
|
101
|
+
if nothing:
|
|
102
|
+
theme = "colors" # style() restores the default
|
|
103
|
+
|
|
104
|
+
if (theme is not None or sub_theme is not None) and reset:
|
|
105
|
+
th = theme if theme is not None else utils.get_option(
|
|
106
|
+
"theme", "colors")
|
|
107
|
+
st = (sub_theme if sub_theme is not None
|
|
108
|
+
else None if theme is not None
|
|
109
|
+
else utils.get_option("sub_theme"))
|
|
110
|
+
base = copy.deepcopy(_BASELINE)
|
|
111
|
+
base.update(copy.deepcopy(_theme_settings(th, st)))
|
|
112
|
+
utils._OPTIONS.clear()
|
|
113
|
+
utils._OPTIONS.update(base)
|
|
114
|
+
utils._OPTIONS["theme"] = th
|
|
115
|
+
utils._OPTIONS["sub_theme"] = st
|
|
116
|
+
|
|
117
|
+
for k, v in settings.items():
|
|
118
|
+
utils._OPTIONS[k] = v
|
|
119
|
+
|
|
120
|
+
if get:
|
|
121
|
+
return {k: copy.deepcopy(utils._OPTIONS.get(k))
|
|
122
|
+
for k in sorted(keys | {"theme", "sub_theme"})}
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _AppliedTheme:
|
|
127
|
+
"""A theme for the duration of one call, the settings restored
|
|
128
|
+
after it. R analog: the theme= of Chart(), X(), and XY(), which
|
|
129
|
+
call style(theme) with on.exit(style(old_theme))"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, theme):
|
|
132
|
+
self.theme = theme
|
|
133
|
+
|
|
134
|
+
def __enter__(self):
|
|
135
|
+
self.saved = copy.deepcopy(utils._OPTIONS)
|
|
136
|
+
if self.theme is not None and \
|
|
137
|
+
self.theme != utils.get_option("theme"):
|
|
138
|
+
style(self.theme)
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def __exit__(self, *exc):
|
|
142
|
+
utils._OPTIONS.clear()
|
|
143
|
+
utils._OPTIONS.update(self.saved)
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def with_theme(func, own_param=False):
|
|
148
|
+
"""Give a visualization function the theme= of its lessR analog:
|
|
149
|
+
the theme applies to that call only. own_param: the function takes
|
|
150
|
+
theme itself (Chart), so it is passed on as well."""
|
|
151
|
+
import functools
|
|
152
|
+
|
|
153
|
+
@functools.wraps(func)
|
|
154
|
+
def wrapper(*args, theme=None, **kwargs):
|
|
155
|
+
if own_param:
|
|
156
|
+
kwargs["theme"] = theme
|
|
157
|
+
with _AppliedTheme(theme):
|
|
158
|
+
return func(*args, **kwargs)
|
|
159
|
+
|
|
160
|
+
return wrapper
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# sunflower_plotly.py — XY(form="sunflower")
|
|
2
|
+
#
|
|
3
|
+
# A DESIGN, not a translation: lessR draws the sunflower only with
|
|
4
|
+
# base R's sunflowerplot() and, faceted, a lattice panel of its own
|
|
5
|
+
# (.plt.sunflower.facet), so there is no plotly source to port. As
|
|
6
|
+
# there: one dot at each distinct (x, y), and where k > 1
|
|
7
|
+
# observations coincide, k petals radiating from it, the first
|
|
8
|
+
# vertical, evenly spaced (sunflowerplot()'s angles), 1/8 inch long.
|
|
9
|
+
# Dots take bar_fill_cont, petals the point color, as .plt.main().
|
|
10
|
+
# Plotly has no petal symbol, so the petals are line segments in data
|
|
11
|
+
# units, sized from the plotting region of the default figure so that
|
|
12
|
+
# they read as equal-length spokes. facet= draws one panel per level,
|
|
13
|
+
# each with its own coordinates and so its own petal counts.
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import plotly.graph_objects as go
|
|
17
|
+
|
|
18
|
+
from .plotly_utils import (
|
|
19
|
+
axis_format, axis_num, facet_fig, facet_panels, finish_facet,
|
|
20
|
+
make_trans, plot_border, plotly_style, to_hex, x_grid,
|
|
21
|
+
)
|
|
22
|
+
from .utils import get_option, pretty
|
|
23
|
+
|
|
24
|
+
_PETAL_PX = 12 # 1/8 inch at 96 dpi
|
|
25
|
+
_PLOT_PX = (540, 330) # plotting region of the default figure
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _counts(x, y):
|
|
29
|
+
"""Distinct coordinates and how many observations share each.
|
|
30
|
+
R analog: table(x, y) of .plt.main()"""
|
|
31
|
+
ok = np.isfinite(x) & np.isfinite(y)
|
|
32
|
+
pts, cnt = np.unique(np.column_stack((x[ok], y[ok])), axis=0,
|
|
33
|
+
return_counts=True)
|
|
34
|
+
return pts[:, 0], pts[:, 1], cnt
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _petals(xx, yy, cnt, dx, dy):
|
|
38
|
+
"""Segments for every coordinate with more than one observation:
|
|
39
|
+
cnt petals, the first vertical. R analog: sunflowerplot()"""
|
|
40
|
+
sx, sy = [], []
|
|
41
|
+
for x0, y0, k in zip(xx, yy, cnt):
|
|
42
|
+
if k < 2:
|
|
43
|
+
continue
|
|
44
|
+
ang = np.pi / 2 + 2 * np.pi * np.arange(k) / k
|
|
45
|
+
for a in ang:
|
|
46
|
+
sx += [x0, x0 + dx * np.cos(a), None]
|
|
47
|
+
sy += [y0, y0 + dy * np.sin(a), None]
|
|
48
|
+
return sx, sy
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _panel_traces(x, y, xr, yr, w_px, h_px, fill, color, pt_size,
|
|
52
|
+
hover):
|
|
53
|
+
xx, yy, cnt = _counts(x, y)
|
|
54
|
+
dx = _PETAL_PX * (xr[1] - xr[0]) / w_px
|
|
55
|
+
dy = _PETAL_PX * (yr[1] - yr[0]) / h_px
|
|
56
|
+
sx, sy = _petals(xx, yy, cnt, dx, dy)
|
|
57
|
+
px = max(3.0, 5.5 * float(pt_size))
|
|
58
|
+
return [
|
|
59
|
+
go.Scatter(x=sx, y=sy, mode="lines",
|
|
60
|
+
line=dict(color=to_hex(color), width=1),
|
|
61
|
+
hoverinfo="skip", showlegend=False),
|
|
62
|
+
go.Scatter(x=xx, y=yy, mode="markers",
|
|
63
|
+
marker=dict(size=px, color=to_hex(fill),
|
|
64
|
+
line=dict(width=0)),
|
|
65
|
+
customdata=cnt, hovertemplate=hover,
|
|
66
|
+
showlegend=False)]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _range(v):
|
|
70
|
+
t = pretty(float(np.nanmin(v)), float(np.nanmax(v)))
|
|
71
|
+
pad = 0.04 * (t[-1] - t[0])
|
|
72
|
+
return t, (t[0] - pad, t[-1] + pad)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def sunflower_plotly(x, y, x_lab, y_lab, main=None, facet=None,
|
|
76
|
+
facet_order=None, facet_name=None, facet2=None,
|
|
77
|
+
facet2_order=None, facet2_name=None, n_col=1,
|
|
78
|
+
pt_size=1, fill=None, color=None, overlay=None,
|
|
79
|
+
digits_d=2, axis_fmt="K", axis_x_pre="",
|
|
80
|
+
axis_y_pre=""):
|
|
81
|
+
"""The sunflower plot of x and y, single panel or faceted.
|
|
82
|
+
overlay(fig, x, y, row, col) adds a fit line or an ellipse to a
|
|
83
|
+
panel. Returns a plotly Figure."""
|
|
84
|
+
x = np.asarray(x, dtype=float)
|
|
85
|
+
y = np.asarray(y, dtype=float)
|
|
86
|
+
fill = get_option("bar_fill_cont", "#96AAC3") if fill is None \
|
|
87
|
+
else fill
|
|
88
|
+
color = get_option("pt_color", "#324E5C") if color is None \
|
|
89
|
+
else color
|
|
90
|
+
style_opts = plotly_style()
|
|
91
|
+
axT1, xr = _range(x)
|
|
92
|
+
axT2, yr = _range(y)
|
|
93
|
+
hover = (f"{x_lab}: %{{x}}<br>{y_lab}: %{{y}}<br>"
|
|
94
|
+
"count: %{customdata}<extra></extra>")
|
|
95
|
+
|
|
96
|
+
if facet is None:
|
|
97
|
+
fig = go.Figure(_panel_traces(x, y, xr, yr, *_PLOT_PX, fill,
|
|
98
|
+
color, pt_size, hover))
|
|
99
|
+
if overlay is not None:
|
|
100
|
+
overlay(fig, x, y, None, None)
|
|
101
|
+
ax_x = axis_num(x_lab, axT1, axis_format(axT1, digits_d,
|
|
102
|
+
axis_fmt, axis_x_pre))
|
|
103
|
+
ax_y = axis_num(y_lab, axT2, axis_format(axT2, digits_d,
|
|
104
|
+
axis_fmt, axis_y_pre))
|
|
105
|
+
ax_x.update(range=list(xr))
|
|
106
|
+
ax_y.update(range=list(yr), showgrid=True,
|
|
107
|
+
gridcolor=to_hex(style_opts["grid_col"]),
|
|
108
|
+
gridwidth=1, griddash="dot")
|
|
109
|
+
fig.update_layout(
|
|
110
|
+
xaxis=ax_x, yaxis=ax_y, template=None,
|
|
111
|
+
shapes=x_grid(axT1) + plot_border(),
|
|
112
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
113
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]))
|
|
114
|
+
else:
|
|
115
|
+
labels, pos, sel, n_row_g, n_col = facet_panels(
|
|
116
|
+
facet, facet_order, facet2, facet2_order, facet_name,
|
|
117
|
+
facet2_name, n_col)
|
|
118
|
+
fig = facet_fig(n_row_g, n_col)
|
|
119
|
+
w_px = _PLOT_PX[0] / n_col
|
|
120
|
+
h_px = _PLOT_PX[1] * 1.3 / n_row_g
|
|
121
|
+
for i, m in enumerate(sel):
|
|
122
|
+
row, col = pos[i]
|
|
123
|
+
for tr in _panel_traces(x[m], y[m], xr, yr, w_px, h_px,
|
|
124
|
+
fill, color, pt_size, hover):
|
|
125
|
+
fig.add_trace(tr, row=row, col=col)
|
|
126
|
+
if overlay is not None and m.sum() > 2:
|
|
127
|
+
overlay(fig, x[m], y[m], row, col)
|
|
128
|
+
ax = {"axT1": axT1,
|
|
129
|
+
"axL1": axis_format(axT1, digits_d, axis_fmt, axis_x_pre),
|
|
130
|
+
"axT2": axT2,
|
|
131
|
+
"axL2": axis_format(axT2, digits_d, axis_fmt, axis_y_pre)}
|
|
132
|
+
finish_facet(fig, labels, ax, x_lab, y_lab, gridT1=axT1,
|
|
133
|
+
style_opts=style_opts, n_col=n_col, pos=pos)
|
|
134
|
+
fig.update_xaxes(range=list(xr))
|
|
135
|
+
fig.update_yaxes(range=list(yr))
|
|
136
|
+
if main:
|
|
137
|
+
fig.update_layout(title=dict(
|
|
138
|
+
text=main, x=0.5, xanchor="center",
|
|
139
|
+
font=dict(size=round(16 * get_option("main_size", 1)))))
|
|
140
|
+
return fig
|
lessPy/ttest.py
CHANGED
|
@@ -24,7 +24,7 @@ import numpy as np
|
|
|
24
24
|
import pandas as pd
|
|
25
25
|
|
|
26
26
|
from .Regression import _getdigits
|
|
27
|
-
from .utils import fmt, get_column, get_option
|
|
27
|
+
from .utils import fmt, get_column, get_option, show_plots
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class ttestResults:
|
|
@@ -309,14 +309,14 @@ def _two_group(n1, m1, s1, n2, m2, s2, Ynm, Xnm, X1nm, X2nm,
|
|
|
309
309
|
if graph and from_data:
|
|
310
310
|
plots["two_group"] = _two_group_plot(
|
|
311
311
|
YA, YB, Ynm, X1nm, X2nm, m1, m2, d)
|
|
312
|
-
return ttestResults(
|
|
312
|
+
return show_plots(ttestResults(
|
|
313
313
|
kind="two-group", n_total=n1 + n2, digits_d=d,
|
|
314
314
|
group1={"name": X1nm, "n": n1, "mean": m1, "sd": s1},
|
|
315
315
|
group2={"name": X2nm, "n": n2, "mean": m2, "sd": s2},
|
|
316
316
|
mean_diff=m1 - m2, pooled_sd=sw, cohen_d=smd,
|
|
317
317
|
equal_var={"t": tvalue, "df": df, "p_value": pvalue,
|
|
318
318
|
"se": sterr, "lb": lb, "ub": ub},
|
|
319
|
-
welch=welch, plots=plots)
|
|
319
|
+
welch=welch, plots=plots))
|
|
320
320
|
|
|
321
321
|
|
|
322
322
|
def _assumptions(YA, YB, X1nm, X2nm, Ynm, n1, n2, v1, v2, df1,
|
|
@@ -529,12 +529,12 @@ def _one_group(Y, n, m, s, mu, Ynm, brief, conf_level,
|
|
|
529
529
|
if graph and from_data:
|
|
530
530
|
plots["one_group"] = _one_group_plot(
|
|
531
531
|
Y, Ynm, m, lb, ub, d, paired)
|
|
532
|
-
return ttestResults(
|
|
532
|
+
return show_plots(ttestResults(
|
|
533
533
|
kind="paired" if paired else "one-group", n_total=n,
|
|
534
534
|
digits_d=d, n=n, mean=m, sd=s, mu=mu,
|
|
535
535
|
infer={"t": tvalue, "df": df, "p_value": pvalue,
|
|
536
536
|
"se": sterr, "lb": lb, "ub": ub},
|
|
537
|
-
cohen_d=cohen, plots=plots)
|
|
537
|
+
cohen_d=cohen, plots=plots))
|
|
538
538
|
|
|
539
539
|
|
|
540
540
|
def _needed_1(Edesired, conf_level, s, E, n, d):
|