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/classical.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Classical biplots: GH (CMP), JK (RMP), SQRT and HJ-Biplot.
|
|
2
|
+
|
|
3
|
+
Port of ``PCA.Biplot`` and ``HJ.Biplot`` (MultBiplotR, Vicente-Villardón).
|
|
4
|
+
With the singular value decomposition X = U D V' of the transformed data:
|
|
5
|
+
|
|
6
|
+
* alpha = 1 : JK / RMP-Biplot rows A = U D, columns B = V
|
|
7
|
+
* alpha = 0 : GH / CMP-Biplot rows A = U, columns B = V D
|
|
8
|
+
* alpha = 0.5: SQRT biplot rows A = U D^{1/2}, columns B = V D^{1/2}
|
|
9
|
+
* HJ-Biplot (Galindo, 1986): A = U D and B = V D, both with optimal quality.
|
|
10
|
+
|
|
11
|
+
For alpha <= 1 the markers are rescaled by a common factor so that rows and
|
|
12
|
+
columns have comparable spread (``scale_factor``), as in MultBiplotR.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from .transforms import initial_transform, transform_name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContinuousBiplot:
|
|
23
|
+
"""Result of a classical biplot (fields mirror MultBiplotR's ``ContinuousBiplot``)."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, **kw):
|
|
26
|
+
self.__dict__.update(kw)
|
|
27
|
+
|
|
28
|
+
# labelled views ---------------------------------------------------------
|
|
29
|
+
def _df(self, M, idx):
|
|
30
|
+
return pd.DataFrame(M, index=idx, columns=[f"Dim {d + 1}" for d in range(M.shape[1])])
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def rows(self):
|
|
34
|
+
return self._df(self.row_coordinates, self.row_names)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def columns(self):
|
|
38
|
+
return self._df(self.col_coordinates, self.col_names)
|
|
39
|
+
|
|
40
|
+
def qualities(self, which="columns"):
|
|
41
|
+
"""Quality of representation: cumulative relative contributions (%)."""
|
|
42
|
+
if which == "columns":
|
|
43
|
+
return self._df(np.cumsum(self.col_contributions, 1), self.col_names)
|
|
44
|
+
return self._df(np.cumsum(self.row_contributions, 1), self.row_names)
|
|
45
|
+
|
|
46
|
+
def summary(self) -> dict:
|
|
47
|
+
d = self.dim
|
|
48
|
+
eig = pd.DataFrame({"Eigenvalue": self.eigenvalues[:d], "Exp. Var": self.inertia[:d],
|
|
49
|
+
"Cumulative": self.cum_inertia[:d]}, index=[f"Dim {i + 1}" for i in range(d)])
|
|
50
|
+
return {"eigen": eig,
|
|
51
|
+
"col_contributions": self._df(self.col_contributions, self.col_names),
|
|
52
|
+
"col_qualities": self.qualities("columns"),
|
|
53
|
+
"row_contributions": self._df(self.row_contributions, self.row_names),
|
|
54
|
+
"row_qualities": self.qualities("rows"),
|
|
55
|
+
"structure": self._df(self.structure, self.col_names),
|
|
56
|
+
"row_contrib_to_factor": self._df(self.row_contrib_to_factor, self.row_names),
|
|
57
|
+
"col_contrib_to_factor": self._df(self.col_contrib_to_factor, self.col_names)}
|
|
58
|
+
|
|
59
|
+
def plot(self, *args, **kwargs):
|
|
60
|
+
from .plot import plot_biplot
|
|
61
|
+
return plot_biplot(self, *args, **kwargs)
|
|
62
|
+
|
|
63
|
+
def __repr__(self):
|
|
64
|
+
s = self.summary()
|
|
65
|
+
return (f"{self.title} ({self.normalization}); transformation: {self.transformation}\n"
|
|
66
|
+
f"{s['eigen'].round(3).to_string()}\n\nQuality of representation of the columns (%):\n"
|
|
67
|
+
f"{s['col_qualities'].round(2).to_string()}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_NORMALIZATION = {2: "Principal normalization (HJ-Biplot)", 1: "Row principal normalization (JK / RMP-Biplot)",
|
|
71
|
+
0: "Column principal normalization (GH / CMP-Biplot)", 0.5: "Symmetrical normalization (SQRT-Biplot)"}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def pca_biplot(X, alpha=1.0, dim=2, scaling=5, sup_rows=None, sup_cols=None, grouping=None,
|
|
75
|
+
row_names=None, col_names=None) -> ContinuousBiplot:
|
|
76
|
+
"""Classical PCA biplot with normalization ``alpha`` (``alpha > 1`` gives the HJ-Biplot).
|
|
77
|
+
|
|
78
|
+
``scaling`` is the initial transformation (name or MultBiplotR number; 5 =
|
|
79
|
+
standardize columns, 4 = column centering).
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(X, pd.DataFrame):
|
|
82
|
+
row_names = row_names or [str(i) for i in X.index]
|
|
83
|
+
col_names = col_names or [str(c) for c in X.columns]
|
|
84
|
+
X = X.to_numpy(float)
|
|
85
|
+
X0 = np.array(X, float)
|
|
86
|
+
n, p = X0.shape
|
|
87
|
+
row_names = list(row_names) if row_names is not None else [f"I{i + 1}" for i in range(n)]
|
|
88
|
+
col_names = list(col_names) if col_names is not None else [f"V{j + 1}" for j in range(p)]
|
|
89
|
+
tr = initial_transform(X0, transform_name(scaling), sup_rows, sup_cols, grouping)
|
|
90
|
+
Xs = tr["X"]
|
|
91
|
+
U, d, Vt = np.linalg.svd(Xs, full_matrices=False)
|
|
92
|
+
V = Vt.T
|
|
93
|
+
ev = d ** 2
|
|
94
|
+
inertia = ev / ev.sum() * 100
|
|
95
|
+
a = U[:, :dim] * d[:dim]
|
|
96
|
+
b = V[:, :dim] * d[:dim]
|
|
97
|
+
structure = np.corrcoef(np.column_stack([Xs, a]), rowvar=False)[:p, p:]
|
|
98
|
+
sf = (Xs ** 2).sum(1)
|
|
99
|
+
rc = a ** 2 / sf[:, None] * 100
|
|
100
|
+
scc = (Xs ** 2).sum(0)
|
|
101
|
+
cc = b ** 2 / scc[:, None] * 100
|
|
102
|
+
# contributions of each element to the factor (absolute contributions, %)
|
|
103
|
+
rcf = a ** 2 / (a ** 2).sum(0) * 100
|
|
104
|
+
ccf = b ** 2 / (b ** 2).sum(0) * 100
|
|
105
|
+
sr, sc = tr["sup_rows"], tr["sup_cols"]
|
|
106
|
+
a_s = b_s = None
|
|
107
|
+
if sr is not None:
|
|
108
|
+
a_s = sr @ V[:, :dim]
|
|
109
|
+
rcs = a_s ** 2 / (sr ** 2).sum(1)[:, None] * 100
|
|
110
|
+
if sc is not None:
|
|
111
|
+
b_s = sc.T @ U[:, :dim]
|
|
112
|
+
ccs = b_s ** 2 / (sc ** 2).sum(0)[:, None] * 100
|
|
113
|
+
if alpha <= 1:
|
|
114
|
+
a = a * (1 / d[:dim]) ** (1 - alpha)
|
|
115
|
+
b = b * (1 / d[:dim]) ** alpha
|
|
116
|
+
if a_s is not None:
|
|
117
|
+
a_s = a_s * (1 / d[:dim]) ** (1 - alpha)
|
|
118
|
+
if b_s is not None:
|
|
119
|
+
b_s = b_s * (1 / d[:dim]) ** alpha
|
|
120
|
+
sca = (a ** 2).sum() / n
|
|
121
|
+
scb = (b ** 2).sum() / p
|
|
122
|
+
scf = np.sqrt(np.sqrt(scb / sca))
|
|
123
|
+
a, b = a * scf, b / scf
|
|
124
|
+
a_s = None if a_s is None else a_s * scf
|
|
125
|
+
b_s = None if b_s is None else b_s / scf
|
|
126
|
+
else:
|
|
127
|
+
scf = 1.0
|
|
128
|
+
norm = _NORMALIZATION.get(alpha if alpha <= 1 else 2, f"Custom normalization (alpha = {alpha})")
|
|
129
|
+
return ContinuousBiplot(
|
|
130
|
+
title="HJ Biplot" if alpha > 1 else "PCA Biplot", type="HJ" if alpha > 1 else "PCA", alpha=alpha,
|
|
131
|
+
normalization=norm, dim=dim, transformation=tr["transform"], non_scaled_data=X0, scaled_data=Xs,
|
|
132
|
+
means=X0.mean(0), medians=np.median(X0, 0), deviations=tr["col_sds"], minima=X0.min(0), maxima=X0.max(0),
|
|
133
|
+
p25=np.quantile(X0, 0.25, axis=0), p75=np.quantile(X0, 0.75, axis=0), gmean=X0.mean(),
|
|
134
|
+
eigenvalues=ev, inertia=inertia, cum_inertia=np.cumsum(inertia), V=V[:, :dim], U=U[:, :dim], d=d,
|
|
135
|
+
structure=structure, row_coordinates=a, col_coordinates=b, row_contributions=rc, col_contributions=cc,
|
|
136
|
+
row_contrib_to_factor=rcf, col_contrib_to_factor=ccf, scale_factor=scf, row_names=row_names,
|
|
137
|
+
col_names=col_names, n=n, p=p,
|
|
138
|
+
sup_row_coordinates=a_s, sup_col_coordinates=b_s,
|
|
139
|
+
sup_row_contributions=None if sr is None else rcs, sup_col_contributions=None if sc is None else ccs)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def hj_biplot(X, dim=3, scaling=5, **kw) -> ContinuousBiplot:
|
|
143
|
+
"""HJ-Biplot (Galindo, 1986): rows and columns with optimal quality of representation."""
|
|
144
|
+
return pca_biplot(X, alpha=2, dim=dim, scaling=scaling, **kw)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def gh_biplot(X, dim=2, scaling=5, **kw) -> ContinuousBiplot:
|
|
148
|
+
"""GH-Biplot (column metric preserving, alpha = 0)."""
|
|
149
|
+
return pca_biplot(X, alpha=0, dim=dim, scaling=scaling, **kw)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def jk_biplot(X, dim=2, scaling=5, **kw) -> ContinuousBiplot:
|
|
153
|
+
"""JK-Biplot (row metric preserving, alpha = 1)."""
|
|
154
|
+
return pca_biplot(X, alpha=1, dim=dim, scaling=scaling, **kw)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def sqrt_biplot(X, dim=2, scaling=5, **kw) -> ContinuousBiplot:
|
|
158
|
+
"""Symmetrical (SQRT) biplot, alpha = 0.5."""
|
|
159
|
+
return pca_biplot(X, alpha=0.5, dim=dim, scaling=scaling, **kw)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Correspondence-type biplots: simple and multiple correspondence analysis,
|
|
2
|
+
canonical correspondence analysis (CCA) and redundancy analysis (RDA).
|
|
3
|
+
|
|
4
|
+
* ``ca`` — port of ``MultBiplotR::CA`` (alpha = 1 rows principal / columns
|
|
5
|
+
standard, 0 the reverse, 0.5 symmetric biplot, 2 = both principal, the
|
|
6
|
+
usual "symmetric map" of ``ca``/``FactoMineR``); also returns the absolute
|
|
7
|
+
contributions (to the axis) of ``ca::ca``/``FactoMineR::CA``.
|
|
8
|
+
* ``mca`` — multiple correspondence analysis as the CA of the indicator matrix
|
|
9
|
+
(``FactoMineR::MCA`` conventions), with the Benzécri and Greenacre adjusted
|
|
10
|
+
inertias.
|
|
11
|
+
* ``cca`` — port of ``MultBiplotR::CCA`` (ter Braak, 1986), eigenvalues equal
|
|
12
|
+
to ``vegan::cca``.
|
|
13
|
+
* ``rda`` — redundancy analysis with the scores and scalings of ``vegan::rda``.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from .transforms import initial_transform, transform_name
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OrdinationBiplot:
|
|
24
|
+
"""Result of a CA/MCA/CCA/RDA/PCoA/MDS-type biplot."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, **kw):
|
|
27
|
+
self.__dict__.update(kw)
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
ev = np.asarray(self.eigenvalues)[:5]
|
|
31
|
+
return f"{self.title}: eigenvalues {np.round(ev, 4)}; inertia (%) {np.round(np.asarray(self.inertia)[:5], 2)}"
|
|
32
|
+
|
|
33
|
+
def plot(self, *args, **kwargs):
|
|
34
|
+
from .plot import plot_ordination
|
|
35
|
+
return plot_ordination(self, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _names(x, n, p, rp="I", cp="V"):
|
|
39
|
+
if isinstance(x, pd.DataFrame):
|
|
40
|
+
return [str(i) for i in x.index], [str(c) for c in x.columns]
|
|
41
|
+
return [f"{rp}{i + 1}" for i in range(n)], [f"{cp}{j + 1}" for j in range(p)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------------------- CA
|
|
45
|
+
def ca(N, dim=2, alpha=2.0, sup_rows=None, sup_cols=None) -> OrdinationBiplot:
|
|
46
|
+
"""Simple correspondence analysis of a contingency table ``N``.
|
|
47
|
+
|
|
48
|
+
``alpha``: 2 = symmetric map (rows and columns in principal coordinates);
|
|
49
|
+
1 = row principal (RMP); 0 = column principal (CMP); 0.5 = symmetric
|
|
50
|
+
biplot (MultBiplotR's convention for alpha <= 1)."""
|
|
51
|
+
rn, cn = _names(N, *np.shape(N))
|
|
52
|
+
X = np.asarray(N, float)
|
|
53
|
+
n, p = X.shape
|
|
54
|
+
tot = X.sum()
|
|
55
|
+
P = X / tot
|
|
56
|
+
r, c = P.sum(1), P.sum(0)
|
|
57
|
+
S = (P - np.outer(r, c)) / np.sqrt(np.outer(r, c))
|
|
58
|
+
U, d, Vt = np.linalg.svd(S, full_matrices=False)
|
|
59
|
+
k = min(n, p) - 1
|
|
60
|
+
U, d, V = U[:, :k], d[:k], Vt.T[:, :k]
|
|
61
|
+
Fs, Gs = U / np.sqrt(r)[:, None], V / np.sqrt(c)[:, None] # standard coordinates
|
|
62
|
+
F, G = Fs * d, Gs * d # principal coordinates
|
|
63
|
+
ev = d ** 2
|
|
64
|
+
inertia = ev / ev.sum() * 100
|
|
65
|
+
qual_r = F ** 2 / (F ** 2).sum(1, keepdims=True) * 100
|
|
66
|
+
qual_c = G ** 2 / (G ** 2).sum(1, keepdims=True) * 100
|
|
67
|
+
ctr_r = r[:, None] * F ** 2 / ev * 100
|
|
68
|
+
ctr_c = c[:, None] * G ** 2 / ev * 100
|
|
69
|
+
if alpha > 1:
|
|
70
|
+
A, B = F, G
|
|
71
|
+
else:
|
|
72
|
+
A, B = Fs * d ** alpha, Gs * d ** (1 - alpha)
|
|
73
|
+
res = OrdinationBiplot(title="Correspondence Analysis", type="CA", alpha=alpha, dim=dim, eigenvalues=ev,
|
|
74
|
+
inertia=inertia, cum_inertia=np.cumsum(inertia), total_inertia=ev.sum(),
|
|
75
|
+
chi2=ev.sum() * tot, row_coordinates=A[:, :dim], col_coordinates=B[:, :dim],
|
|
76
|
+
row_principal=F, col_principal=G, row_standard=Fs, col_standard=Gs,
|
|
77
|
+
row_masses=r, col_masses=c, row_qualities=qual_r[:, :dim], col_qualities=qual_c[:, :dim],
|
|
78
|
+
row_contributions=ctr_r[:, :dim], col_contributions=ctr_c[:, :dim],
|
|
79
|
+
row_names=rn, col_names=cn, data=X)
|
|
80
|
+
if sup_rows is not None:
|
|
81
|
+
R = np.asarray(sup_rows, float); R = R / R.sum(1, keepdims=True)
|
|
82
|
+
res.sup_row_coordinates = (R @ Gs)[:, :dim]
|
|
83
|
+
if sup_cols is not None:
|
|
84
|
+
C = np.asarray(sup_cols, float); C = C / C.sum(0, keepdims=True)
|
|
85
|
+
res.sup_col_coordinates = (C.T @ Fs)[:, :dim]
|
|
86
|
+
return res
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------------- MCA
|
|
90
|
+
def indicator_matrix(X):
|
|
91
|
+
"""Complete disjunctive table of a data frame of categorical variables."""
|
|
92
|
+
df = pd.DataFrame(X).astype(str)
|
|
93
|
+
cols, levs, var_of = [], [], []
|
|
94
|
+
for v in df.columns:
|
|
95
|
+
for lev in sorted(df[v].unique()):
|
|
96
|
+
cols.append((df[v] == lev).astype(float).to_numpy())
|
|
97
|
+
levs.append(lev); var_of.append(v)
|
|
98
|
+
# category labels as in FactoMineR: the level itself when unique, else variable_level
|
|
99
|
+
names = levs if len(set(levs)) == len(levs) else [f"{v}_{l}" for v, l in zip(var_of, levs)]
|
|
100
|
+
return np.column_stack(cols), names, var_of
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def mca(X, dim=2, sup_rows=None) -> OrdinationBiplot:
|
|
104
|
+
"""Multiple correspondence analysis (CA of the indicator matrix).
|
|
105
|
+
|
|
106
|
+
Coordinates follow ``FactoMineR::MCA``: individuals and categories in
|
|
107
|
+
principal coordinates; eigenvalues of the indicator matrix; also the
|
|
108
|
+
Benzécri-corrected and Greenacre-adjusted percentages of inertia."""
|
|
109
|
+
df = pd.DataFrame(X)
|
|
110
|
+
rn = [str(i) for i in df.index]
|
|
111
|
+
Z, names, var_of = indicator_matrix(df)
|
|
112
|
+
Q = df.shape[1]
|
|
113
|
+
res = ca(pd.DataFrame(Z, index=rn, columns=names), dim=max(dim, 2), alpha=2)
|
|
114
|
+
ev = res.eigenvalues
|
|
115
|
+
keep = ev > 1 / Q
|
|
116
|
+
benz = ((Q / (Q - 1)) * (ev[keep] - 1 / Q)) ** 2
|
|
117
|
+
J = Z.shape[1]
|
|
118
|
+
green_total = Q / (Q - 1) * (np.sum(ev ** 2) - (J - Q) / Q ** 2)
|
|
119
|
+
res.title, res.type = "Multiple Correspondence Analysis", "MCA"
|
|
120
|
+
res.row_coordinates, res.col_coordinates = res.row_principal[:, :dim], res.col_principal[:, :dim]
|
|
121
|
+
res.benzecri = benz / benz.sum() * 100
|
|
122
|
+
res.greenacre = benz / green_total * 100
|
|
123
|
+
res.variables = var_of
|
|
124
|
+
res.eta2 = np.array([[np.sum(res.col_masses[np.array(var_of) == v] * res.col_principal[np.array(var_of) == v, k] ** 2)
|
|
125
|
+
* Q for k in range(dim)] for v in df.columns])
|
|
126
|
+
res.var_names = list(df.columns)
|
|
127
|
+
return res
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# --------------------------------------------------------------------------- CCA
|
|
131
|
+
def _msqrt(S, inv=False):
|
|
132
|
+
w, U = np.linalg.eigh(S)
|
|
133
|
+
w = np.maximum(w, 0)
|
|
134
|
+
return U @ np.diag(1 / np.sqrt(w) if inv else np.sqrt(w)) @ U.T
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cca(Y, Z, alpha=1.0, dim=2) -> OrdinationBiplot:
|
|
138
|
+
"""Canonical correspondence analysis of an abundance table ``Y`` (sites x
|
|
139
|
+
species) constrained by environmental variables ``Z`` (sites x variables).
|
|
140
|
+
|
|
141
|
+
Port of ``MultBiplotR::CCA``: species scores, site scores (weighted
|
|
142
|
+
averages, WA) and linear-combination site scores (LC), environmental
|
|
143
|
+
variable scores (biplot arrows), qualities and inertia decompositions.
|
|
144
|
+
``alpha`` = 1 gives species in principal coordinates."""
|
|
145
|
+
sn, spn = _names(Y, *np.shape(Y), "Si", "Sp")
|
|
146
|
+
en = list(Z.columns) if isinstance(Z, pd.DataFrame) else [f"Ev{k + 1}" for k in range(np.shape(Z)[1])]
|
|
147
|
+
P = np.asarray(Y, float); Zm = np.asarray(Z, float)
|
|
148
|
+
P = P / P.sum()
|
|
149
|
+
r, c = P.sum(1), P.sum(0)
|
|
150
|
+
PC = (P - np.outer(r, c)) / np.sqrt(np.outer(r, c))
|
|
151
|
+
valsin = np.linalg.svd(PC, compute_uv=False)
|
|
152
|
+
in_total = np.sum(PC ** 2)
|
|
153
|
+
iner_rows, iner_cols = (PC ** 2).sum(1), (PC ** 2).sum(0)
|
|
154
|
+
W1 = (P.T @ Zm) / c[:, None]
|
|
155
|
+
mean_z = r @ Zm
|
|
156
|
+
Zc = Zm - mean_z
|
|
157
|
+
s11 = r @ Zc ** 2
|
|
158
|
+
Zs = Zc / np.sqrt(s11)
|
|
159
|
+
W = (P.T @ Zs) / c[:, None]
|
|
160
|
+
S22 = Zs.T @ (r[:, None] * Zs)
|
|
161
|
+
sq, isq = _msqrt(S22), _msqrt(S22, inv=True)
|
|
162
|
+
WW = np.sqrt(c)[:, None] * W @ isq
|
|
163
|
+
U, d, Vt = np.linalg.svd(WW, full_matrices=False)
|
|
164
|
+
V = Vt.T
|
|
165
|
+
x = (P / r[:, None]) @ (U / np.sqrt(c)[:, None])
|
|
166
|
+
x2 = Zs @ isq @ V
|
|
167
|
+
A = (U / np.sqrt(c)[:, None]) * d ** alpha
|
|
168
|
+
B = sq @ V * d ** (1 - alpha)
|
|
169
|
+
Afull = (U / np.sqrt(c)[:, None]) * d
|
|
170
|
+
Bfull = sq @ V * d
|
|
171
|
+
sp_q = Afull ** 2 / (Afull ** 2).sum(1, keepdims=True) * 100
|
|
172
|
+
ev_q = Bfull ** 2 / (Bfull ** 2).sum(1, keepdims=True) * 100
|
|
173
|
+
site_q = (np.sqrt(r)[:, None] * x) ** 2 / iner_rows[:, None] * 100
|
|
174
|
+
sp_q_ca = (U * d) ** 2 / iner_cols[:, None] * 100
|
|
175
|
+
interset = np.corrcoef(np.column_stack([x, Zm]), rowvar=False)[:x.shape[1], x.shape[1]:]
|
|
176
|
+
ev = d ** 2
|
|
177
|
+
return OrdinationBiplot(title="Canonical Correspondence Analysis", type="CCA", alpha=alpha, dim=dim,
|
|
178
|
+
eigenvalues=ev, inertia=ev / ev.sum() * 100, exp_species_constrained=ev / in_total * 100,
|
|
179
|
+
exp_species_ca=valsin ** 2 / in_total * 100, total_inertia=in_total,
|
|
180
|
+
row_coordinates=(x * d ** (-alpha))[:, :dim], lc_site_scores=(x2 * d ** (-alpha))[:, :dim],
|
|
181
|
+
col_coordinates=A[:, :dim], env_coordinates=B[:, :dim], weighted_averages=W1,
|
|
182
|
+
species_quality=sp_q[:, :dim], species_quality_ca=sp_q_ca[:, :dim],
|
|
183
|
+
env_quality=ev_q[:, :dim], site_quality=site_q[:, :dim], interset=interset[:, :dim],
|
|
184
|
+
row_names=sn, col_names=spn, env_names=en, means_z=mean_z, sd_z=np.sqrt(s11))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# --------------------------------------------------------------------------- RDA
|
|
188
|
+
def rda(Y, X, scale=False, scaling=2, dim=2) -> OrdinationBiplot:
|
|
189
|
+
"""Redundancy analysis (constrained PCA) of ``Y`` on ``X``.
|
|
190
|
+
|
|
191
|
+
Scores and scalings follow ``vegan::rda``/``scores.rda`` (scaling 1 =
|
|
192
|
+
sites, 2 = species, 3 = symmetric; constant sqrt(sqrt((n-1) * total
|
|
193
|
+
variance))): site scores (WA), constrained site scores (LC), species scores
|
|
194
|
+
and biplot scores of the explanatory variables; unconstrained (residual)
|
|
195
|
+
axes are returned as ``ca_eigenvalues``."""
|
|
196
|
+
sn, spn = _names(Y, *np.shape(Y), "S", "Sp")
|
|
197
|
+
xn = list(X.columns) if isinstance(X, pd.DataFrame) else [f"X{k + 1}" for k in range(np.shape(X)[1])]
|
|
198
|
+
Ym = np.asarray(Y, float); Xm = np.asarray(X, float)
|
|
199
|
+
n = Ym.shape[0]
|
|
200
|
+
Yc = Ym - Ym.mean(0)
|
|
201
|
+
if scale:
|
|
202
|
+
Yc = Yc / Ym.std(0, ddof=1)
|
|
203
|
+
Ys = Yc / np.sqrt(n - 1)
|
|
204
|
+
tot = np.sum(Ys ** 2)
|
|
205
|
+
Xc = Xm - Xm.mean(0)
|
|
206
|
+
Qx, _ = np.linalg.qr(Xc)
|
|
207
|
+
rank = np.linalg.matrix_rank(Xc)
|
|
208
|
+
Qx = Qx[:, :rank]
|
|
209
|
+
Fit = Qx @ (Qx.T @ Ys)
|
|
210
|
+
U, d, Vt = np.linalg.svd(Fit, full_matrices=False)
|
|
211
|
+
k = int(np.sum(d > 1e-10 * d[0]))
|
|
212
|
+
U, d, V = U[:, :k], d[:k], Vt.T[:, :k]
|
|
213
|
+
ev = d ** 2
|
|
214
|
+
wa = Ys @ V / d
|
|
215
|
+
biplot = np.corrcoef(np.column_stack([Xm, U]), rowvar=False)[:Xm.shape[1], Xm.shape[1]:]
|
|
216
|
+
Res = Ys - Fit
|
|
217
|
+
res_ev = np.linalg.svd(Res, compute_uv=False) ** 2
|
|
218
|
+
res_ev = res_ev[res_ev > 1e-10 * max(res_ev.max(), 1e-300)]
|
|
219
|
+
const = np.sqrt(np.sqrt((n - 1) * tot))
|
|
220
|
+
eig_all = np.concatenate([ev, res_ev])
|
|
221
|
+
slam = np.sqrt(ev / tot)
|
|
222
|
+
sp_s, site_s = {1: (1, slam), 2: (slam, 1), 3: (np.sqrt(slam), np.sqrt(slam))}[scaling]
|
|
223
|
+
species = const * V * sp_s
|
|
224
|
+
sites = const * wa * site_s
|
|
225
|
+
lc = const * U * site_s
|
|
226
|
+
bp = biplot * site_s
|
|
227
|
+
return OrdinationBiplot(title="Redundancy Analysis", type="RDA", dim=dim, scaling=scaling, eigenvalues=ev,
|
|
228
|
+
inertia=ev / tot * 100, ca_eigenvalues=res_ev, total_inertia=tot,
|
|
229
|
+
constrained_proportion=ev.sum() / tot, row_coordinates=sites[:, :dim],
|
|
230
|
+
lc_site_scores=lc[:, :dim], col_coordinates=species[:, :dim], env_coordinates=bp[:, :dim],
|
|
231
|
+
row_names=sn, col_names=spn, env_names=xn, const=const, all_eigenvalues=eig_all)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Classic datasets used in the examples."""
|
|
2
|
+
from importlib import resources
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _read(name):
|
|
8
|
+
with resources.files(__package__).joinpath(name).open("r") as f:
|
|
9
|
+
return pd.read_csv(f)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_iris() -> pd.DataFrame:
|
|
13
|
+
"""Fisher's iris data (150 x 5)."""
|
|
14
|
+
return _read("iris.csv")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_mtcars() -> pd.DataFrame:
|
|
18
|
+
"""Motor Trend cars (32 x 12, with a ``name`` column)."""
|
|
19
|
+
return _read("mtcars.csv").set_index("name")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"Sepal.Length","Sepal.Width","Petal.Length","Petal.Width","Species"
|
|
2
|
+
5.1,3.5,1.4,0.2,"setosa"
|
|
3
|
+
4.9,3,1.4,0.2,"setosa"
|
|
4
|
+
4.7,3.2,1.3,0.2,"setosa"
|
|
5
|
+
4.6,3.1,1.5,0.2,"setosa"
|
|
6
|
+
5,3.6,1.4,0.2,"setosa"
|
|
7
|
+
5.4,3.9,1.7,0.4,"setosa"
|
|
8
|
+
4.6,3.4,1.4,0.3,"setosa"
|
|
9
|
+
5,3.4,1.5,0.2,"setosa"
|
|
10
|
+
4.4,2.9,1.4,0.2,"setosa"
|
|
11
|
+
4.9,3.1,1.5,0.1,"setosa"
|
|
12
|
+
5.4,3.7,1.5,0.2,"setosa"
|
|
13
|
+
4.8,3.4,1.6,0.2,"setosa"
|
|
14
|
+
4.8,3,1.4,0.1,"setosa"
|
|
15
|
+
4.3,3,1.1,0.1,"setosa"
|
|
16
|
+
5.8,4,1.2,0.2,"setosa"
|
|
17
|
+
5.7,4.4,1.5,0.4,"setosa"
|
|
18
|
+
5.4,3.9,1.3,0.4,"setosa"
|
|
19
|
+
5.1,3.5,1.4,0.3,"setosa"
|
|
20
|
+
5.7,3.8,1.7,0.3,"setosa"
|
|
21
|
+
5.1,3.8,1.5,0.3,"setosa"
|
|
22
|
+
5.4,3.4,1.7,0.2,"setosa"
|
|
23
|
+
5.1,3.7,1.5,0.4,"setosa"
|
|
24
|
+
4.6,3.6,1,0.2,"setosa"
|
|
25
|
+
5.1,3.3,1.7,0.5,"setosa"
|
|
26
|
+
4.8,3.4,1.9,0.2,"setosa"
|
|
27
|
+
5,3,1.6,0.2,"setosa"
|
|
28
|
+
5,3.4,1.6,0.4,"setosa"
|
|
29
|
+
5.2,3.5,1.5,0.2,"setosa"
|
|
30
|
+
5.2,3.4,1.4,0.2,"setosa"
|
|
31
|
+
4.7,3.2,1.6,0.2,"setosa"
|
|
32
|
+
4.8,3.1,1.6,0.2,"setosa"
|
|
33
|
+
5.4,3.4,1.5,0.4,"setosa"
|
|
34
|
+
5.2,4.1,1.5,0.1,"setosa"
|
|
35
|
+
5.5,4.2,1.4,0.2,"setosa"
|
|
36
|
+
4.9,3.1,1.5,0.2,"setosa"
|
|
37
|
+
5,3.2,1.2,0.2,"setosa"
|
|
38
|
+
5.5,3.5,1.3,0.2,"setosa"
|
|
39
|
+
4.9,3.6,1.4,0.1,"setosa"
|
|
40
|
+
4.4,3,1.3,0.2,"setosa"
|
|
41
|
+
5.1,3.4,1.5,0.2,"setosa"
|
|
42
|
+
5,3.5,1.3,0.3,"setosa"
|
|
43
|
+
4.5,2.3,1.3,0.3,"setosa"
|
|
44
|
+
4.4,3.2,1.3,0.2,"setosa"
|
|
45
|
+
5,3.5,1.6,0.6,"setosa"
|
|
46
|
+
5.1,3.8,1.9,0.4,"setosa"
|
|
47
|
+
4.8,3,1.4,0.3,"setosa"
|
|
48
|
+
5.1,3.8,1.6,0.2,"setosa"
|
|
49
|
+
4.6,3.2,1.4,0.2,"setosa"
|
|
50
|
+
5.3,3.7,1.5,0.2,"setosa"
|
|
51
|
+
5,3.3,1.4,0.2,"setosa"
|
|
52
|
+
7,3.2,4.7,1.4,"versicolor"
|
|
53
|
+
6.4,3.2,4.5,1.5,"versicolor"
|
|
54
|
+
6.9,3.1,4.9,1.5,"versicolor"
|
|
55
|
+
5.5,2.3,4,1.3,"versicolor"
|
|
56
|
+
6.5,2.8,4.6,1.5,"versicolor"
|
|
57
|
+
5.7,2.8,4.5,1.3,"versicolor"
|
|
58
|
+
6.3,3.3,4.7,1.6,"versicolor"
|
|
59
|
+
4.9,2.4,3.3,1,"versicolor"
|
|
60
|
+
6.6,2.9,4.6,1.3,"versicolor"
|
|
61
|
+
5.2,2.7,3.9,1.4,"versicolor"
|
|
62
|
+
5,2,3.5,1,"versicolor"
|
|
63
|
+
5.9,3,4.2,1.5,"versicolor"
|
|
64
|
+
6,2.2,4,1,"versicolor"
|
|
65
|
+
6.1,2.9,4.7,1.4,"versicolor"
|
|
66
|
+
5.6,2.9,3.6,1.3,"versicolor"
|
|
67
|
+
6.7,3.1,4.4,1.4,"versicolor"
|
|
68
|
+
5.6,3,4.5,1.5,"versicolor"
|
|
69
|
+
5.8,2.7,4.1,1,"versicolor"
|
|
70
|
+
6.2,2.2,4.5,1.5,"versicolor"
|
|
71
|
+
5.6,2.5,3.9,1.1,"versicolor"
|
|
72
|
+
5.9,3.2,4.8,1.8,"versicolor"
|
|
73
|
+
6.1,2.8,4,1.3,"versicolor"
|
|
74
|
+
6.3,2.5,4.9,1.5,"versicolor"
|
|
75
|
+
6.1,2.8,4.7,1.2,"versicolor"
|
|
76
|
+
6.4,2.9,4.3,1.3,"versicolor"
|
|
77
|
+
6.6,3,4.4,1.4,"versicolor"
|
|
78
|
+
6.8,2.8,4.8,1.4,"versicolor"
|
|
79
|
+
6.7,3,5,1.7,"versicolor"
|
|
80
|
+
6,2.9,4.5,1.5,"versicolor"
|
|
81
|
+
5.7,2.6,3.5,1,"versicolor"
|
|
82
|
+
5.5,2.4,3.8,1.1,"versicolor"
|
|
83
|
+
5.5,2.4,3.7,1,"versicolor"
|
|
84
|
+
5.8,2.7,3.9,1.2,"versicolor"
|
|
85
|
+
6,2.7,5.1,1.6,"versicolor"
|
|
86
|
+
5.4,3,4.5,1.5,"versicolor"
|
|
87
|
+
6,3.4,4.5,1.6,"versicolor"
|
|
88
|
+
6.7,3.1,4.7,1.5,"versicolor"
|
|
89
|
+
6.3,2.3,4.4,1.3,"versicolor"
|
|
90
|
+
5.6,3,4.1,1.3,"versicolor"
|
|
91
|
+
5.5,2.5,4,1.3,"versicolor"
|
|
92
|
+
5.5,2.6,4.4,1.2,"versicolor"
|
|
93
|
+
6.1,3,4.6,1.4,"versicolor"
|
|
94
|
+
5.8,2.6,4,1.2,"versicolor"
|
|
95
|
+
5,2.3,3.3,1,"versicolor"
|
|
96
|
+
5.6,2.7,4.2,1.3,"versicolor"
|
|
97
|
+
5.7,3,4.2,1.2,"versicolor"
|
|
98
|
+
5.7,2.9,4.2,1.3,"versicolor"
|
|
99
|
+
6.2,2.9,4.3,1.3,"versicolor"
|
|
100
|
+
5.1,2.5,3,1.1,"versicolor"
|
|
101
|
+
5.7,2.8,4.1,1.3,"versicolor"
|
|
102
|
+
6.3,3.3,6,2.5,"virginica"
|
|
103
|
+
5.8,2.7,5.1,1.9,"virginica"
|
|
104
|
+
7.1,3,5.9,2.1,"virginica"
|
|
105
|
+
6.3,2.9,5.6,1.8,"virginica"
|
|
106
|
+
6.5,3,5.8,2.2,"virginica"
|
|
107
|
+
7.6,3,6.6,2.1,"virginica"
|
|
108
|
+
4.9,2.5,4.5,1.7,"virginica"
|
|
109
|
+
7.3,2.9,6.3,1.8,"virginica"
|
|
110
|
+
6.7,2.5,5.8,1.8,"virginica"
|
|
111
|
+
7.2,3.6,6.1,2.5,"virginica"
|
|
112
|
+
6.5,3.2,5.1,2,"virginica"
|
|
113
|
+
6.4,2.7,5.3,1.9,"virginica"
|
|
114
|
+
6.8,3,5.5,2.1,"virginica"
|
|
115
|
+
5.7,2.5,5,2,"virginica"
|
|
116
|
+
5.8,2.8,5.1,2.4,"virginica"
|
|
117
|
+
6.4,3.2,5.3,2.3,"virginica"
|
|
118
|
+
6.5,3,5.5,1.8,"virginica"
|
|
119
|
+
7.7,3.8,6.7,2.2,"virginica"
|
|
120
|
+
7.7,2.6,6.9,2.3,"virginica"
|
|
121
|
+
6,2.2,5,1.5,"virginica"
|
|
122
|
+
6.9,3.2,5.7,2.3,"virginica"
|
|
123
|
+
5.6,2.8,4.9,2,"virginica"
|
|
124
|
+
7.7,2.8,6.7,2,"virginica"
|
|
125
|
+
6.3,2.7,4.9,1.8,"virginica"
|
|
126
|
+
6.7,3.3,5.7,2.1,"virginica"
|
|
127
|
+
7.2,3.2,6,1.8,"virginica"
|
|
128
|
+
6.2,2.8,4.8,1.8,"virginica"
|
|
129
|
+
6.1,3,4.9,1.8,"virginica"
|
|
130
|
+
6.4,2.8,5.6,2.1,"virginica"
|
|
131
|
+
7.2,3,5.8,1.6,"virginica"
|
|
132
|
+
7.4,2.8,6.1,1.9,"virginica"
|
|
133
|
+
7.9,3.8,6.4,2,"virginica"
|
|
134
|
+
6.4,2.8,5.6,2.2,"virginica"
|
|
135
|
+
6.3,2.8,5.1,1.5,"virginica"
|
|
136
|
+
6.1,2.6,5.6,1.4,"virginica"
|
|
137
|
+
7.7,3,6.1,2.3,"virginica"
|
|
138
|
+
6.3,3.4,5.6,2.4,"virginica"
|
|
139
|
+
6.4,3.1,5.5,1.8,"virginica"
|
|
140
|
+
6,3,4.8,1.8,"virginica"
|
|
141
|
+
6.9,3.1,5.4,2.1,"virginica"
|
|
142
|
+
6.7,3.1,5.6,2.4,"virginica"
|
|
143
|
+
6.9,3.1,5.1,2.3,"virginica"
|
|
144
|
+
5.8,2.7,5.1,1.9,"virginica"
|
|
145
|
+
6.8,3.2,5.9,2.3,"virginica"
|
|
146
|
+
6.7,3.3,5.7,2.5,"virginica"
|
|
147
|
+
6.7,3,5.2,2.3,"virginica"
|
|
148
|
+
6.3,2.5,5,1.9,"virginica"
|
|
149
|
+
6.5,3,5.2,2,"virginica"
|
|
150
|
+
6.2,3.4,5.4,2.3,"virginica"
|
|
151
|
+
5.9,3,5.1,1.8,"virginica"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb","name"
|
|
2
|
+
21,6,160,110,3.9,2.62,16.46,0,1,4,4,"Mazda RX4"
|
|
3
|
+
21,6,160,110,3.9,2.875,17.02,0,1,4,4,"Mazda RX4 Wag"
|
|
4
|
+
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1,"Datsun 710"
|
|
5
|
+
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1,"Hornet 4 Drive"
|
|
6
|
+
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2,"Hornet Sportabout"
|
|
7
|
+
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1,"Valiant"
|
|
8
|
+
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4,"Duster 360"
|
|
9
|
+
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2,"Merc 240D"
|
|
10
|
+
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2,"Merc 230"
|
|
11
|
+
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4,"Merc 280"
|
|
12
|
+
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4,"Merc 280C"
|
|
13
|
+
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3,"Merc 450SE"
|
|
14
|
+
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3,"Merc 450SL"
|
|
15
|
+
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3,"Merc 450SLC"
|
|
16
|
+
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4,"Cadillac Fleetwood"
|
|
17
|
+
10.4,8,460,215,3,5.424,17.82,0,0,3,4,"Lincoln Continental"
|
|
18
|
+
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4,"Chrysler Imperial"
|
|
19
|
+
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1,"Fiat 128"
|
|
20
|
+
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2,"Honda Civic"
|
|
21
|
+
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1,"Toyota Corolla"
|
|
22
|
+
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1,"Toyota Corona"
|
|
23
|
+
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2,"Dodge Challenger"
|
|
24
|
+
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2,"AMC Javelin"
|
|
25
|
+
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4,"Camaro Z28"
|
|
26
|
+
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2,"Pontiac Firebird"
|
|
27
|
+
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1,"Fiat X1-9"
|
|
28
|
+
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2,"Porsche 914-2"
|
|
29
|
+
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2,"Lotus Europa"
|
|
30
|
+
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4,"Ford Pantera L"
|
|
31
|
+
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6,"Ferrari Dino"
|
|
32
|
+
15,8,301,335,3.54,3.57,14.6,0,1,5,8,"Maserati Bora"
|
|
33
|
+
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2,"Volvo 142E"
|