canbiplotme 0.3.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.
- canbiplotme/__init__.py +34 -0
- canbiplotme/data.py +304 -0
- canbiplotme/datasets/__init__.py +56 -0
- canbiplotme/datasets/gcb_trade.npz +0 -0
- canbiplotme/datasets/gcb_trade_a21.npz +0 -0
- canbiplotme/datasets/sim_me.npz +0 -0
- canbiplotme/datasets/sim_trade.csv +2521 -0
- canbiplotme/dynamic.py +105 -0
- canbiplotme/inference.py +281 -0
- canbiplotme/meta.py +118 -0
- canbiplotme/plot.py +48 -0
- canbiplotme/regions.py +94 -0
- canbiplotme/reml.py +124 -0
- canbiplotme/rng.py +100 -0
- canbiplotme/simulation.py +77 -0
- canbiplotme/stage1.py +189 -0
- canbiplotme/stage2.py +336 -0
- canbiplotme-0.3.0.dist-info/METADATA +107 -0
- canbiplotme-0.3.0.dist-info/RECORD +22 -0
- canbiplotme-0.3.0.dist-info/WHEEL +5 -0
- canbiplotme-0.3.0.dist-info/licenses/LICENSE +674 -0
- canbiplotme-0.3.0.dist-info/top_level.txt +1 -0
canbiplotme/stage2.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Second stage: canonical biplot of estimates measured with error (port of ``cbme``)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from itertools import combinations
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .data import MEData
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Settings:
|
|
15
|
+
dim: int = 2
|
|
16
|
+
T_errors: str = "independent"
|
|
17
|
+
mean_errors: str = "joint"
|
|
18
|
+
eps: float = 1e-6
|
|
19
|
+
metric: str = "mean"
|
|
20
|
+
weights: str = "n"
|
|
21
|
+
S_idx: list | None = None
|
|
22
|
+
T_fixed: np.ndarray | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CBMEFit:
|
|
26
|
+
"""Result of :func:`cbme`. Attributes mirror the R object.
|
|
27
|
+
|
|
28
|
+
``T``, ``T_raw``, ``T_eigen_raw``, ``mu`` (G x K), ``mu_cov`` (G x K x K),
|
|
29
|
+
``mu0``, ``n``, ``W``, ``Vbar``, ``H``, ``mbar``, ``lambda_`` (eigenvalues),
|
|
30
|
+
``V`` (canonical directions), ``A`` (group markers), ``Afull``, ``C``
|
|
31
|
+
(component markers), ``cos2``, ``quality``, ``ind`` (individuals with every
|
|
32
|
+
component identified), ``ind_ids``, ``stats`` and ``D2``.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, core: dict, data: MEData, settings: Settings, cache: dict):
|
|
36
|
+
self.__dict__.update(core)
|
|
37
|
+
self.data = data
|
|
38
|
+
self.settings = settings
|
|
39
|
+
self.cache = cache
|
|
40
|
+
|
|
41
|
+
def _update(self, core: dict):
|
|
42
|
+
self.__dict__.update(core)
|
|
43
|
+
|
|
44
|
+
# convenient labelled views -------------------------------------------
|
|
45
|
+
def markers(self) -> pd.DataFrame:
|
|
46
|
+
cols = [f"Dim{d + 1}" for d in range(self.A.shape[1])]
|
|
47
|
+
out = pd.DataFrame(self.A, index=self.data.levels, columns=cols)
|
|
48
|
+
out["quality"] = self.quality
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
def component_markers(self) -> pd.DataFrame:
|
|
52
|
+
return pd.DataFrame(self.C, index=self.data.components,
|
|
53
|
+
columns=[f"Dim{d + 1}" for d in range(self.C.shape[1])])
|
|
54
|
+
|
|
55
|
+
def means(self) -> pd.DataFrame:
|
|
56
|
+
return pd.DataFrame(self.mu, index=self.data.levels, columns=self.data.components)
|
|
57
|
+
|
|
58
|
+
def se_means(self) -> pd.DataFrame:
|
|
59
|
+
se = np.sqrt(np.einsum("gkk->gk", self.mu_cov))
|
|
60
|
+
return pd.DataFrame(se, index=self.data.levels, columns=self.data.components)
|
|
61
|
+
|
|
62
|
+
def summary(self) -> dict:
|
|
63
|
+
lam = self.lambda_
|
|
64
|
+
return {
|
|
65
|
+
"eigen": pd.DataFrame({"lambda": lam, "inertia": np.cumsum(lam) / lam.sum()},
|
|
66
|
+
index=range(1, len(lam) + 1)),
|
|
67
|
+
"heterogeneity": pd.DataFrame({"T": np.diag(self.T), "T_raw": np.diag(self.T_raw),
|
|
68
|
+
"Vbar": np.diag(self.Vbar)}, index=self.data.components),
|
|
69
|
+
"T_eigen_raw": self.T_eigen_raw,
|
|
70
|
+
"condition_W": np.linalg.cond(self.W),
|
|
71
|
+
"means": self.means(), "se_means": self.se_means(),
|
|
72
|
+
"structure": pd.DataFrame(self.cos2[:, :self.A.shape[1]], index=self.data.components),
|
|
73
|
+
"D2": self.D2, "stats": self.stats,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def plot(self, *args, **kwargs):
|
|
77
|
+
from .plot import plot_cbme
|
|
78
|
+
return plot_cbme(self, *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
L = min(len(self.n) - 1, self.W.shape[0])
|
|
82
|
+
lam = self.lambda_[:L]
|
|
83
|
+
s = ["Canonical biplot of estimates measured with error",
|
|
84
|
+
f"{int(sum(self.n))} individuals, {len(self.n)} groups, {self.W.shape[0]} components", "",
|
|
85
|
+
"Eigenvalues: " + ", ".join(f"{v:.3f}" for v in lam),
|
|
86
|
+
"Cumulative inertia: " + ", ".join(f"{v:.3f}" for v in np.cumsum(lam) / lam.sum()), "",
|
|
87
|
+
"Statistics: " + ", ".join(f"{k} = {v:.4g}" for k, v in self.stats.items()), "",
|
|
88
|
+
"Group markers:", self.markers().round(3).to_string(), "",
|
|
89
|
+
"Component markers:", self.component_markers().round(3).to_string()]
|
|
90
|
+
return "\n".join(s)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cbme(x: MEData, dim: int = 2, T_errors: str = "independent", mean_errors: str = "joint",
|
|
94
|
+
eps: float = 1e-6, metric: str = "mean", weights: str = "n", S_vars=None,
|
|
95
|
+
T_fixed=None) -> CBMEFit:
|
|
96
|
+
"""Canonical biplot of estimates measured with error (chapter 3 of the thesis).
|
|
97
|
+
|
|
98
|
+
Estimates the heterogeneity T by moments (Definition 3.3.1) and projects it
|
|
99
|
+
onto the PSD cone, computes the precision-weighted group means and their
|
|
100
|
+
covariance (Definition 3.3.2), the metric W = T + Vbar, the between matrix H
|
|
101
|
+
and the canonical solution H v = lambda W v with v'Wv = 1.
|
|
102
|
+
|
|
103
|
+
Parameters mirror the R function ``cbme``.
|
|
104
|
+
"""
|
|
105
|
+
if T_errors not in ("independent", "joint"):
|
|
106
|
+
raise ValueError("T_errors must be 'independent' or 'joint'")
|
|
107
|
+
if mean_errors not in ("joint", "independent"):
|
|
108
|
+
raise ValueError("mean_errors must be 'joint' or 'independent'")
|
|
109
|
+
if metric not in ("mean", "median", "group"):
|
|
110
|
+
raise ValueError("metric must be 'mean', 'median' or 'group'")
|
|
111
|
+
if weights not in ("n", "equal"):
|
|
112
|
+
raise ValueError("weights must be 'n' or 'equal'")
|
|
113
|
+
st = Settings(dim=dim, T_errors=T_errors, mean_errors=mean_errors, eps=eps, metric=metric,
|
|
114
|
+
weights=weights, T_fixed=None if T_fixed is None else np.asarray(T_fixed, float))
|
|
115
|
+
if not x.has_joint:
|
|
116
|
+
st.T_errors = "independent"
|
|
117
|
+
st.mean_errors = "independent"
|
|
118
|
+
if S_vars is not None:
|
|
119
|
+
st.S_idx = [x.components.index(v) if isinstance(v, str) else int(v) for v in S_vars]
|
|
120
|
+
cache = make_cache(x)
|
|
121
|
+
core = fit_core(x, x.beta, np.arange(x.N), st, cache)
|
|
122
|
+
return CBMEFit(core, x, st, cache)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def make_cache(x: MEData) -> dict:
|
|
126
|
+
pos = np.zeros(x.N, dtype=int)
|
|
127
|
+
for lev in x.levels:
|
|
128
|
+
idx = np.where(x.group == lev)[0]
|
|
129
|
+
pos[idx] = np.arange(len(idx))
|
|
130
|
+
Om = {lev: x.group_omega(lev) for lev in x.levels} if x.has_joint else None
|
|
131
|
+
out = {"pos": pos, "Omega": Om}
|
|
132
|
+
if x.block is not None and x.has_joint:
|
|
133
|
+
bpos = np.zeros(x.N, dtype=int)
|
|
134
|
+
for b in x.block_levels:
|
|
135
|
+
idx = np.where(x.block == b)[0]
|
|
136
|
+
bpos[idx] = np.arange(len(idx))
|
|
137
|
+
out["bpos"] = bpos
|
|
138
|
+
out["OmB"] = {b: x.block_omega(b) for b in x.block_levels}
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _omega_cross(x: MEData, cache: dict, r1: np.ndarray, r2: np.ndarray) -> np.ndarray:
|
|
143
|
+
b = x.block[r1[0]]
|
|
144
|
+
K = x.K
|
|
145
|
+
c1 = (cache["bpos"][r1][:, None] * K + np.arange(K)[None, :]).ravel()
|
|
146
|
+
c2 = (cache["bpos"][r2][:, None] * K + np.arange(K)[None, :]).ravel()
|
|
147
|
+
return cache["OmB"][b][np.ix_(c1, c2)]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _omega_rows(x: MEData, cache: dict, rows: np.ndarray) -> np.ndarray:
|
|
151
|
+
lev = x.group[rows[0]]
|
|
152
|
+
K = x.K
|
|
153
|
+
cols = (cache["pos"][rows][:, None] * K + np.arange(K)[None, :]).ravel()
|
|
154
|
+
return cache["Omega"][lev][np.ix_(cols, cols)]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def fit_core(x: MEData, beta: np.ndarray, rows: np.ndarray, st: Settings, cache: dict,
|
|
158
|
+
fill_missing: bool = True) -> dict:
|
|
159
|
+
K = x.K
|
|
160
|
+
rows = np.asarray(rows)
|
|
161
|
+
grp = x.group[rows]
|
|
162
|
+
levs = x.levels
|
|
163
|
+
Va = x.V[rows]
|
|
164
|
+
ok = np.isfinite(beta)
|
|
165
|
+
joint_T = st.T_errors == "joint"
|
|
166
|
+
joint_m = st.mean_errors == "joint"
|
|
167
|
+
Om = None
|
|
168
|
+
if joint_T or joint_m:
|
|
169
|
+
Om = {}
|
|
170
|
+
for lev in levs:
|
|
171
|
+
r = np.where(grp == lev)[0]
|
|
172
|
+
Om[lev] = _omega_rows(x, cache, rows[r]) if len(r) else None
|
|
173
|
+
# ---- heterogeneity (Definition 3.3.1) ----
|
|
174
|
+
if st.T_fixed is None:
|
|
175
|
+
Traw = np.zeros((K, K))
|
|
176
|
+
for k in range(K):
|
|
177
|
+
for l in range(k, K):
|
|
178
|
+
S = E = den = 0.0
|
|
179
|
+
for lev in levs:
|
|
180
|
+
r = np.where((grp == lev) & ok[:, k] & ok[:, l])[0]
|
|
181
|
+
m = len(r)
|
|
182
|
+
if m < 2:
|
|
183
|
+
continue
|
|
184
|
+
dk = beta[r, k] - beta[r, k].mean()
|
|
185
|
+
dl = beta[r, l] - beta[r, l].mean()
|
|
186
|
+
S += np.sum(dk * dl)
|
|
187
|
+
if joint_T:
|
|
188
|
+
rg = np.where(grp == lev)[0]
|
|
189
|
+
j = np.searchsorted(rg, r)
|
|
190
|
+
Okl = Om[lev][np.ix_(j * K + k, j * K + l)]
|
|
191
|
+
Okl = np.where(np.isfinite(Okl), Okl, 0.0)
|
|
192
|
+
E += np.trace(Okl) - Okl.sum() / m
|
|
193
|
+
else:
|
|
194
|
+
E += (1 - 1 / m) * np.sum(Va[r, k, l])
|
|
195
|
+
den += m - 1
|
|
196
|
+
Traw[k, l] = Traw[l, k] = (S - E) / den if den > 0 else 0.0
|
|
197
|
+
ev, evec = np.linalg.eigh(Traw)
|
|
198
|
+
ev, evec = ev[::-1], evec[:, ::-1]
|
|
199
|
+
T = evec @ np.diag(np.maximum(ev, st.eps)) @ evec.T
|
|
200
|
+
T_eig = ev
|
|
201
|
+
else:
|
|
202
|
+
T = Traw = st.T_fixed
|
|
203
|
+
T_eig = np.linalg.eigvalsh(T)[::-1]
|
|
204
|
+
# ---- weights and means (Definition 3.3.2) ----
|
|
205
|
+
w = np.zeros((len(rows), K))
|
|
206
|
+
for k in range(K):
|
|
207
|
+
w[ok[:, k], k] = 1.0 / (T[k, k] + Va[ok[:, k], k, k])
|
|
208
|
+
bz = np.where(ok, beta, 0.0)
|
|
209
|
+
mu0 = (w * bz).sum(0) / w.sum(0)
|
|
210
|
+
G = len(levs)
|
|
211
|
+
mu = np.full((G, K), np.nan)
|
|
212
|
+
cv = np.zeros((G, K, K))
|
|
213
|
+
n = np.array([np.sum(grp == lev) for lev in levs], dtype=float)
|
|
214
|
+
for gi, lev in enumerate(levs):
|
|
215
|
+
r = np.where(grp == lev)[0]
|
|
216
|
+
if not len(r):
|
|
217
|
+
continue
|
|
218
|
+
wg = w[r]
|
|
219
|
+
sw = wg.sum(0)
|
|
220
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
221
|
+
mu[gi] = np.where(sw > 0, (wg * bz[r]).sum(0) / np.where(sw > 0, sw, 1), np.nan)
|
|
222
|
+
for k in range(K):
|
|
223
|
+
for l in range(k, K):
|
|
224
|
+
if sw[k] == 0 or sw[l] == 0:
|
|
225
|
+
cv[gi, k, l] = cv[gi, l, k] = np.nan
|
|
226
|
+
continue
|
|
227
|
+
num = T[k, l] * np.sum(wg[:, k] * wg[:, l])
|
|
228
|
+
if joint_m:
|
|
229
|
+
jj = np.arange(len(r))
|
|
230
|
+
Okl = Om[lev][np.ix_(jj * K + k, jj * K + l)]
|
|
231
|
+
Okl = np.where(np.isfinite(Okl), Okl, 0.0)
|
|
232
|
+
num += wg[:, k] @ Okl @ wg[:, l]
|
|
233
|
+
else:
|
|
234
|
+
vv = np.where(np.isfinite(Va[r, k, l]), Va[r, k, l], 0.0)
|
|
235
|
+
num += np.sum(wg[:, k] * wg[:, l] * vv)
|
|
236
|
+
cv[gi, k, l] = cv[gi, l, k] = num / (sw[k] * sw[l])
|
|
237
|
+
# covariance between the means of two groups of the same error block
|
|
238
|
+
cx = None
|
|
239
|
+
if joint_m and "OmB" in cache:
|
|
240
|
+
cx = np.zeros((G, G, K, K))
|
|
241
|
+
blk = x.block[rows]
|
|
242
|
+
blk_g = [blk[grp == lev][0] if np.any(grp == lev) else None for lev in levs]
|
|
243
|
+
for gi in range(G - 1):
|
|
244
|
+
for hi in range(gi + 1, G):
|
|
245
|
+
if blk_g[gi] is None or blk_g[gi] != blk_g[hi]:
|
|
246
|
+
continue
|
|
247
|
+
rg = np.where(grp == levs[gi])[0]
|
|
248
|
+
rh = np.where(grp == levs[hi])[0]
|
|
249
|
+
Ogh = _omega_cross(x, cache, rows[rg], rows[rh])
|
|
250
|
+
Ogh = np.where(np.isfinite(Ogh), Ogh, 0.0)
|
|
251
|
+
wg, wh = w[rg], w[rh]
|
|
252
|
+
sg, sh = wg.sum(0), wh.sum(0)
|
|
253
|
+
for k in range(K):
|
|
254
|
+
for l in range(K):
|
|
255
|
+
if sg[k] == 0 or sh[l] == 0:
|
|
256
|
+
cx[gi, hi, k, l] = np.nan
|
|
257
|
+
continue
|
|
258
|
+
blkm = Ogh[np.ix_(np.arange(len(rg)) * K + k, np.arange(len(rh)) * K + l)]
|
|
259
|
+
cx[gi, hi, k, l] = wg[:, k] @ blkm @ wh[:, l] / (sg[k] * sh[l])
|
|
260
|
+
cx[hi, gi] = cx[gi, hi].T
|
|
261
|
+
missing = np.isnan(mu)
|
|
262
|
+
if fill_missing and missing.any():
|
|
263
|
+
# a component without individuals in a group takes the common mean (section 3.6.6)
|
|
264
|
+
for gi in np.where(missing.any(1))[0]:
|
|
265
|
+
for k in np.where(missing[gi])[0]:
|
|
266
|
+
mu[gi, k] = mu0[k]
|
|
267
|
+
cv[gi, k, :] = 0.0
|
|
268
|
+
cv[gi, :, k] = 0.0
|
|
269
|
+
cv[gi, k, k] = T[k, k]
|
|
270
|
+
# ---- metric ----
|
|
271
|
+
Vbar = np.zeros((K, K))
|
|
272
|
+
for k in range(K):
|
|
273
|
+
for l in range(K):
|
|
274
|
+
r = np.where(ok[:, k] & ok[:, l])[0]
|
|
275
|
+
v = Va[r, k, l]
|
|
276
|
+
if st.metric == "mean":
|
|
277
|
+
Vbar[k, l] = v.mean()
|
|
278
|
+
elif st.metric == "median":
|
|
279
|
+
Vbar[k, l] = np.median(v)
|
|
280
|
+
else:
|
|
281
|
+
gm = [v[grp[r] == lev].mean() for lev in levs if np.any(grp[r] == lev)]
|
|
282
|
+
Vbar[k, l] = np.mean(gm)
|
|
283
|
+
Vbar = (Vbar + Vbar.T) / 2
|
|
284
|
+
W = T + Vbar
|
|
285
|
+
# ---- between matrix and canonical solution ----
|
|
286
|
+
nw = n if st.weights == "n" else np.full(G, n.sum() / G)
|
|
287
|
+
keep = n > 0
|
|
288
|
+
mbar = (mu[keep] * nw[keep, None]).sum(0) / nw[keep].sum()
|
|
289
|
+
Dm = mu - mbar
|
|
290
|
+
Dm[~keep] = 0.0
|
|
291
|
+
H = (Dm * np.sqrt(nw)[:, None]).T @ (Dm * np.sqrt(nw)[:, None]) / (keep.sum() - 1)
|
|
292
|
+
L = np.linalg.cholesky(W)
|
|
293
|
+
Li = np.linalg.inv(L)
|
|
294
|
+
ev, evec = np.linalg.eigh(Li @ H @ Li.T)
|
|
295
|
+
ev, evec = ev[::-1], evec[:, ::-1]
|
|
296
|
+
Vc = Li.T @ evec
|
|
297
|
+
s = np.sign(Vc[np.abs(Vc).argmax(0), np.arange(K)])
|
|
298
|
+
s[s == 0] = 1
|
|
299
|
+
s[min(st.dim, K):] = 1 # sign fixed only for the first `dim` axes, as in the thesis
|
|
300
|
+
Vc = Vc * s
|
|
301
|
+
lam = np.maximum(ev, 0.0)
|
|
302
|
+
d = st.dim
|
|
303
|
+
Afull = Dm @ Vc
|
|
304
|
+
A = Afull[:, :d]
|
|
305
|
+
WV = W @ Vc
|
|
306
|
+
C = WV[:, :d]
|
|
307
|
+
cos2 = WV ** 2 / np.diag(W)[:, None]
|
|
308
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
309
|
+
quality = (A ** 2).sum(1) / (Afull ** 2).sum(1)
|
|
310
|
+
comp = np.where(ok.sum(1) == K)[0]
|
|
311
|
+
ind = (beta[comp] - mbar) @ Vc[:, :d]
|
|
312
|
+
stats = {"trace": lam.sum(), "lambda1": lam[0], "Pi2": lam[:min(2, K)].sum() / lam.sum()}
|
|
313
|
+
if st.S_idx is not None:
|
|
314
|
+
p = cos2[:, 0] / cos2[:, 0].sum()
|
|
315
|
+
c2 = cos2[:, :min(2, K)]
|
|
316
|
+
stats["S"] = p[st.S_idx].sum()
|
|
317
|
+
stats["S_plane"] = c2[st.S_idx].sum() / c2.sum()
|
|
318
|
+
D2 = pairwise_D2(mu, cv, levs, cx)
|
|
319
|
+
return {"T": T, "T_raw": Traw, "T_eigen_raw": T_eig, "mu": mu, "mu_cov": cv, "mu_cross": cx, "mu0": mu0,
|
|
320
|
+
"missing_components": missing, "n": n, "W": W, "Vbar": Vbar, "H": H, "mbar": mbar,
|
|
321
|
+
"lambda_": lam, "V": Vc, "A": A, "Afull": Afull, "C": C, "cos2": cos2,
|
|
322
|
+
"quality": quality, "ind": ind, "ind_ids": [x.id[i] for i in rows[comp]],
|
|
323
|
+
"stats": stats, "D2": D2, "weights_means": w}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def pairwise_D2(mu: np.ndarray, cv: np.ndarray, levs, cx=None) -> pd.Series:
|
|
327
|
+
out = {}
|
|
328
|
+
for a, b in combinations(range(mu.shape[0]), 2):
|
|
329
|
+
dlt = mu[a] - mu[b]
|
|
330
|
+
S = cv[a] + cv[b]
|
|
331
|
+
if cx is not None:
|
|
332
|
+
C = np.where(np.isfinite(cx[a, b]), cx[a, b], 0.0)
|
|
333
|
+
S = S - C - C.T
|
|
334
|
+
ok = np.isfinite(dlt) & np.isfinite(np.diag(S))
|
|
335
|
+
out[f"{levs[a]}-{levs[b]}"] = float(dlt[ok] @ np.linalg.solve(S[np.ix_(ok, ok)], dlt[ok]))
|
|
336
|
+
return pd.Series(out)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: canbiplotme
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Canonical biplot for grouped estimates measured with error (Python port of the R package gcbiplot, Gravity Canonical Biplot)
|
|
5
|
+
Author-email: Carlos Freire-Cadme <cfreirec@unemi.edu.ec>
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/cfreirec/canbiplotme
|
|
8
|
+
Project-URL: Source, https://github.com/cfreirec/canbiplotme
|
|
9
|
+
Project-URL: Issues, https://github.com/cfreirec/canbiplotme/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.22
|
|
17
|
+
Requires-Dist: scipy>=1.8
|
|
18
|
+
Requires-Dist: pandas>=1.4
|
|
19
|
+
Provides-Extra: gravity
|
|
20
|
+
Requires-Dist: pyfixest>=0.30; extra == "gravity"
|
|
21
|
+
Provides-Extra: plot
|
|
22
|
+
Requires-Dist: matplotlib>=3.5; extra == "plot"
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: pyfixest>=0.30; extra == "all"
|
|
25
|
+
Requires-Dist: matplotlib>=3.5; extra == "all"
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest; extra == "test"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# canbiplotme
|
|
31
|
+
|
|
32
|
+
Canonical biplot for grouped estimates measured with error — Python port of the
|
|
33
|
+
R package **gcbiplot** (Gravity Canonical Biplot, formerly canbiplotME; Freire-Cadme, 2026, doctoral thesis *Biplot canónico y
|
|
34
|
+
modelo gravitacional de comercio: regímenes comerciales y exportadores medidos con error*).
|
|
35
|
+
|
|
36
|
+
The individuals of a canonical (MANOVA) biplot are estimated coefficient vectors,
|
|
37
|
+
each with its own measurement-error covariance, correlated within groups and with
|
|
38
|
+
components that may be undefined for some individuals.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install canbiplotme # core
|
|
42
|
+
pip install canbiplotme[all] # + pyfixest (first stage) and matplotlib
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Reproducing the thesis
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import canbiplotme as cb
|
|
49
|
+
x = cb.load_gcb_trade() # 115 exporters, 6 regimes, 5 frictions
|
|
50
|
+
fit = cb.gcb(x, S_vars=["log_dist", "comlang_off"]) # trace 18.579
|
|
51
|
+
t = cb.gcb_test(fit, B=1999, seed=20251025 + 17000000)
|
|
52
|
+
print(t) # p(trace) = 0.096; 2 of 15 pairs (Holm)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With an integer `seed`, `gcb_test` draws replicate `b` with `cb.RRNG(seed + b)`,
|
|
56
|
+
a generator that reproduces R's `set.seed()` and `rnorm()`: the replicates are
|
|
57
|
+
those of `gcbiplot::gcb_test()` and of the thesis (differences below 1e-11).
|
|
58
|
+
The `gcb*` names are aliases of the `cbme*` ones.
|
|
59
|
+
|
|
60
|
+
## Minimal example
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import canbiplotme as cb
|
|
64
|
+
x = cb.load_sim_me()
|
|
65
|
+
fit = cb.cbme(x, S_vars=["z1", "z2"]) # second stage
|
|
66
|
+
tt = cb.cbme_test(fit, B=1999, seed=1) # G0 bootstrap: trace, largest root, pairs (Holm), S
|
|
67
|
+
rg = cb.cbme_regions(fit, seed=1) # regions R1-R4
|
|
68
|
+
fit.plot(rg, region="R3")
|
|
69
|
+
cb.cbme_influence(fit, B_ref=200) # jackknife + reference
|
|
70
|
+
cb.cbme_simulate_tests(fit, nsim=1000, B=199) # validate the size in your own design
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
First stage from trade flows (needs `pyfixest`):
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
d = cb.load_sim_trade()
|
|
77
|
+
s1 = cb.gravity_by_individual(d, "flow", "orig", "dest", "year", ["ldist", "lang", "fta"], "regime", controls=["contig"])
|
|
78
|
+
bt = cb.pairs_bootstrap(s1, d, R=199)
|
|
79
|
+
cnt = cb.support_counts(d, "orig", "dest", ["lang", "fta"])
|
|
80
|
+
idn = cb.identify_d6(s1.beta, bt["V"], s1.id, cnt, ["lang", "fta"], components=s1.spec["frictions"])
|
|
81
|
+
fit = cb.cbme(cb.as_me_data(s1, bt, idn))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Validation against R
|
|
85
|
+
|
|
86
|
+
`tests/test_vs_r.py` compares with fixtures produced by the R package
|
|
87
|
+
(`tests/fixtures/make_fixtures.R`):
|
|
88
|
+
|
|
89
|
+
* second stage (heterogeneity, means and covariances, metric, eigenvalues,
|
|
90
|
+
markers, structure correlations, statistics, D²) in four configurations: max.
|
|
91
|
+
difference ~1e-14;
|
|
92
|
+
* regions R1–R3 and jackknife ρ: < 1e-10;
|
|
93
|
+
* first stage with pyfixest vs fixest: coefficients < 1e-7, also on a resampled
|
|
94
|
+
bootstrap dataset and in the dynamic extension. Clustered covariances differ by
|
|
95
|
+
~0.1 % because of the small-sample degrees-of-freedom convention of each library
|
|
96
|
+
(the analysis uses the bootstrap covariance).
|
|
97
|
+
|
|
98
|
+
Random draws (bootstrap, G0) use NumPy's generator, so p-values agree with R in
|
|
99
|
+
distribution, not replicate by replicate. The REML comparator `cbme_reml` reproduces `metafor::rma.mv` (T < 6e-7, means
|
|
100
|
+
< 2e-6, LRT < 1e-8). `cbme_permutation_test` is the permutation comparator.
|
|
101
|
+
|
|
102
|
+
`mvmeta` is a general multivariate random-effects meta-analysis and
|
|
103
|
+
meta-regression (unstructured between-study covariance, missing outcomes,
|
|
104
|
+
REML / ML / fixed), validated against `mixmeta` 1.2.2: coefficients, Psi,
|
|
105
|
+
covariance of the coefficients, log-likelihood and Cochran's Q agree to
|
|
106
|
+
< 1e-6 (REML with a badly scaled moderator, where the surface is flat: 4e-5 in
|
|
107
|
+
the coefficients at the same log-likelihood).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
canbiplotme/__init__.py,sha256=EDN7iB98LAPX5Ln8_4d2h9-X9oL2iR25v8LV0V-Ke0E,1957
|
|
2
|
+
canbiplotme/data.py,sha256=xw4M6FVCdMw99QFUjpf8omeESD5cLSxwPZulboPMZos,13222
|
|
3
|
+
canbiplotme/dynamic.py,sha256=SuwBZ8P9vjOXeVqfAvEyT8C1H2ATH6Eyy2Y8aecT7eg,4778
|
|
4
|
+
canbiplotme/inference.py,sha256=8kYyC2lijrlUEgWCoXstqySGZQ9MTEnb42zhTj5XWsk,11833
|
|
5
|
+
canbiplotme/meta.py,sha256=Ng_sUbBVssWDanG4DCZvjiICA3vD_mS5w-zR1k71kFA,5234
|
|
6
|
+
canbiplotme/plot.py,sha256=j7gw5TcuiCJzco9xUDSGcliwiElxy4loCgkoNGBNc30,2506
|
|
7
|
+
canbiplotme/regions.py,sha256=SNsXGRKRerGqEwZ2D6elUKxqmiDuBokEJBVW9kktiqU,3888
|
|
8
|
+
canbiplotme/reml.py,sha256=S1F5bTd1nAK73034WsjfK_uPSt-nYis4tgg9GpKN7Q4,5196
|
|
9
|
+
canbiplotme/rng.py,sha256=f5jfFbhsGwgnynwaETjxwvVa1CnZfUEBlxZnpD7EIfU,4747
|
|
10
|
+
canbiplotme/simulation.py,sha256=nhTnsw3c4qhEuueEHuh2Nme5LgWrLfhViiGjZyE-fFU,3644
|
|
11
|
+
canbiplotme/stage1.py,sha256=rdvTsTL3Db1o2Y6-mVv4BjrfsulGMk0-fxItN_TajbE,8197
|
|
12
|
+
canbiplotme/stage2.py,sha256=515qOD9RcCtuWWB3YnMuyV7_WMLhB5TPTxGkgEzwy2Y,14193
|
|
13
|
+
canbiplotme/datasets/__init__.py,sha256=gcX9ECNBsGZ4f-SIZF-lz73wdTQEjGaXXSpNwp27lQE,2688
|
|
14
|
+
canbiplotme/datasets/gcb_trade.npz,sha256=oSWUy9DPjpHzCuP-3rSIaH8TkNtLhsmkZ40BO5lCfdQ,699067
|
|
15
|
+
canbiplotme/datasets/gcb_trade_a21.npz,sha256=m7dBHbpJkzx-hTxE-6fxX8DjbwrM0B5OEjx5ML8LVZo,699580
|
|
16
|
+
canbiplotme/datasets/sim_me.npz,sha256=xXLuoNkXmQKtn41lrk7-4gZJCEi-l4JP72N03_PYgsY,128338
|
|
17
|
+
canbiplotme/datasets/sim_trade.csv,sha256=9qegJSkU9iTOqbtbQxbtCbjKQbwduZX1QQASoR_AQnk,120664
|
|
18
|
+
canbiplotme-0.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
19
|
+
canbiplotme-0.3.0.dist-info/METADATA,sha256=kA395qIeHJS3ttzbufwKdpPd87hfWgSnLOdCStiNeTk,4773
|
|
20
|
+
canbiplotme-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
21
|
+
canbiplotme-0.3.0.dist-info/top_level.txt,sha256=VsxDhM0lVq9n4PGzIucb_XheIKROJ7Zzhbgbz_ysxAI,12
|
|
22
|
+
canbiplotme-0.3.0.dist-info/RECORD,,
|