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/plsbin.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""PLS regression with binary responses (MultBiplotR).
|
|
2
|
+
|
|
3
|
+
* ``plsr1_bin``: PLS regression of one binary response on continuous
|
|
4
|
+
predictors (``PLSR1Bin``, algorithm 1 of Bastien, Esposito Vinzi & Tenenhaus
|
|
5
|
+
2005): each component is built from the slopes of univariate ridge logistic
|
|
6
|
+
regressions and the response is finally regressed on the components.
|
|
7
|
+
* ``binary_plsr``: PLS regression for binary data (``BinaryPLSR``), with
|
|
8
|
+
binary predictors and binary responses, both modelled by logistic biplots
|
|
9
|
+
whose row scores are shared; fitted by alternating conjugate gradients
|
|
10
|
+
(R's ``optim(method = "CG")``) from random starts.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
from scipy.special import expit
|
|
17
|
+
|
|
18
|
+
from .distances import ridge_logistic
|
|
19
|
+
from .roptim import optim_cg
|
|
20
|
+
from .rrng import RRNG
|
|
21
|
+
from .transforms import initial_transform, transform_name
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def plsr1_bin(y, X, S=2, scaling=5, penalization=0.0, row_names=None, col_names=None) -> dict:
|
|
25
|
+
"""PLS regression of a binary response ``y`` (0/1) on ``X``
|
|
26
|
+
(``MultBiplotR::PLSR1Bin`` with ``Algorithm = 1``).
|
|
27
|
+
|
|
28
|
+
Component ``s``: for each predictor, a ridge logistic regression (penalty
|
|
29
|
+
0.2, as in R) of ``y`` on the previous components and the predictor gives
|
|
30
|
+
a slope; the normalized slopes are the weights ``w``, the scores are
|
|
31
|
+
``t = X_(s-1) w`` (deflated ``X``) and the loadings ``p = X't / t't``. The
|
|
32
|
+
response is finally regressed on the components with ``penalization``.
|
|
33
|
+
|
|
34
|
+
Returns scores ``T``, weights ``W``, loadings ``P``, the final logistic fit
|
|
35
|
+
and the structure correlations between the predictors and the scores."""
|
|
36
|
+
if isinstance(X, pd.DataFrame):
|
|
37
|
+
col_names = col_names or [str(c) for c in X.columns]
|
|
38
|
+
row_names = row_names or [str(i) for i in X.index]
|
|
39
|
+
Xo = np.asarray(X, float)
|
|
40
|
+
y = np.asarray(y, float).ravel()
|
|
41
|
+
if not np.all(np.isin(y, (0, 1))):
|
|
42
|
+
raise ValueError("The response must be binary (0 or 1)")
|
|
43
|
+
Xs = initial_transform(Xo, transform_name(scaling))["X"]
|
|
44
|
+
n, J = Xs.shape
|
|
45
|
+
T = np.zeros((n, S)); W = np.zeros((J, S)); P = np.zeros((J, S))
|
|
46
|
+
w = np.zeros(J)
|
|
47
|
+
for j in range(J):
|
|
48
|
+
w[j] = ridge_logistic(y, Xs[:, j], 0.2, intercept=True)["beta"][1]
|
|
49
|
+
w = w / np.sqrt(np.sum(w ** 2))
|
|
50
|
+
t = Xs @ w
|
|
51
|
+
T[:, 0] = t; W[:, 0] = w
|
|
52
|
+
p = Xs.T @ t / np.sum(t ** 2); P[:, 0] = p
|
|
53
|
+
X1 = Xs - np.outer(t, p)
|
|
54
|
+
for s in range(1, S):
|
|
55
|
+
for j in range(J):
|
|
56
|
+
x = np.column_stack([T[:, :s], Xs[:, j]])
|
|
57
|
+
w[j] = ridge_logistic(y, x, 0.2, intercept=True)["beta"][s + 1]
|
|
58
|
+
w = w / np.sqrt(np.sum(w ** 2))
|
|
59
|
+
t = X1 @ w
|
|
60
|
+
T[:, s] = t; W[:, s] = w
|
|
61
|
+
p = Xs.T @ t / np.sum(t ** 2); P[:, s] = p
|
|
62
|
+
X1 = X1 - np.outer(t, p)
|
|
63
|
+
fit = ridge_logistic(y, T, penalization, intercept=True)
|
|
64
|
+
comps = [f"Comp. {k + 1}" for k in range(S)]
|
|
65
|
+
col_names = col_names or [f"X{j + 1}" for j in range(J)]
|
|
66
|
+
row_names = row_names or [f"I{i + 1}" for i in range(n)]
|
|
67
|
+
structure = np.corrcoef(np.column_stack([Xo, T]), rowvar=False)[:J, J:]
|
|
68
|
+
return {"method": "PLSR1 Binary", "scores": pd.DataFrame(T, index=row_names, columns=comps),
|
|
69
|
+
"weights": pd.DataFrame(W, index=col_names, columns=comps),
|
|
70
|
+
"loadings": pd.DataFrame(P, index=col_names, columns=comps),
|
|
71
|
+
"structure": pd.DataFrame(structure, index=col_names, columns=comps), "fit": fit,
|
|
72
|
+
"y_weights": fit["beta"], "scaled_X": Xs}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------------------- BinaryPLSR
|
|
76
|
+
def _rsum(v):
|
|
77
|
+
"""``sum(v, na.rm = TRUE)`` of R: sequential accumulation in long double,
|
|
78
|
+
in column-major order, skipping NaN (keeps the CG path close to R's)."""
|
|
79
|
+
v = np.asarray(v, float).ravel(order="F")
|
|
80
|
+
v = v[~np.isnan(v)]
|
|
81
|
+
return float(np.cumsum(v.astype(np.longdouble))[-1]) if v.size else 0.0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _mm(A, B):
|
|
85
|
+
"""``A %*% B`` with the summation order of the reference BLAS used by R
|
|
86
|
+
(sequential over the inner index, in double precision)."""
|
|
87
|
+
A = np.atleast_2d(A); B = np.atleast_2d(B)
|
|
88
|
+
C = np.zeros((A.shape[0], B.shape[1]))
|
|
89
|
+
for l in range(A.shape[1]):
|
|
90
|
+
C += A[:, l:l + 1] * B[l:l + 1, :]
|
|
91
|
+
return C
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _sig(z):
|
|
95
|
+
return 1 / (1 + np.exp(-1 * z))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cost(H, X):
|
|
99
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
100
|
+
return _rsum(-1 * X * np.log(H) - (1 - X) * np.log(1 - H))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _J_b0(par, X):
|
|
104
|
+
H = _sig(_mm(np.ones((X.shape[0], 1)), par[None, :]))
|
|
105
|
+
return _cost(H, X) / 2
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _g_b0(par, X):
|
|
109
|
+
H = _sig(_mm(np.ones((X.shape[0], 1)), par[None, :]))
|
|
110
|
+
return _mm((H - X).T, np.ones((X.shape[0], 1))).ravel()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _J_B(par, X, A, B, lam):
|
|
114
|
+
r = A.shape[1] - 1
|
|
115
|
+
B = B.copy(); B[:, r] = par
|
|
116
|
+
H = _sig(_mm(A, B.T))
|
|
117
|
+
return _cost(H, X) / 2 + lam * _rsum(B[:, 1:] ** 2) / 2
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _g_B(par, X, A, B, lam):
|
|
121
|
+
r = A.shape[1] - 1
|
|
122
|
+
B = B.copy(); B[:, r] = par
|
|
123
|
+
E = _sig(_mm(A, B.T)) - X
|
|
124
|
+
E[np.isnan(X)] = 0
|
|
125
|
+
G = _mm(E.T, A) + lam * np.column_stack([np.zeros(B.shape[0]), B[:, 1:]])
|
|
126
|
+
return G[:, r]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _J_A(par, X, A, B, lam):
|
|
130
|
+
r = B.shape[1] - 1
|
|
131
|
+
A = A.copy(); A[:, r] = par
|
|
132
|
+
H = _sig(_mm(A, B.T))
|
|
133
|
+
return _cost(H, X) / 2 + lam * _rsum(A ** 2) / 2
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _g_A(par, X, A, B, lam):
|
|
137
|
+
r = B.shape[1] - 1
|
|
138
|
+
A = A.copy(); A[:, r] = par
|
|
139
|
+
E = _sig(_mm(A, B.T)) - X
|
|
140
|
+
E[np.isnan(X)] = 0
|
|
141
|
+
return (_mm(E, B) + lam * A)[:, r]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _scale(v):
|
|
145
|
+
"""R's ``scale()`` of a vector: ``colMeans`` (long double sum / n) and the root
|
|
146
|
+
of the sum of squares (long double sum) over n - 1."""
|
|
147
|
+
v = np.asarray(v, float).ravel()
|
|
148
|
+
n = v.size
|
|
149
|
+
m = float(np.cumsum(v.astype(np.longdouble))[-1] / n)
|
|
150
|
+
c = v - m
|
|
151
|
+
ss = float(np.cumsum((c * c).astype(np.longdouble))[-1])
|
|
152
|
+
return c / np.sqrt(ss / max(1, n - 1))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def binary_plsr(Y, X, S=2, penalization=0.1, tol=5e-5, maxiter=100, seed=0, row_names=None,
|
|
156
|
+
x_names=None, y_names=None) -> dict:
|
|
157
|
+
"""PLS regression for binary data (``MultBiplotR::BinaryPLSR``).
|
|
158
|
+
|
|
159
|
+
``X`` (n x J) and ``Y`` (n x K) are binary. Both are modelled by logistic
|
|
160
|
+
biplots with intercepts ``p0`` and ``q0``; component ``s`` alternates, until
|
|
161
|
+
the scores stabilize, ridge-penalized conjugate-gradient updates of the
|
|
162
|
+
``X`` loadings, the ``X`` scores, the ``Y`` loadings and the ``Y`` scores,
|
|
163
|
+
the scores being standardized after each update (R's ``optim`` CG, ported in
|
|
164
|
+
``biplotpy.roptim``). Starting values are normal random numbers drawn
|
|
165
|
+
with R's generator after ``set.seed(seed)``, so a given seed gives R's
|
|
166
|
+
result. Returns scores, loadings, intercepts, fitted probabilities and the
|
|
167
|
+
percentages of correct classification of ``Y``."""
|
|
168
|
+
if isinstance(X, pd.DataFrame):
|
|
169
|
+
x_names = x_names or [str(c) for c in X.columns]; row_names = row_names or [str(i) for i in X.index]
|
|
170
|
+
if isinstance(Y, pd.DataFrame):
|
|
171
|
+
y_names = y_names or [str(c) for c in Y.columns]
|
|
172
|
+
X = np.asarray(X, float); Y = np.asarray(Y, float)
|
|
173
|
+
if Y.ndim == 1:
|
|
174
|
+
Y = Y[:, None]
|
|
175
|
+
if not np.all(np.isin(Y[np.isfinite(Y)], (0, 1))):
|
|
176
|
+
raise ValueError("The response must be binary (0 or 1)")
|
|
177
|
+
I, J = X.shape
|
|
178
|
+
K = Y.shape[1]
|
|
179
|
+
rng = RRNG(seed)
|
|
180
|
+
p0 = optim_cg(_J_b0, _g_b0, rng.rnorm(J), args=(X,))["par"]
|
|
181
|
+
q0 = optim_cg(_J_b0, _g_b0, rng.rnorm(K), args=(Y,))["par"]
|
|
182
|
+
TT = np.ones((I, 1)); U = np.ones((I, 1)); P = p0[:, None]; Q = q0[:, None]
|
|
183
|
+
iters = []
|
|
184
|
+
for k in range(S):
|
|
185
|
+
tt = rng.rnorm(I); TT = np.column_stack([TT, tt])
|
|
186
|
+
u = rng.rnorm(I); U = np.column_stack([U, u])
|
|
187
|
+
parP = rng.rnorm(J); P = np.column_stack([P, parP])
|
|
188
|
+
parQ = rng.rnorm(K); Q = np.column_stack([Q, parQ])
|
|
189
|
+
t = u
|
|
190
|
+
error, it = 1.0, 0
|
|
191
|
+
while error > tol and it < maxiter:
|
|
192
|
+
it += 1
|
|
193
|
+
told = t
|
|
194
|
+
parP = optim_cg(_J_B, _g_B, parP, args=(X, TT, P, penalization))["par"]
|
|
195
|
+
P[:, k + 1] = parP
|
|
196
|
+
t = optim_cg(_J_A, _g_A, t, args=(X, TT, P, penalization))["par"]
|
|
197
|
+
t = _scale(t)
|
|
198
|
+
tt = t
|
|
199
|
+
u = t
|
|
200
|
+
U[:, k + 1] = u
|
|
201
|
+
parQ = optim_cg(_J_B, _g_B, parQ, args=(Y, U, Q, penalization))["par"]
|
|
202
|
+
Q[:, k + 1] = parQ
|
|
203
|
+
u = optim_cg(_J_A, _g_A, u, args=(Y, U, Q, penalization))["par"]
|
|
204
|
+
U[:, k + 1] = _scale(u)
|
|
205
|
+
t = _scale(u)
|
|
206
|
+
TT[:, k + 1] = t
|
|
207
|
+
error = np.sum((told - t) ** 2)
|
|
208
|
+
TT[:, k + 1] = tt
|
|
209
|
+
iters.append(it)
|
|
210
|
+
U, Q, P = U[:, 1:], Q[:, 1:], P[:, 1:]
|
|
211
|
+
lin = TT @ np.column_stack([q0, Q]).T
|
|
212
|
+
E = expit(lin)
|
|
213
|
+
pred = (E > 0.5).astype(float)
|
|
214
|
+
right = Y == pred
|
|
215
|
+
comps = [f"Comp. {k + 1}" for k in range(S)]
|
|
216
|
+
row_names = row_names or [f"I{i + 1}" for i in range(I)]
|
|
217
|
+
x_names = x_names or [f"X{j + 1}" for j in range(J)]
|
|
218
|
+
y_names = y_names or [f"Y{j + 1}" for j in range(K)]
|
|
219
|
+
return {"method": "PLSR for binary data", "x_scores": pd.DataFrame(TT[:, 1:], index=row_names, columns=comps),
|
|
220
|
+
"x_loadings": pd.DataFrame(P, index=x_names, columns=comps),
|
|
221
|
+
"y_scores": pd.DataFrame(U, index=row_names, columns=comps),
|
|
222
|
+
"y_loadings": pd.DataFrame(Q, index=y_names, columns=comps), "x_intercepts": p0, "y_intercepts": q0,
|
|
223
|
+
"linear_terms": lin, "expected": E, "predictions": pred, "percent_correct": right.mean(),
|
|
224
|
+
"percent_correct_cols": right.mean(0), "iterations": iters, "seed": seed}
|
biplotpy/restricted.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Constrained (restricted) logistic biplots and the external ordinal logistic
|
|
2
|
+
biplot of MultBiplotR.
|
|
3
|
+
|
|
4
|
+
* ``constrained_logistic_biplot``: binary responses ``Y`` explained by
|
|
5
|
+
continuous variables ``X`` (``ConstrainedLogisticBiplot``). Each response is
|
|
6
|
+
regressed on ``X`` by ridge logistic regression; the standardized linear
|
|
7
|
+
predictors are summarized by an SVD, whose first components are the row
|
|
8
|
+
coordinates, and each response is then refitted on those coordinates.
|
|
9
|
+
* ``constrained_ordinal_logistic_biplot``: the same with ordinal responses and
|
|
10
|
+
ridge cumulative-logit regressions (``ConstrainedOrdinalLogisticBiplot``).
|
|
11
|
+
* ``external_ordinal_logistic_biplot``: ordinal variables fitted by ridge
|
|
12
|
+
cumulative-logit regressions on a given configuration (PCA, PCoA, MDS...),
|
|
13
|
+
as ``AddOrdVars2Biplot``.
|
|
14
|
+
* ``ridge_ordinal_logistic``: the Newton (Fisher scoring) fit of MultBiplotR
|
|
15
|
+
(``OrdinalLogisticFit``/``RidgeOrdinalLogistic``), with the ridge penalty on
|
|
16
|
+
thresholds and slopes.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pandas as pd
|
|
22
|
+
import scipy.stats as sps
|
|
23
|
+
from scipy.special import expit
|
|
24
|
+
|
|
25
|
+
from .distances import ridge_logistic
|
|
26
|
+
from .extras import add_continuous_variables
|
|
27
|
+
from .logistic import LogisticBiplot
|
|
28
|
+
from .transforms import initial_transform, transform_name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --------------------------------------------------------------------------- helpers
|
|
32
|
+
def _ridge_binary_fit(y, Xd, penalization=0.2, tol=1e-5, maxiter=100):
|
|
33
|
+
"""``RidgeBinaryLogisticFit``: Newton-Raphson with ridge penalty (first column,
|
|
34
|
+
the intercept, not penalized). ``Xd`` already contains the intercept."""
|
|
35
|
+
n, m = Xd.shape
|
|
36
|
+
beta = np.zeros(m)
|
|
37
|
+
Im = np.eye(m); Im[0, 0] = 0.0
|
|
38
|
+
err, it = 0.1, 0
|
|
39
|
+
while err > tol and it < maxiter:
|
|
40
|
+
it += 1
|
|
41
|
+
old = beta
|
|
42
|
+
mu = expit(Xd @ old)
|
|
43
|
+
In = Xd.T @ ((mu * (1 - mu))[:, None] * Xd) + 2 * penalization * Im
|
|
44
|
+
U = Xd.T @ (y - mu) - 2 * penalization * Im @ old
|
|
45
|
+
beta = old + np.linalg.pinv(In) @ U
|
|
46
|
+
err = np.sum(np.abs(old - beta))
|
|
47
|
+
return beta
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _codes(y):
|
|
51
|
+
"""Ordinal variable -> integer codes 1..C (pandas ordered categorical, or numbers)."""
|
|
52
|
+
if hasattr(y, "cat"):
|
|
53
|
+
return y.cat.codes.to_numpy() + 1, list(y.cat.categories)
|
|
54
|
+
y = np.asarray(y)
|
|
55
|
+
lev = sorted(set(y.tolist()))
|
|
56
|
+
return np.array([lev.index(v) + 1 for v in y]), lev
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ridge_ordinal_logistic(y, x=None, penalization=0.1, tol=1e-4, maxiter=200) -> dict:
|
|
60
|
+
"""Ridge cumulative-logit regression, ``logit P(y <= c) = tau_c - x beta``,
|
|
61
|
+
fitted by Fisher scoring as ``MultBiplotR::OrdinalLogisticFit`` (the
|
|
62
|
+
penalty ``penalization * |(tau, beta)|^2`` includes the thresholds).
|
|
63
|
+
|
|
64
|
+
``y``: integer codes 1..J. Returns thresholds, coefficients, linear
|
|
65
|
+
predictors, fitted category probabilities, log-likelihood, deviance and,
|
|
66
|
+
against the model without covariates, the likelihood-ratio test and the
|
|
67
|
+
pseudo R2 of Cox-Snell, Nagelkerke and McFadden."""
|
|
68
|
+
def fit(xm):
|
|
69
|
+
y_ = np.asarray(y, int)
|
|
70
|
+
n = len(y_)
|
|
71
|
+
p = 0 if xm is None else xm.shape[1]
|
|
72
|
+
J = int(y_.max())
|
|
73
|
+
if J < 2:
|
|
74
|
+
raise ValueError("There is a variable with the same value for all the items.")
|
|
75
|
+
npar = J + p - 1
|
|
76
|
+
Y = np.zeros((n, J))
|
|
77
|
+
for i in range(n):
|
|
78
|
+
if y_[i] > 0:
|
|
79
|
+
Y[i, y_[i] - 1] = 1
|
|
80
|
+
R = np.cumsum(Y, axis=1)
|
|
81
|
+
A = R[:, :J - 1].sum(0) / n
|
|
82
|
+
Beta = np.zeros(p)
|
|
83
|
+
B = np.concatenate([A, Beta])
|
|
84
|
+
err, it = 0.1, 0
|
|
85
|
+
while err > tol and it < maxiter:
|
|
86
|
+
it += 1
|
|
87
|
+
ETA = np.tile(A, (n, 1)) if xm is None else A[None, :] - (xm @ Beta)[:, None]
|
|
88
|
+
PIA = np.column_stack([np.exp(ETA) / (1 + np.exp(ETA)), np.ones(n)])
|
|
89
|
+
PI = np.column_stack([PIA[:, :1], PIA[:, 1:J] - PIA[:, :J - 1]])
|
|
90
|
+
Rho = np.log(PIA[:, :J - 1] / (PIA[:, 1:J] - PIA[:, :J - 1]))
|
|
91
|
+
gRho = np.log(1 + np.exp(Rho))
|
|
92
|
+
U = PIA[:, 1:J] / (PIA[:, :J - 1] * (PIA[:, 1:J] - PIA[:, :J - 1]))
|
|
93
|
+
D = R[:, :J - 1] - R[:, 1:J] * (PIA[:, :J - 1] / PIA[:, 1:J])
|
|
94
|
+
D2 = PIA[:, :J] * (1 - PIA[:, :J])
|
|
95
|
+
Q = np.zeros((n, J - 1, npar))
|
|
96
|
+
GR = np.zeros(npar)
|
|
97
|
+
for k in range(npar):
|
|
98
|
+
P = np.zeros((n, J))
|
|
99
|
+
if k < J - 1:
|
|
100
|
+
P[:, k] = 1.0
|
|
101
|
+
else:
|
|
102
|
+
P[:, :J - 1] = -xm[:, k - (J - 1)][:, None]
|
|
103
|
+
Q[:, :, k] = P[:, :J - 1] * D2[:, :J - 1] - P[:, 1:J] * (PIA[:, :J - 1] / PIA[:, 1:J]) * D2[:, 1:J]
|
|
104
|
+
GR[k] = np.sum(D * U * Q[:, :, k])
|
|
105
|
+
HESS = np.einsum("nj,njs,njk->sk", U, Q, Q) + 2 * np.eye(npar) * penalization
|
|
106
|
+
Bnew = B + np.linalg.solve(HESS, GR - 2 * B * penalization)
|
|
107
|
+
err = np.sum((Bnew - B) ** 2)
|
|
108
|
+
B = Bnew
|
|
109
|
+
A = B[:J - 1]
|
|
110
|
+
Beta = np.zeros(p) if xm is None else B[J - 1:J - 1 + p]
|
|
111
|
+
L = np.sum(R[:, :J - 1] * Rho - R[:, 1:J] * gRho)
|
|
112
|
+
pred = PI.argmax(1) + 1
|
|
113
|
+
return {"thresholds": A, "coefficients": Beta, "eta": ETA, "fitted": PI, "pred": pred,
|
|
114
|
+
"logLik": L, "deviance": -2 * L, "percent_correct": np.mean(y_ == pred), "iter": it,
|
|
115
|
+
"J": J, "nvar": p, "covariances": np.linalg.inv(HESS)}
|
|
116
|
+
x = None if x is None else np.asarray(x, float).reshape(len(y), -1)
|
|
117
|
+
model = fit(x)
|
|
118
|
+
null = fit(None)
|
|
119
|
+
n = len(y)
|
|
120
|
+
dif = null["deviance"] - model["deviance"]
|
|
121
|
+
cs = 1 - np.exp(-dif / n)
|
|
122
|
+
model.update({"null_deviance": null["deviance"], "dif": dif, "df": model["nvar"],
|
|
123
|
+
"p": sps.chi2.sf(dif, model["nvar"]), "cox_snell": cs,
|
|
124
|
+
"nagelkerke": cs / (1 - np.exp(null["deviance"] / -2) ** (2 / n)),
|
|
125
|
+
"mcfadden": 1 - model["deviance"] / null["deviance"]})
|
|
126
|
+
return model
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _ordinal_columns(Ycodes, A, penalization, tol, maxiter, names):
|
|
130
|
+
"""Fit each ordinal variable on the configuration ``A`` (``AddOrdVars2Biplot``)."""
|
|
131
|
+
n, p = Ycodes.shape
|
|
132
|
+
dim = A.shape[1]
|
|
133
|
+
ncat = [int(Ycodes[:, j].max()) for j in range(p)]
|
|
134
|
+
coef = np.zeros((p, dim)); thr = np.zeros((p, max(ncat) - 1))
|
|
135
|
+
fitcols = ["logLik", "Deviance", "df", "p-value", "PCC", "CoxSnell", "Macfaden", "Nagelkerke", "NullDeviance"]
|
|
136
|
+
fit = np.zeros((p, 9)); models = []
|
|
137
|
+
for j in range(p):
|
|
138
|
+
m = ridge_ordinal_logistic(Ycodes[:, j], A, penalization, tol, maxiter)
|
|
139
|
+
models.append(m)
|
|
140
|
+
coef[j] = m["coefficients"]; thr[j, :len(m["thresholds"])] = m["thresholds"]
|
|
141
|
+
fit[j] = [m["logLik"], m["deviance"], m["df"], m["p"], m["percent_correct"], m["cox_snell"],
|
|
142
|
+
m["mcfadden"], m["nagelkerke"], m["null_deviance"]]
|
|
143
|
+
d = np.sqrt((coef ** 2).sum(1) + 1)
|
|
144
|
+
loadings = coef / d[:, None]
|
|
145
|
+
nn = n * p
|
|
146
|
+
dev0, dev, df = fit[:, 8].sum(), fit[:, 1].sum(), fit[:, 2].sum()
|
|
147
|
+
dif = dev0 - dev
|
|
148
|
+
cs = 1 - np.exp(-dif / nn)
|
|
149
|
+
ll = fit[:, 0].sum()
|
|
150
|
+
return {"coefficients": pd.DataFrame(coef, index=names, columns=[f"Dim_{k + 1}" for k in range(dim)]),
|
|
151
|
+
"thresholds": pd.DataFrame(thr, index=names, columns=[f"C_{c + 1}" for c in range(thr.shape[1])]),
|
|
152
|
+
"fit": pd.DataFrame(fit, index=names, columns=fitcols), "loadings": loadings,
|
|
153
|
+
"communalities": (loadings ** 2).sum(1), "ncats": ncat, "logLik": ll, "deviance": dev,
|
|
154
|
+
"null_deviance": dev0, "df": df, "dif": dif, "p": sps.chi2.sf(dif, df), "cox_snell": cs,
|
|
155
|
+
"nagelkerke": cs / (1 - np.exp(dev0 / -2) ** (2 / nn)), "mcfadden": 1 - dev / dev0,
|
|
156
|
+
"AIC": -2 * ll + 2 * df, "BIC": -2 * ll + df * np.log(nn), "models": models}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _ordinal_frame(Y):
|
|
160
|
+
if isinstance(Y, pd.DataFrame):
|
|
161
|
+
names = [str(c) for c in Y.columns]
|
|
162
|
+
cols = [_codes(Y[c]) for c in Y.columns]
|
|
163
|
+
else:
|
|
164
|
+
Y = np.asarray(Y)
|
|
165
|
+
names = [f"V{j + 1}" for j in range(Y.shape[1])]
|
|
166
|
+
cols = [_codes(Y[:, j]) for j in range(Y.shape[1])]
|
|
167
|
+
return np.column_stack([c[0] for c in cols]), [c[1] for c in cols], names
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --------------------------------------------------------------------------- binary
|
|
171
|
+
def constrained_logistic_biplot(Y, X, dim=2, scaling=5, penalization=0.1, tol=1e-5, maxiter=100,
|
|
172
|
+
row_names=None) -> LogisticBiplot:
|
|
173
|
+
"""Constrained binary logistic biplot (``MultBiplotR::ConstrainedLogisticBiplot``).
|
|
174
|
+
|
|
175
|
+
The row coordinates are linear combinations of the explanatory variables
|
|
176
|
+
``X``: each binary response is regressed on ``X`` (ridge logistic
|
|
177
|
+
regression, ``penalization``), the standardized linear predictors are
|
|
178
|
+
decomposed by SVD and the first ``dim`` components (scaled by the singular
|
|
179
|
+
values) are the row coordinates. Each response is then refitted on them
|
|
180
|
+
(ridge penalty 0.2, as in R) and the explanatory variables are projected as
|
|
181
|
+
supplementary continuous variables.
|
|
182
|
+
|
|
183
|
+
Two details differ from MultBiplotR on purpose: the p-values of the
|
|
184
|
+
deviance tests use ``dim`` degrees of freedom (R uses 1), and the
|
|
185
|
+
percentages of correct classification threshold the probability at 0.5 (R
|
|
186
|
+
thresholds the linear predictor at 0.5)."""
|
|
187
|
+
if isinstance(Y, pd.DataFrame):
|
|
188
|
+
col_names = [str(c) for c in Y.columns]; row_names = row_names or [str(i) for i in Y.index]
|
|
189
|
+
else:
|
|
190
|
+
col_names = [f"V{j + 1}" for j in range(np.shape(Y)[1])]
|
|
191
|
+
x_names = [str(c) for c in X.columns] if isinstance(X, pd.DataFrame) else None
|
|
192
|
+
Y = np.asarray(Y, float); Xo = np.asarray(X, float)
|
|
193
|
+
n, p = Y.shape
|
|
194
|
+
q = Xo.shape[1]
|
|
195
|
+
r = min(p, q)
|
|
196
|
+
tname = transform_name(scaling)
|
|
197
|
+
Xs = initial_transform(Xo, tname)["X"]
|
|
198
|
+
Xd = np.column_stack([np.ones(n), Xs])
|
|
199
|
+
Yh = np.zeros((n, p)); Betas = np.zeros((p, q + 1))
|
|
200
|
+
for j in range(p):
|
|
201
|
+
fit = ridge_logistic(Y[:, j], Xd, penalization=penalization, intercept=False, tol=tol, maxiter=maxiter)
|
|
202
|
+
Betas[j] = fit["beta"]; Yh[:, j] = Xd @ fit["beta"]
|
|
203
|
+
Yh = initial_transform(Yh, "Standardize columns")["X"]
|
|
204
|
+
U, d, Vt = np.linalg.svd(Yh, full_matrices=False)
|
|
205
|
+
ev = d[:r] ** 2
|
|
206
|
+
A = U[:, :dim] * d[:dim]
|
|
207
|
+
aa = np.column_stack([np.ones(n), A])
|
|
208
|
+
B = np.vstack([_ridge_binary_fit(Y[:, j], aa, 0.2, tol, maxiter) for j in range(p)])
|
|
209
|
+
esp = aa @ B.T
|
|
210
|
+
P = expit(esp)
|
|
211
|
+
pred = (P > 0.5).astype(float)
|
|
212
|
+
acier = pred == Y
|
|
213
|
+
pres = Y.sum(0); abs_ = n - pres
|
|
214
|
+
sens = ((acier) & (Y == 1)).sum(0) / pres
|
|
215
|
+
spec = ((acier) & (Y == 0)).sum(0) / abs_
|
|
216
|
+
p0 = expit(B[:, 0])
|
|
217
|
+
d1 = -2 * (Y * np.log(p0) + (1 - Y) * np.log(1 - p0)).sum(0)
|
|
218
|
+
d2 = -2 * (Y * np.log(P) + (1 - Y) * np.log(1 - P)).sum(0)
|
|
219
|
+
dev = d1 - d2
|
|
220
|
+
cs = 1 - np.exp(-dev / n)
|
|
221
|
+
dd = np.sqrt(1 + (B[:, 1:] ** 2).sum(1))
|
|
222
|
+
loadings = B[:, 1:] / dd[:, None]
|
|
223
|
+
row_names = row_names or [f"I{i + 1}" for i in range(n)]
|
|
224
|
+
cols = pd.DataFrame(np.column_stack([B, dev, sps.chi2.sf(dev, dim), acier.mean(0) * 100, 1 - d2 / d1]),
|
|
225
|
+
index=col_names, columns=["b0"] + [f"b{k + 1}" for k in range(dim)] +
|
|
226
|
+
["Deviance", "p-value", "% correct", "McFadden R2"])
|
|
227
|
+
out = LogisticBiplot(
|
|
228
|
+
title="Constrained Binary Logistic Biplot", n=n, p=p, dim=dim, penalization=penalization, A=A, B=B,
|
|
229
|
+
fitted=P, objective=float(d2.sum() / 2), converged=True, columns=cols,
|
|
230
|
+
betas=pd.DataFrame(Betas, index=col_names, columns=["Intercept"] + (x_names or [f"X{k + 1}" for k in range(q)])),
|
|
231
|
+
eigenvalues=ev, inertia=ev / ev.sum() * 100, null_deviances=d1, model_deviances=d2, deviances=dev,
|
|
232
|
+
cox_snell=cs, nagelkerke=cs / (1 - np.exp(d1 / -2) ** (2 / n)), mcfadden=1 - d2 / d1,
|
|
233
|
+
loadings=loadings, communalities=(loadings ** 2).sum(1), sensitivity=sens, specificity=spec,
|
|
234
|
+
global_percent=acier.mean() * 100, row_percent=acier.mean(1) * 100, row_names=row_names, col_names=col_names)
|
|
235
|
+
out.row_coordinates = A
|
|
236
|
+
out.sup_variables = add_continuous_variables(out, Xo, dims=dim, scaling=scaling, names=x_names)
|
|
237
|
+
return out
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------------------- ordinal
|
|
241
|
+
def constrained_ordinal_logistic_biplot(Y, X, dim=2, scaling=5, penalization=0.1, tol=1e-5, maxiter=100,
|
|
242
|
+
row_names=None) -> LogisticBiplot:
|
|
243
|
+
"""Constrained ordinal logistic biplot (``MultBiplotR::ConstrainedOrdinalLogisticBiplot``).
|
|
244
|
+
|
|
245
|
+
``Y``: ordinal variables (DataFrame of ordered categoricals, or integer
|
|
246
|
+
codes 1..C). Each is regressed on the transformed ``X`` by ridge
|
|
247
|
+
cumulative-logit regression; the standardized linear predictors (first
|
|
248
|
+
threshold) are decomposed by SVD; the first ``dim`` components are the row
|
|
249
|
+
coordinates and each ordinal variable is refitted on them."""
|
|
250
|
+
Yc, cats, names = _ordinal_frame(Y)
|
|
251
|
+
if isinstance(Y, pd.DataFrame):
|
|
252
|
+
row_names = row_names or [str(i) for i in Y.index]
|
|
253
|
+
x_names = [str(c) for c in X.columns] if isinstance(X, pd.DataFrame) else None
|
|
254
|
+
Xs = initial_transform(np.asarray(X, float), transform_name(scaling))["X"]
|
|
255
|
+
n, p = Yc.shape
|
|
256
|
+
r = min(p, Xs.shape[1])
|
|
257
|
+
Yh = np.zeros((n, p)); fits = []
|
|
258
|
+
for j in range(p):
|
|
259
|
+
m = ridge_ordinal_logistic(Yc[:, j], Xs, penalization, tol, maxiter)
|
|
260
|
+
fits.append(m)
|
|
261
|
+
e = m["eta"][:, 0]
|
|
262
|
+
Yh[:, j] = (e - e.mean()) / e.std(ddof=1)
|
|
263
|
+
U, d, Vt = np.linalg.svd(Yh, full_matrices=False)
|
|
264
|
+
ev = d[:r] ** 2
|
|
265
|
+
A = U[:, :dim] * d[:dim]
|
|
266
|
+
cp = _ordinal_columns(Yc, A, penalization, tol, maxiter, names)
|
|
267
|
+
row_names = row_names or [f"I{i + 1}" for i in range(n)]
|
|
268
|
+
cols = pd.concat([cp["coefficients"], cp["fit"][["Deviance", "p-value", "PCC"]]], axis=1)
|
|
269
|
+
out = LogisticBiplot(
|
|
270
|
+
title="Constrained Ordinal Logistic Biplot", n=n, p=p, dim=dim, penalization=penalization, A=A,
|
|
271
|
+
B=cp["coefficients"].to_numpy(), thresholds=[cp["thresholds"].to_numpy()[j, :cp["ncats"][j] - 1] for j in range(p)],
|
|
272
|
+
objective=-cp["logLik"], converged=True, columns=cols, column_parameters=cp, eigenvalues=ev,
|
|
273
|
+
inertia=ev / ev.sum() * 100, categories=cats, regressions=fits,
|
|
274
|
+
global_percent=float(np.mean(cp["fit"]["PCC"]) * 100), row_names=row_names, col_names=names,
|
|
275
|
+
loadings=cp["loadings"], communalities=cp["communalities"], deviance=cp["deviance"],
|
|
276
|
+
null_deviance=cp["null_deviance"], df=cp["df"], pvalue=cp["p"], AIC=cp["AIC"], BIC=cp["BIC"])
|
|
277
|
+
out.row_coordinates = A
|
|
278
|
+
out.sup_variables = add_continuous_variables(out, Xs, dims=dim, scaling=scaling, names=x_names)
|
|
279
|
+
return out
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def external_ordinal_logistic_biplot(coords, Y, penalization=0.2, tol=1e-6, maxiter=100,
|
|
283
|
+
row_names=None) -> LogisticBiplot:
|
|
284
|
+
"""External ordinal logistic biplot: each ordinal variable is fitted by a
|
|
285
|
+
ridge cumulative-logit regression on a given configuration ``coords``
|
|
286
|
+
(principal components, coordinates of a PCoA or an MDS...), as
|
|
287
|
+
``MultBiplotR::AddOrdVars2Biplot``. The lines of the variables and the
|
|
288
|
+
category boundaries are drawn with ``plot()``."""
|
|
289
|
+
A = np.asarray(getattr(coords, "row_coordinates", coords), float)
|
|
290
|
+
Yc, cats, names = _ordinal_frame(Y)
|
|
291
|
+
n, p = Yc.shape
|
|
292
|
+
cp = _ordinal_columns(Yc, A, penalization, tol, maxiter, names)
|
|
293
|
+
cols = pd.concat([cp["coefficients"], cp["fit"][["Deviance", "p-value", "PCC"]]], axis=1)
|
|
294
|
+
return LogisticBiplot(
|
|
295
|
+
title="External Ordinal Logistic Biplot", n=n, p=p, dim=A.shape[1], penalization=penalization, A=A,
|
|
296
|
+
B=cp["coefficients"].to_numpy(), thresholds=[cp["thresholds"].to_numpy()[j, :cp["ncats"][j] - 1] for j in range(p)],
|
|
297
|
+
objective=-cp["logLik"], converged=True, columns=cols, column_parameters=cp, categories=cats,
|
|
298
|
+
global_percent=float(np.mean(cp["fit"]["PCC"]) * 100), row_names=row_names or [f"I{i + 1}" for i in range(n)],
|
|
299
|
+
col_names=names, loadings=cp["loadings"], communalities=cp["communalities"], deviance=cp["deviance"],
|
|
300
|
+
null_deviance=cp["null_deviance"], df=cp["df"], pvalue=cp["p"], AIC=cp["AIC"], BIC=cp["BIC"])
|
biplotpy/roptim.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""R's ``optim(method = "CG")``: conjugate gradients of Nash (1990), as coded in
|
|
2
|
+
R's ``src/appl/optim.c`` (``cgmin``), so that procedures that MultBiplotR runs
|
|
3
|
+
with ``optim(..., method = "CG")`` follow the same path in Python."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
_STEPREDN = 0.2
|
|
11
|
+
_ACCTOL = 0.0001
|
|
12
|
+
_RELTEST = 10.0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def optim_cg(fn, gr, par, args=(), type: int = 1, maxit: int = 100, abstol: float = -math.inf,
|
|
16
|
+
reltol: float = 1.4901161193847656e-08):
|
|
17
|
+
"""Minimize ``fn`` from ``par`` with R's CG method (``type`` 1 Fletcher-Reeves,
|
|
18
|
+
2 Polak-Ribiere, 3 Beale-Sorenson). Returns a dict like ``optim``: ``par``,
|
|
19
|
+
``value``, ``counts`` and ``convergence`` (1 when ``maxit`` is reached)."""
|
|
20
|
+
B = [float(v) for v in np.asarray(par, float).ravel()]
|
|
21
|
+
n = len(B)
|
|
22
|
+
f_ = lambda b: float(fn(np.array(b), *args))
|
|
23
|
+
g_ = lambda b: [float(v) for v in np.asarray(gr(np.array(b), *args), float).ravel()]
|
|
24
|
+
if maxit <= 0:
|
|
25
|
+
return {"par": np.array(B), "value": f_(B), "counts": (0, 0), "convergence": 0}
|
|
26
|
+
c = [0.0] * n; t = [0.0] * n; X = [0.0] * n
|
|
27
|
+
setstep = 1.7
|
|
28
|
+
fail = 0
|
|
29
|
+
cyclimit = n
|
|
30
|
+
tol = reltol * n * math.sqrt(reltol)
|
|
31
|
+
f = f_(B)
|
|
32
|
+
if not math.isfinite(f):
|
|
33
|
+
raise ValueError("Function cannot be evaluated at initial parameters")
|
|
34
|
+
Fmin = f
|
|
35
|
+
funcount, gradcount = 1, 0
|
|
36
|
+
steplength = 1.0
|
|
37
|
+
while True:
|
|
38
|
+
t = [0.0] * n; c = [0.0] * n
|
|
39
|
+
cycle = 0
|
|
40
|
+
oldstep = 1.0
|
|
41
|
+
count = 0
|
|
42
|
+
while True:
|
|
43
|
+
cycle += 1
|
|
44
|
+
gradcount += 1
|
|
45
|
+
if gradcount > maxit:
|
|
46
|
+
return {"par": np.array(X), "value": Fmin, "counts": (funcount, gradcount), "convergence": 1}
|
|
47
|
+
g = g_(B)
|
|
48
|
+
G1 = 0.0; G2 = 0.0
|
|
49
|
+
for i in range(n):
|
|
50
|
+
X[i] = B[i]
|
|
51
|
+
if type == 1:
|
|
52
|
+
G1 += g[i] * g[i]; G2 += c[i] * c[i]
|
|
53
|
+
elif type == 2:
|
|
54
|
+
G1 += g[i] * (g[i] - c[i]); G2 += c[i] * c[i]
|
|
55
|
+
else:
|
|
56
|
+
G1 += g[i] * (g[i] - c[i]); G2 += t[i] * (g[i] - c[i])
|
|
57
|
+
c[i] = g[i]
|
|
58
|
+
if G1 > tol:
|
|
59
|
+
G3 = G1 / G2 if G2 > 0.0 else 1.0
|
|
60
|
+
gradproj = 0.0
|
|
61
|
+
for i in range(n):
|
|
62
|
+
t[i] = t[i] * G3 - g[i]
|
|
63
|
+
gradproj += t[i] * g[i]
|
|
64
|
+
steplength = oldstep
|
|
65
|
+
accpoint = False
|
|
66
|
+
while True:
|
|
67
|
+
count = 0
|
|
68
|
+
for i in range(n):
|
|
69
|
+
B[i] = X[i] + steplength * t[i]
|
|
70
|
+
if _RELTEST + X[i] == _RELTEST + B[i]:
|
|
71
|
+
count += 1
|
|
72
|
+
if count < n:
|
|
73
|
+
f = f_(B)
|
|
74
|
+
funcount += 1
|
|
75
|
+
accpoint = math.isfinite(f) and f <= Fmin + gradproj * steplength * _ACCTOL
|
|
76
|
+
if not accpoint:
|
|
77
|
+
steplength *= _STEPREDN
|
|
78
|
+
else:
|
|
79
|
+
Fmin = f
|
|
80
|
+
if count == n or accpoint:
|
|
81
|
+
break
|
|
82
|
+
if count < n:
|
|
83
|
+
newstep = 2 * (f - Fmin - gradproj * steplength)
|
|
84
|
+
if newstep > 0:
|
|
85
|
+
newstep = -(gradproj * steplength * steplength / newstep)
|
|
86
|
+
for i in range(n):
|
|
87
|
+
B[i] = X[i] + newstep * t[i]
|
|
88
|
+
Fmin = f
|
|
89
|
+
f = f_(B)
|
|
90
|
+
funcount += 1
|
|
91
|
+
if f < Fmin:
|
|
92
|
+
Fmin = f
|
|
93
|
+
else:
|
|
94
|
+
for i in range(n):
|
|
95
|
+
B[i] = X[i] + steplength * t[i]
|
|
96
|
+
oldstep = setstep * steplength
|
|
97
|
+
if oldstep > 1.0:
|
|
98
|
+
oldstep = 1.0
|
|
99
|
+
if not ((count != n) and (G1 > tol) and (cycle != cyclimit)):
|
|
100
|
+
break
|
|
101
|
+
if not ((cycle != 1) or ((count != n) and (G1 > tol) and Fmin > abstol)):
|
|
102
|
+
break
|
|
103
|
+
# like R's optim, the result is the last base point X (not the last trial point)
|
|
104
|
+
return {"par": np.array(X), "value": Fmin, "counts": (funcount, gradcount), "convergence": fail}
|