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/distances.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Distance-based biplots: proximities, principal coordinates (PCoA),
|
|
2
|
+
metric/non-metric MDS by SMACOF, external logistic biplots and canonical
|
|
3
|
+
analysis of distances.
|
|
4
|
+
|
|
5
|
+
* ``binary_distances`` / ``continuous_distances`` / ``gower_distances`` —
|
|
6
|
+
the coefficients of ``MultBiplotR::BinaryDistances`` and
|
|
7
|
+
``ContinuousDistances`` (with three defects of the R code corrected, see
|
|
8
|
+
``FIXES``).
|
|
9
|
+
* ``pcoa`` — ``MultBiplotR::PrincipalCoordinates`` (= ``ade4::dudi.pco``).
|
|
10
|
+
* ``mds`` — symmetric SMACOF of ``smacof::mds`` (ratio, interval, ordinal
|
|
11
|
+
with primary/secondary/tertiary ties).
|
|
12
|
+
* ``ridge_logistic`` / ``external_logistic_biplot`` —
|
|
13
|
+
``MultBiplotR::RidgeBinaryLogistic`` and ``ExternalBinaryLogisticBiplot``:
|
|
14
|
+
binary variables projected onto a PCoA/MDS configuration by penalized
|
|
15
|
+
logistic regression.
|
|
16
|
+
* ``canonical_distance_analysis`` — ``MultBiplotR::CanonicalDistanceAnalysis``.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pandas as pd
|
|
22
|
+
from scipy import stats as sps
|
|
23
|
+
|
|
24
|
+
from .correspondence import OrdinationBiplot
|
|
25
|
+
|
|
26
|
+
BINARY_COEFFICIENTS = ["Kulczynski", "Russell_and_Rao", "Jaccard", "Simple_Matching", "Anderberg",
|
|
27
|
+
"Rogers_and_Tanimoto", "Sorensen_Dice_and_Czekanowski", "Sneath_and_Sokal", "Hamman",
|
|
28
|
+
"Kulczynski2", "Anderberg2", "Ochiai", "S13", "Pearson_phi", "Yule", "Sorensen", "Dice"]
|
|
29
|
+
TRANSFORMATIONS = ["Identity", "1-S", "sqrt(1-S)", "-log(s)", "1/S-1", "sqrt(2(1-S))", "1-(S+1)/2", "1-abs(S)",
|
|
30
|
+
"1/(S+1)"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def binary_similarity(X, Y=None, coefficient="Jaccard"):
|
|
34
|
+
X = np.asarray(X, float)
|
|
35
|
+
Y = X if Y is None else np.asarray(Y, float)
|
|
36
|
+
a = Y @ X.T; b = Y @ (1 - X).T; c = (1 - Y) @ X.T; d = (1 - Y) @ (1 - X).T
|
|
37
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
38
|
+
S = {"Kulczynski": a / (b + c), "Russell_and_Rao": a / (a + b + c + d), "Jaccard": a / (a + b + c),
|
|
39
|
+
"Simple_Matching": (a + d) / (a + b + c + d), "Anderberg": a / (a + 2 * (b + c)),
|
|
40
|
+
"Rogers_and_Tanimoto": (a + d) / (a + 2 * (b + c) + d),
|
|
41
|
+
"Sorensen_Dice_and_Czekanowski": a / (a + 0.5 * (b + c)),
|
|
42
|
+
"Sneath_and_Sokal": (a + d) / (a + 0.5 * (b + c) + d), "Hamman": (a - (b + c) + d) / (a + b + c + d),
|
|
43
|
+
"Kulczynski2": 0.5 * (a / (a + b) + a / (a + c)),
|
|
44
|
+
"Anderberg2": 0.25 * (a / (a + b) + a / (a + c) + d / (c + d) + d / (b + d)),
|
|
45
|
+
"Ochiai": a / np.sqrt((a + b) * (a + c)), "S13": a * d / np.sqrt((a + b) * (a + c) * (d + b) * (d + c)),
|
|
46
|
+
"Pearson_phi": (a * d - b * c) / np.sqrt((a + b) * (a + c) * (d + b) * (d + c)),
|
|
47
|
+
"Yule": (a * d - b * c) / (a * d + b * c), "Sorensen": 2 * a / (2 * a + b + c),
|
|
48
|
+
"Dice": 2 * a / (2 * a + b + c)}[coefficient]
|
|
49
|
+
return S
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _transform_similarity(S, transformation):
|
|
53
|
+
f = {"Identity": lambda: S, "1-S": lambda: 1 - S, "sqrt(1-S)": lambda: np.sqrt(np.maximum(1 - S, 0)),
|
|
54
|
+
"-log(s)": lambda: -np.log(S), "1/S-1": lambda: 1 / S - 1,
|
|
55
|
+
"sqrt(2(1-S))": lambda: np.sqrt(np.maximum(2 * (1 - S), 0)), "1-(S+1)/2": lambda: 1 - (S + 1) / 2,
|
|
56
|
+
"1-abs(S)": lambda: 1 - np.abs(S), "1/(S+1)": lambda: 1 / (S + 1)}
|
|
57
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
58
|
+
return f[transformation]()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def binary_distances(X, Y=None, coefficient="Jaccard", transformation=None):
|
|
62
|
+
"""Dissimilarities between the rows of a 0/1 matrix (``Y`` rows against
|
|
63
|
+
``X`` rows when given). Default transformation as MultBiplotR:
|
|
64
|
+
sqrt(1-S) for most coefficients, 1-(S+1)/2 for Hamman, phi and Yule,
|
|
65
|
+
1/(S+1) for Kulczynski."""
|
|
66
|
+
if isinstance(coefficient, int):
|
|
67
|
+
coefficient = BINARY_COEFFICIENTS[coefficient - 1]
|
|
68
|
+
if transformation is None:
|
|
69
|
+
transformation = {"Kulczynski": "1/(S+1)", "Hamman": "1-(S+1)/2", "Pearson_phi": "1-(S+1)/2",
|
|
70
|
+
"Yule": "1-(S+1)/2"}.get(coefficient, "sqrt(1-S)")
|
|
71
|
+
return _transform_similarity(binary_similarity(X, Y, coefficient), transformation)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def continuous_distances(X, Y=None, coef="Pythagorean", r=1.0):
|
|
75
|
+
"""Dissimilarities between rows (``MultBiplotR::ContinuousDistances``)."""
|
|
76
|
+
X = np.asarray(X, float)
|
|
77
|
+
Y = X if Y is None else np.asarray(Y, float)
|
|
78
|
+
Dif = Y[:, None, :] - X[None, :, :]
|
|
79
|
+
Sm = Y[:, None, :] + X[None, :, :]
|
|
80
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
81
|
+
if coef == "Pythagorean":
|
|
82
|
+
return np.sqrt((Dif ** 2).sum(2))
|
|
83
|
+
if coef == "Taxonomic":
|
|
84
|
+
return np.sqrt((Dif ** 2 / r ** 2).sum(2))
|
|
85
|
+
if coef == "City":
|
|
86
|
+
return np.abs(Dif).sum(2)
|
|
87
|
+
if coef == "Minkowski":
|
|
88
|
+
return (np.abs(Dif) ** r).sum(2) ** (1 / r)
|
|
89
|
+
if coef == "Divergence":
|
|
90
|
+
return np.sqrt((Dif ** 2 / Sm ** 2).sum(2))
|
|
91
|
+
if coef == "dif_sum":
|
|
92
|
+
return (np.abs(Dif) / np.abs(Sm)).sum(2)
|
|
93
|
+
if coef == "Canberra":
|
|
94
|
+
return (np.abs(Dif) / (np.abs(Y)[:, None, :] + np.abs(X)[None, :, :])).sum(2)
|
|
95
|
+
if coef == "Bray_Curtis":
|
|
96
|
+
return np.abs(Dif).sum(2) / Sm.sum(2)
|
|
97
|
+
if coef == "Soergel":
|
|
98
|
+
return np.abs(Dif).sum(2) / np.maximum(Y[:, None, :], X[None, :, :]).sum(2)
|
|
99
|
+
if coef == "Ware_Hedges":
|
|
100
|
+
return (1 - np.minimum(Y[:, None, :], X[None, :, :]) / np.maximum(Y[:, None, :], X[None, :, :])).sum(2)
|
|
101
|
+
if coef == "Gower":
|
|
102
|
+
rng = np.vstack([X, Y]).max(0) - np.vstack([X, Y]).min(0)
|
|
103
|
+
return (np.abs(Dif) / rng).sum(2)
|
|
104
|
+
raise ValueError(f"unknown coefficient {coef}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def gower_distances(df: pd.DataFrame):
|
|
108
|
+
"""Gower dissimilarity (1 - similarity) for mixed numeric / categorical data."""
|
|
109
|
+
n = len(df)
|
|
110
|
+
S = np.zeros((n, n)); W = np.zeros((n, n))
|
|
111
|
+
for col in df.columns:
|
|
112
|
+
v = df[col]
|
|
113
|
+
ok = v.notna().to_numpy()
|
|
114
|
+
w = np.outer(ok, ok).astype(float)
|
|
115
|
+
if pd.api.types.is_numeric_dtype(v) and v.nunique() > 2:
|
|
116
|
+
x = v.to_numpy(float); rng = np.nanmax(x) - np.nanmin(x)
|
|
117
|
+
s = 1 - np.abs(x[:, None] - x[None, :]) / rng
|
|
118
|
+
else:
|
|
119
|
+
x = v.astype(str).to_numpy()
|
|
120
|
+
s = (x[:, None] == x[None, :]).astype(float)
|
|
121
|
+
S += np.nan_to_num(s) * w; W += w
|
|
122
|
+
return 1 - S / W
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --------------------------------------------------------------------------- PCoA
|
|
126
|
+
def pcoa(D, dim=2, weights=None, row_names=None, sup_distances=None) -> OrdinationBiplot:
|
|
127
|
+
"""Principal coordinates of a dissimilarity matrix (Gower, 1966).
|
|
128
|
+
|
|
129
|
+
Weighted version with the correct weighted centring (I - 1 w') on both
|
|
130
|
+
sides. Fit statistics: stress-1 and -2, s-stress, R2 and Spearman's rho
|
|
131
|
+
between the dissimilarities and the configuration distances."""
|
|
132
|
+
D = np.asarray(D, float)
|
|
133
|
+
n = D.shape[0]
|
|
134
|
+
w = np.full(n, 1 / n) if weights is None else np.asarray(weights, float) / np.sum(weights)
|
|
135
|
+
J = np.eye(n) - np.outer(np.ones(n), w)
|
|
136
|
+
b = -0.5 * J @ D ** 2 @ J.T
|
|
137
|
+
Bw = np.sqrt(w)[:, None] * b * np.sqrt(w)[None, :]
|
|
138
|
+
ev, U = np.linalg.eigh(Bw)
|
|
139
|
+
ev, U = ev[::-1], U[:, ::-1]
|
|
140
|
+
pos = ev > 1e-10 * ev[0]
|
|
141
|
+
g = U[:, pos] * np.sqrt(ev[pos]) / np.sqrt(w)[:, None]
|
|
142
|
+
qual = g ** 2 / (g ** 2).sum(1, keepdims=True) * 100
|
|
143
|
+
G = g[:, :dim]
|
|
144
|
+
iu = np.triu_indices(n, 1)
|
|
145
|
+
Dh = D[iu]
|
|
146
|
+
Dc = np.sqrt(((G[:, None] - G[None]) ** 2).sum(2))[iu]
|
|
147
|
+
res = OrdinationBiplot(title="Principal Coordinates Analysis", type="PCoA", dim=dim, eigenvalues=ev,
|
|
148
|
+
inertia=ev / ev.sum() * 100, row_coordinates=G, row_qualities=qual[:, :dim],
|
|
149
|
+
negative_eigenvalues=ev[ev < -1e-10 * ev[0]], centered_products=b,
|
|
150
|
+
stress1=np.sqrt(np.sum((Dc - Dh) ** 2) / np.sum(Dc ** 2)),
|
|
151
|
+
stress2=np.sqrt(np.sum((Dc - Dh) ** 2) / np.sum((Dc - Dc.mean()) ** 2)),
|
|
152
|
+
sstress1=np.sqrt(np.sum((Dc ** 2 - Dh ** 2) ** 2) / np.sum(Dc ** 4)),
|
|
153
|
+
rsq=np.corrcoef(Dh, Dc)[0, 1] ** 2, rho=sps.spearmanr(Dh, Dc)[0],
|
|
154
|
+
row_names=row_names or [f"I{i + 1}" for i in range(n)], col_coordinates=None)
|
|
155
|
+
if sup_distances is not None:
|
|
156
|
+
Ds = np.asarray(sup_distances, float)
|
|
157
|
+
sc = 0.5 * (np.diag(b)[None, :] - Ds ** 2) @ g / ev[pos]
|
|
158
|
+
res.sup_row_coordinates = sc[:, :dim]
|
|
159
|
+
return res
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# --------------------------------------------------------------------------- SMACOF (symmetric)
|
|
163
|
+
def _pava(y, w):
|
|
164
|
+
"""Weighted isotonic (non-decreasing) regression by pool-adjacent-violators."""
|
|
165
|
+
vals, wts, cnt = [], [], []
|
|
166
|
+
for yi, wi in zip(y, w):
|
|
167
|
+
vals.append(yi); wts.append(wi); cnt.append(1)
|
|
168
|
+
while len(vals) > 1 and vals[-2] > vals[-1]:
|
|
169
|
+
ws = wts[-2] + wts[-1]
|
|
170
|
+
v = (vals[-2] * wts[-2] + vals[-1] * wts[-1]) / ws if ws > 0 else (vals[-2] + vals[-1]) / 2
|
|
171
|
+
c = cnt[-2] + cnt[-1]
|
|
172
|
+
vals[-2:] = [v]; wts[-2:] = [ws]; cnt[-2:] = [c]
|
|
173
|
+
return np.repeat(vals, cnt)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _torgerson(D, p):
|
|
177
|
+
n = D.shape[0]
|
|
178
|
+
D2 = D ** 2
|
|
179
|
+
B = -0.5 * (D2 - D2.mean(1)[:, None] - D2.mean(0)[None, :] + D2.mean())
|
|
180
|
+
ev, U = np.linalg.eigh(B)
|
|
181
|
+
ev, U = ev[::-1], U[:, ::-1]
|
|
182
|
+
return U[:, :p] * np.sqrt(np.maximum(ev[:p], 0))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def mds(D, dim=2, type="ratio", ties="primary", weights=None, init=None, itmax=1000, eps=1e-6,
|
|
186
|
+
row_names=None) -> OrdinationBiplot:
|
|
187
|
+
"""Symmetric SMACOF (``smacof::mds``): metric (ratio, interval) or
|
|
188
|
+
non-metric (ordinal) multidimensional scaling with Torgerson start.
|
|
189
|
+
Returns the configuration, disparities, stress-1 and stress per point."""
|
|
190
|
+
D = np.asarray(D, float)
|
|
191
|
+
n = D.shape[0]
|
|
192
|
+
iu = np.triu_indices(n, 1)
|
|
193
|
+
diss = D.T[np.tril_indices(n, -1)] # column-major lower triangle, as R's dist
|
|
194
|
+
li, lj = np.tril_indices(n, -1)
|
|
195
|
+
order_idx = np.lexsort((li, lj)) # R stores dist by columns
|
|
196
|
+
li, lj, diss = li[order_idx], lj[order_idx], D[li[order_idx], lj[order_idx]]
|
|
197
|
+
m = len(diss)
|
|
198
|
+
W = np.ones((n, n)) if weights is None else np.asarray(weights, float)
|
|
199
|
+
wg = W[li, lj]
|
|
200
|
+
nn = n * (n - 1) / 2
|
|
201
|
+
|
|
202
|
+
def dist(x):
|
|
203
|
+
return np.sqrt(((x[li] - x[lj]) ** 2).sum(1))
|
|
204
|
+
|
|
205
|
+
def normq(v):
|
|
206
|
+
return v * np.sqrt(nn / np.sum(wg * v ** 2))
|
|
207
|
+
|
|
208
|
+
x = _torgerson(D, dim) if init is None else np.asarray(init, float)
|
|
209
|
+
dhat = normq(diss)
|
|
210
|
+
V = -np.zeros((n, n)); V[li, lj] = -wg; V[lj, li] = -wg
|
|
211
|
+
V[np.diag_indices(n)] = -V.sum(1)
|
|
212
|
+
Vinv = np.linalg.inv(V + 1 / n) - 1 / n
|
|
213
|
+
d = dist(x)
|
|
214
|
+
lb = np.sum(wg * d * dhat) / np.sum(wg * d ** 2)
|
|
215
|
+
x, d = lb * x, lb * d
|
|
216
|
+
sold = np.sum(wg * (dhat - d) ** 2) / nn
|
|
217
|
+
# tie structure of the dissimilarities
|
|
218
|
+
iord = np.lexsort((np.arange(m), diss))
|
|
219
|
+
xs = diss[iord]
|
|
220
|
+
starts = np.r_[0, np.where(np.diff(xs) != 0)[0] + 1]
|
|
221
|
+
blocks = np.split(iord, starts[1:])
|
|
222
|
+
xu = xs[starts]
|
|
223
|
+
itel = 1
|
|
224
|
+
while True:
|
|
225
|
+
z = d < 1e-12
|
|
226
|
+
bv = wg * dhat * (~z) / (d + z)
|
|
227
|
+
Bm = np.zeros((n, n)); Bm[li, lj] = -bv; Bm[lj, li] = -bv
|
|
228
|
+
Bm[np.diag_indices(n)] = -Bm.sum(1)
|
|
229
|
+
y = Vinv @ (Bm @ x)
|
|
230
|
+
e = dist(y)
|
|
231
|
+
if type == "ratio":
|
|
232
|
+
new = diss.copy()
|
|
233
|
+
elif type == "interval":
|
|
234
|
+
yb = np.array([np.sum(wg[b] * e[b]) / np.sum(wg[b]) for b in blocks])
|
|
235
|
+
wb = np.array([np.sum(wg[b]) for b in blocks])
|
|
236
|
+
Xb = np.column_stack([np.ones(len(xu)), xu - xu[0]])
|
|
237
|
+
coef = np.linalg.solve(Xb.T @ (wb[:, None] * Xb), Xb.T @ (wb * yb))
|
|
238
|
+
fitb = Xb @ coef
|
|
239
|
+
new = np.empty(m)
|
|
240
|
+
for b, v in zip(blocks, fitb):
|
|
241
|
+
new[b] = v
|
|
242
|
+
elif type == "ordinal" and ties == "primary":
|
|
243
|
+
o = np.lexsort((e, diss))
|
|
244
|
+
new = np.empty(m); new[o] = _pava(e[o], wg[o])
|
|
245
|
+
elif type == "ordinal":
|
|
246
|
+
yb = np.array([np.sum(wg[b] * e[b]) / np.sum(wg[b]) for b in blocks])
|
|
247
|
+
wb = np.array([np.sum(wg[b]) for b in blocks])
|
|
248
|
+
ycon = _pava(yb, wb)
|
|
249
|
+
new = np.empty(m)
|
|
250
|
+
for b, v, ym in zip(blocks, ycon, yb):
|
|
251
|
+
new[b] = v if ties == "secondary" else e[b] + (v - ym)
|
|
252
|
+
else:
|
|
253
|
+
raise ValueError("type must be ratio, interval or ordinal")
|
|
254
|
+
dhat = normq(new)
|
|
255
|
+
snon = np.sum(wg * (dhat - e) ** 2) / nn
|
|
256
|
+
if (sold - snon) < eps or itel == itmax:
|
|
257
|
+
break
|
|
258
|
+
x, d, sold = y, e, snon
|
|
259
|
+
itel += 1
|
|
260
|
+
Dh = np.zeros((n, n)); Dh[li, lj] = dhat; Dh = Dh + Dh.T
|
|
261
|
+
Dc = np.sqrt(((y[:, None] - y[None]) ** 2).sum(2))
|
|
262
|
+
res_mat = W * (Dh - Dc) ** 2
|
|
263
|
+
np.fill_diagonal(res_mat, np.nan)
|
|
264
|
+
spp = np.nanmean(res_mat, 0); spp = spp / spp.sum() * 100
|
|
265
|
+
return OrdinationBiplot(title=f"MDS ({type})", type="MDS", dim=dim, row_coordinates=y, stress=np.sqrt(snon),
|
|
266
|
+
disparities=Dh, config_distances=Dc, spp=spp, niter=itel,
|
|
267
|
+
eigenvalues=np.var(y, axis=0) * n, inertia=np.var(y, axis=0) / np.var(y, axis=0).sum() * 100,
|
|
268
|
+
row_names=row_names or [f"I{i + 1}" for i in range(n)], col_coordinates=None)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# --------------------------------------------------------------------------- logistic regression on a configuration
|
|
272
|
+
def ridge_logistic(y, X, penalization=0.2, intercept=True, tol=1e-5, maxiter=100):
|
|
273
|
+
"""Ridge-penalized binary logistic regression by Newton-Raphson
|
|
274
|
+
(``MultBiplotR::RidgeBinaryLogistic``): maximizes loglik - lambda * sum(beta_j^2),
|
|
275
|
+
intercept not penalized. Returns coefficients and fit statistics."""
|
|
276
|
+
y = np.asarray(y, float).ravel()
|
|
277
|
+
X = np.asarray(X, float)
|
|
278
|
+
if X.ndim == 1:
|
|
279
|
+
X = X[:, None]
|
|
280
|
+
n = len(y)
|
|
281
|
+
Xd = np.column_stack([np.ones(n), X]) if intercept else X
|
|
282
|
+
|
|
283
|
+
def fit(Xm):
|
|
284
|
+
m = Xm.shape[1]
|
|
285
|
+
beta = np.zeros(m)
|
|
286
|
+
Im = np.eye(m); Im[0, 0] = 0
|
|
287
|
+
for _ in range(maxiter):
|
|
288
|
+
mu = 1 / (1 + np.exp(-(Xm @ beta)))
|
|
289
|
+
In = Xm.T @ ((mu * (1 - mu))[:, None] * Xm) + 2 * penalization * Im
|
|
290
|
+
U = Xm.T @ (y - mu) - 2 * penalization * Im @ beta
|
|
291
|
+
new = beta + np.linalg.pinv(In) @ U
|
|
292
|
+
err = np.sum(np.abs(new - beta)); beta = new
|
|
293
|
+
if err <= tol:
|
|
294
|
+
break
|
|
295
|
+
return beta
|
|
296
|
+
b0 = fit(np.ones((n, 1)))
|
|
297
|
+
beta = fit(Xd)
|
|
298
|
+
p0 = 1 / (1 + np.exp(-b0[0])); p = 1 / (1 + np.exp(-(Xd @ beta)))
|
|
299
|
+
dev0 = -2 * np.sum(y * np.log(p0) + (1 - y) * np.log(1 - p0))
|
|
300
|
+
dev = -2 * np.sum(y * np.log(p) + (1 - y) * np.log(1 - p))
|
|
301
|
+
dif = dev0 - dev; df = Xd.shape[1] - 1
|
|
302
|
+
cs = 1 - np.exp(-dif / n)
|
|
303
|
+
pred = (p >= 0.5).astype(float)
|
|
304
|
+
return {"beta": beta, "fitted": p, "deviance": dev, "null_deviance": dev0, "dif": dif, "df": df,
|
|
305
|
+
"p": sps.chi2.sf(dif, df), "cox_snell": cs, "nagelkerke": cs / (1 - np.exp(-dev0 / 2) ** (2 / n)),
|
|
306
|
+
"mcfadden": 1 - dev / dev0, "R2": 1 - np.sum((y - p) ** 2) / np.sum(y ** 2),
|
|
307
|
+
"percent_correct": np.mean(pred == y)}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def external_logistic_biplot(coords, Xbin, penalization=0.2, col_names=None, row_names=None) -> OrdinationBiplot:
|
|
311
|
+
"""External binary logistic biplot: each binary variable is fitted by a
|
|
312
|
+
ridge logistic regression on a given configuration (PCoA, MDS...), giving
|
|
313
|
+
its direction in the plane (``MultBiplotR::ExternalBinaryLogisticBiplot``)."""
|
|
314
|
+
A = np.asarray(coords, float)
|
|
315
|
+
Xb = np.asarray(Xbin, float)
|
|
316
|
+
p = Xb.shape[1]
|
|
317
|
+
fits = [ridge_logistic(Xb[:, j], A, penalization) for j in range(p)]
|
|
318
|
+
B = np.array([f["beta"] for f in fits])
|
|
319
|
+
dd = np.sqrt(1 + (B[:, 1:] ** 2).sum(1))
|
|
320
|
+
info = pd.DataFrame({"Deviance": [f["dif"] for f in fits], "df": [f["df"] for f in fits],
|
|
321
|
+
"p-value": [f["p"] for f in fits],
|
|
322
|
+
"Bonferroni": np.minimum([f["p"] * p for f in fits], 1),
|
|
323
|
+
"Nagelkerke": [f["nagelkerke"] for f in fits], "R2": [f["R2"] for f in fits],
|
|
324
|
+
"% correct": [f["percent_correct"] * 100 for f in fits]},
|
|
325
|
+
index=col_names or [f"V{j + 1}" for j in range(p)])
|
|
326
|
+
return OrdinationBiplot(title="External Binary Logistic Biplot", type="ExternalLogistic", dim=A.shape[1],
|
|
327
|
+
row_coordinates=A, col_parameters=B, col_coordinates=B[:, 1:], loadings=B[:, 1:] / dd[:, None],
|
|
328
|
+
var_info=info, total_percent=np.mean([f["percent_correct"] for f in fits]) * 100,
|
|
329
|
+
eigenvalues=np.var(A, axis=0), inertia=np.var(A, axis=0) / np.var(A, axis=0).sum() * 100,
|
|
330
|
+
row_names=row_names or [f"I{i + 1}" for i in range(A.shape[0])],
|
|
331
|
+
col_names=list(info.index), data=Xb)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# --------------------------------------------------------------------------- canonical analysis of distances
|
|
335
|
+
def canonical_distance_analysis(D, groups, dim=2, nperm=999, weighted=False, X=None, seed=None) -> OrdinationBiplot:
|
|
336
|
+
"""Canonical analysis of distances (Gower & Krzanowski, 1999; port of
|
|
337
|
+
``MultBiplotR::CanonicalDistanceAnalysis``): pseudo-F (distance-based
|
|
338
|
+
ANOVA) with permutation p-value, PCoA of the group means and projection of
|
|
339
|
+
the individuals (Euclidean distances on ``X``, Gower's add-a-point formula)."""
|
|
340
|
+
D = np.asarray(D, float)
|
|
341
|
+
g = np.asarray([str(v) for v in groups])
|
|
342
|
+
levels = list(groups.cat.categories) if hasattr(groups, "cat") else sorted(set(g))
|
|
343
|
+
levels = [str(v) for v in levels]
|
|
344
|
+
G = np.column_stack([(g == l).astype(float) for l in levels])
|
|
345
|
+
ng = G.sum(0); n = len(g); k = len(levels)
|
|
346
|
+
D2 = 0.5 * D ** 2
|
|
347
|
+
|
|
348
|
+
def stats(Dm):
|
|
349
|
+
Fm = (G.T @ Dm @ G) / np.outer(ng, ng)
|
|
350
|
+
f = np.diag(Fm)
|
|
351
|
+
DE = 0.5 * (2 * Fm - f[:, None] - f[None, :])
|
|
352
|
+
tss = Dm.sum() / n
|
|
353
|
+
bss = ng @ DE @ ng / n
|
|
354
|
+
return Fm, tss, bss
|
|
355
|
+
Fm, tss, bss = stats(D2)
|
|
356
|
+
wss = tss - bss
|
|
357
|
+
Fexp = (bss / (k - 1)) / (wss / (n - k))
|
|
358
|
+
rng = np.random.default_rng(seed)
|
|
359
|
+
perm = np.empty(nperm)
|
|
360
|
+
for i in range(nperm):
|
|
361
|
+
o = rng.permutation(n)
|
|
362
|
+
_, _, b = stats(D2[np.ix_(o, o)])
|
|
363
|
+
perm[i] = (b / (k - 1)) / ((tss - b) / (n - k))
|
|
364
|
+
H = np.eye(k) - (np.outer(np.ones(k), ng) / n if weighted else np.ones((k, k)) / k)
|
|
365
|
+
B = -H @ Fm @ H.T
|
|
366
|
+
ev, U = np.linalg.eigh((B + B.T) / 2)
|
|
367
|
+
ev, U = ev[::-1], U[:, ::-1]
|
|
368
|
+
Y = U * np.sqrt(np.maximum(ev, 0))
|
|
369
|
+
d0 = (Y ** 2).sum(1)
|
|
370
|
+
qual = Y[:, :dim] ** 2 / d0[:, None] * 100
|
|
371
|
+
res = OrdinationBiplot(title="Canonical Analysis of Distances", type="CDA", dim=dim, F=Fexp, TSS=tss, BSS=bss,
|
|
372
|
+
WSS=wss, df=(k - 1, n - k), p_value=float(np.mean(perm > Fexp)), nperm=nperm,
|
|
373
|
+
eigenvalues=ev, inertia=ev / ev.sum() * 100, group_coordinates=Y[:, :dim],
|
|
374
|
+
group_qualities=qual, group_names=levels, groups=g, row_coordinates=None,
|
|
375
|
+
col_coordinates=None)
|
|
376
|
+
if X is not None:
|
|
377
|
+
Xm = np.asarray(X, float)
|
|
378
|
+
means = (G.T @ Xm) / ng[:, None]
|
|
379
|
+
Di = continuous_distances(means, Xm) ** 2
|
|
380
|
+
Yd = Y[:, :dim]
|
|
381
|
+
Yi = -0.5 * np.linalg.solve(Yd.T @ Yd, Yd.T @ H @ (Di - d0[None, :]).T)
|
|
382
|
+
res.row_coordinates = Yi.T
|
|
383
|
+
return res
|
biplotpy/distatis.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""DISTATIS (Abdi et al., 2005, 2012): STATIS for a set of distance matrices
|
|
2
|
+
on the same objects (port of ``DistatisR::distatis``)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _double_center(D):
|
|
9
|
+
n = D.shape[0]
|
|
10
|
+
C = np.eye(n) - 1 / n
|
|
11
|
+
return -0.5 * C @ D @ C
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def distatis(cube, norm="MFA", distance=True, double_centering=True, rv=True, nfact=3, design=None) -> dict:
|
|
15
|
+
"""``cube``: array n x n x T (or list of T n x n matrices) of distances.
|
|
16
|
+
|
|
17
|
+
Returns the RV (or scalar product) matrix C with its eigen-analysis and
|
|
18
|
+
table factor scores, the weights alpha, the compromise S+ and its factor
|
|
19
|
+
scores, contributions and squared cosines, and the partial factor scores
|
|
20
|
+
of every table."""
|
|
21
|
+
D = np.stack(cube, axis=2) if isinstance(cube, (list, tuple)) else np.asarray(cube, float)
|
|
22
|
+
if distance and not double_centering:
|
|
23
|
+
raise ValueError("Distance matrices must be double centered")
|
|
24
|
+
T = D.shape[2]
|
|
25
|
+
CP = np.stack([_double_center(D[:, :, t] if distance else -D[:, :, t]) for t in range(T)], 2) \
|
|
26
|
+
if double_centering else D.copy()
|
|
27
|
+
for t in range(T):
|
|
28
|
+
Y = CP[:, :, t]
|
|
29
|
+
if norm == "MFA":
|
|
30
|
+
CP[:, :, t] = Y / np.linalg.eigvalsh(Y)[-1]
|
|
31
|
+
elif norm == "SUMPCA":
|
|
32
|
+
CP[:, :, t] = Y / np.trace(Y)
|
|
33
|
+
elif str(norm).upper() == "NUCLEAR":
|
|
34
|
+
ev = np.linalg.eigvalsh(Y)
|
|
35
|
+
CP[:, :, t] = Y / np.sum(np.sqrt(ev[ev > 2 * np.finfo(float).eps]))
|
|
36
|
+
n = CP.shape[0]
|
|
37
|
+
CP2 = CP.reshape(n * n, T, order="F")
|
|
38
|
+
C = CP2.T @ CP2
|
|
39
|
+
if rv:
|
|
40
|
+
nr = np.sqrt((CP2 ** 2).sum(0))
|
|
41
|
+
C = C / np.outer(nr, nr)
|
|
42
|
+
ev, U = np.linalg.eigh(C)
|
|
43
|
+
ev, U = ev[::-1], U[:, ::-1]
|
|
44
|
+
U[:, 0] = np.abs(U[:, 0])
|
|
45
|
+
G = U * np.sqrt(np.abs(ev))
|
|
46
|
+
if design is None:
|
|
47
|
+
alpha = U[:, 0] / U[:, 0].sum()
|
|
48
|
+
else:
|
|
49
|
+
design = np.asarray(design); levs = np.unique(design); alpha = U[:, 0].copy()
|
|
50
|
+
for l in levs:
|
|
51
|
+
m = design == l
|
|
52
|
+
alpha[m] = alpha[m] / (len(levs) * alpha[m].sum())
|
|
53
|
+
S = np.tensordot(CP, alpha, axes=([2], [0]))
|
|
54
|
+
evS, US = np.linalg.eigh(S)
|
|
55
|
+
evS, US = evS[::-1], US[:, ::-1]
|
|
56
|
+
sv = np.sqrt(np.abs(evS))
|
|
57
|
+
F = US * sv
|
|
58
|
+
F2 = F ** 2
|
|
59
|
+
d2 = F2.sum(1)
|
|
60
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
61
|
+
ctr = F2 / np.abs(evS)
|
|
62
|
+
cos2 = F2 / d2[:, None]
|
|
63
|
+
Proj = US / sv
|
|
64
|
+
Proj = Proj[:, :nfact]
|
|
65
|
+
partial = np.stack([CP[:, :, t] @ Proj for t in range(T)], 2)
|
|
66
|
+
return {"C": C, "eig_C": ev, "tau_C": np.round(100 * ev / ev.sum()), "G": G, "alpha": alpha,
|
|
67
|
+
"cubeSP": CP, "Splus": S, "eig_Splus": evS, "tau_Splus": np.round(100 * evS / evS.sum()),
|
|
68
|
+
"F": F[:, :nfact], "ctr": ctr[:, :nfact], "cos2": cos2[:, :nfact], "d2": d2,
|
|
69
|
+
"projection": Proj, "partial_F": partial}
|
biplotpy/dynamic.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Dynamic biplot (Egido & Galindo, 2015).
|
|
2
|
+
|
|
3
|
+
A reference biplot (HJ by default) is computed on a reference matrix (the mean
|
|
4
|
+
of the occasions or one chosen occasion). The data of every occasion are
|
|
5
|
+
transformed with the statistics of the reference and projected as
|
|
6
|
+
supplementary rows (and, optionally, columns), giving the trajectory of each
|
|
7
|
+
individual and variable in the reference plane.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from .classical import pca_biplot
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dynamic_biplot(X, reference="mean", alpha=2, dim=2, scaling=5, names=None, row_names=None,
|
|
18
|
+
col_names=None) -> dict:
|
|
19
|
+
"""``X``: list (or dict) of n x p matrices with the same rows and columns.
|
|
20
|
+
|
|
21
|
+
Returns the reference biplot, the coordinates of the rows and columns at
|
|
22
|
+
every occasion, displacement (first to last occasion) and path length of
|
|
23
|
+
each row, and the quality (%) of the projection of each row at each
|
|
24
|
+
occasion (squared cosine on the reference plane)."""
|
|
25
|
+
if isinstance(X, dict):
|
|
26
|
+
names = names or list(X)
|
|
27
|
+
X = list(X.values())
|
|
28
|
+
if isinstance(X[0], pd.DataFrame):
|
|
29
|
+
row_names = row_names or [str(i) for i in X[0].index]
|
|
30
|
+
col_names = col_names or [str(c) for c in X[0].columns]
|
|
31
|
+
X = [np.asarray(x, float) for x in X]
|
|
32
|
+
T = len(X)
|
|
33
|
+
names = names or [f"t{i + 1}" for i in range(T)]
|
|
34
|
+
ref = np.mean(X, axis=0) if reference == "mean" else X[int(reference)]
|
|
35
|
+
bip = pca_biplot(ref, alpha=alpha, dim=dim, scaling=scaling, row_names=row_names, col_names=col_names)
|
|
36
|
+
mu, sd = ref.mean(0), ref.std(0, ddof=1)
|
|
37
|
+
t = bip.transformation
|
|
38
|
+
if t == "Standardize columns":
|
|
39
|
+
Z = [(x - mu) / sd for x in X]
|
|
40
|
+
elif t == "Column centering":
|
|
41
|
+
Z = [x - mu for x in X]
|
|
42
|
+
elif t == "Raw Data":
|
|
43
|
+
Z = X
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("dynamic_biplot supports raw data, column centering and standardized columns")
|
|
46
|
+
d = bip.d[:dim]
|
|
47
|
+
U, V = bip.U, bip.V
|
|
48
|
+
if alpha > 1:
|
|
49
|
+
rf, cf = np.ones(dim), np.ones(dim)
|
|
50
|
+
else:
|
|
51
|
+
rf = (1 / d) ** (1 - alpha) * bip.scale_factor
|
|
52
|
+
cf = (1 / d) ** alpha / bip.scale_factor
|
|
53
|
+
rows = [z @ V * rf for z in Z] # supplementary rows
|
|
54
|
+
cols = [z.T @ U * cf for z in Z] # supplementary columns
|
|
55
|
+
qual = np.array([((z @ V) ** 2).sum(1) / np.maximum((z ** 2).sum(1), 1e-300) * 100 for z in Z]).T
|
|
56
|
+
traj = np.stack(rows, axis=1) # n x T x dim
|
|
57
|
+
disp = np.sqrt(((traj[:, -1] - traj[:, 0]) ** 2).sum(1))
|
|
58
|
+
path = np.sqrt((np.diff(traj, axis=1) ** 2).sum(2)).sum(1)
|
|
59
|
+
rn = bip.row_names
|
|
60
|
+
return {"reference": bip, "row_coordinates": dict(zip(names, rows)), "col_coordinates": dict(zip(names, cols)),
|
|
61
|
+
"trajectories": traj, "displacement": pd.Series(disp, index=rn), "path_length": pd.Series(path, index=rn),
|
|
62
|
+
"quality": pd.DataFrame(qual, index=rn, columns=names), "occasions": names}
|
biplotpy/extras.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Bootstrap of the PCA biplot, supplementary variables and calibrated axes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from .transforms import initial_transform, transform_name
|
|
8
|
+
from .utils import r_pretty
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# --------------------------------------------------------------------------- bootstrap
|
|
12
|
+
def pca_bootstrap(X, dim=2, scaling="Standardize columns", B=1000, seed=None, level=0.95) -> dict:
|
|
13
|
+
"""Non-parametric bootstrap of the PCA biplot (port of ``PCA.Bootstrap``).
|
|
14
|
+
|
|
15
|
+
Individuals are resampled, the data re-transformed and the SVD recomputed;
|
|
16
|
+
each axis is reflected to agree in sign with the original solution. Returns
|
|
17
|
+
the replicated eigenvalues, explained inertia, loadings, structure
|
|
18
|
+
correlations and qualities of the variables, with percentile and normal
|
|
19
|
+
("basic", as in MultBiplotR) intervals for the eigenvalues and percentile
|
|
20
|
+
intervals for the rest.
|
|
21
|
+
"""
|
|
22
|
+
rng = np.random.default_rng(seed)
|
|
23
|
+
X = np.asarray(X, float)
|
|
24
|
+
n, p = X.shape
|
|
25
|
+
XT = initial_transform(X, transform_name(scaling))["X"]
|
|
26
|
+
U0, d0, V0t = np.linalg.svd(XT, full_matrices=False)
|
|
27
|
+
V0 = V0t.T[:, :dim]
|
|
28
|
+
k = min(n, p)
|
|
29
|
+
eig = np.zeros((B, k)); iner = np.zeros((B, k))
|
|
30
|
+
Vs = np.zeros((B, p, dim)); struct = np.zeros((B, p, dim)); qual = np.zeros((B, p, dim))
|
|
31
|
+
samples = np.zeros((B, n), dtype=int)
|
|
32
|
+
for i in range(B):
|
|
33
|
+
s = rng.integers(n, size=n)
|
|
34
|
+
samples[i] = s
|
|
35
|
+
XB = initial_transform(X[s], transform_name(scaling))["X"]
|
|
36
|
+
U, d, Vt = np.linalg.svd(XB, full_matrices=False)
|
|
37
|
+
V = Vt.T[:, :dim]
|
|
38
|
+
signs = np.sign(np.diag(V0.T @ V)); signs[signs == 0] = 1
|
|
39
|
+
V = V * signs
|
|
40
|
+
U = U[:, :dim] * signs
|
|
41
|
+
eig[i, :len(d)] = d[:k]
|
|
42
|
+
iner[i, :len(d)] = 100 * d[:k] ** 2 / np.sum(d ** 2)
|
|
43
|
+
Vs[i] = V
|
|
44
|
+
A = XB @ V
|
|
45
|
+
struct[i] = np.corrcoef(np.column_stack([XB, A]), rowvar=False)[:p, p:]
|
|
46
|
+
b = V * d[:dim]
|
|
47
|
+
qual[i] = np.cumsum(b ** 2 / (XB ** 2).sum(0)[:, None] * 100, axis=1)
|
|
48
|
+
a = (1 - level) / 2
|
|
49
|
+
ev2 = eig ** 2
|
|
50
|
+
med = ev2.mean(0)
|
|
51
|
+
sdev = ev2.std(0, ddof=1)
|
|
52
|
+
return {"eigenvalues": eig, "inertia": iner, "loadings": Vs, "structure": struct, "qualities": qual,
|
|
53
|
+
"samples": samples, "initial_svd": (U0[:, :dim], d0, V0),
|
|
54
|
+
"mean_eigenvalue": med,
|
|
55
|
+
"ci_percentile_eigenvalue": np.quantile(ev2, [a, 1 - a], axis=0),
|
|
56
|
+
"ci_basic_eigenvalue": np.vstack([med - 1.96 * sdev, med + 1.96 * sdev]),
|
|
57
|
+
"ci_structure": np.quantile(struct, [a, 1 - a], axis=0),
|
|
58
|
+
"ci_qualities": np.quantile(qual, [a, 1 - a], axis=0),
|
|
59
|
+
"ci_inertia": np.quantile(iner, [a, 1 - a], axis=0)}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# --------------------------------------------------------------------------- supplementary
|
|
63
|
+
def add_continuous_variables(bip, X, dims=None, scaling=5, names=None) -> dict:
|
|
64
|
+
"""Project continuous supplementary variables on a biplot by regressing each
|
|
65
|
+
(transformed) variable on the row coordinates (port of ``AddContVars2Biplot``).
|
|
66
|
+
|
|
67
|
+
Returns intercepts ``b0``, coordinates, R2 (%) and structure correlations."""
|
|
68
|
+
if isinstance(X, pd.DataFrame):
|
|
69
|
+
names = names or [str(c) for c in X.columns]
|
|
70
|
+
X = X.to_numpy(float)
|
|
71
|
+
X = np.asarray(X, float)
|
|
72
|
+
if X.ndim == 1:
|
|
73
|
+
X = X[:, None]
|
|
74
|
+
n, p = X.shape
|
|
75
|
+
dims = dims or bip.row_coordinates.shape[1]
|
|
76
|
+
Xs = initial_transform(X, transform_name(scaling))["X"]
|
|
77
|
+
A = bip.row_coordinates[:, :dims]
|
|
78
|
+
DE = np.column_stack([np.ones(n), A])
|
|
79
|
+
b = np.linalg.solve(DE.T @ DE, DE.T @ Xs).T
|
|
80
|
+
res = Xs - DE @ b.T
|
|
81
|
+
R2 = (1 - (res ** 2).sum(0) / (Xs ** 2).sum(0)) * 100
|
|
82
|
+
structure = np.corrcoef(np.column_stack([Xs, A]), rowvar=False)[:p, p:]
|
|
83
|
+
names = names or [f"SV{j + 1}" for j in range(p)]
|
|
84
|
+
return {"b0": b[:, 0], "coordinates": b[:, 1:], "R2": R2, "structure": structure, "names": names,
|
|
85
|
+
"scaled_data": Xs}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def project_rows(bip, X_new):
|
|
89
|
+
"""Coordinates of supplementary individuals (transformed with the statistics
|
|
90
|
+
of the active data) on a classical biplot."""
|
|
91
|
+
X_new = np.asarray(X_new, float)
|
|
92
|
+
t = bip.transformation
|
|
93
|
+
if t == "Standardize columns":
|
|
94
|
+
Z = (X_new - bip.means) / bip.deviations
|
|
95
|
+
elif t == "Column centering":
|
|
96
|
+
Z = X_new - bip.means
|
|
97
|
+
elif t == "Substract the global mean":
|
|
98
|
+
Z = X_new - bip.gmean
|
|
99
|
+
elif t == "Raw Data":
|
|
100
|
+
Z = X_new
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError(f"Supplementary rows are not defined for '{t}'")
|
|
103
|
+
a = Z @ bip.V
|
|
104
|
+
d = bip.d[:bip.dim]
|
|
105
|
+
if bip.alpha <= 1:
|
|
106
|
+
a = a * (1 / d) ** (1 - bip.alpha) * bip.scale_factor
|
|
107
|
+
return a # HJ-Biplot: principal coordinates X V = U D
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------- calibrated axes
|
|
111
|
+
def biplot_scales(bip, nticks=3, type_scale="Complete", values_scale="Original") -> dict:
|
|
112
|
+
"""Ticks (on the transformed scale) and labels (on the original or transformed
|
|
113
|
+
scale) of the calibrated axis of every variable (port of ``GetBiplotScales``
|
|
114
|
+
for raw data, global-mean, column-centred and standardized data)."""
|
|
115
|
+
ticks, labels = [], []
|
|
116
|
+
t = bip.transformation
|
|
117
|
+
for j in range(bip.p):
|
|
118
|
+
mn, mx, mean, sd = bip.minima[j], bip.maxima[j], bip.means[j], bip.deviations[j]
|
|
119
|
+
if t in ("Raw Data", "Substract the global mean"):
|
|
120
|
+
lab = r_pretty([mn, mx], nticks)
|
|
121
|
+
tk = lab - (bip.gmean if t == "Substract the global mean" else 0)
|
|
122
|
+
elif t == "Column centering":
|
|
123
|
+
if type_scale == "Complete":
|
|
124
|
+
if values_scale == "Original":
|
|
125
|
+
lab = r_pretty([mn, mx], nticks); tk = lab - mean
|
|
126
|
+
else:
|
|
127
|
+
tk = r_pretty([mn - mean, mx - mean], nticks); lab = np.round(tk, 2)
|
|
128
|
+
elif type_scale == "StdDev":
|
|
129
|
+
k = np.array([-3, -2, -1, 1, 2, 3])
|
|
130
|
+
tk = np.round(k * sd, 2); lab = np.round(mean + k * sd, 2) if values_scale == "Original" else tk
|
|
131
|
+
else:
|
|
132
|
+
q = np.array([mn, bip.p25[j], bip.medians[j], bip.p75[j], mx])
|
|
133
|
+
tk = q - mean; lab = q if values_scale == "Original" else np.round(tk, 2)
|
|
134
|
+
elif t == "Standardize columns":
|
|
135
|
+
if type_scale == "Complete":
|
|
136
|
+
if values_scale == "Original":
|
|
137
|
+
lab = r_pretty([mn, mx], nticks); tk = (lab - mean) / sd
|
|
138
|
+
else:
|
|
139
|
+
tk = np.array([-3, -2.5, -2, -1.5, -1, -0.5, 0.5, 1, 1.5, 2, 2.5, 3]); lab = np.round(tk, 2)
|
|
140
|
+
elif type_scale == "StdDev":
|
|
141
|
+
tk = np.array([-3., -2, -1, 1, 2, 3])
|
|
142
|
+
lab = np.round(tk * sd + mean, 1) if values_scale == "Original" else np.round(tk, 2)
|
|
143
|
+
else:
|
|
144
|
+
q = np.array([mn, bip.p25[j], bip.medians[j], bip.p75[j], mx])
|
|
145
|
+
tk = (q - mean) / sd; lab = q if values_scale == "Original" else np.round(tk, 2)
|
|
146
|
+
else:
|
|
147
|
+
col = bip.scaled_data[:, j]
|
|
148
|
+
lab = r_pretty([col.min(), col.max()], nticks); tk = lab
|
|
149
|
+
ticks.append(np.asarray(tk, float)); labels.append(np.asarray(lab, float))
|
|
150
|
+
return {"ticks": ticks, "labels": labels}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def calibrated_axis(b, ticks):
|
|
154
|
+
"""Positions of the ticks of a calibrated (predictive) axis with direction
|
|
155
|
+
``b`` (column marker): the point of the axis whose projection predicts the
|
|
156
|
+
value ``t`` is ``t * b / |b|^2``."""
|
|
157
|
+
b = np.asarray(b, float)
|
|
158
|
+
return np.outer(np.asarray(ticks, float), b / (b @ b))
|