lessPython 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lessPy/ANOVA.py +680 -0
- lessPy/Chart.py +1055 -0
- lessPy/Correlation.py +236 -0
- lessPy/Flows.py +116 -0
- lessPy/Logit.py +615 -0
- lessPy/Prop_test.py +267 -0
- lessPy/Regression.py +1491 -0
- lessPy/VariableLabels.py +119 -0
- lessPy/X.py +426 -0
- lessPy/XY.py +2007 -0
- lessPy/__init__.py +60 -0
- lessPy/anova_rmd.py +227 -0
- lessPy/bc_plotly.py +575 -0
- lessPy/bubble_plotly.py +470 -0
- lessPy/corCFA.py +316 -0
- lessPy/corEFA.py +220 -0
- lessPy/corPrint.py +45 -0
- lessPy/corProp.py +73 -0
- lessPy/corRead.py +48 -0
- lessPy/corReflect.py +72 -0
- lessPy/corReorder.py +161 -0
- lessPy/corScree.py +87 -0
- lessPy/data/Anova_1way.csv +25 -0
- lessPy/data/Anova_2way.csv +49 -0
- lessPy/data/Anova_rb.csv +8 -0
- lessPy/data/Anova_rbf.csv +49 -0
- lessPy/data/Anova_sp.csv +57 -0
- lessPy/data/BodyMeas.csv +341 -0
- lessPy/data/Cars93.csv +94 -0
- lessPy/data/Employee.csv +38 -0
- lessPy/data/Employee_lbl.csv +9 -0
- lessPy/data/FreqTable99.csv +5 -0
- lessPy/data/Jackets.csv +1026 -0
- lessPy/data/Learn.csv +35 -0
- lessPy/data/Mach4.csv +352 -0
- lessPy/data/Mach4_lbl.csv +21 -0
- lessPy/data/Reading.csv +101 -0
- lessPy/data/StockPrice.csv +1489 -0
- lessPy/data/WeightLoss.csv +11 -0
- lessPy/datasets.py +46 -0
- lessPy/date_infer.py +112 -0
- lessPy/details.py +314 -0
- lessPy/dn_plotly.py +495 -0
- lessPy/dot_plotly.py +385 -0
- lessPy/freq_poly_plotly.py +324 -0
- lessPy/getColors.py +399 -0
- lessPy/hier_plotly.py +352 -0
- lessPy/hs_plotly.py +395 -0
- lessPy/logit_rmd.py +410 -0
- lessPy/order_by.py +94 -0
- lessPy/pie_plotly.py +292 -0
- lessPy/pivot.py +158 -0
- lessPy/plotly_utils.py +787 -0
- lessPy/plt_add.py +129 -0
- lessPy/plt_contour.py +192 -0
- lessPy/plt_contour_facet.py +194 -0
- lessPy/plt_forecast.py +677 -0
- lessPy/plt_mat_plotly.py +201 -0
- lessPy/plt_plotly.py +216 -0
- lessPy/plt_smooth.py +170 -0
- lessPy/plt_time.py +143 -0
- lessPy/prob_norm.py +111 -0
- lessPy/prob_tcut.py +131 -0
- lessPy/prob_znorm.py +110 -0
- lessPy/radar_plotly.py +201 -0
- lessPy/reg_rmd.py +754 -0
- lessPy/rename.py +33 -0
- lessPy/reshape.py +95 -0
- lessPy/showColors.py +130 -0
- lessPy/simCImean.py +165 -0
- lessPy/simCLT.py +265 -0
- lessPy/simFlips.py +104 -0
- lessPy/simMeans.py +146 -0
- lessPy/stats_out.py +189 -0
- lessPy/ttest.py +641 -0
- lessPy/utils.py +235 -0
- lessPy/vbs_plotly.py +545 -0
- lesspython-0.1.0.dist-info/METADATA +93 -0
- lesspython-0.1.0.dist-info/RECORD +82 -0
- lesspython-0.1.0.dist-info/WHEEL +5 -0
- lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
- lesspython-0.1.0.dist-info/top_level.txt +1 -0
lessPy/plt_add.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# plt_add.py — analog of plt.add.R
|
|
2
|
+
#
|
|
3
|
+
# The add= annotation vocabulary shared by Chart(), X(), and
|
|
4
|
+
# XY(): "v_line", "h_line", "line", "rect", "arrow", "point",
|
|
5
|
+
# and any other string draws as text at (x1, y1). Style comes
|
|
6
|
+
# from the add_ options (set_option): add_color (gray10),
|
|
7
|
+
# add_fill (#D9D9D920), add_lwd (0.5), add_lty (solid),
|
|
8
|
+
# add_size (1), add_trans (0) — each may be a list, recycled
|
|
9
|
+
# per object as in R. One object with vector coordinates
|
|
10
|
+
# repeats at every location; several objects consume one
|
|
11
|
+
# coordinate per object, in order.
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import plotly.graph_objects as go
|
|
15
|
+
|
|
16
|
+
from .plotly_utils import as_plotly_color, make_trans, to_hex
|
|
17
|
+
from .utils import get_option
|
|
18
|
+
|
|
19
|
+
_DASH = {"dashed": "dash", "dotted": "dot",
|
|
20
|
+
"dotdash": "dashdot", "longdash": "longdash",
|
|
21
|
+
"twodash": "longdashdot"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _as_list(v):
|
|
25
|
+
if v is None:
|
|
26
|
+
return []
|
|
27
|
+
if isinstance(v, (list, tuple, np.ndarray)):
|
|
28
|
+
return list(v)
|
|
29
|
+
return [v]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def plt_add(fig, add, x1=None, x2=None, y1=None, y2=None):
|
|
33
|
+
"""Draw the add= annotation objects onto a built figure.
|
|
34
|
+
R analog: .plt.add()"""
|
|
35
|
+
add = _as_list(add)
|
|
36
|
+
x1v, x2v = _as_list(x1), _as_list(x2)
|
|
37
|
+
y1v, y2v = _as_list(y1), _as_list(y2)
|
|
38
|
+
n_obj = len(add)
|
|
39
|
+
|
|
40
|
+
colors = _as_list(get_option("add_color", "gray10"))
|
|
41
|
+
fills = _as_list(get_option("add_fill", "#D9D9D920"))
|
|
42
|
+
lwds = _as_list(get_option("add_lwd", 0.5))
|
|
43
|
+
ltys = _as_list(get_option("add_lty", "solid"))
|
|
44
|
+
sizes = _as_list(get_option("add_size", 1.0))
|
|
45
|
+
transs = _as_list(get_option("add_trans", 0.0))
|
|
46
|
+
|
|
47
|
+
idx = {"x1": 0, "x2": 0, "y1": 0, "y2": 0}
|
|
48
|
+
|
|
49
|
+
def locs(obj):
|
|
50
|
+
"""Coordinate tuples for one object: all locations when
|
|
51
|
+
there is a single object, else the next one per vector."""
|
|
52
|
+
need_x = obj != "h_line"
|
|
53
|
+
need_y = obj != "v_line"
|
|
54
|
+
two = obj in ("line", "rect", "arrow")
|
|
55
|
+
if n_obj == 1:
|
|
56
|
+
n_loc = max(len(x1v) if need_x else 0,
|
|
57
|
+
len(y1v) if need_y else 0, 1)
|
|
58
|
+
out = []
|
|
59
|
+
for k in range(n_loc):
|
|
60
|
+
out.append((
|
|
61
|
+
x1v[k % len(x1v)] if need_x and x1v else None,
|
|
62
|
+
y1v[k % len(y1v)] if need_y and y1v else None,
|
|
63
|
+
x2v[k % len(x2v)] if two and x2v else None,
|
|
64
|
+
y2v[k % len(y2v)] if two and y2v else None))
|
|
65
|
+
return out
|
|
66
|
+
vals = []
|
|
67
|
+
for name, vec, need in (("x1", x1v, need_x),
|
|
68
|
+
("y1", y1v, need_y),
|
|
69
|
+
("x2", x2v, two),
|
|
70
|
+
("y2", y2v, two)):
|
|
71
|
+
if need and vec:
|
|
72
|
+
vals.append(vec[idx[name] % len(vec)])
|
|
73
|
+
idx[name] += 1
|
|
74
|
+
else:
|
|
75
|
+
vals.append(None)
|
|
76
|
+
return [tuple(vals)]
|
|
77
|
+
|
|
78
|
+
for i, obj in enumerate(add):
|
|
79
|
+
col = to_hex(colors[i % len(colors)])
|
|
80
|
+
lwd = float(lwds[i % len(lwds)])
|
|
81
|
+
lty = str(ltys[i % len(ltys)])
|
|
82
|
+
cex = float(sizes[i % len(sizes)])
|
|
83
|
+
trn = float(transs[i % len(transs)])
|
|
84
|
+
fll = fills[i % len(fills)]
|
|
85
|
+
fill_c = (make_trans(fll, 1 - trn) if trn > 0
|
|
86
|
+
else as_plotly_color(fll))
|
|
87
|
+
line = dict(color=col, width=max(0.5, lwd * 2))
|
|
88
|
+
if lty != "solid":
|
|
89
|
+
line["dash"] = _DASH.get(lty, lty)
|
|
90
|
+
|
|
91
|
+
for xx1, yy1, xx2, yy2 in locs(obj):
|
|
92
|
+
if obj == "v_line":
|
|
93
|
+
fig.add_shape(type="line", xref="x",
|
|
94
|
+
yref="paper", x0=xx1, x1=xx1,
|
|
95
|
+
y0=0, y1=1, line=line)
|
|
96
|
+
elif obj == "h_line":
|
|
97
|
+
fig.add_shape(type="line", xref="paper",
|
|
98
|
+
yref="y", x0=0, x1=1,
|
|
99
|
+
y0=yy1, y1=yy1, line=line)
|
|
100
|
+
elif obj == "line":
|
|
101
|
+
fig.add_shape(type="line", xref="x", yref="y",
|
|
102
|
+
x0=xx1, y0=yy1, x1=xx2, y1=yy2,
|
|
103
|
+
line=line)
|
|
104
|
+
elif obj == "rect":
|
|
105
|
+
fig.add_shape(type="rect", xref="x", yref="y",
|
|
106
|
+
x0=xx1, y0=yy1, x1=xx2, y1=yy2,
|
|
107
|
+
line=line, fillcolor=fill_c)
|
|
108
|
+
elif obj == "arrow":
|
|
109
|
+
fig.add_annotation(
|
|
110
|
+
x=xx2, y=yy2, ax=xx1, ay=yy1,
|
|
111
|
+
xref="x", yref="y", axref="x", ayref="y",
|
|
112
|
+
showarrow=True, arrowhead=2,
|
|
113
|
+
arrowcolor=col,
|
|
114
|
+
arrowwidth=max(0.5, lwd * 2), text="")
|
|
115
|
+
elif obj == "point":
|
|
116
|
+
fig.add_trace(go.Scatter(
|
|
117
|
+
x=[xx1], y=[yy1], mode="markers",
|
|
118
|
+
marker=dict(symbol="circle",
|
|
119
|
+
size=max(3, cex * 8),
|
|
120
|
+
color=fill_c,
|
|
121
|
+
line=dict(color=col, width=1)),
|
|
122
|
+
hoverinfo="skip", showlegend=False))
|
|
123
|
+
else: # any other string: text
|
|
124
|
+
fig.add_annotation(
|
|
125
|
+
x=xx1, y=yy1, text=str(obj),
|
|
126
|
+
xref="x", yref="y", showarrow=False,
|
|
127
|
+
font=dict(color=col,
|
|
128
|
+
size=max(6, round(13 * cex))))
|
|
129
|
+
return fig
|
lessPy/plt_contour.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# plt_contour.py — analog of plt.contour.R
|
|
2
|
+
#
|
|
3
|
+
# form="contour" for XY(): filled contours of the 2-D kernel
|
|
4
|
+
# density of <x, y>. The density grid ports MASS::kde2d() directly
|
|
5
|
+
# (product normal kernel, bandwidth.nrd) so the contours match R.
|
|
6
|
+
# Renderer conventions follow plt_plotly.py: the fit line and the
|
|
7
|
+
# data ellipses arrive as prepared coordinates computed by XY().
|
|
8
|
+
# Deviation: the fill ramp is fixed at the default-theme blues,
|
|
9
|
+
# colorRampPalette(c("white", getColors("blues"))) — lessPy does
|
|
10
|
+
# not port themes.
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import plotly.graph_objects as go
|
|
14
|
+
from scipy import stats as sps
|
|
15
|
+
|
|
16
|
+
from .plotly_utils import (
|
|
17
|
+
axis_format, axis_num, get_tick_fmt, plot_border,
|
|
18
|
+
plotly_style, to_hex)
|
|
19
|
+
from .utils import get_option, pretty
|
|
20
|
+
|
|
21
|
+
# getColors("blues") preceded by white: the filled-contour ramp
|
|
22
|
+
# of the default lessR theme
|
|
23
|
+
_RAMP = [
|
|
24
|
+
"#FFFFFF", "#CCECFF", "#B4D8FC", "#9DC5EB", "#84B2DB",
|
|
25
|
+
"#6B9FCC", "#4F8DBC", "#2D7CAE", "#006BA0", "#005B93",
|
|
26
|
+
"#004C8A", "#004087", "#0040A9",
|
|
27
|
+
]
|
|
28
|
+
_COLORSCALE = [[i / (len(_RAMP) - 1), c]
|
|
29
|
+
for i, c in enumerate(_RAMP)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _bw_nrd(v):
|
|
33
|
+
"""Normal reference bandwidth.
|
|
34
|
+
R analog: MASS::bandwidth.nrd()"""
|
|
35
|
+
q1, q3 = np.percentile(v, [25, 75])
|
|
36
|
+
return (4 * 1.06 * min(v.std(ddof=1), (q3 - q1) / 1.34)
|
|
37
|
+
* len(v) ** (-0.2))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _kde2d(x, y, n, lims, h=None):
|
|
41
|
+
"""2-D product-normal kernel density on an n x n grid over
|
|
42
|
+
lims=(x_lo, x_hi, y_lo, y_hi). h: bandwidth pair on the
|
|
43
|
+
kde2d scale (kernel sd is h/4), default bandwidth.nrd per
|
|
44
|
+
axis. R analog: MASS::kde2d()"""
|
|
45
|
+
if h is None:
|
|
46
|
+
hx, hy = _bw_nrd(x) / 4, _bw_nrd(y) / 4
|
|
47
|
+
else:
|
|
48
|
+
hx, hy = h[0] / 4, h[1] / 4
|
|
49
|
+
if hx <= 0 or hy <= 0:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"cannot estimate the density: x or y has no spread")
|
|
52
|
+
gx = np.linspace(lims[0], lims[1], n)
|
|
53
|
+
gy = np.linspace(lims[2], lims[3], n)
|
|
54
|
+
ax = sps.norm.pdf((gx[:, None] - x[None, :]) / hx)
|
|
55
|
+
ay = sps.norm.pdf((gy[:, None] - y[None, :]) / hy)
|
|
56
|
+
z = ax @ ay.T / (len(x) * hx * hy)
|
|
57
|
+
return gx, gy, z # z[i, j] at (gx[i], gy[j])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def plt_contour(xv, yv, contour_n, contour_nbins, contour_points,
|
|
61
|
+
pt_size, x_lab, y_lab, main, digits_d, ell95,
|
|
62
|
+
ellipses, ellipse_color, ellipse_lwd,
|
|
63
|
+
fit_lines, fit_color, fit_lwd, legend=False,
|
|
64
|
+
axis_fmt="K", axis_x_pre="", axis_y_pre=""):
|
|
65
|
+
"""Filled-contour display of the joint density of x and y,
|
|
66
|
+
with optional data point, ellipse, and fit line overlays.
|
|
67
|
+
R analog: .plt.contour()"""
|
|
68
|
+
|
|
69
|
+
# KDE — extend grid beyond data so contours are not clipped
|
|
70
|
+
# at the data edges
|
|
71
|
+
x_ext = np.ptp(xv) * 0.12
|
|
72
|
+
y_ext = np.ptp(yv) * 0.12
|
|
73
|
+
gx, gy, z = _kde2d(xv, yv, contour_nbins,
|
|
74
|
+
(xv.min() - x_ext, xv.max() + x_ext,
|
|
75
|
+
yv.min() - y_ext, yv.max() + y_ext))
|
|
76
|
+
|
|
77
|
+
# 95% mass threshold, then contour-derived ranges
|
|
78
|
+
dz = z / z.sum()
|
|
79
|
+
zs = np.sort(dz.ravel())[::-1]
|
|
80
|
+
thr = zs[np.searchsorted(np.cumsum(zs), 0.95)]
|
|
81
|
+
mask = dz >= thr
|
|
82
|
+
x_in = gx[mask.any(axis=1)]
|
|
83
|
+
y_in = gy[mask.any(axis=0)]
|
|
84
|
+
|
|
85
|
+
# ellipse-derived ranges: the 95% data ellipse truncated to
|
|
86
|
+
# the data range
|
|
87
|
+
e_x, e_y = ell95
|
|
88
|
+
keep = ((e_x >= xv.min()) & (e_x <= xv.max())
|
|
89
|
+
& (e_y >= yv.min()) & (e_y <= yv.max()))
|
|
90
|
+
e_x, e_y = e_x[keep], e_y[keep]
|
|
91
|
+
|
|
92
|
+
# plot limits: when the kde grid covers the full data range,
|
|
93
|
+
# use the grid extent so contours are not clipped; otherwise
|
|
94
|
+
# the union of the data and truncated-ellipse ranges
|
|
95
|
+
def _lim(cont, data, ell, grid):
|
|
96
|
+
if cont[0] > data[0] or cont[1] < data[1]:
|
|
97
|
+
if len(ell) > 0:
|
|
98
|
+
return [min(data[0], ell.min()),
|
|
99
|
+
max(data[1], ell.max())]
|
|
100
|
+
return list(data)
|
|
101
|
+
return list(grid)
|
|
102
|
+
x_lim = _lim((x_in.min(), x_in.max()), (xv.min(), xv.max()),
|
|
103
|
+
e_x, (gx[0], gx[-1]))
|
|
104
|
+
y_lim = _lim((y_in.min(), y_in.max()), (yv.min(), yv.max()),
|
|
105
|
+
e_y, (gy[0], gy[-1]))
|
|
106
|
+
|
|
107
|
+
if contour_points: # room for edge points
|
|
108
|
+
x_lim[1] += 0.015 * (x_lim[1] - x_lim[0])
|
|
109
|
+
y_lim[1] += 0.015 * (y_lim[1] - y_lim[0])
|
|
110
|
+
|
|
111
|
+
# ticks within the limits
|
|
112
|
+
axT1 = pretty(x_lim[0], x_lim[1])
|
|
113
|
+
axT2 = pretty(y_lim[0], y_lim[1])
|
|
114
|
+
fmt1 = get_tick_fmt(axT1, digits_d)
|
|
115
|
+
fmt2 = get_tick_fmt(axT2, digits_d)
|
|
116
|
+
axL1 = axis_format(axT1, digits_d, axis_fmt, axis_x_pre)
|
|
117
|
+
axL2 = axis_format(axT2, digits_d, axis_fmt, axis_y_pre)
|
|
118
|
+
|
|
119
|
+
style_opts = plotly_style()
|
|
120
|
+
fig = go.Figure()
|
|
121
|
+
|
|
122
|
+
# the filled contours: contour_n bands from min to max
|
|
123
|
+
# density; start at the first interior level so the region
|
|
124
|
+
# below it fills white, as in filled.contour()
|
|
125
|
+
lv0, lv1 = float(z.min()), float(z.max())
|
|
126
|
+
step = (lv1 - lv0) / contour_n
|
|
127
|
+
xpart = f"%{{x:{fmt1}}}" if fmt1 else "%{x}"
|
|
128
|
+
ypart = f"%{{y:{fmt2}}}" if fmt2 else "%{y}"
|
|
129
|
+
fig.add_trace(go.Contour(
|
|
130
|
+
x=gx, y=gy, z=z.T,
|
|
131
|
+
colorscale=_COLORSCALE,
|
|
132
|
+
contours=dict(start=lv0 + step, end=lv1, size=step),
|
|
133
|
+
line=dict(width=0),
|
|
134
|
+
showscale=bool(legend),
|
|
135
|
+
colorbar=dict(
|
|
136
|
+
tickfont=dict(
|
|
137
|
+
color=to_hex(get_option("axis_color", "black")),
|
|
138
|
+
size=16 * get_option("axis_size", 0.9)),
|
|
139
|
+
outlinewidth=0),
|
|
140
|
+
hovertemplate=(f"{x_lab}: {xpart}<br>{y_lab}: {ypart}"
|
|
141
|
+
"<extra></extra>"),
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
# R overlay style: translucent black fill, thin white border
|
|
145
|
+
if contour_points:
|
|
146
|
+
px = float(pt_size) * 7.25
|
|
147
|
+
if not np.isfinite(px) or px <= 0:
|
|
148
|
+
px = 5
|
|
149
|
+
fig.add_trace(go.Scatter(
|
|
150
|
+
x=xv, y=yv, mode="markers",
|
|
151
|
+
marker=dict(symbol="circle", size=px,
|
|
152
|
+
sizemode="diameter",
|
|
153
|
+
color="rgba(0,0,0,0.27)",
|
|
154
|
+
line=dict(color="#FFFFFF", width=0.2)),
|
|
155
|
+
hoverinfo="skip", showlegend=False))
|
|
156
|
+
|
|
157
|
+
# data ellipses: line only on a contour (no fill, as in R)
|
|
158
|
+
for ex_l, ey_l in ellipses or []:
|
|
159
|
+
fig.add_trace(go.Scatter(
|
|
160
|
+
x=ex_l, y=ey_l, mode="lines",
|
|
161
|
+
line=dict(color=to_hex(ellipse_color),
|
|
162
|
+
width=ellipse_lwd),
|
|
163
|
+
hoverinfo="skip", showlegend=False))
|
|
164
|
+
|
|
165
|
+
for fl in fit_lines or []:
|
|
166
|
+
if len(fl["x"]) < 2:
|
|
167
|
+
continue
|
|
168
|
+
fig.add_trace(go.Scatter(
|
|
169
|
+
x=fl["x"], y=fl["y"], mode="lines",
|
|
170
|
+
line=dict(color=to_hex(fit_color), width=fit_lwd),
|
|
171
|
+
hoverinfo="skip", showlegend=False))
|
|
172
|
+
|
|
173
|
+
ax_x = axis_num(x_lab, axT1, axL1)
|
|
174
|
+
ax_y = axis_num(y_lab, axT2, axL2)
|
|
175
|
+
ax_x["range"] = x_lim
|
|
176
|
+
ax_y["range"] = y_lim
|
|
177
|
+
fig.update_layout(
|
|
178
|
+
xaxis=ax_x, yaxis=ax_y,
|
|
179
|
+
shapes=plot_border(),
|
|
180
|
+
template=None,
|
|
181
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
182
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if main:
|
|
186
|
+
title_size = round(16 * get_option("main_size", 1))
|
|
187
|
+
fig.update_layout(
|
|
188
|
+
title=dict(text=main, x=0.5, xanchor="center",
|
|
189
|
+
y=0.99, yanchor="top",
|
|
190
|
+
font=dict(size=title_size)),
|
|
191
|
+
margin=dict(t=round(title_size * 2.2)))
|
|
192
|
+
return fig
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# plt_contour_facet.py — analog of plt.contourFacet.R
|
|
2
|
+
#
|
|
3
|
+
# Faceted form="contour" / form="smooth" for XY(): one density
|
|
4
|
+
# panel per facet cell, computed on a common KDE grid and
|
|
5
|
+
# bandwidth so density is comparable panel-to-panel — shared
|
|
6
|
+
# contour levels (contour) or a shared color range on the
|
|
7
|
+
# smooth_power-transformed density (smooth). As in R, the smooth
|
|
8
|
+
# panels use kde2d with smoothScatter's bandwidth ((q95-q05)/25
|
|
9
|
+
# per axis, h = 4x the kernel sd) so faceted and single-panel
|
|
10
|
+
# smooths are consistent; contour keeps the normal-reference
|
|
11
|
+
# bandwidth. A cell with < 3 points draws an empty panel; only
|
|
12
|
+
# if no cell can estimate does the call stop. Layout follows R:
|
|
13
|
+
# near-square single-facet grid (n_col computed by the caller),
|
|
14
|
+
# rows = facet2 levels for two facets.
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import plotly.graph_objects as go
|
|
18
|
+
|
|
19
|
+
from .plotly_utils import (
|
|
20
|
+
axis_format, facet_fig, facet_panels, finish_facet,
|
|
21
|
+
plotly_style, to_hex)
|
|
22
|
+
from .plt_contour import _COLORSCALE, _bw_nrd, _kde2d
|
|
23
|
+
from .plt_smooth import _RAMP as _SMOOTH_RAMP
|
|
24
|
+
from .utils import get_option, pretty
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _ss_bw(v):
|
|
28
|
+
"""smoothScatter's kernel sd: the 5-95% quantile spread / 25.
|
|
29
|
+
R analog: the ss.bw() local of .plt.contour.facet"""
|
|
30
|
+
q05, q95 = np.percentile(v, [5, 95])
|
|
31
|
+
return (q95 - q05) / 25
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def plt_contour_facet(xv, yv, facet_arr, facet_order, facet_name,
|
|
35
|
+
facet2_arr, facet2_order, facet2_name,
|
|
36
|
+
contour_n, contour_nbins, contour_points,
|
|
37
|
+
pt_size, x_lab, y_lab, main, digits_d,
|
|
38
|
+
ellipse, ellipse_color, ellipse_lwd,
|
|
39
|
+
fit, fit_power, fit_color, fit_lwd,
|
|
40
|
+
fit_fn, ellipse_fn,
|
|
41
|
+
render="contour", smooth_power=0.25,
|
|
42
|
+
smooth_points=100, smooth_bins=128,
|
|
43
|
+
n_col=1, axis_fmt="K", axis_x_pre="",
|
|
44
|
+
axis_y_pre=""):
|
|
45
|
+
"""Faceted joint-density display. fit_fn/ellipse_fn: the
|
|
46
|
+
caller's per-panel fit and ellipse constructors (XY._plt_fit,
|
|
47
|
+
XY._ellipse_region). R analog: .plt.contour.facet()"""
|
|
48
|
+
smooth = render == "smooth"
|
|
49
|
+
n_grid = smooth_bins if smooth else contour_nbins
|
|
50
|
+
|
|
51
|
+
labels, pos, sel, n_row_g, n_col = facet_panels(
|
|
52
|
+
facet_arr, facet_order, facet2_arr, facet2_order,
|
|
53
|
+
facet_name, facet2_name, n_col)
|
|
54
|
+
n_f = len(labels)
|
|
55
|
+
|
|
56
|
+
# common KDE grid + bandwidth: densities comparable across
|
|
57
|
+
# panels
|
|
58
|
+
x_ext = np.ptp(xv) * 0.12
|
|
59
|
+
y_ext = np.ptp(yv) * 0.12
|
|
60
|
+
lims = (xv.min() - x_ext, xv.max() + x_ext,
|
|
61
|
+
yv.min() - y_ext, yv.max() + y_ext)
|
|
62
|
+
if smooth:
|
|
63
|
+
h_common = [4 * _ss_bw(xv), 4 * _ss_bw(yv)]
|
|
64
|
+
else:
|
|
65
|
+
h_common = [_bw_nrd(xv), _bw_nrd(yv)]
|
|
66
|
+
|
|
67
|
+
dens = [None] * n_f
|
|
68
|
+
z_lo = z_hi = None
|
|
69
|
+
for i in range(n_f):
|
|
70
|
+
xi, yi = xv[sel[i]], yv[sel[i]]
|
|
71
|
+
if len(xi) < 3:
|
|
72
|
+
continue
|
|
73
|
+
h_i = [h if h > 0 else fb for h, fb in
|
|
74
|
+
zip(h_common, (_bw_nrd(xi), _bw_nrd(yi)))]
|
|
75
|
+
if min(h_i) <= 0: # still degenerate (e.g., collinear)
|
|
76
|
+
continue
|
|
77
|
+
gx, gy, z = _kde2d(xi, yi, n_grid, lims, h=h_i)
|
|
78
|
+
dens[i] = (gx, gy, z)
|
|
79
|
+
z_lo = float(z.min()) if z_lo is None \
|
|
80
|
+
else min(z_lo, float(z.min()))
|
|
81
|
+
z_hi = float(z.max()) if z_hi is None \
|
|
82
|
+
else max(z_hi, float(z.max()))
|
|
83
|
+
if z_hi is None:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"No facet cell has enough data (>= 3 points) for a "
|
|
86
|
+
+ ("smooth density." if smooth else "contour."))
|
|
87
|
+
step = (z_hi - z_lo) / contour_n
|
|
88
|
+
zlim_s = (max(z_lo, 0) ** smooth_power,
|
|
89
|
+
max(z_hi, 0) ** smooth_power)
|
|
90
|
+
|
|
91
|
+
# shared plot limits and ticks across panels
|
|
92
|
+
axT1 = pretty(float(xv.min()), float(xv.max()))
|
|
93
|
+
axT2 = pretty(float(yv.min()), float(yv.max()))
|
|
94
|
+
ax = {"axT1": axT1,
|
|
95
|
+
"axL1": axis_format(axT1, digits_d, axis_fmt,
|
|
96
|
+
axis_x_pre),
|
|
97
|
+
"axT2": axT2,
|
|
98
|
+
"axL2": axis_format(axT2, digits_d, axis_fmt,
|
|
99
|
+
axis_y_pre)}
|
|
100
|
+
|
|
101
|
+
ell_levels = [lv for lv in np.atleast_1d(ellipse) if lv]
|
|
102
|
+
px = float(pt_size) * 7.25
|
|
103
|
+
if not np.isfinite(px) or px <= 0:
|
|
104
|
+
px = 5
|
|
105
|
+
|
|
106
|
+
style_opts = plotly_style()
|
|
107
|
+
fig = facet_fig(n_row_g, n_col)
|
|
108
|
+
for i in range(n_f):
|
|
109
|
+
row, col = pos[i]
|
|
110
|
+
if dens[i] is None:
|
|
111
|
+
continue
|
|
112
|
+
gx, gy, z = dens[i]
|
|
113
|
+
xi, yi = xv[sel[i]], yv[sel[i]]
|
|
114
|
+
if smooth:
|
|
115
|
+
fig.add_trace(go.Heatmap(
|
|
116
|
+
x=gx, y=gy, z=(z ** smooth_power).T,
|
|
117
|
+
colorscale=_SMOOTH_RAMP, zsmooth="best",
|
|
118
|
+
zmin=zlim_s[0], zmax=zlim_s[1],
|
|
119
|
+
showscale=False, hoverinfo="skip",
|
|
120
|
+
), row=row, col=col)
|
|
121
|
+
if smooth_points > 0: # least-dense points
|
|
122
|
+
ix = np.clip(np.searchsorted(gx, xi), 0,
|
|
123
|
+
len(gx) - 1)
|
|
124
|
+
iy = np.clip(np.searchsorted(gy, yi), 0,
|
|
125
|
+
len(gy) - 1)
|
|
126
|
+
take = np.argsort(z[ix, iy],
|
|
127
|
+
kind="stable")[:smooth_points]
|
|
128
|
+
fig.add_trace(go.Scatter(
|
|
129
|
+
x=xi[take], y=yi[take], mode="markers",
|
|
130
|
+
marker=dict(symbol="circle", size=px,
|
|
131
|
+
sizemode="diameter",
|
|
132
|
+
color="rgba(0,0,0,0.33)",
|
|
133
|
+
line=dict(color="#FFFFFF",
|
|
134
|
+
width=0.2)),
|
|
135
|
+
hoverinfo="skip", showlegend=False,
|
|
136
|
+
), row=row, col=col)
|
|
137
|
+
else:
|
|
138
|
+
fig.add_trace(go.Contour(
|
|
139
|
+
x=gx, y=gy, z=z.T,
|
|
140
|
+
colorscale=_COLORSCALE,
|
|
141
|
+
contours=dict(start=z_lo + step, end=z_hi,
|
|
142
|
+
size=step),
|
|
143
|
+
line=dict(width=0), showscale=False,
|
|
144
|
+
hoverinfo="skip",
|
|
145
|
+
), row=row, col=col)
|
|
146
|
+
if contour_points:
|
|
147
|
+
fig.add_trace(go.Scatter(
|
|
148
|
+
x=xi, y=yi, mode="markers",
|
|
149
|
+
marker=dict(symbol="circle", size=px,
|
|
150
|
+
sizemode="diameter",
|
|
151
|
+
color="rgba(0,0,0,0.27)",
|
|
152
|
+
line=dict(color="#FFFFFF",
|
|
153
|
+
width=0.2)),
|
|
154
|
+
hoverinfo="skip", showlegend=False,
|
|
155
|
+
), row=row, col=col)
|
|
156
|
+
for lv in ell_levels: # data ellipse(s)
|
|
157
|
+
if len(xi) < 3:
|
|
158
|
+
break
|
|
159
|
+
ex, ey = ellipse_fn(xi, yi, float(lv))
|
|
160
|
+
fig.add_trace(go.Scatter(
|
|
161
|
+
x=ex, y=ey, mode="lines",
|
|
162
|
+
line=dict(color=to_hex(ellipse_color),
|
|
163
|
+
width=ellipse_lwd),
|
|
164
|
+
hoverinfo="skip", showlegend=False,
|
|
165
|
+
), row=row, col=col)
|
|
166
|
+
if fit != "off" and len(xi) >= 3:
|
|
167
|
+
xs, _, f = fit_fn(xi, yi, fit, fit_power)
|
|
168
|
+
okf = np.isfinite(f)
|
|
169
|
+
if okf.sum() >= 2:
|
|
170
|
+
fig.add_trace(go.Scatter(
|
|
171
|
+
x=xs[okf], y=f[okf], mode="lines",
|
|
172
|
+
line=dict(color=to_hex(fit_color),
|
|
173
|
+
width=fit_lwd),
|
|
174
|
+
hoverinfo="skip", showlegend=False,
|
|
175
|
+
), row=row, col=col)
|
|
176
|
+
|
|
177
|
+
# size the figure so each contour panel is roughly square
|
|
178
|
+
# (they read as scatter panels, not wide distribution strips)
|
|
179
|
+
panel = 300
|
|
180
|
+
finish_facet(fig, labels, ax, x_lab, y_lab, gridT1=None,
|
|
181
|
+
style_opts=style_opts, n_col=n_col, pos=pos,
|
|
182
|
+
height=150 + panel * n_row_g,
|
|
183
|
+
width=130 + panel * n_col)
|
|
184
|
+
# density panels span the data, clipped to the shared limits
|
|
185
|
+
pad_x = 0.04 * (axT1[-1] - axT1[0])
|
|
186
|
+
pad_y = 0.04 * (axT2[-1] - axT2[0])
|
|
187
|
+
fig.update_xaxes(range=[axT1[0] - pad_x, axT1[-1] + pad_x])
|
|
188
|
+
fig.update_yaxes(range=[axT2[0] - pad_y, axT2[-1] + pad_y])
|
|
189
|
+
if main:
|
|
190
|
+
title_size = round(16 * get_option("main_size", 1))
|
|
191
|
+
fig.update_layout(
|
|
192
|
+
title=dict(text=main, x=0.5, xanchor="center",
|
|
193
|
+
font=dict(size=title_size)))
|
|
194
|
+
return fig
|