lessPython 0.1.0__py3-none-any.whl → 0.2.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/Chart.py +754 -118
- lessPy/Correlation.py +12 -1
- lessPy/Flows.py +5 -1
- lessPy/X.py +1 -1
- lessPy/XY.py +95 -95
- lessPy/bc_items_plotly.py +195 -0
- lessPy/bc_plotly.py +97 -33
- lessPy/dn_plotly.py +1 -1
- lessPy/dot_plotly.py +112 -41
- lessPy/freq_poly_plotly.py +1 -1
- lessPy/getColors.py +6 -1
- lessPy/hier_plotly.py +50 -7
- lessPy/hs_plotly.py +3 -3
- lessPy/pie_plotly.py +17 -7
- lessPy/pivot.py +6 -3
- lessPy/plotly_utils.py +69 -15
- lessPy/plt_plotly.py +15 -3
- lessPy/plt_time.py +1 -1
- lessPy/profile_plotly.py +121 -0
- lessPy/stats_out.py +382 -27
- lessPy/utils.py +54 -14
- lessPy/vbs_plotly.py +14 -10
- {lesspython-0.1.0.dist-info → lesspython-0.2.0.dist-info}/METADATA +35 -3
- {lesspython-0.1.0.dist-info → lesspython-0.2.0.dist-info}/RECORD +27 -25
- {lesspython-0.1.0.dist-info → lesspython-0.2.0.dist-info}/WHEEL +1 -1
- {lesspython-0.1.0.dist-info → lesspython-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {lesspython-0.1.0.dist-info → lesspython-0.2.0.dist-info}/top_level.txt +0 -0
lessPy/Correlation.py
CHANGED
|
@@ -27,7 +27,8 @@ class CorrelationResults:
|
|
|
27
27
|
f"r={self.r}, p={self.pvalue}>")
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def Correlation(x=None, y=None, data=None,
|
|
30
|
+
def Correlation(x=None, y=None, data=None, filter=None,
|
|
31
|
+
miss="pairwise",
|
|
31
32
|
show="cor", method="pearson", brief=False,
|
|
32
33
|
digits_d=None, heat_map=True, main=None):
|
|
33
34
|
"""Correlation of two variables (with a significance test) or
|
|
@@ -42,6 +43,16 @@ def Correlation(x=None, y=None, data=None, miss="pairwise",
|
|
|
42
43
|
if method not in ("pearson", "spearman", "kendall"):
|
|
43
44
|
raise ValueError('method: "pearson", "spearman", '
|
|
44
45
|
'"kendall"')
|
|
46
|
+
# subset rows, as in ttest(), ANOVA() and Regression(). Correlation()
|
|
47
|
+
# reaches its data by two routes, so filter whichever one carries
|
|
48
|
+
# it, always before a variable list subsets the columns
|
|
49
|
+
if filter is not None:
|
|
50
|
+
if data is not None:
|
|
51
|
+
data = data.query(filter)
|
|
52
|
+
elif isinstance(x, pd.DataFrame):
|
|
53
|
+
x = x.query(filter)
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError("filter needs data= or a DataFrame x")
|
|
45
56
|
if y is not None:
|
|
46
57
|
xv = _resolve1(x, data, "x")
|
|
47
58
|
yv = _resolve1(y, data, "y")
|
lessPy/Flows.py
CHANGED
|
@@ -109,7 +109,11 @@ def Flows(value, stage1, stage2, stage3=None, data=None,
|
|
|
109
109
|
y=min(0.98, y_dom[1] + 0.03),
|
|
110
110
|
xanchor="center", yanchor="top",
|
|
111
111
|
font=dict(size=round(18 * labels_size))),
|
|
112
|
-
margin
|
|
112
|
+
# the top margin only needs to clear the title, which is
|
|
113
|
+
# positioned in container coordinates; a fixed 90 left a
|
|
114
|
+
# large gap below it
|
|
115
|
+
margin=dict(t=max(30, round(18 * labels_size * 2.2)),
|
|
116
|
+
r=30, b=30, l=30),
|
|
113
117
|
font=dict(size=round(15 * labels_size)),
|
|
114
118
|
template=None,
|
|
115
119
|
paper_bgcolor=to_hex(get_option("window_fill", "white")))
|
lessPy/X.py
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
# violin bandwidth search (now the default bandwidth, as R),
|
|
36
36
|
# out_cut/ID/ID_size outlier labels above the strip.
|
|
37
37
|
# n_row/n_col lay the histogram/density/freq_poly facets out
|
|
38
|
-
# as a grid (
|
|
38
|
+
# as a grid (reading-order fill, top-left first); not for the VBS bands.
|
|
39
39
|
# Not ported: aspect= (panel aspect is figure sizing in
|
|
40
40
|
# plotly), the axis-format/margin family, add= annotations,
|
|
41
41
|
# themes.
|
lessPy/XY.py
CHANGED
|
@@ -42,9 +42,10 @@
|
|
|
42
42
|
# shared scatter_matrix helper (plt_mat_plotly). The faceted
|
|
43
43
|
# scatter prints a summary table per grouping variable
|
|
44
44
|
# (facet_summary ~ .vbs_summary_table).
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
45
|
+
# A categorical x or y is refused with the call of the view it
|
|
46
|
+
# belongs to, as XY.R's master control funnel now does (Aug
|
|
47
|
+
# 2026); the dot plots XY() once drew for row_names and for a
|
|
48
|
+
# categorical variable with stat= are Chart(form="dot")'s. R's form="hexbin" is dropped by design:
|
|
48
49
|
# plotly has no hexbin trace.
|
|
49
50
|
|
|
50
51
|
import math
|
|
@@ -262,7 +263,7 @@ def _xy_facet(xv, yv, by_arr, by_order, facet_arr, facet_order,
|
|
|
262
263
|
facet2_name=None, n_col=1):
|
|
263
264
|
"""One scatter panel per facet level on shared axes,
|
|
264
265
|
following the faceted-histogram conventions (_hs_facet):
|
|
265
|
-
first level on the
|
|
266
|
+
first level on the top panel, strip labels, legend
|
|
266
267
|
entries from the first panel only. facet2: the two-facet
|
|
267
268
|
grid, rows = facet2 levels."""
|
|
268
269
|
labels, pos, sel, n_row_g, n_col = facet_panels(
|
|
@@ -738,7 +739,7 @@ def _ts_facet(xv, yv, facet_arr, facet_order, fill0, border0,
|
|
|
738
739
|
facet2_order=None, facet2_name=None, n_col=1):
|
|
739
740
|
"""One time-series panel per facet level on shared axes,
|
|
740
741
|
following the faceted-scatter conventions (_xy_facet):
|
|
741
|
-
first level on the
|
|
742
|
+
first level on the top panel, strip labels. The x axis
|
|
742
743
|
uses plotly's native date ticks, as the single-panel time
|
|
743
744
|
series does. R analog: the lattice cont_cont path for a
|
|
744
745
|
date x. facet2: the two-facet grid, rows = facet2 levels."""
|
|
@@ -790,7 +791,7 @@ def _ts_facet(xv, yv, facet_arr, facet_order, fill0, border0,
|
|
|
790
791
|
if "markers" in mode_pts else None),
|
|
791
792
|
line=dict(color=to_hex(border0), width=1.5),
|
|
792
793
|
hovertemplate=hover, showlegend=False,
|
|
793
|
-
), row=row, col=col) # first level
|
|
794
|
+
), row=row, col=col) # first level top-left
|
|
794
795
|
|
|
795
796
|
finish_facet(fig, labels, ax, x_lab, y_lab,
|
|
796
797
|
gridT1=None, style_opts=style_opts,
|
|
@@ -988,10 +989,11 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
988
989
|
segments_x=None, segments_y=None,
|
|
989
990
|
add=None, x1=None, y1=None, x2=None, y2=None,
|
|
990
991
|
fill=None, color=None, transparency=None,
|
|
991
|
-
pt_size=1, pt_shape="circle",
|
|
992
|
+
pt_size=1, pt_shape="circle", line_width=1.5,
|
|
993
|
+
jitter_x=None, jitter_y=None,
|
|
992
994
|
MD_cut=0, out_cut=0, out_shape="circle-open", out_size=1,
|
|
993
995
|
ID=None, ID_color="gray50", ID_size=0.6,
|
|
994
|
-
fit="off", fit_power=1, fit_se=None,
|
|
996
|
+
fit="off", fit_power=1, fit_se=None, fit_errors=False,
|
|
995
997
|
fit_new=None,
|
|
996
998
|
fit_color=None, fit_lwd=None, span=0.75,
|
|
997
999
|
ellipse=0, ellipse_fill=None, ellipse_color=None,
|
|
@@ -1007,7 +1009,8 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1007
1009
|
axis_fmt="K", axis_x_pre="", axis_y_pre="",
|
|
1008
1010
|
rotate_x=0, rotate_y=0, scale_x=None, scale_y=None,
|
|
1009
1011
|
xlab=None, ylab=None, main=None, digits_d=None,
|
|
1010
|
-
quiet=None
|
|
1012
|
+
quiet=None,
|
|
1013
|
+
plot_errors=None):
|
|
1011
1014
|
"""Analytic view of the relationship between two numerical
|
|
1012
1015
|
variables, optionally grouped (by=). A date x displays as a
|
|
1013
1016
|
time series. Variables are strings naming columns of the
|
|
@@ -1051,19 +1054,25 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1051
1054
|
if center_line not in ("off", "mean", "median", "zero"):
|
|
1052
1055
|
raise ValueError(
|
|
1053
1056
|
'center_line: "off", "mean", "median", or "zero"')
|
|
1057
|
+
if plot_errors is not None:
|
|
1058
|
+
raise ValueError(
|
|
1059
|
+
"Parameter plot_errors has been renamed to fit_errors. "
|
|
1060
|
+
"The segments it draws join each point to the fitted line, "
|
|
1061
|
+
"so they are the errors of the fit, and the name now says "
|
|
1062
|
+
"which fit they belong to, as fit, fit_color, and fit_se do.")
|
|
1054
1063
|
if center_line != "off" and facet is not None:
|
|
1055
1064
|
raise ValueError(
|
|
1056
1065
|
"center_line draws on a single panel: no facet=")
|
|
1057
|
-
if
|
|
1066
|
+
if fit_errors:
|
|
1058
1067
|
if fit == "off":
|
|
1059
1068
|
raise ValueError(
|
|
1060
|
-
"
|
|
1069
|
+
"fit_errors draws residual segments to the fit "
|
|
1061
1070
|
"line: specify fit=")
|
|
1062
1071
|
if form != "scatter":
|
|
1063
|
-
raise ValueError('
|
|
1072
|
+
raise ValueError('fit_errors applies to form="scatter"')
|
|
1064
1073
|
if facet is not None:
|
|
1065
1074
|
raise ValueError(
|
|
1066
|
-
"
|
|
1075
|
+
"fit_errors draws on a single panel: no facet=")
|
|
1067
1076
|
if (scale_x is not None or scale_y is not None) \
|
|
1068
1077
|
and facet is not None:
|
|
1069
1078
|
raise ValueError(
|
|
@@ -1152,39 +1161,43 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1152
1161
|
xlab, ylab, main, digits_d, quiet),
|
|
1153
1162
|
rotate_x, rotate_y)
|
|
1154
1163
|
|
|
1155
|
-
# ----- row_names:
|
|
1156
|
-
# axis
|
|
1157
|
-
#
|
|
1158
|
-
# its value ------------------------------------------------------
|
|
1164
|
+
# ----- row_names: each row label identifies a single case, a
|
|
1165
|
+
# categorical axis that belongs to Chart(form="dot")
|
|
1166
|
+
# R analog: XY.R categorical refusal, x.unique branch
|
|
1159
1167
|
_ROW_KW = ("row_names", "row.names")
|
|
1160
1168
|
x_row = isinstance(x, str) and x in _ROW_KW
|
|
1161
1169
|
y_row = isinstance(y, str) and y in _ROW_KW
|
|
1162
1170
|
if x_row or y_row:
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1171
|
+
cont = y if x_row else x
|
|
1172
|
+
raise TypeError(
|
|
1173
|
+
"XY() requires a continuous x and a continuous y.\n"
|
|
1174
|
+
"row_names is categorical.\n\n"
|
|
1175
|
+
"Each level of row_names identifies a single case, so\n"
|
|
1176
|
+
f" display the values of {cont} directly with Chart():\n"
|
|
1177
|
+
f" Chart('row_names', y='{cont}', form='dot')")
|
|
1178
|
+
|
|
1179
|
+
# stat, sort, and the segments_x/_y droplines served the dot
|
|
1180
|
+
# plot XY() once drew of a categorical variable, now Chart()'s;
|
|
1181
|
+
# R's stat aggregates y within bins of x (n_bins), not ported
|
|
1182
|
+
if stat is not None:
|
|
1183
|
+
cat_, cont = "x", "y"
|
|
1184
|
+
for a, b in ((x, y), (y, x)):
|
|
1185
|
+
if (isinstance(a, str) and isinstance(b, str)
|
|
1186
|
+
and a in data.columns and b in data.columns
|
|
1187
|
+
and not pd.api.types.is_numeric_dtype(data[a])):
|
|
1188
|
+
cat_, cont = a, b
|
|
1189
|
+
raise ValueError(
|
|
1190
|
+
"stat in XY() aggregates y within bins of x (n_bins), "
|
|
1191
|
+
"which is not yet ported. For a statistic of a numerical "
|
|
1192
|
+
"variable across the levels of a categorical one, use "
|
|
1193
|
+
f"Chart():\n Chart('{cat_}', y='{cont}', stat='{stat}')")
|
|
1194
|
+
for nm, v in (("sort", None if sort == "0" else sort),
|
|
1195
|
+
("segments_x", segments_x),
|
|
1196
|
+
("segments_y", segments_y)):
|
|
1197
|
+
if v is not None:
|
|
1168
1198
|
raise ValueError(
|
|
1169
|
-
"
|
|
1170
|
-
|
|
1171
|
-
d2 = data.copy()
|
|
1172
|
-
d2["row_names"] = pd.Categorical(
|
|
1173
|
-
names, categories=list(dict.fromkeys(names)))
|
|
1174
|
-
# no by= here, so a single scatterplot fill (XY's pt_color
|
|
1175
|
-
# default) rather than Chart's per-category dot hues
|
|
1176
|
-
dot_fill = (get_option("pt_color", "#324E5C")
|
|
1177
|
-
if fill is None else fill)
|
|
1178
|
-
# XY/lessR names droplines by the axis they reach (segments_y
|
|
1179
|
-
# = to the y axis, i.e. horizontal); Chart names them by
|
|
1180
|
-
# direction (segments_x = horizontal) -- so they swap
|
|
1181
|
-
return Chart(
|
|
1182
|
-
"row_names", y=val, data=d2, form="dot", horiz=y_row,
|
|
1183
|
-
sort=sort, segments_x=segments_y, segments_y=segments_x,
|
|
1184
|
-
fill=dot_fill, color=color, pt_size=pt_size, main=main,
|
|
1185
|
-
xlab=xlab, ylab=ylab, digits_d=digits_d,
|
|
1186
|
-
rotate_x=rotate_x, rotate_y=rotate_y,
|
|
1187
|
-
axis_fmt=axis_fmt, quiet=quiet)
|
|
1199
|
+
f"{nm} applies to the dot plot of a categorical "
|
|
1200
|
+
"variable, which is Chart(form='dot'), not XY()")
|
|
1188
1201
|
|
|
1189
1202
|
# ----- resolve variables and filter ---------------------------
|
|
1190
1203
|
# ".Index" is the row-number pseudo-variable (1..n) for a run
|
|
@@ -1204,27 +1217,6 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1204
1217
|
if index_y:
|
|
1205
1218
|
y = "Index"
|
|
1206
1219
|
|
|
1207
|
-
# stat= aggregates a numerical variable by a categorical one and
|
|
1208
|
-
# shows a Cleveland dot plot -- delegate to Chart(form="dot"),
|
|
1209
|
-
# its tested renderer (categorical variables live in Chart)
|
|
1210
|
-
if stat is not None:
|
|
1211
|
-
from .Chart import Chart
|
|
1212
|
-
x_num = pd.api.types.is_numeric_dtype(x_ser)
|
|
1213
|
-
y_num = pd.api.types.is_numeric_dtype(y_ser)
|
|
1214
|
-
common = dict(
|
|
1215
|
-
data=data, by=by, facet=facet, form="dot", stat=stat,
|
|
1216
|
-
sort=sort, fill=fill, color=color,
|
|
1217
|
-
transparency=transparency, pt_size=pt_size, main=main,
|
|
1218
|
-
digits_d=digits_d, rotate_x=rotate_x, rotate_y=rotate_y,
|
|
1219
|
-
axis_fmt=axis_fmt, quiet=quiet)
|
|
1220
|
-
if not x_num and y_num: # categorical x, value y
|
|
1221
|
-
return Chart(x, y=y, xlab=xlab, ylab=ylab, **common)
|
|
1222
|
-
if x_num and not y_num: # value x, categorical y
|
|
1223
|
-
return Chart(y, y=x, horiz=True, xlab=xlab, ylab=ylab,
|
|
1224
|
-
**common)
|
|
1225
|
-
raise ValueError(
|
|
1226
|
-
"XY() with stat= needs one categorical and one "
|
|
1227
|
-
"numerical variable (a Cleveland dot plot)")
|
|
1228
1220
|
by_ser = get_column(data, by, "by") if by is not None else None
|
|
1229
1221
|
(facet_ser, facet_name,
|
|
1230
1222
|
facet2_ser, facet2_name) = resolve_facet(data, facet, "XY")
|
|
@@ -1268,33 +1260,40 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1268
1260
|
pass
|
|
1269
1261
|
|
|
1270
1262
|
is_date = pd.api.types.is_datetime64_any_dtype(x_ser)
|
|
1271
|
-
#
|
|
1272
|
-
#
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
+ "]"
|
|
1280
|
-
if isinstance(facet, (list, tuple))
|
|
1281
|
-
else f"'{facet}'" if isinstance(facet, str)
|
|
1282
|
-
else f"'{facet_name}'")
|
|
1263
|
+
# XY() displays the relationship of two continuous variables; a
|
|
1264
|
+
# categorical x or y belongs to another view, so name its call
|
|
1265
|
+
# rather than render. R analog: XY.R master control funnel
|
|
1266
|
+
x_cat = not is_date and not pd.api.types.is_numeric_dtype(x_ser)
|
|
1267
|
+
y_cat = not pd.api.types.is_numeric_dtype(y_ser)
|
|
1268
|
+
if x_cat or y_cat:
|
|
1269
|
+
head = "XY() requires a continuous x and a continuous y.\n"
|
|
1270
|
+
if x_cat and y_cat:
|
|
1283
1271
|
raise TypeError(
|
|
1284
|
-
|
|
1285
|
-
"
|
|
1286
|
-
f"
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
if s is x_ser and is_date:
|
|
1292
|
-
continue
|
|
1293
|
-
if not pd.api.types.is_numeric_dtype(s):
|
|
1272
|
+
head + f"Both {x} and {y} are categorical.\n\n"
|
|
1273
|
+
"For two categorical variables, use Chart() with a "
|
|
1274
|
+
f"by variable:\n Chart('{x}', by='{y}', "
|
|
1275
|
+
"form='bubble')")
|
|
1276
|
+
cont, cat_, cat_s = ((y, x, x_ser) if x_cat
|
|
1277
|
+
else (x, y, y_ser))
|
|
1278
|
+
if not cat_s.dropna().duplicated().any():
|
|
1294
1279
|
raise TypeError(
|
|
1295
|
-
f"
|
|
1296
|
-
f"
|
|
1297
|
-
"
|
|
1280
|
+
head + f"{cat_} is categorical.\n\n"
|
|
1281
|
+
f"Each level of {cat_} identifies a single case, so\n"
|
|
1282
|
+
f" display the values of {cont} directly with "
|
|
1283
|
+
f"Chart():\n Chart('{cat_}', y='{cont}', "
|
|
1284
|
+
"form='dot')")
|
|
1285
|
+
f_txt = ""
|
|
1286
|
+
if facet is not None:
|
|
1287
|
+
f_txt = (", facet=[" + ", ".join(f"'{f}'" for f in facet)
|
|
1288
|
+
+ "]" if isinstance(facet, (list, tuple))
|
|
1289
|
+
else f", facet='{facet}'"
|
|
1290
|
+
if isinstance(facet, str)
|
|
1291
|
+
else f", facet='{facet_name}'")
|
|
1292
|
+
raise TypeError(
|
|
1293
|
+
head + f"{cat_} is categorical.\n\n"
|
|
1294
|
+
f"For the distribution of the continuous {cont} across\n"
|
|
1295
|
+
f" the levels of the categorical {cat_}, use X() with "
|
|
1296
|
+
f"a by variable:\n X('{cont}', by='{cat_}'{f_txt})")
|
|
1298
1297
|
|
|
1299
1298
|
# ts_NA= replaces missing y with a set value before the NA
|
|
1300
1299
|
# rows are dropped, so a gap in the series becomes that value
|
|
@@ -1659,17 +1658,17 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1659
1658
|
|
|
1660
1659
|
# ----- fit lines and SE bands ---------------------------------
|
|
1661
1660
|
# with by= the bands default off, as in R (XY.R line ~484);
|
|
1662
|
-
#
|
|
1661
|
+
# fit_errors shows residuals, not a band, so it too defaults
|
|
1663
1662
|
# the band off (XY.R line ~483)
|
|
1664
1663
|
if fit_se is None:
|
|
1665
|
-
fit_se = 0 if
|
|
1664
|
+
fit_se = 0 if fit_errors else (0.95 if by is None else 0)
|
|
1666
1665
|
se_levels = [lv for lv in (fit_se if isinstance(
|
|
1667
1666
|
fit_se, (list, tuple)) else [fit_se]) if lv]
|
|
1668
1667
|
if form == "contour": # R: no SE bands on contour
|
|
1669
1668
|
se_levels = []
|
|
1670
1669
|
|
|
1671
1670
|
fit_lines, se_polys, fit_stats = [], [], []
|
|
1672
|
-
err_lines = [] #
|
|
1671
|
+
err_lines = [] # fit_errors residual segments
|
|
1673
1672
|
if fit != "off":
|
|
1674
1673
|
for nm, xg, yg in groups:
|
|
1675
1674
|
if len(xg) < 2:
|
|
@@ -1678,7 +1677,7 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1678
1677
|
xs, ys_s, f, se_f = _loess(xg, yg, span)
|
|
1679
1678
|
fit_lines.append({"name": nm, "x": xs, "y": f})
|
|
1680
1679
|
fit_stats.append((nm, fit, ys_s, f))
|
|
1681
|
-
if
|
|
1680
|
+
if fit_errors:
|
|
1682
1681
|
err_lines.append((xs, ys_s, f))
|
|
1683
1682
|
for lv in se_levels:
|
|
1684
1683
|
tq = sps.t.ppf((1 + lv) / 2, len(xs) - 1)
|
|
@@ -1692,7 +1691,7 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1692
1691
|
fit_lines.append({"name": nm, "x": xs[okf],
|
|
1693
1692
|
"y": f[okf]})
|
|
1694
1693
|
fit_stats.append((nm, fit, ys_s[okf], f[okf]))
|
|
1695
|
-
if
|
|
1694
|
+
if fit_errors:
|
|
1696
1695
|
err_lines.append((xs[okf], ys_s[okf], f[okf]))
|
|
1697
1696
|
if fit == "lm":
|
|
1698
1697
|
for lv in se_levels:
|
|
@@ -1845,7 +1844,8 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1845
1844
|
y_lab=y if ylab is None else ylab,
|
|
1846
1845
|
ax=ax, gridT1=gridT1, gridT2=ax["axT2"],
|
|
1847
1846
|
main=main, digits_d=digits_d,
|
|
1848
|
-
connect=is_date or show_runs or index_x,
|
|
1847
|
+
connect=is_date or show_runs or index_x, ln_width=line_width,
|
|
1848
|
+
is_date=is_date,
|
|
1849
1849
|
pt_opacity=1 - transparency,
|
|
1850
1850
|
area_polys=area_polys,
|
|
1851
1851
|
fit_lines=fit_lines, fit_color=fit_color, fit_lwd=fit_lwd,
|
|
@@ -1866,9 +1866,9 @@ def XY(x, y=None, data=None, filter=None, by=None, facet=None,
|
|
|
1866
1866
|
# .Index run chart, or a runs chart keeps its wide aspect)
|
|
1867
1867
|
fig.update_layout(**square_layout(main=bool(main)))
|
|
1868
1868
|
|
|
1869
|
-
#
|
|
1869
|
+
# fit_errors: residual segment from each point to its fitted
|
|
1870
1870
|
# value, drawn over the points (R plt.main.R ~1403, rgb 130,40,35)
|
|
1871
|
-
if
|
|
1871
|
+
if fit_errors and err_lines:
|
|
1872
1872
|
ex, ey = [], []
|
|
1873
1873
|
for xs_, yo_, ff_ in err_lines:
|
|
1874
1874
|
for i in range(len(xs_)):
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# bc_items_plotly.py — the multi-item stacked chart, Chart(x=[...]),
|
|
2
|
+
# for a set of items that share one response scale, such as Likert
|
|
3
|
+
# items. R analog: the multi-x path of Chart.R, its frequency table
|
|
4
|
+
# from .bc.main() (multi), drawn there with base graphics, and the
|
|
5
|
+
# faceted form .bar.stackedLattice(); this is the plotly design.
|
|
6
|
+
#
|
|
7
|
+
# Each item is one horizontal bar, divided into the counts of its
|
|
8
|
+
# responses in a divergent palette, so the bars compose the response
|
|
9
|
+
# distributions of all the items on one scale. Unfaceted, the items
|
|
10
|
+
# are ordered by their mean response, the largest at the top; each
|
|
11
|
+
# segment carries its percent of the item, omitted below 4%. With
|
|
12
|
+
# facet=, one panel per level, the items in the order given, first
|
|
13
|
+
# at the top, on one count scale (drawn by bc_facet_plotly).
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
import plotly.graph_objects as go
|
|
18
|
+
|
|
19
|
+
from .utils import get_option, pretty
|
|
20
|
+
from .plotly_utils import (
|
|
21
|
+
as_plotly_color, auto_text_color, axis_cat, axis_format,
|
|
22
|
+
axis_num, legend_style, plot_border, plotly_style, to_hex,
|
|
23
|
+
x_grid,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# theme -> the two sequential palettes of the divergent fill
|
|
27
|
+
# R analog: .get_fill(theme, diverge=TRUE)
|
|
28
|
+
_DIVERGE = {
|
|
29
|
+
"gray": ("grays", "grays"), "white": ("grays", "grays"),
|
|
30
|
+
"darkred": ("turquoises", "reds"), "red": ("turquoises", "reds"),
|
|
31
|
+
"rose": ("turquoises", "reds"),
|
|
32
|
+
"slatered": ("turquoises", "reds"),
|
|
33
|
+
"darkgreen": ("violets", "greens"),
|
|
34
|
+
"green": ("violets", "greens"), "purple": ("violets", "greens"),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def items_fill(theme, n):
|
|
39
|
+
"""The divergent palette of n response colors for a theme, the
|
|
40
|
+
low responses in the first hue. R analog: .color_range(
|
|
41
|
+
.get_fill(theme, diverge=TRUE), n)"""
|
|
42
|
+
from .getColors import getColors
|
|
43
|
+
p1, p2 = _DIVERGE.get(theme or "colors", ("browns", "blues"))
|
|
44
|
+
return [c[:7] for c in getColors(p1, p2, n=max(n, 2),
|
|
45
|
+
quiet=True)][:n]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resp_str(v):
|
|
49
|
+
if isinstance(v, float) and v.is_integer():
|
|
50
|
+
return str(int(v))
|
|
51
|
+
return str(v)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def items_table(cols, names):
|
|
55
|
+
"""Responses x items table of counts over the union of the
|
|
56
|
+
response values, with the weighted mean response of each item.
|
|
57
|
+
Items that do not share one response set compose no single
|
|
58
|
+
chart: raise. R analog: .bc.main() multi tabulation and the
|
|
59
|
+
one_plot test of Chart.R"""
|
|
60
|
+
# one_plot: every item's responses fall within the largest set
|
|
61
|
+
sets = [set(c.dropna().unique()) for c in cols]
|
|
62
|
+
widest = max(sets, key=len)
|
|
63
|
+
if any(s - widest for s in sets):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"The x variables do not share one set of response "
|
|
66
|
+
"categories, so they do not compose a single multi-item "
|
|
67
|
+
"chart. Chart each variable separately.")
|
|
68
|
+
first = cols[0]
|
|
69
|
+
if isinstance(first.dtype, pd.CategoricalDtype):
|
|
70
|
+
resp = list(first.cat.categories)
|
|
71
|
+
for c in cols[1:]:
|
|
72
|
+
if isinstance(c.dtype, pd.CategoricalDtype):
|
|
73
|
+
resp += [v for v in c.cat.categories if v not in resp]
|
|
74
|
+
else:
|
|
75
|
+
vals = set()
|
|
76
|
+
for c in cols:
|
|
77
|
+
vals |= set(c.dropna().unique())
|
|
78
|
+
resp = sorted(vals)
|
|
79
|
+
frq = pd.DataFrame(
|
|
80
|
+
{nm: c.value_counts().reindex(resp, fill_value=0)
|
|
81
|
+
for nm, c in zip(names, cols)}, index=resp)
|
|
82
|
+
frq.index = [_resp_str(r) for r in resp]
|
|
83
|
+
numeric = all(isinstance(r, (int, float, np.integer, np.floating))
|
|
84
|
+
for r in resp)
|
|
85
|
+
wt = (np.array(resp, dtype=float) if numeric
|
|
86
|
+
else np.arange(1, len(resp) + 1, dtype=float))
|
|
87
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
88
|
+
wm = pd.Series((frq.to_numpy(dtype=float) * wt[:, None]).sum(0)
|
|
89
|
+
/ frq.to_numpy(dtype=float).sum(0),
|
|
90
|
+
index=names)
|
|
91
|
+
return frq, wm, numeric
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def items_stats_lines(frq, wm, title, numeric=True):
|
|
95
|
+
"""'Frequencies of Responses by Variable': one row per item,
|
|
96
|
+
its counts and its mean. R analog: .prntbl(t(x), 0) with the
|
|
97
|
+
Mean column of .bc.main() multi"""
|
|
98
|
+
t = frq.T
|
|
99
|
+
rows = [str(r) for r in t.index]
|
|
100
|
+
max_c1 = max(len(r) for r in rows) + 2
|
|
101
|
+
widths = [max(4, max(len(str(c)),
|
|
102
|
+
max(len(f"{v:.0f}") for v in t[c])) + 1)
|
|
103
|
+
for c in t.columns]
|
|
104
|
+
mx = max(len(f"{round(v, 2):.15g}") for v in wm if np.isfinite(v))
|
|
105
|
+
hdr = " " * max_c1 + "".join(str(c).rjust(w)
|
|
106
|
+
for c, w in zip(t.columns, widths))
|
|
107
|
+
lines = [title, "", hdr + " " + "Mean".rjust(mx + 1)]
|
|
108
|
+
for r in t.index:
|
|
109
|
+
ln = (" " + str(r)).rjust(max_c1) + "".join(
|
|
110
|
+
f"{v:.0f}".rjust(w) for v, w in zip(t.loc[r], widths))
|
|
111
|
+
lines.append(ln + " " + f"{wm[r]:.3f}".rjust(mx + 1))
|
|
112
|
+
if not numeric:
|
|
113
|
+
lines += ["", "Computation of the mean based on coding "
|
|
114
|
+
"response categories from 1 to "
|
|
115
|
+
f"{len(frq.index)}"]
|
|
116
|
+
return lines + [""]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def bc_items_plotly(frq, order, fill, labels="%", labels_size=None,
|
|
120
|
+
labels_cut=0.04, stack100=False, x_lab="",
|
|
121
|
+
y_lab="", legend_title="Responses", main=None,
|
|
122
|
+
digits_d=0, axis_fmt="K", axis_x_pre="",
|
|
123
|
+
rotate_x=0, rotate_y=0, style_opts=None):
|
|
124
|
+
"""One horizontal bar per item, stacked by response. frq:
|
|
125
|
+
responses x items counts; order: the items bottom to top."""
|
|
126
|
+
if style_opts is None:
|
|
127
|
+
style_opts = plotly_style()
|
|
128
|
+
cnt = frq[order].to_numpy(dtype=float)
|
|
129
|
+
tot = cnt.sum(axis=0)
|
|
130
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
131
|
+
share = np.where(tot > 0, cnt / tot, 0.0)
|
|
132
|
+
mat = share if stack100 else cnt
|
|
133
|
+
if labels_size is None:
|
|
134
|
+
labels_size = 0.8 - 0.008 * len(order)
|
|
135
|
+
txt_size = round(16 * labels_size)
|
|
136
|
+
fills = [as_plotly_color(f) for f in fill]
|
|
137
|
+
txt_cols = auto_text_color(fills, bg=style_opts["panel_fill"])
|
|
138
|
+
|
|
139
|
+
fig = go.Figure()
|
|
140
|
+
for i, r in enumerate(frq.index):
|
|
141
|
+
if labels == "off":
|
|
142
|
+
txt = None
|
|
143
|
+
else:
|
|
144
|
+
txt = [("" if s < labels_cut else
|
|
145
|
+
f"{100 * s:.0f}%" if labels == "%" else
|
|
146
|
+
f"{s:.2f}" if labels == "prop" else
|
|
147
|
+
f"{c:.0f}")
|
|
148
|
+
for s, c in zip(share[i], cnt[i])]
|
|
149
|
+
fig.add_trace(go.Bar(
|
|
150
|
+
x=mat[i], y=list(order), orientation="h", name=str(r),
|
|
151
|
+
marker=dict(color=fills[i % len(fills)],
|
|
152
|
+
line=dict(width=0)),
|
|
153
|
+
text=txt, textposition="inside",
|
|
154
|
+
insidetextanchor="middle", textangle=0,
|
|
155
|
+
textfont=dict(size=txt_size,
|
|
156
|
+
color=txt_cols[i % len(txt_cols)]),
|
|
157
|
+
customdata=np.stack([cnt[i], share[i]], axis=-1),
|
|
158
|
+
hovertemplate=(f"%{{y}}<br>{legend_title}: {r}"
|
|
159
|
+
"<br>Count: %{customdata[0]:.0f}"
|
|
160
|
+
"<br>% of item: %{customdata[1]:.1%}"
|
|
161
|
+
"<extra></extra>"),
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
top = 1.0 if stack100 else float(np.nanmax(tot)) if tot.size else 1
|
|
165
|
+
axT = pretty(0.0, top)
|
|
166
|
+
if stack100:
|
|
167
|
+
d = 2
|
|
168
|
+
else:
|
|
169
|
+
d = digits_d
|
|
170
|
+
x_ax = axis_num(x_lab, axT, axis_format(axT, d, axis_fmt,
|
|
171
|
+
axis_x_pre))
|
|
172
|
+
x_ax.update(range=[0, axT[-1] * (1 if stack100 else 1.02)])
|
|
173
|
+
y_ax = axis_cat(y_lab)
|
|
174
|
+
y_ax.update(categoryorder="array", categoryarray=list(order))
|
|
175
|
+
if rotate_x:
|
|
176
|
+
x_ax["tickangle"] = -rotate_x
|
|
177
|
+
if rotate_y:
|
|
178
|
+
y_ax["tickangle"] = -rotate_y
|
|
179
|
+
leg = legend_style(legend_title, style_opts)
|
|
180
|
+
leg["traceorder"] = "normal"
|
|
181
|
+
fig.update_layout(
|
|
182
|
+
barmode="stack", bargap=0.2,
|
|
183
|
+
xaxis=x_ax, yaxis=y_ax,
|
|
184
|
+
shapes=x_grid(axT) + plot_border(),
|
|
185
|
+
legend=leg, template=None,
|
|
186
|
+
plot_bgcolor=to_hex(style_opts["panel_fill"]),
|
|
187
|
+
paper_bgcolor=to_hex(style_opts["window_fill"]),
|
|
188
|
+
)
|
|
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", y=0.98,
|
|
193
|
+
yanchor="top", font=dict(size=title_size)),
|
|
194
|
+
margin=dict(t=round(title_size * 2.2)))
|
|
195
|
+
return fig
|