biplotpy 0.4.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.
- biplotpy/__init__.py +69 -0
- biplotpy/bootdist.py +174 -0
- biplotpy/canonical.py +198 -0
- biplotpy/cdpca.py +121 -0
- biplotpy/classical.py +159 -0
- biplotpy/correspondence.py +231 -0
- biplotpy/datasets/__init__.py +19 -0
- biplotpy/datasets/iris.csv +151 -0
- biplotpy/datasets/mtcars.csv +33 -0
- biplotpy/distances.py +383 -0
- biplotpy/distatis.py +69 -0
- biplotpy/dynamic.py +62 -0
- biplotpy/extras.py +158 -0
- biplotpy/factor.py +521 -0
- biplotpy/gxe.py +128 -0
- biplotpy/logfreq.py +107 -0
- biplotpy/logistic.py +343 -0
- biplotpy/multitest.py +281 -0
- biplotpy/plot.py +873 -0
- biplotpy/plsbin.py +224 -0
- biplotpy/restricted.py +300 -0
- biplotpy/roptim.py +104 -0
- biplotpy/rrng.py +119 -0
- biplotpy/statis.py +162 -0
- biplotpy/transforms.py +129 -0
- biplotpy/unfolding.py +74 -0
- biplotpy/utils.py +79 -0
- biplotpy-0.4.0.dist-info/METADATA +242 -0
- biplotpy-0.4.0.dist-info/RECORD +32 -0
- biplotpy-0.4.0.dist-info/WHEEL +5 -0
- biplotpy-0.4.0.dist-info/licenses/LICENSE +674 -0
- biplotpy-0.4.0.dist-info/top_level.txt +1 -0
biplotpy/multitest.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Multivariate tests that accompany canonical biplots and are not available
|
|
2
|
+
in Python: Mardia's multivariate normality test (``psych::mardia``),
|
|
3
|
+
heteroscedastic MANOVA with the Wald-type and modified ANOVA-type statistics
|
|
4
|
+
and parametric / wild bootstrap (``MANOVA.RM``; Konietschke et al., 2015;
|
|
5
|
+
Friedrich & Pauly, 2018), and the hypothesis and error matrices of HE plots
|
|
6
|
+
(``heplots``; Friendly, 2007)."""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from scipy import stats as sps
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# --------------------------------------------------------------------------- Mardia
|
|
15
|
+
def mardia(X, unbiased: bool = True) -> dict:
|
|
16
|
+
"""Mardia's multivariate skewness and kurtosis.
|
|
17
|
+
|
|
18
|
+
``unbiased=True`` uses the covariance with n - 1 (as ``psych::mardia``);
|
|
19
|
+
``False`` uses the ML covariance of Mardia (1970). Returns b1p, b2p, the
|
|
20
|
+
skewness statistic (chi-square with p(p+1)(p+2)/6 df), its small-sample
|
|
21
|
+
version, the standardized kurtosis (normal) and their p-values."""
|
|
22
|
+
X = np.asarray(X, float)
|
|
23
|
+
X = X[np.isfinite(X).all(1)]
|
|
24
|
+
n, p = X.shape
|
|
25
|
+
Xc = X - X.mean(0)
|
|
26
|
+
S = np.cov(Xc, rowvar=False, ddof=1 if unbiased else 0)
|
|
27
|
+
D = Xc @ np.linalg.solve(S, Xc.T)
|
|
28
|
+
b1p = np.sum(D ** 3) / n ** 2
|
|
29
|
+
b2p = np.trace(D ** 2) / n
|
|
30
|
+
df = p * (p + 1) * (p + 2) / 6
|
|
31
|
+
k = (p + 1) * (n + 1) * (n + 3) / (n * ((n + 1) * (p + 1) - 6))
|
|
32
|
+
skew, small = n * b1p / 6, n * k * b1p / 6
|
|
33
|
+
kurt = (b2p - p * (p + 2)) * np.sqrt(n / (8 * p * (p + 2)))
|
|
34
|
+
return {"n": n, "p": p, "b1p": b1p, "b2p": b2p, "skew": skew, "small_skew": small, "df": df,
|
|
35
|
+
"p_skew": sps.chi2.sf(skew, df), "p_small": sps.chi2.sf(small, df), "kurtosis": kurt,
|
|
36
|
+
"p_kurt": 2 * sps.norm.sf(abs(kurt)), "d": np.sqrt(np.diag(D))}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --------------------------------------------------------------------------- MANOVA.RM
|
|
40
|
+
def _wts_mats(means, V, n, H):
|
|
41
|
+
N = n.sum()
|
|
42
|
+
Sn = N * _bdiag(V)
|
|
43
|
+
T = H.T @ np.linalg.pinv(H @ Sn @ H.T) @ H
|
|
44
|
+
wts = N * means @ T @ means
|
|
45
|
+
D = np.diag(np.diag(Sn))
|
|
46
|
+
mats = N * means @ H.T @ np.linalg.pinv(H @ D @ H.T) @ H @ means
|
|
47
|
+
return wts, mats
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _bdiag(blocks):
|
|
51
|
+
p = sum(b.shape[0] for b in blocks)
|
|
52
|
+
M = np.zeros((p, p)); o = 0
|
|
53
|
+
for b in blocks:
|
|
54
|
+
k = b.shape[0]; M[o:o + k, o:o + k] = b; o += k
|
|
55
|
+
return M
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def manova_wts(Y, groups, H=None, iter: int = 10000, resampling: str = "paramBS", alpha: float = 0.05,
|
|
59
|
+
seed=None) -> dict:
|
|
60
|
+
"""One-way MANOVA without normality or equal covariances (``MANOVA.RM::MANOVA``).
|
|
61
|
+
|
|
62
|
+
``H`` is the hypothesis matrix acting on the stacked vector of group means
|
|
63
|
+
(default: equality of all group mean vectors, (I_a - J_a/a) kron I_p).
|
|
64
|
+
Returns the Wald-type statistic (WTS) with its asymptotic chi-square
|
|
65
|
+
p-value, the modified ANOVA-type statistic (MATS) and the bootstrap p-values
|
|
66
|
+
of both (``resampling`` = 'paramBS' or 'WildBS')."""
|
|
67
|
+
Y = np.asarray(Y, float)
|
|
68
|
+
g = np.asarray([str(v) for v in groups])
|
|
69
|
+
levels = list(groups.cat.categories) if hasattr(groups, "cat") else sorted(set(g))
|
|
70
|
+
levels = [str(v) for v in levels]
|
|
71
|
+
p = Y.shape[1]; a = len(levels)
|
|
72
|
+
Ys = [Y[g == lev] for lev in levels]
|
|
73
|
+
n = np.array([len(y) for y in Ys])
|
|
74
|
+
if H is None:
|
|
75
|
+
H = np.kron(np.eye(a) - np.ones((a, a)) / a, np.eye(p))
|
|
76
|
+
H = np.asarray(H, float)
|
|
77
|
+
means = np.concatenate([y.mean(0) for y in Ys])
|
|
78
|
+
V = [np.cov(y, rowvar=False) / len(y) for y in Ys]
|
|
79
|
+
wts, mats = _wts_mats(means, V, n, H)
|
|
80
|
+
df = np.linalg.matrix_rank(H)
|
|
81
|
+
rng = seed if isinstance(seed, np.random.Generator) else np.random.default_rng(seed)
|
|
82
|
+
bw, bm = np.empty(iter), np.empty(iter)
|
|
83
|
+
for b in range(iter):
|
|
84
|
+
if resampling == "paramBS":
|
|
85
|
+
Xb = [rng.multivariate_normal(np.zeros(p), len(y) * Vi, size=len(y), method="cholesky")
|
|
86
|
+
for y, Vi in zip(Ys, V)]
|
|
87
|
+
elif resampling == "WildBS":
|
|
88
|
+
Xb = [(2 * rng.integers(0, 2, len(y)) - 1)[:, None] * (y - y.mean(0)) for y in Ys]
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError("resampling must be 'paramBS' or 'WildBS'")
|
|
91
|
+
mb = np.concatenate([x.mean(0) for x in Xb])
|
|
92
|
+
bw[b], bm[b] = _wts_mats(mb, [np.cov(x, rowvar=False) / len(x) for x in Xb], n, H)
|
|
93
|
+
return {"levels": levels, "n": n, "means": means.reshape(a, p), "WTS": wts, "df": df,
|
|
94
|
+
"p_WTS_chisq": sps.chi2.sf(wts, df), "MATS": mats,
|
|
95
|
+
"p_WTS_boot": float(np.mean(bw > wts)), "p_MATS_boot": float(np.mean(bm > mats)),
|
|
96
|
+
"quantiles": {"WTS": float(np.quantile(bw, 1 - alpha)), "MATS": float(np.quantile(bm, 1 - alpha))},
|
|
97
|
+
"resampling": resampling, "iter": iter}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --------------------------------------------------------------------------- HE plots
|
|
101
|
+
def roy_crit(alpha, p, dfh, dfe):
|
|
102
|
+
"""Critical value of Roy's largest root (F upper bound), as ``heplots``."""
|
|
103
|
+
df1 = max(p, dfh); df2 = dfe - df1 + dfh
|
|
104
|
+
return df1 / df2 * sps.f.isf(alpha, df1, df2)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def hlt_crit(alpha, p, dfh, dfe):
|
|
108
|
+
s = min(p, dfh); m = (abs(p - dfh) - 1) / 2; nn = (dfe - p - 1) / 2
|
|
109
|
+
df1 = 2 * m + s + 1; df2 = 2 * (s * nn + 1)
|
|
110
|
+
return s * df1 / df2 * sps.f.isf(alpha, df1, df2)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def he_matrices(Y, groups) -> dict:
|
|
114
|
+
"""Hypothesis (H) and error (E) SSP matrices of a one-way MANOVA, their
|
|
115
|
+
degrees of freedom and the four multivariate statistics."""
|
|
116
|
+
Y = np.asarray(Y, float)
|
|
117
|
+
g = np.asarray([str(v) for v in groups])
|
|
118
|
+
levels = list(groups.cat.categories) if hasattr(groups, "cat") else sorted(set(g))
|
|
119
|
+
levels = [str(v) for v in levels]
|
|
120
|
+
gm = Y.mean(0)
|
|
121
|
+
H = np.zeros((Y.shape[1],) * 2); E = np.zeros_like(H)
|
|
122
|
+
means = []
|
|
123
|
+
for lev in levels:
|
|
124
|
+
y = Y[g == lev]; m = y.mean(0); means.append(m)
|
|
125
|
+
H += len(y) * np.outer(m - gm, m - gm)
|
|
126
|
+
E += (y - m).T @ (y - m)
|
|
127
|
+
dfh, dfe = len(levels) - 1, len(Y) - len(levels)
|
|
128
|
+
ev = np.sort(np.real(np.linalg.eigvals(np.linalg.solve(E, H))))[::-1]
|
|
129
|
+
stats = {"Pillai": np.sum(ev / (1 + ev)), "Wilks": np.prod(1 / (1 + ev)),
|
|
130
|
+
"Hotelling-Lawley": np.sum(ev), "Roy": ev[0]}
|
|
131
|
+
return {"H": H, "E": E, "dfh": dfh, "dfe": dfe, "eigenvalues": ev, "stats": stats,
|
|
132
|
+
"means": pd.DataFrame(means, index=levels), "grand_mean": gm}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def he_ellipses(Y, groups, variables=(0, 1), size="evidence", alpha=0.05, level=0.68, segments=60) -> dict:
|
|
136
|
+
"""Coordinates of the H and E ellipses of an HE plot (``heplots::heplot``).
|
|
137
|
+
|
|
138
|
+
E is scaled by 1/dfe; H by 1/dfe and, with ``size='evidence'``, also by the
|
|
139
|
+
critical value of Roy's test, so that H projects outside E in some
|
|
140
|
+
direction if and only if Roy's test is significant at ``alpha``."""
|
|
141
|
+
he = he_matrices(Y, groups)
|
|
142
|
+
v = list(variables)
|
|
143
|
+
dfe = he["dfe"]; p = np.asarray(Y).shape[1]
|
|
144
|
+
radius = np.sqrt(2 * sps.f.ppf(level, 2, dfe))
|
|
145
|
+
factor = roy_crit(alpha, p, he["dfh"], dfe) if size in ("evidence", "significance") else 1.0
|
|
146
|
+
th = np.linspace(0, 2 * np.pi, segments + 1)
|
|
147
|
+
circle = radius * np.column_stack([np.cos(th), np.sin(th)])
|
|
148
|
+
c = he["grand_mean"][v]
|
|
149
|
+
|
|
150
|
+
def ell(S):
|
|
151
|
+
ev, U = np.linalg.eigh(S)
|
|
152
|
+
return c + circle @ (U @ np.diag(np.sqrt(np.maximum(ev, 0)))).T
|
|
153
|
+
H = he["H"][np.ix_(v, v)] / dfe / factor
|
|
154
|
+
E = he["E"][np.ix_(v, v)] / dfe
|
|
155
|
+
return {"H": ell(H), "E": ell(E), "H_shape": H, "E_shape": E, "means": he["means"].iloc[:, v],
|
|
156
|
+
"radius": radius, "factor": factor, "roy_significant": he["eigenvalues"][0] > roy_crit(alpha, p, he["dfh"], dfe)}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def he_plot(Y, groups, variables=(0, 1), size="evidence", alpha=0.05, ax=None, labels=None):
|
|
160
|
+
"""Draw an HE plot with matplotlib."""
|
|
161
|
+
import matplotlib.pyplot as plt
|
|
162
|
+
r = he_ellipses(Y, groups, variables, size, alpha)
|
|
163
|
+
ax = ax or plt.gca()
|
|
164
|
+
ax.plot(*r["E"].T, color="tab:red", label="Error")
|
|
165
|
+
ax.plot(*r["H"].T, color="tab:blue", label="Groups")
|
|
166
|
+
ax.scatter(*r["means"].to_numpy().T, color="tab:blue", s=12)
|
|
167
|
+
for name, (x, y) in r["means"].iterrows():
|
|
168
|
+
ax.annotate(name, (x, y), fontsize=8)
|
|
169
|
+
if labels:
|
|
170
|
+
ax.set_xlabel(labels[0]); ax.set_ylabel(labels[1])
|
|
171
|
+
ax.legend(frameon=False)
|
|
172
|
+
return ax
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --------------------------------------------------------------------------- multi-factor MANOVA and HE plots
|
|
176
|
+
def _sum_contrast_cols(f, levels):
|
|
177
|
+
"""Columns of ``contr.sum`` coding of a factor (k - 1 columns)."""
|
|
178
|
+
k = len(levels)
|
|
179
|
+
C = np.zeros((len(f), k - 1))
|
|
180
|
+
for a, lev in enumerate(levels[:-1]):
|
|
181
|
+
C[:, a] = (f == lev).astype(float)
|
|
182
|
+
C[f == levels[-1]] = -1.0
|
|
183
|
+
return C
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _design(factors: pd.DataFrame, terms):
|
|
187
|
+
"""Model matrix (intercept + sum-coded terms) and the columns of each term."""
|
|
188
|
+
n = len(factors)
|
|
189
|
+
cols = {}
|
|
190
|
+
for name in factors.columns:
|
|
191
|
+
f = factors[name].astype(str).to_numpy()
|
|
192
|
+
levels = list(factors[name].cat.categories) if hasattr(factors[name], "cat") else sorted(set(f))
|
|
193
|
+
cols[name] = _sum_contrast_cols(f, [str(v) for v in levels])
|
|
194
|
+
blocks, Xs = {}, [np.ones((n, 1))]
|
|
195
|
+
start = 1
|
|
196
|
+
for t in terms:
|
|
197
|
+
parts = t.split(":")
|
|
198
|
+
M = np.ones((n, 1))
|
|
199
|
+
for p_ in parts:
|
|
200
|
+
M = (M[:, :, None] * cols[p_][:, None, :]).reshape(n, -1)
|
|
201
|
+
Xs.append(M); blocks[t] = list(range(start, start + M.shape[1])); start += M.shape[1]
|
|
202
|
+
return np.column_stack(Xs), blocks
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _sse(Y, X):
|
|
206
|
+
b, *_ = np.linalg.lstsq(X, Y, rcond=None)
|
|
207
|
+
R = Y - X @ b
|
|
208
|
+
return R.T @ R
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def manova_terms(Y, factors, terms=None, type="II") -> dict:
|
|
212
|
+
"""Multivariate linear model with several factors (and their interactions):
|
|
213
|
+
hypothesis SSP matrix of each term, error SSP matrix and the four
|
|
214
|
+
multivariate statistics, as ``car::Anova(lm(cbind(...) ~ ...), type =)``.
|
|
215
|
+
|
|
216
|
+
``factors``: DataFrame with one column per factor. ``terms``: list such as
|
|
217
|
+
``["A", "B", "A:B"]`` (default: all main effects and interactions).
|
|
218
|
+
``type='II'``: each term adjusted for the terms that do not contain it;
|
|
219
|
+
``type='III'``: each term adjusted for all the others (sum contrasts)."""
|
|
220
|
+
Y = np.asarray(Y, float)
|
|
221
|
+
factors = pd.DataFrame(factors).copy()
|
|
222
|
+
names = list(factors.columns)
|
|
223
|
+
if terms is None:
|
|
224
|
+
from itertools import combinations
|
|
225
|
+
terms = [":".join(c) for k in range(1, len(names) + 1) for c in combinations(names, k)]
|
|
226
|
+
X, blocks = _design(factors, terms)
|
|
227
|
+
n = len(Y)
|
|
228
|
+
E = _sse(Y, X)
|
|
229
|
+
dfe = n - np.linalg.matrix_rank(X)
|
|
230
|
+
out = {}
|
|
231
|
+
for t in terms:
|
|
232
|
+
tset = set(t.split(":"))
|
|
233
|
+
if type == "III":
|
|
234
|
+
keep = [c for c in range(X.shape[1]) if c not in blocks[t]]
|
|
235
|
+
H = _sse(Y, X[:, keep]) - E
|
|
236
|
+
else:
|
|
237
|
+
contain = [u for u in terms if u != t and tset <= set(u.split(":"))]
|
|
238
|
+
base = [0] + [c for u in terms if u != t and u not in contain for c in blocks[u]]
|
|
239
|
+
H = _sse(Y, X[:, base]) - _sse(Y, X[:, sorted(base + blocks[t])])
|
|
240
|
+
H = (H + H.T) / 2
|
|
241
|
+
ev = np.sort(np.real(np.linalg.eigvals(np.linalg.solve(E, H))))[::-1]
|
|
242
|
+
ev = np.where(np.abs(ev) < 1e-12, 0.0, ev)
|
|
243
|
+
out[t] = {"H": H, "df": len(blocks[t]), "eigenvalues": ev,
|
|
244
|
+
"stats": {"Pillai": np.sum(ev / (1 + ev)), "Wilks": np.prod(1 / (1 + ev)),
|
|
245
|
+
"Hotelling-Lawley": np.sum(ev), "Roy": ev[0]}}
|
|
246
|
+
return {"terms": out, "E": E, "dfe": dfe, "type": type}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def he_plot_terms(Y, factors, terms=None, type="II", variables=(0, 1), alpha=0.05, ax=None, labels=None,
|
|
250
|
+
level=0.68, segments=60):
|
|
251
|
+
"""HE plot of a multi-factor MANOVA: the error ellipse and one hypothesis
|
|
252
|
+
ellipse per term, each scaled by the critical value of Roy's test for its
|
|
253
|
+
degrees of freedom (``size = 'evidence'`` of ``heplots::heplot``): a term
|
|
254
|
+
is significant by Roy's test at ``alpha`` exactly when its ellipse goes
|
|
255
|
+
outside the error ellipse in some direction."""
|
|
256
|
+
import matplotlib.pyplot as plt
|
|
257
|
+
m = manova_terms(Y, factors, terms, type)
|
|
258
|
+
Y = np.asarray(Y, float)
|
|
259
|
+
v = list(variables)
|
|
260
|
+
dfe = m["dfe"]; p = Y.shape[1]
|
|
261
|
+
radius = np.sqrt(2 * sps.f.ppf(level, 2, dfe))
|
|
262
|
+
th = np.linspace(0, 2 * np.pi, segments + 1)
|
|
263
|
+
circle = radius * np.column_stack([np.cos(th), np.sin(th)])
|
|
264
|
+
c = Y.mean(0)[v]
|
|
265
|
+
|
|
266
|
+
def ell(S):
|
|
267
|
+
ev, U = np.linalg.eigh(S)
|
|
268
|
+
return c + circle @ (U @ np.diag(np.sqrt(np.maximum(ev, 0)))).T
|
|
269
|
+
ax = ax or plt.gca()
|
|
270
|
+
ax.plot(*ell(m["E"][np.ix_(v, v)] / dfe).T, color="tab:red", label="Error")
|
|
271
|
+
cols = ["tab:blue", "tab:green", "tab:purple", "tab:orange", "tab:brown", "tab:olive", "tab:cyan"]
|
|
272
|
+
for k, (t, r) in enumerate(m["terms"].items()):
|
|
273
|
+
crit = roy_crit(alpha, p, r["df"], dfe)
|
|
274
|
+
sig = r["eigenvalues"][0] > crit
|
|
275
|
+
ax.plot(*ell(r["H"][np.ix_(v, v)] / dfe / crit).T, color=cols[k % len(cols)],
|
|
276
|
+
lw=2 if sig else 1, ls="-" if sig else "--", label=t + (" *" if sig else ""))
|
|
277
|
+
if labels:
|
|
278
|
+
ax.set_xlabel(labels[0]); ax.set_ylabel(labels[1])
|
|
279
|
+
ax.legend(fontsize=8, frameon=False)
|
|
280
|
+
ax.set_title(f"HE plot (type {type})")
|
|
281
|
+
return m
|