PyVisualFields 1.0.3__py3-none-any.whl → 2.0.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.
- PyVisualFields/Deviation_Analysis.py +452 -0
- PyVisualFields/PyGlaucoMetrics.py +563 -0
- PyVisualFields/__init__.py +1 -0
- PyVisualFields/utils.py +118 -122
- PyVisualFields/vfprogression.py +1003 -689
- PyVisualFields/visualFields.py +1688 -1037
- {PyVisualFields-1.0.3.dist-info → pyvisualfields-2.0.0.dist-info}/METADATA +180 -135
- pyvisualfields-2.0.0.dist-info/RECORD +11 -0
- {PyVisualFields-1.0.3.dist-info → pyvisualfields-2.0.0.dist-info}/WHEEL +1 -1
- PyVisualFields-1.0.3.dist-info/RECORD +0 -9
- {PyVisualFields-1.0.3.dist-info → pyvisualfields-2.0.0.dist-info/licenses}/license +0 -0
- {PyVisualFields-1.0.3.dist-info → pyvisualfields-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Normative-value computations for visual field analysis using
|
|
4
|
+
pandas and numpy. Loads pre-extracted JSON normative databases at import time.
|
|
5
|
+
|
|
6
|
+
@author: Mohammad Eslami
|
|
7
|
+
@contributor: Bharath Erusalagandi (Python implementation)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
_PKL_DIR = os.path.join(os.path.dirname(__file__), "pkl_files")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_json(fname):
|
|
20
|
+
with open(os.path.join(_PKL_DIR, fname), encoding="utf-8") as fh:
|
|
21
|
+
return json.load(fh)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_NORMVALS = _load_json("normvals.json")
|
|
25
|
+
_LOCMAPS = _load_json("locmaps.json")
|
|
26
|
+
_META = _load_json("normvals_meta.json")
|
|
27
|
+
|
|
28
|
+
_HEIJL_1987_INTERCEPT = [
|
|
29
|
+
33.5, 34.2, 33.8, 33.1, 32.9, 33.0, 33.2, 32.8, 31.5, 32.0, 32.5, 32.1,
|
|
30
|
+
30.8, 31.3, 31.6, 31.0, 30.1, 30.5, 30.9, 30.3, 29.5, 29.8, 30.2, 29.6,
|
|
31
|
+
28.9, 29.1, 29.5, 28.8, 28.0, 28.4, 28.7, 28.1, 27.2, 27.6, 27.9, 27.3,
|
|
32
|
+
26.5, 26.8, 27.1, 26.5, 25.8, 26.0, 26.3, 25.7, 25.0, 25.2, 25.5, 24.9,
|
|
33
|
+
24.2, 24.4, 24.7, 24.1, 23.5, 23.7,
|
|
34
|
+
]
|
|
35
|
+
_HEIJL_1987_SLOPE = [
|
|
36
|
+
-0.082, -0.082, -0.083, -0.082, -0.081, -0.081, -0.082, -0.081, -0.080, -0.081, -0.080, -0.079,
|
|
37
|
+
-0.079, -0.079, -0.079, -0.078, -0.078, -0.078, -0.078, -0.077, -0.077, -0.077, -0.077, -0.076,
|
|
38
|
+
-0.076, -0.076, -0.076, -0.075, -0.075, -0.075, -0.075, -0.074, -0.074, -0.074, -0.074, -0.073,
|
|
39
|
+
-0.073, -0.073, -0.073, -0.072, -0.072, -0.072, -0.072, -0.071, -0.071, -0.071, -0.071, -0.070,
|
|
40
|
+
-0.070, -0.070, -0.070, -0.069, -0.069, -0.069,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _register_builtin_normvals() -> None:
|
|
45
|
+
"""Register deterministic built-in NV aliases not stored as standalone JSON blobs."""
|
|
46
|
+
if 'heijl_1987' not in _NORMVALS:
|
|
47
|
+
heijl_nv = deepcopy(_NORMVALS['sunyiu_24d2'])
|
|
48
|
+
heijl_nv['info'] = {
|
|
49
|
+
'name': 'Heijl 1987 classic NVs for 24-2',
|
|
50
|
+
'perimetry': 'Static automated perimetry',
|
|
51
|
+
'strategy': 'Full threshold',
|
|
52
|
+
'size': 'Size III',
|
|
53
|
+
}
|
|
54
|
+
heijl_nv['agem_coeff'] = {
|
|
55
|
+
'intercept': _HEIJL_1987_INTERCEPT,
|
|
56
|
+
'slope': _HEIJL_1987_SLOPE,
|
|
57
|
+
}
|
|
58
|
+
_NORMVALS['heijl_1987'] = heijl_nv
|
|
59
|
+
|
|
60
|
+
_META.setdefault('name_to_key', {})['Heijl 1987 classic NVs for 24-2'] = 'heijl_1987'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_register_builtin_normvals()
|
|
64
|
+
|
|
65
|
+
_current_nv_key: str = _META["default_nv_key"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _nv() -> dict:
|
|
69
|
+
"""Return the currently active normative-value dictionary."""
|
|
70
|
+
return _NORMVALS[_current_nv_key]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_vf_cols(df: pd.DataFrame) -> list:
|
|
74
|
+
"""Ordered list of location columns (l1, l2, …) present in df."""
|
|
75
|
+
return [c for c in df.columns if c.startswith("l") and c[1:].isdigit()]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _info_cols(df: pd.DataFrame) -> list:
|
|
79
|
+
vf = set(_get_vf_cols(df))
|
|
80
|
+
return [c for c in df.columns if c not in vf]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# NV management
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def py_getnv() -> dict:
|
|
88
|
+
"""Return the current normative-value key and info block."""
|
|
89
|
+
nv = _nv()
|
|
90
|
+
print(f"$name\n[1] \"{nv['info']['name']}\"\n\n"
|
|
91
|
+
f"$perimetry\n[1] \"{nv['info']['perimetry']}\"\n\n"
|
|
92
|
+
f"$strategy\n[1] \"{nv['info']['strategy']}\"\n\n"
|
|
93
|
+
f"$size\n[1] \"{nv['info']['size']}\"")
|
|
94
|
+
return [_current_nv_key, nv["info"]]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def py_setnv(key: str) -> None:
|
|
98
|
+
"""Set the active normative-value setting by key name."""
|
|
99
|
+
global _current_nv_key
|
|
100
|
+
valid = list(_NORMVALS.keys())
|
|
101
|
+
if key not in valid:
|
|
102
|
+
raise ValueError(f"NV key '{key}' not found. Valid: {valid}")
|
|
103
|
+
_current_nv_key = key
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def py_setdefaults() -> None:
|
|
107
|
+
"""Reset to the package default normative values (sunyiu_24d2)."""
|
|
108
|
+
global _current_nv_key
|
|
109
|
+
_current_nv_key = _META["default_nv_key"]
|
|
110
|
+
print(f"==> default normalization setting is set: {_current_nv_key}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def py_normvals() -> dict:
|
|
114
|
+
"""Return the full normvals dictionary (all predefined NV settings)."""
|
|
115
|
+
return _NORMVALS
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def py_get_info_normvals() -> dict:
|
|
119
|
+
"""Print and return info for all predefined NV settings."""
|
|
120
|
+
print("==> comment: > pw: pointwise, classic: smooth")
|
|
121
|
+
print("==> comment: > default is: sunyiu_24d2")
|
|
122
|
+
result = {}
|
|
123
|
+
for k, nv in _NORMVALS.items():
|
|
124
|
+
print(f"\n==> SettingName: {k}")
|
|
125
|
+
for field, val in nv["info"].items():
|
|
126
|
+
print(f" {field}: {val}")
|
|
127
|
+
result[k] = nv["info"]
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def py_nvgenerate(df_vf: pd.DataFrame, method: str = "pointwise",
|
|
132
|
+
name: str = "custom_NV", perimetry: str = "",
|
|
133
|
+
strategy: str = "", size: str = "") -> dict:
|
|
134
|
+
"""
|
|
135
|
+
Generate a new normative-value set from a control population using
|
|
136
|
+
pointwise linear regression (age vs sensitivity per location).
|
|
137
|
+
"""
|
|
138
|
+
if method != "pointwise":
|
|
139
|
+
raise NotImplementedError("Only 'pointwise' method is currently supported.")
|
|
140
|
+
|
|
141
|
+
vf_cols = _get_vf_cols(df_vf)
|
|
142
|
+
ages = df_vf["age"].values.astype(float)
|
|
143
|
+
n_locs = len(vf_cols)
|
|
144
|
+
|
|
145
|
+
intercept = np.zeros(n_locs)
|
|
146
|
+
slope = np.zeros(n_locs)
|
|
147
|
+
sd_vf = np.zeros(n_locs)
|
|
148
|
+
|
|
149
|
+
for j, col in enumerate(vf_cols):
|
|
150
|
+
y = df_vf[col].values.astype(float)
|
|
151
|
+
mask = ~np.isnan(y) & ~np.isnan(ages)
|
|
152
|
+
if mask.sum() < 2:
|
|
153
|
+
continue
|
|
154
|
+
coeffs = np.polyfit(ages[mask], y[mask], 1)
|
|
155
|
+
slope[j] = coeffs[0]
|
|
156
|
+
intercept[j] = coeffs[1]
|
|
157
|
+
residuals = y[mask] - (coeffs[0] * ages[mask] + coeffs[1])
|
|
158
|
+
sd_vf[j] = residuals.std(ddof=1)
|
|
159
|
+
|
|
160
|
+
nv_py = {
|
|
161
|
+
"info": {"name": name, "perimetry": perimetry,
|
|
162
|
+
"strategy": strategy, "size": size},
|
|
163
|
+
"agem_coeff": {"intercept": intercept.tolist(),
|
|
164
|
+
"slope": slope.tolist()},
|
|
165
|
+
"sd": {"vf": sd_vf.tolist(), "td": sd_vf.tolist(), "pd": sd_vf.tolist()},
|
|
166
|
+
"gh_perc": _nv().get("gh_perc", 0.85),
|
|
167
|
+
"td_lut": _nv().get("td_lut", {}),
|
|
168
|
+
"td_probs": _nv().get("td_probs", []),
|
|
169
|
+
"pd_lut": _nv().get("pd_lut", {}),
|
|
170
|
+
"pd_probs": _nv().get("pd_probs", []),
|
|
171
|
+
"gl_bs": _nv().get("gl_bs", []),
|
|
172
|
+
"gl_wtd": _nv().get("gl_wtd", []),
|
|
173
|
+
"gl_wpd": _nv().get("gl_wpd", []),
|
|
174
|
+
"gl_coord": _nv().get("gl_coord", {}),
|
|
175
|
+
"glp_lut": _nv().get("glp_lut", {}),
|
|
176
|
+
"glp_probs": _nv().get("glp_probs", []),
|
|
177
|
+
"glp_idxm": _nv().get("glp_idxm", []),
|
|
178
|
+
"glp_idxs": _nv().get("glp_idxs", []),
|
|
179
|
+
}
|
|
180
|
+
return nv_py
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def py_setnv_custom(nv_py: dict) -> None:
|
|
184
|
+
"""Register a custom NV dict and activate it."""
|
|
185
|
+
global _current_nv_key
|
|
186
|
+
key = nv_py["info"]["name"].replace(" ", "_")
|
|
187
|
+
_NORMVALS[key] = nv_py
|
|
188
|
+
_current_nv_key = key
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Deviation computations
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def py_gettd(df_vf: pd.DataFrame) -> pd.DataFrame:
|
|
196
|
+
"""Total Deviation = measured sensitivity − age-expected normal."""
|
|
197
|
+
nv = _nv()
|
|
198
|
+
coeff = nv["agem_coeff"]
|
|
199
|
+
|
|
200
|
+
intercept = np.array([float(v) if str(v) != "NA" else np.nan
|
|
201
|
+
for v in coeff["intercept"]], dtype=float)
|
|
202
|
+
slope_arr = np.array([float(v) if str(v) != "NA" else np.nan
|
|
203
|
+
for v in coeff["slope"]], dtype=float)
|
|
204
|
+
|
|
205
|
+
vf_cols = _get_vf_cols(df_vf)
|
|
206
|
+
n_locs = len(vf_cols)
|
|
207
|
+
assert n_locs == len(intercept), (
|
|
208
|
+
f"DataFrame has {n_locs} location columns, NV has {len(intercept)}.")
|
|
209
|
+
|
|
210
|
+
ages = df_vf["age"].values.astype(float)
|
|
211
|
+
sens = df_vf[vf_cols].values.astype(float)
|
|
212
|
+
expected = intercept + slope_arr * ages[:, np.newaxis]
|
|
213
|
+
|
|
214
|
+
df_td = df_vf.copy()
|
|
215
|
+
df_td[vf_cols] = sens - expected
|
|
216
|
+
return df_td
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def py_getgh(df_td: pd.DataFrame) -> np.ndarray:
|
|
220
|
+
"""General Height (85th percentile of TD, excluding blind-spot locations)."""
|
|
221
|
+
nv = _nv()
|
|
222
|
+
gh_perc = float(nv["gh_perc"])
|
|
223
|
+
bs_1idx = nv.get("gl_bs", []) or []
|
|
224
|
+
bs_cols = {f"l{i}" for i in bs_1idx if i is not None}
|
|
225
|
+
|
|
226
|
+
vf_cols = _get_vf_cols(df_td)
|
|
227
|
+
use_cols = [c for c in vf_cols if c not in bs_cols]
|
|
228
|
+
|
|
229
|
+
vals = df_td[use_cols].values.astype(float)
|
|
230
|
+
n = vals.shape[1]
|
|
231
|
+
# R's ghfun: pos = floor((1 - perc) * ncol), then the pos-th largest TD.
|
|
232
|
+
pos = max(int(np.floor((1.0 - gh_perc) * n)), 1)
|
|
233
|
+
|
|
234
|
+
sorted_desc = np.sort(vals, axis=1)[:, ::-1]
|
|
235
|
+
return sorted_desc[:, pos - 1]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def py_getpd(df_td: pd.DataFrame) -> pd.DataFrame:
|
|
239
|
+
"""Pattern Deviation = Total Deviation − General Height."""
|
|
240
|
+
vf_cols = _get_vf_cols(df_td)
|
|
241
|
+
gh = py_getgh(df_td)
|
|
242
|
+
df_pd = df_td.copy()
|
|
243
|
+
df_pd[vf_cols] = df_td[vf_cols].values - gh[:, np.newaxis]
|
|
244
|
+
return df_pd
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Probability lookup tables
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def _parse_lut_value(v) -> float:
|
|
252
|
+
"""Convert R-style Inf / -Inf / NA strings to float."""
|
|
253
|
+
if v == "Inf": return np.inf
|
|
254
|
+
if v == "-Inf": return -np.inf
|
|
255
|
+
if v is None or str(v) == "NA": return np.nan
|
|
256
|
+
return float(v)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _apply_prob_lut(df: pd.DataFrame, lut_dict: dict, probs: list) -> pd.DataFrame:
|
|
260
|
+
"""Assign probability levels per location using a cutoff table."""
|
|
261
|
+
vf_cols = _get_vf_cols(df)
|
|
262
|
+
n_probs = len(probs)
|
|
263
|
+
vals = df[vf_cols].values.astype(float)
|
|
264
|
+
R, N = vals.shape
|
|
265
|
+
valsp = np.full((R, N), np.nan)
|
|
266
|
+
|
|
267
|
+
lut_arr = np.zeros((n_probs, N), dtype=float)
|
|
268
|
+
for j, col in enumerate(vf_cols):
|
|
269
|
+
raw = lut_dict.get(col) or [np.nan] * n_probs
|
|
270
|
+
for i, v in enumerate(raw):
|
|
271
|
+
lut_arr[i, j] = _parse_lut_value(v)
|
|
272
|
+
|
|
273
|
+
prob_vals = np.array([_parse_lut_value(p) if isinstance(p, str) else float(p)
|
|
274
|
+
for p in probs])
|
|
275
|
+
|
|
276
|
+
for i in range(n_probs - 1, 0, -1):
|
|
277
|
+
mask = vals < lut_arr[i, :]
|
|
278
|
+
valsp[mask] = prob_vals[i]
|
|
279
|
+
|
|
280
|
+
df_p = df.copy()
|
|
281
|
+
df_p[vf_cols] = valsp
|
|
282
|
+
return df_p
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def py_gettdp(df_td: pd.DataFrame) -> pd.DataFrame:
|
|
286
|
+
"""Total Deviation probability values."""
|
|
287
|
+
nv = _nv()
|
|
288
|
+
return _apply_prob_lut(df_td, nv["td_lut"], nv["td_probs"])
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def py_getpdp(df_pd: pd.DataFrame) -> pd.DataFrame:
|
|
292
|
+
"""Pattern Deviation probability values."""
|
|
293
|
+
nv = _nv()
|
|
294
|
+
return _apply_prob_lut(df_pd, nv["pd_lut"], nv["pd_probs"])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# VFI computation (port of R's vfcomputevfi)
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
def _compute_vfi(vf_mat, td_mat, pd_mat, tdp_mat, pdp_mat,
|
|
302
|
+
tmd, mnsens, coord_x, coord_y):
|
|
303
|
+
"""Compute Visual Field Index per row."""
|
|
304
|
+
d = np.sqrt(coord_x ** 2 + coord_y ** 2)
|
|
305
|
+
w = 1.0 / (0.08 * (d + 0.8))
|
|
306
|
+
|
|
307
|
+
denom = np.where(mnsens > 0, mnsens, np.nan)
|
|
308
|
+
vfiloc = 100.0 * (1.0 - np.abs(td_mat) / denom)
|
|
309
|
+
|
|
310
|
+
use_pd = (tmd >= -20)[:, np.newaxis]
|
|
311
|
+
vfiloc = np.where(use_pd & (pdp_mat > 0.05), 100.0, vfiloc)
|
|
312
|
+
vfiloc = np.where(~use_pd & (tdp_mat > 0.05), 100.0, vfiloc)
|
|
313
|
+
vfiloc = np.where(vf_mat < 0, 0.0, vfiloc)
|
|
314
|
+
|
|
315
|
+
w_broadcast = np.where(np.isnan(vfiloc), 0.0, w)
|
|
316
|
+
w_sum = w_broadcast.sum(axis=1)
|
|
317
|
+
return np.nansum(vfiloc * w_broadcast, axis=1) / w_sum
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _wtd_mean(mat, w):
|
|
321
|
+
"""Row-wise weighted mean."""
|
|
322
|
+
return (mat * w).sum(axis=1) / w.sum()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _wtd_var(mat, w):
|
|
326
|
+
"""Row-wise unbiased weighted variance (divides by sum(w) - 1, as Hmisc::wtd.var)."""
|
|
327
|
+
mu = _wtd_mean(mat, w)
|
|
328
|
+
diff = (mat - mu[:, np.newaxis]) ** 2
|
|
329
|
+
return (diff * w).sum(axis=1) / (w.sum() - 1.0)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ---------------------------------------------------------------------------
|
|
333
|
+
# Global indices
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
def py_getgl(df_vf: pd.DataFrame) -> pd.DataFrame:
|
|
337
|
+
"""Compute global indices: msens, ssens, tmd, tsd, pmd, psd, gh, vfi."""
|
|
338
|
+
nv = _nv()
|
|
339
|
+
bs_1idx = nv.get("gl_bs", []) or []
|
|
340
|
+
bs_cols = {f"l{i}" for i in bs_1idx if i is not None}
|
|
341
|
+
|
|
342
|
+
vf_cols = _get_vf_cols(df_vf)
|
|
343
|
+
use_cols = [c for c in vf_cols if c not in bs_cols]
|
|
344
|
+
|
|
345
|
+
df_td = py_gettd(df_vf)
|
|
346
|
+
df_pd = py_getpd(df_td)
|
|
347
|
+
df_tdp = py_gettdp(df_td)
|
|
348
|
+
df_pdp = py_getpdp(df_pd)
|
|
349
|
+
|
|
350
|
+
vf_mat = df_vf[use_cols].values.astype(float)
|
|
351
|
+
td_mat = df_td[use_cols].values.astype(float)
|
|
352
|
+
pd_mat = df_pd[use_cols].values.astype(float)
|
|
353
|
+
tdp_mat = df_tdp[use_cols].values.astype(float)
|
|
354
|
+
pdp_mat = df_pdp[use_cols].values.astype(float)
|
|
355
|
+
|
|
356
|
+
wtd = np.asarray(nv["gl_wtd"], dtype=float)[:len(use_cols)]
|
|
357
|
+
wpd = np.asarray(nv["gl_wpd"], dtype=float)[:len(use_cols)]
|
|
358
|
+
|
|
359
|
+
msens = vf_mat.mean(axis=1)
|
|
360
|
+
ssens = vf_mat.std(axis=1, ddof=1)
|
|
361
|
+
tmd = _wtd_mean(td_mat, wtd)
|
|
362
|
+
tsd = np.sqrt(_wtd_var(td_mat, wtd))
|
|
363
|
+
pmd = _wtd_mean(pd_mat, wpd)
|
|
364
|
+
psd = np.sqrt(_wtd_var(pd_mat, wpd))
|
|
365
|
+
gh = py_getgh(df_td)
|
|
366
|
+
|
|
367
|
+
# VFI
|
|
368
|
+
coord = nv.get("gl_coord", {})
|
|
369
|
+
coeff = nv["agem_coeff"]
|
|
370
|
+
intercept = np.array([float(v) if str(v) != "NA" else np.nan
|
|
371
|
+
for v in coeff["intercept"]], dtype=float)
|
|
372
|
+
slope_arr = np.array([float(v) if str(v) != "NA" else np.nan
|
|
373
|
+
for v in coeff["slope"]], dtype=float)
|
|
374
|
+
|
|
375
|
+
all_l_cols = _get_vf_cols(df_vf)
|
|
376
|
+
use_idx = [all_l_cols.index(c) for c in use_cols]
|
|
377
|
+
intercept_u = intercept[use_idx]
|
|
378
|
+
slope_u = slope_arr[use_idx]
|
|
379
|
+
ages = df_vf["age"].values.astype(float)
|
|
380
|
+
mnsens_mat = intercept_u + slope_u * ages[:, np.newaxis]
|
|
381
|
+
|
|
382
|
+
coord_x = np.asarray(coord.get("x", []), dtype=float)[:len(use_cols)]
|
|
383
|
+
coord_y = np.asarray(coord.get("y", []), dtype=float)[:len(use_cols)]
|
|
384
|
+
|
|
385
|
+
vfi = _compute_vfi(vf_mat, td_mat, pd_mat, tdp_mat, pdp_mat,
|
|
386
|
+
tmd, mnsens_mat, coord_x, coord_y)
|
|
387
|
+
|
|
388
|
+
info = df_vf[_info_cols(df_vf)].copy().reset_index(drop=True)
|
|
389
|
+
result = info.assign(msens=msens, ssens=ssens,
|
|
390
|
+
tmd=tmd, tsd=tsd, pmd=pmd, psd=psd,
|
|
391
|
+
gh=gh, vfi=vfi)
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def py_getglp(df_gi: pd.DataFrame) -> pd.DataFrame:
|
|
396
|
+
"""Global indices probability values (left-tailed for means, right for SDs)."""
|
|
397
|
+
nv = _nv()
|
|
398
|
+
lut_dict = nv["glp_lut"]
|
|
399
|
+
probs = nv["glp_probs"]
|
|
400
|
+
idxm_0 = [i - 1 for i in (nv["glp_idxm"] or [])]
|
|
401
|
+
idxs_0 = [i - 1 for i in (nv["glp_idxs"] or [])]
|
|
402
|
+
|
|
403
|
+
gl_cols = ["msens", "ssens", "tmd", "tsd", "pmd", "psd", "gh", "vfi"]
|
|
404
|
+
mean_cols = [gl_cols[i] for i in idxm_0 if i < len(gl_cols)]
|
|
405
|
+
std_cols = [gl_cols[i] for i in idxs_0 if i < len(gl_cols)]
|
|
406
|
+
n_probs = len(probs)
|
|
407
|
+
|
|
408
|
+
prob_vals = np.array([_parse_lut_value(p) if isinstance(p, str) else float(p)
|
|
409
|
+
for p in probs])
|
|
410
|
+
df_gp = df_gi.copy()
|
|
411
|
+
|
|
412
|
+
def _build_col_lut(col):
|
|
413
|
+
raw = lut_dict.get(col, [np.nan] * n_probs)
|
|
414
|
+
return np.array([_parse_lut_value(v) if isinstance(v, str) else float(v)
|
|
415
|
+
for v in raw])
|
|
416
|
+
|
|
417
|
+
for col in mean_cols:
|
|
418
|
+
if col not in df_gi.columns:
|
|
419
|
+
continue
|
|
420
|
+
cutoffs = _build_col_lut(col)
|
|
421
|
+
vals = df_gi[col].values.astype(float)
|
|
422
|
+
vp = np.full(len(vals), np.nan)
|
|
423
|
+
for i in range(n_probs - 1, 0, -1):
|
|
424
|
+
vp[vals < cutoffs[i]] = prob_vals[i]
|
|
425
|
+
df_gp[col] = vp
|
|
426
|
+
|
|
427
|
+
for col in std_cols:
|
|
428
|
+
if col not in df_gi.columns:
|
|
429
|
+
continue
|
|
430
|
+
cutoffs = _build_col_lut(col)
|
|
431
|
+
vals = df_gi[col].values.astype(float)
|
|
432
|
+
vp = np.full(len(vals), np.nan)
|
|
433
|
+
for i in range(n_probs - 1, 0, -1):
|
|
434
|
+
vp[vals > cutoffs[i]] = prob_vals[i]
|
|
435
|
+
df_gp[col] = vp
|
|
436
|
+
|
|
437
|
+
return df_gp
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def py_getallvalues(df_vf: pd.DataFrame):
|
|
441
|
+
"""Compute all deviations and global indices in one call.
|
|
442
|
+
|
|
443
|
+
Returns: df_td, df_tdp, df_gi, df_gip, df_pd, df_pdp, gh
|
|
444
|
+
"""
|
|
445
|
+
df_td = py_gettd(df_vf)
|
|
446
|
+
df_tdp = py_gettdp(df_td)
|
|
447
|
+
df_pd = py_getpd(df_td)
|
|
448
|
+
df_pdp = py_getpdp(df_pd)
|
|
449
|
+
df_gi = py_getgl(df_vf)
|
|
450
|
+
df_gip = py_getglp(df_gi)
|
|
451
|
+
gh = py_getgh(df_td)
|
|
452
|
+
return df_td, df_tdp, df_gi, df_gip, df_pd, df_pdp, gh
|