PyVisualFields 2.0.3__py3-none-any.whl → 2.0.5__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 +540 -106
- PyVisualFields/PyGlaucoMetrics.py +380 -111
- PyVisualFields/__init__.py +1 -1
- PyVisualFields/utils.py +845 -0
- PyVisualFields/vfprogression.py +73 -19
- PyVisualFields/visualFields.py +360 -171
- pyvisualfields-2.0.5.dist-info/METADATA +242 -0
- {pyvisualfields-2.0.3.dist-info → pyvisualfields-2.0.5.dist-info}/RECORD +11 -10
- pyvisualfields-2.0.5.dist-info/licenses/LICENSE +29 -0
- pyvisualfields-2.0.3.dist-info/METADATA +0 -178
- {pyvisualfields-2.0.3.dist-info → pyvisualfields-2.0.5.dist-info}/WHEEL +0 -0
- {pyvisualfields-2.0.3.dist-info → pyvisualfields-2.0.5.dist-info}/top_level.txt +0 -0
|
@@ -70,9 +70,9 @@ def _nv() -> dict:
|
|
|
70
70
|
return _NORMVALS[_current_nv_key]
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def _get_vf_cols(df: pd.DataFrame) -> list:
|
|
73
|
+
def _get_vf_cols(df: pd.DataFrame, colname='l') -> list:
|
|
74
74
|
"""Ordered list of location columns (l1, l2, …) present in df."""
|
|
75
|
-
return [c for c in df.columns if c.startswith(
|
|
75
|
+
return [c for c in df.columns if c.startswith(colname) and c[len(colname):].isdigit()]
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def _info_cols(df: pd.DataFrame) -> list:
|
|
@@ -194,25 +194,38 @@ def py_setnv_custom(nv_py: dict) -> None:
|
|
|
194
194
|
|
|
195
195
|
def py_gettd(df_vf: pd.DataFrame) -> pd.DataFrame:
|
|
196
196
|
"""Total Deviation = measured sensitivity − age-expected normal."""
|
|
197
|
-
|
|
197
|
+
|
|
198
|
+
nv = _nv()
|
|
198
199
|
coeff = nv["agem_coeff"]
|
|
199
200
|
|
|
200
|
-
intercept = np.array(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
intercept = np.array(
|
|
202
|
+
[float(v) if str(v) != "NA" else np.nan
|
|
203
|
+
for v in coeff["intercept"]],
|
|
204
|
+
dtype=float,
|
|
205
|
+
)
|
|
204
206
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
slope_arr = np.array(
|
|
208
|
+
[float(v) if str(v) != "NA" else np.nan
|
|
209
|
+
for v in coeff["slope"]],
|
|
210
|
+
dtype=float,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
vf_cols = _get_vf_cols(df_vf, colname='l')
|
|
214
|
+
|
|
215
|
+
ages = df_vf["age"].values.astype(float)
|
|
216
|
+
sens = df_vf[vf_cols].values.astype(float)
|
|
209
217
|
|
|
210
|
-
ages = df_vf["age"].values.astype(float)
|
|
211
|
-
sens = df_vf[vf_cols].values.astype(float)
|
|
212
218
|
expected = intercept + slope_arr * ages[:, np.newaxis]
|
|
219
|
+
td_vals = sens - expected
|
|
220
|
+
|
|
221
|
+
meta_cols = [c for c in df_vf.columns if c not in vf_cols]
|
|
222
|
+
|
|
223
|
+
df_td = df_vf[meta_cols].copy()
|
|
224
|
+
|
|
225
|
+
td_cols = [f"td{i}" for i in range(1, len(vf_cols) + 1)]
|
|
226
|
+
|
|
227
|
+
df_td[td_cols] = td_vals
|
|
213
228
|
|
|
214
|
-
df_td = df_vf.copy()
|
|
215
|
-
df_td[vf_cols] = sens - expected
|
|
216
229
|
return df_td
|
|
217
230
|
|
|
218
231
|
|
|
@@ -220,11 +233,13 @@ def py_getgh(df_td: pd.DataFrame) -> np.ndarray:
|
|
|
220
233
|
"""General Height (85th percentile of TD, excluding blind-spot locations)."""
|
|
221
234
|
nv = _nv()
|
|
222
235
|
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}
|
|
236
|
+
bs_1idx = {i for i in (nv.get("gl_bs", []) or []) if i is not None}
|
|
225
237
|
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
td_cols = _get_vf_cols(df_td, colname='td')
|
|
239
|
+
# Exclude blind-spot locations by their 1-indexed position, independent of the
|
|
240
|
+
# column prefix (td columns are named td1..tdN, so a name-based 'l{i}' set never
|
|
241
|
+
# matched and left blind spots in the percentile rank).
|
|
242
|
+
use_cols = [c for k, c in enumerate(td_cols, start=1) if k not in bs_1idx]
|
|
228
243
|
|
|
229
244
|
vals = df_td[use_cols].values.astype(float)
|
|
230
245
|
n = vals.shape[1]
|
|
@@ -237,13 +252,23 @@ def py_getgh(df_td: pd.DataFrame) -> np.ndarray:
|
|
|
237
252
|
|
|
238
253
|
def py_getpd(df_td: pd.DataFrame) -> pd.DataFrame:
|
|
239
254
|
"""Pattern Deviation = Total Deviation − General Height."""
|
|
240
|
-
|
|
255
|
+
td_cols = _get_vf_cols(df_td, colname='td')
|
|
241
256
|
gh = py_getgh(df_td)
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
|
|
258
|
+
meta_cols = [c for c in df_td.columns if c not in td_cols]
|
|
259
|
+
|
|
260
|
+
pd_vals = df_td[td_cols].values.astype(float) - gh[:, np.newaxis]
|
|
261
|
+
|
|
262
|
+
df_pd = df_td[meta_cols].copy()
|
|
263
|
+
|
|
264
|
+
pd_cols = [f"pd{i}" for i in range(1, len(td_cols) + 1)]
|
|
265
|
+
|
|
266
|
+
df_pd[pd_cols] = pd_vals
|
|
267
|
+
|
|
244
268
|
return df_pd
|
|
245
269
|
|
|
246
270
|
|
|
271
|
+
|
|
247
272
|
# ---------------------------------------------------------------------------
|
|
248
273
|
# Probability lookup tables
|
|
249
274
|
# ---------------------------------------------------------------------------
|
|
@@ -256,42 +281,83 @@ def _parse_lut_value(v) -> float:
|
|
|
256
281
|
return float(v)
|
|
257
282
|
|
|
258
283
|
|
|
259
|
-
def
|
|
284
|
+
def _get_point_cols(df, prefix):
|
|
285
|
+
return sorted(
|
|
286
|
+
[
|
|
287
|
+
c for c in df.columns
|
|
288
|
+
if c.startswith(prefix) and c[len(prefix):].isdigit()
|
|
289
|
+
],
|
|
290
|
+
key=lambda x: int(x[len(prefix):])
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _apply_prob_lut(
|
|
294
|
+
df: pd.DataFrame,
|
|
295
|
+
lut_dict: dict,
|
|
296
|
+
probs: list,
|
|
297
|
+
input_prefix: str,
|
|
298
|
+
output_prefix: str,
|
|
299
|
+
) -> pd.DataFrame:
|
|
260
300
|
"""Assign probability levels per location using a cutoff table."""
|
|
261
|
-
|
|
301
|
+
|
|
302
|
+
in_cols = _get_point_cols(df, input_prefix)
|
|
303
|
+
|
|
262
304
|
n_probs = len(probs)
|
|
263
|
-
vals
|
|
264
|
-
R, N
|
|
265
|
-
valsp
|
|
305
|
+
vals = df[in_cols].values.astype(float)
|
|
306
|
+
R, N = vals.shape
|
|
307
|
+
valsp = np.full((R, N), np.nan)
|
|
266
308
|
|
|
267
309
|
lut_arr = np.zeros((n_probs, N), dtype=float)
|
|
268
|
-
|
|
269
|
-
|
|
310
|
+
|
|
311
|
+
for j, col in enumerate(in_cols):
|
|
312
|
+
# LUT keys are usually l1...l54, even when input is td1...td54
|
|
313
|
+
loc_num = int(col[len(input_prefix):])
|
|
314
|
+
lut_key = f"l{loc_num}"
|
|
315
|
+
|
|
316
|
+
raw = lut_dict.get(lut_key) or [np.nan] * n_probs
|
|
317
|
+
|
|
270
318
|
for i, v in enumerate(raw):
|
|
271
319
|
lut_arr[i, j] = _parse_lut_value(v)
|
|
272
320
|
|
|
273
|
-
prob_vals = np.array([
|
|
274
|
-
|
|
321
|
+
prob_vals = np.array([
|
|
322
|
+
_parse_lut_value(p) if isinstance(p, str) else float(p)
|
|
323
|
+
for p in probs
|
|
324
|
+
])
|
|
275
325
|
|
|
276
326
|
for i in range(n_probs - 1, 0, -1):
|
|
277
327
|
mask = vals < lut_arr[i, :]
|
|
278
328
|
valsp[mask] = prob_vals[i]
|
|
279
329
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
330
|
+
meta_cols = [c for c in df.columns if c not in in_cols]
|
|
331
|
+
out = df[meta_cols].copy()
|
|
332
|
+
|
|
333
|
+
out_cols = [f"{output_prefix}{i}" for i in range(1, N + 1)]
|
|
334
|
+
out[out_cols] = valsp
|
|
335
|
+
|
|
336
|
+
return out
|
|
283
337
|
|
|
284
338
|
|
|
285
339
|
def py_gettdp(df_td: pd.DataFrame) -> pd.DataFrame:
|
|
286
340
|
"""Total Deviation probability values."""
|
|
287
341
|
nv = _nv()
|
|
288
|
-
return _apply_prob_lut(
|
|
342
|
+
return _apply_prob_lut(
|
|
343
|
+
df_td,
|
|
344
|
+
nv["td_lut"],
|
|
345
|
+
nv["td_probs"],
|
|
346
|
+
input_prefix="td",
|
|
347
|
+
output_prefix="tdp",
|
|
348
|
+
)
|
|
289
349
|
|
|
290
350
|
|
|
291
351
|
def py_getpdp(df_pd: pd.DataFrame) -> pd.DataFrame:
|
|
292
352
|
"""Pattern Deviation probability values."""
|
|
293
353
|
nv = _nv()
|
|
294
|
-
return _apply_prob_lut(
|
|
354
|
+
return _apply_prob_lut(
|
|
355
|
+
df_pd,
|
|
356
|
+
nv["pd_lut"],
|
|
357
|
+
nv["pd_probs"],
|
|
358
|
+
input_prefix="pd",
|
|
359
|
+
output_prefix="pdp",
|
|
360
|
+
)
|
|
295
361
|
|
|
296
362
|
|
|
297
363
|
# ---------------------------------------------------------------------------
|
|
@@ -333,106 +399,470 @@ def _wtd_var(mat, w):
|
|
|
333
399
|
# Global indices
|
|
334
400
|
# ---------------------------------------------------------------------------
|
|
335
401
|
|
|
402
|
+
# def py_getgl(df_vf: pd.DataFrame) -> pd.DataFrame:
|
|
403
|
+
# """Compute global indices: msens, ssens, tmd, tsd, pmd, psd, gh, vfi."""
|
|
404
|
+
# nv = _nv()
|
|
405
|
+
# bs_1idx = nv.get("gl_bs", []) or []
|
|
406
|
+
# bs_cols = {f"l{i}" for i in bs_1idx if i is not None}
|
|
407
|
+
|
|
408
|
+
# vf_cols = _get_vf_cols(df_vf)
|
|
409
|
+
# use_cols = [c for c in vf_cols if c not in bs_cols]
|
|
410
|
+
|
|
411
|
+
# df_td = py_gettd(df_vf)
|
|
412
|
+
# df_pd = py_getpd(df_td)
|
|
413
|
+
# df_tdp = py_gettdp(df_td)
|
|
414
|
+
# df_pdp = py_getpdp(df_pd)
|
|
415
|
+
|
|
416
|
+
# vf_mat = df_vf[use_cols].values.astype(float)
|
|
417
|
+
# td_mat = df_td[use_cols].values.astype(float)
|
|
418
|
+
# pd_mat = df_pd[use_cols].values.astype(float)
|
|
419
|
+
# tdp_mat = df_tdp[use_cols].values.astype(float)
|
|
420
|
+
# pdp_mat = df_pdp[use_cols].values.astype(float)
|
|
421
|
+
|
|
422
|
+
# wtd = np.asarray(nv["gl_wtd"], dtype=float)[:len(use_cols)]
|
|
423
|
+
# wpd = np.asarray(nv["gl_wpd"], dtype=float)[:len(use_cols)]
|
|
424
|
+
|
|
425
|
+
# msens = vf_mat.mean(axis=1)
|
|
426
|
+
# ssens = vf_mat.std(axis=1, ddof=1)
|
|
427
|
+
# tmd = _wtd_mean(td_mat, wtd)
|
|
428
|
+
# tsd = np.sqrt(_wtd_var(td_mat, wtd))
|
|
429
|
+
# pmd = _wtd_mean(pd_mat, wpd)
|
|
430
|
+
# psd = np.sqrt(_wtd_var(pd_mat, wpd))
|
|
431
|
+
# gh = py_getgh(df_td)
|
|
432
|
+
|
|
433
|
+
# # VFI
|
|
434
|
+
# coord = nv.get("gl_coord", {})
|
|
435
|
+
# coeff = nv["agem_coeff"]
|
|
436
|
+
# intercept = np.array([float(v) if str(v) != "NA" else np.nan
|
|
437
|
+
# for v in coeff["intercept"]], dtype=float)
|
|
438
|
+
# slope_arr = np.array([float(v) if str(v) != "NA" else np.nan
|
|
439
|
+
# for v in coeff["slope"]], dtype=float)
|
|
440
|
+
|
|
441
|
+
# all_l_cols = _get_vf_cols(df_vf)
|
|
442
|
+
# use_idx = [all_l_cols.index(c) for c in use_cols]
|
|
443
|
+
# intercept_u = intercept[use_idx]
|
|
444
|
+
# slope_u = slope_arr[use_idx]
|
|
445
|
+
# ages = df_vf["age"].values.astype(float)
|
|
446
|
+
# mnsens_mat = intercept_u + slope_u * ages[:, np.newaxis]
|
|
447
|
+
|
|
448
|
+
# coord_x = np.asarray(coord.get("x", []), dtype=float)[:len(use_cols)]
|
|
449
|
+
# coord_y = np.asarray(coord.get("y", []), dtype=float)[:len(use_cols)]
|
|
450
|
+
|
|
451
|
+
# vfi = _compute_vfi(vf_mat, td_mat, pd_mat, tdp_mat, pdp_mat,
|
|
452
|
+
# tmd, mnsens_mat, coord_x, coord_y)
|
|
453
|
+
|
|
454
|
+
# info = df_vf[_info_cols(df_vf)].copy().reset_index(drop=True)
|
|
455
|
+
# result = info.assign(msens=msens, ssens=ssens,
|
|
456
|
+
# tmd=tmd, tsd=tsd, pmd=pmd, psd=psd,
|
|
457
|
+
# gh=gh, vfi=vfi)
|
|
458
|
+
# return result
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# def py_getgl(df_vf: pd.DataFrame) -> pd.DataFrame:
|
|
462
|
+
# """
|
|
463
|
+
# Compute global indices: msens, ssens, tmd, tsd, pmd, psd, gh, vfi.
|
|
464
|
+
|
|
465
|
+
# Assumes df_vf is already canonicalized and contains:
|
|
466
|
+
# l1-l54, td1-td54, pd1-pd54, tdp1-tdp54, pdp1-pdp54
|
|
467
|
+
# """
|
|
468
|
+
|
|
469
|
+
# nv = _nv()
|
|
470
|
+
|
|
471
|
+
# l_cols = _get_point_cols(df_vf, "l")
|
|
472
|
+
# td_cols = _get_point_cols(df_vf, "td")
|
|
473
|
+
# pd_cols = _get_point_cols(df_vf, "pd")
|
|
474
|
+
# tdp_cols = _get_point_cols(df_vf, "tdp")
|
|
475
|
+
# pdp_cols = _get_point_cols(df_vf, "pdp")
|
|
476
|
+
|
|
477
|
+
# n_locs = len(l_cols)
|
|
478
|
+
|
|
479
|
+
# if not all(len(x) == n_locs for x in [td_cols, pd_cols, tdp_cols, pdp_cols]):
|
|
480
|
+
# raise ValueError(
|
|
481
|
+
# "py_getgl requires matching complete blocks: "
|
|
482
|
+
# "l, td, pd, tdp, and pdp."
|
|
483
|
+
# )
|
|
484
|
+
|
|
485
|
+
# bs_1idx = set(nv.get("gl_bs", []) or [])
|
|
486
|
+
|
|
487
|
+
# use_idx = [
|
|
488
|
+
# i for i in range(n_locs)
|
|
489
|
+
# if (i + 1) not in bs_1idx
|
|
490
|
+
# ]
|
|
491
|
+
|
|
492
|
+
# l_use = [l_cols[i] for i in use_idx]
|
|
493
|
+
# td_use = [td_cols[i] for i in use_idx]
|
|
494
|
+
# pd_use = [pd_cols[i] for i in use_idx]
|
|
495
|
+
# tdp_use = [tdp_cols[i] for i in use_idx]
|
|
496
|
+
# pdp_use = [pdp_cols[i] for i in use_idx]
|
|
497
|
+
|
|
498
|
+
# vf_mat = df_vf[l_use].values.astype(float)
|
|
499
|
+
# td_mat = df_vf[td_use].values.astype(float)
|
|
500
|
+
# pd_mat = df_vf[pd_use].values.astype(float)
|
|
501
|
+
# tdp_mat = df_vf[tdp_use].values.astype(float)
|
|
502
|
+
# pdp_mat = df_vf[pdp_use].values.astype(float)
|
|
503
|
+
|
|
504
|
+
# wtd = np.asarray(nv["gl_wtd"], dtype=float)
|
|
505
|
+
# wpd = np.asarray(nv["gl_wpd"], dtype=float)
|
|
506
|
+
|
|
507
|
+
# if len(wtd) != len(use_idx):
|
|
508
|
+
# raise ValueError(
|
|
509
|
+
# f"Weight length mismatch: gl_wtd has {len(wtd)} weights, "
|
|
510
|
+
# f"but {len(use_idx)} non-blind-spot locations were found."
|
|
511
|
+
# )
|
|
512
|
+
|
|
513
|
+
# if len(wpd) != len(use_idx):
|
|
514
|
+
# raise ValueError(
|
|
515
|
+
# f"Weight length mismatch: gl_wpd has {len(wpd)} weights, "
|
|
516
|
+
# f"but {len(use_idx)} non-blind-spot locations were found."
|
|
517
|
+
# )
|
|
518
|
+
|
|
519
|
+
# msens = np.nanmean(vf_mat, axis=1)
|
|
520
|
+
# ssens = np.nanstd(vf_mat, axis=1, ddof=1)
|
|
521
|
+
|
|
522
|
+
# tmd = _wtd_mean(td_mat, wtd)
|
|
523
|
+
# tsd = np.sqrt(_wtd_var(td_mat, wtd))
|
|
524
|
+
|
|
525
|
+
# pmd = _wtd_mean(pd_mat, wpd)
|
|
526
|
+
# psd = np.sqrt(_wtd_var(pd_mat, wpd))
|
|
527
|
+
|
|
528
|
+
# gh = py_getgh(df_vf)
|
|
529
|
+
|
|
530
|
+
# # VFI
|
|
531
|
+
# coord = nv.get("gl_coord", {})
|
|
532
|
+
# coeff = nv["agem_coeff"]
|
|
533
|
+
|
|
534
|
+
# intercept = np.array(
|
|
535
|
+
# [float(v) if str(v) != "NA" else np.nan for v in coeff["intercept"]],
|
|
536
|
+
# dtype=float,
|
|
537
|
+
# )
|
|
538
|
+
|
|
539
|
+
# slope_arr = np.array(
|
|
540
|
+
# [float(v) if str(v) != "NA" else np.nan for v in coeff["slope"]],
|
|
541
|
+
# dtype=float,
|
|
542
|
+
# )
|
|
543
|
+
|
|
544
|
+
# intercept_u = intercept[use_idx]
|
|
545
|
+
# slope_u = slope_arr[use_idx]
|
|
546
|
+
|
|
547
|
+
# ages = df_vf["age"].values.astype(float)
|
|
548
|
+
# mnsens_mat = intercept_u + slope_u * ages[:, np.newaxis]
|
|
549
|
+
|
|
550
|
+
# coord_x = np.asarray(coord.get("x", []), dtype=float)
|
|
551
|
+
# coord_y = np.asarray(coord.get("y", []), dtype=float)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# if len(coord_x) != len(use_idx):
|
|
555
|
+
# coord_x = coord_x[use_idx]
|
|
556
|
+
|
|
557
|
+
# if len(coord_y) != len(use_idx):
|
|
558
|
+
# coord_y = coord_y[use_idx]
|
|
559
|
+
|
|
560
|
+
# vfi = _compute_vfi(
|
|
561
|
+
# vf_mat,
|
|
562
|
+
# td_mat,
|
|
563
|
+
# pd_mat,
|
|
564
|
+
# tdp_mat,
|
|
565
|
+
# pdp_mat,
|
|
566
|
+
# tmd,
|
|
567
|
+
# mnsens_mat,
|
|
568
|
+
# coord_x,
|
|
569
|
+
# coord_y,
|
|
570
|
+
# )
|
|
571
|
+
|
|
572
|
+
# point_cols = set(l_cols + td_cols + pd_cols + tdp_cols + pdp_cols)
|
|
573
|
+
# info_cols = [c for c in df_vf.columns if c not in point_cols]
|
|
574
|
+
|
|
575
|
+
# result = df_vf[info_cols].copy().reset_index(drop=True)
|
|
576
|
+
|
|
577
|
+
# result = result.assign(
|
|
578
|
+
# msens=msens,
|
|
579
|
+
# ssens=ssens,
|
|
580
|
+
# tmd=tmd,
|
|
581
|
+
# tsd=tsd,
|
|
582
|
+
# pmd=pmd,
|
|
583
|
+
# psd=psd,
|
|
584
|
+
# gh=gh,
|
|
585
|
+
# vfi=vfi,
|
|
586
|
+
# )
|
|
587
|
+
|
|
588
|
+
# return result
|
|
589
|
+
|
|
590
|
+
|
|
336
591
|
def py_getgl(df_vf: pd.DataFrame) -> pd.DataFrame:
|
|
337
|
-
"""
|
|
338
|
-
|
|
339
|
-
bs_1idx = nv.get("gl_bs", []) or []
|
|
340
|
-
bs_cols = {f"l{i}" for i in bs_1idx if i is not None}
|
|
592
|
+
"""
|
|
593
|
+
Compute missing global indices only.
|
|
341
594
|
|
|
342
|
-
|
|
343
|
-
|
|
595
|
+
Assumes df_vf is canonicalized and contains:
|
|
596
|
+
l1-l54, td1-td54, pd1-pd54, tdp1-tdp54, pdp1-pdp54
|
|
597
|
+
"""
|
|
344
598
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
599
|
+
target_cols = ["msens", "ssens", "tmd", "tsd", "pmd", "psd", "gh", "vfi"]
|
|
600
|
+
available = [c for c in target_cols if c in df_vf.columns]
|
|
601
|
+
missing = [c for c in target_cols if c not in df_vf.columns]
|
|
602
|
+
print(f"==> py_getgl: Already available global indices: {available}")
|
|
603
|
+
print(f"==> py_getgl: missing global indices to compute: {missing}")
|
|
604
|
+
|
|
605
|
+
point_prefixes = ("l", "td", "pd", "tdp", "pdp")
|
|
606
|
+
point_cols = []
|
|
607
|
+
for p in point_prefixes:
|
|
608
|
+
point_cols.extend(_get_point_cols(df_vf, p))
|
|
609
|
+
|
|
610
|
+
info_cols = [c for c in df_vf.columns if c not in set(point_cols)]
|
|
611
|
+
result = df_vf[info_cols].copy().reset_index(drop=True)
|
|
612
|
+
|
|
613
|
+
if not missing:
|
|
614
|
+
return result
|
|
615
|
+
|
|
616
|
+
nv = _nv()
|
|
617
|
+
|
|
618
|
+
l_cols = _get_point_cols(df_vf, "l")
|
|
619
|
+
td_cols = _get_point_cols(df_vf, "td")
|
|
620
|
+
pd_cols = _get_point_cols(df_vf, "pd")
|
|
621
|
+
tdp_cols = _get_point_cols(df_vf, "tdp")
|
|
622
|
+
pdp_cols = _get_point_cols(df_vf, "pdp")
|
|
623
|
+
|
|
624
|
+
n_locs = len(l_cols)
|
|
625
|
+
|
|
626
|
+
if not all(len(x) == n_locs for x in [td_cols, pd_cols, tdp_cols, pdp_cols]):
|
|
627
|
+
raise ValueError(
|
|
628
|
+
"py_getgl requires matching complete blocks: "
|
|
629
|
+
"l, td, pd, tdp, and pdp."
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
bs_1idx = set(nv.get("gl_bs", []) or [])
|
|
633
|
+
use_idx = [i for i in range(n_locs) if (i + 1) not in bs_1idx]
|
|
634
|
+
|
|
635
|
+
l_use = [l_cols[i] for i in use_idx]
|
|
636
|
+
td_use = [td_cols[i] for i in use_idx]
|
|
637
|
+
pd_use = [pd_cols[i] for i in use_idx]
|
|
638
|
+
tdp_use = [tdp_cols[i] for i in use_idx]
|
|
639
|
+
pdp_use = [pdp_cols[i] for i in use_idx]
|
|
640
|
+
|
|
641
|
+
vf_mat = td_mat = pd_mat = tdp_mat = pdp_mat = None
|
|
642
|
+
|
|
643
|
+
if any(c in missing for c in ["msens", "ssens", "vfi"]):
|
|
644
|
+
vf_mat = df_vf[l_use].values.astype(float)
|
|
645
|
+
|
|
646
|
+
if any(c in missing for c in ["tmd", "tsd", "vfi"]):
|
|
647
|
+
td_mat = df_vf[td_use].values.astype(float)
|
|
648
|
+
|
|
649
|
+
if any(c in missing for c in ["pmd", "psd", "vfi"]):
|
|
650
|
+
pd_mat = df_vf[pd_use].values.astype(float)
|
|
651
|
+
|
|
652
|
+
if "vfi" in missing:
|
|
653
|
+
tdp_mat = df_vf[tdp_use].values.astype(float)
|
|
654
|
+
pdp_mat = df_vf[pdp_use].values.astype(float)
|
|
655
|
+
|
|
656
|
+
wtd = np.asarray(nv["gl_wtd"], dtype=float)
|
|
657
|
+
wpd = np.asarray(nv["gl_wpd"], dtype=float)
|
|
658
|
+
|
|
659
|
+
if len(wtd) != len(use_idx):
|
|
660
|
+
raise ValueError(
|
|
661
|
+
f"Weight length mismatch: gl_wtd has {len(wtd)} weights, "
|
|
662
|
+
f"but {len(use_idx)} non-blind-spot locations were found."
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
if len(wpd) != len(use_idx):
|
|
666
|
+
raise ValueError(
|
|
667
|
+
f"Weight length mismatch: gl_wpd has {len(wpd)} weights, "
|
|
668
|
+
f"but {len(use_idx)} non-blind-spot locations were found."
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
computed = {}
|
|
672
|
+
|
|
673
|
+
if "msens" in missing:
|
|
674
|
+
computed["msens"] = np.nanmean(vf_mat, axis=1)
|
|
675
|
+
|
|
676
|
+
if "ssens" in missing:
|
|
677
|
+
computed["ssens"] = np.nanstd(vf_mat, axis=1, ddof=1)
|
|
678
|
+
|
|
679
|
+
if "tmd" in missing:
|
|
680
|
+
computed["tmd"] = _wtd_mean(td_mat, wtd)
|
|
681
|
+
|
|
682
|
+
if "tsd" in missing:
|
|
683
|
+
if td_mat is None:
|
|
684
|
+
td_mat = df_vf[td_use].values.astype(float)
|
|
685
|
+
computed["tsd"] = np.sqrt(_wtd_var(td_mat, wtd))
|
|
686
|
+
|
|
687
|
+
if "pmd" in missing:
|
|
688
|
+
computed["pmd"] = _wtd_mean(pd_mat, wpd)
|
|
689
|
+
|
|
690
|
+
if "psd" in missing:
|
|
691
|
+
if pd_mat is None:
|
|
692
|
+
pd_mat = df_vf[pd_use].values.astype(float)
|
|
693
|
+
computed["psd"] = np.sqrt(_wtd_var(pd_mat, wpd))
|
|
694
|
+
|
|
695
|
+
if "gh" in missing:
|
|
696
|
+
computed["gh"] = py_getgh(df_vf)
|
|
697
|
+
|
|
698
|
+
if "vfi" in missing:
|
|
699
|
+
if vf_mat is None:
|
|
700
|
+
vf_mat = df_vf[l_use].values.astype(float)
|
|
701
|
+
if td_mat is None:
|
|
702
|
+
td_mat = df_vf[td_use].values.astype(float)
|
|
703
|
+
if pd_mat is None:
|
|
704
|
+
pd_mat = df_vf[pd_use].values.astype(float)
|
|
705
|
+
|
|
706
|
+
coord = nv.get("gl_coord", {})
|
|
707
|
+
coeff = nv["agem_coeff"]
|
|
708
|
+
|
|
709
|
+
intercept = np.array(
|
|
710
|
+
[float(v) if str(v) != "NA" else np.nan for v in coeff["intercept"]],
|
|
711
|
+
dtype=float,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
slope_arr = np.array(
|
|
715
|
+
[float(v) if str(v) != "NA" else np.nan for v in coeff["slope"]],
|
|
716
|
+
dtype=float,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
intercept_u = intercept[use_idx]
|
|
720
|
+
slope_u = slope_arr[use_idx]
|
|
721
|
+
|
|
722
|
+
ages = df_vf["age"].values.astype(float)
|
|
723
|
+
mnsens_mat = intercept_u + slope_u * ages[:, np.newaxis]
|
|
724
|
+
|
|
725
|
+
coord_x = np.asarray(coord.get("x", []), dtype=float)
|
|
726
|
+
coord_y = np.asarray(coord.get("y", []), dtype=float)
|
|
727
|
+
|
|
728
|
+
if len(coord_x) != len(use_idx):
|
|
729
|
+
coord_x = coord_x[use_idx]
|
|
730
|
+
if len(coord_y) != len(use_idx):
|
|
731
|
+
coord_y = coord_y[use_idx]
|
|
732
|
+
|
|
733
|
+
tmd_for_vfi = (
|
|
734
|
+
result["tmd"].values.astype(float)
|
|
735
|
+
if "tmd" in result.columns
|
|
736
|
+
else computed.get("tmd")
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
if tmd_for_vfi is None:
|
|
740
|
+
tmd_for_vfi = _wtd_mean(td_mat, wtd)
|
|
741
|
+
|
|
742
|
+
computed["vfi"] = _compute_vfi(
|
|
743
|
+
vf_mat,
|
|
744
|
+
td_mat,
|
|
745
|
+
pd_mat,
|
|
746
|
+
tdp_mat,
|
|
747
|
+
pdp_mat,
|
|
748
|
+
tmd_for_vfi,
|
|
749
|
+
mnsens_mat,
|
|
750
|
+
coord_x,
|
|
751
|
+
coord_y,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
if computed:
|
|
755
|
+
result = pd.concat(
|
|
756
|
+
[result, pd.DataFrame(computed, index=result.index)],
|
|
757
|
+
axis=1,
|
|
758
|
+
)
|
|
349
759
|
|
|
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
760
|
return result
|
|
393
761
|
|
|
762
|
+
# def py_getglp(df_gi: pd.DataFrame) -> pd.DataFrame:
|
|
763
|
+
# """Global indices probability values (left-tailed for means, right for SDs)."""
|
|
764
|
+
# nv = _nv()
|
|
765
|
+
# lut_dict = nv["glp_lut"]
|
|
766
|
+
# probs = nv["glp_probs"]
|
|
767
|
+
# idxm_0 = [i - 1 for i in (nv["glp_idxm"] or [])]
|
|
768
|
+
# idxs_0 = [i - 1 for i in (nv["glp_idxs"] or [])]
|
|
769
|
+
|
|
770
|
+
# gl_cols = ["msens", "ssens", "tmd", "tsd", "pmd", "psd", "gh", "vfi"]
|
|
771
|
+
# mean_cols = [gl_cols[i] for i in idxm_0 if i < len(gl_cols)]
|
|
772
|
+
# std_cols = [gl_cols[i] for i in idxs_0 if i < len(gl_cols)]
|
|
773
|
+
# n_probs = len(probs)
|
|
774
|
+
|
|
775
|
+
# prob_vals = np.array([_parse_lut_value(p) if isinstance(p, str) else float(p)
|
|
776
|
+
# for p in probs])
|
|
777
|
+
# df_gp = df_gi.copy()
|
|
778
|
+
|
|
779
|
+
# def _build_col_lut(col):
|
|
780
|
+
# raw = lut_dict.get(col, [np.nan] * n_probs)
|
|
781
|
+
# return np.array([_parse_lut_value(v) if isinstance(v, str) else float(v)
|
|
782
|
+
# for v in raw])
|
|
783
|
+
|
|
784
|
+
# for col in mean_cols:
|
|
785
|
+
# if col not in df_gi.columns:
|
|
786
|
+
# continue
|
|
787
|
+
# cutoffs = _build_col_lut(col)
|
|
788
|
+
# vals = df_gi[col].values.astype(float)
|
|
789
|
+
# vp = np.full(len(vals), np.nan)
|
|
790
|
+
# for i in range(n_probs - 1, 0, -1):
|
|
791
|
+
# vp[vals < cutoffs[i]] = prob_vals[i]
|
|
792
|
+
# df_gp[col] = vp
|
|
793
|
+
|
|
794
|
+
# for col in std_cols:
|
|
795
|
+
# if col not in df_gi.columns:
|
|
796
|
+
# continue
|
|
797
|
+
# cutoffs = _build_col_lut(col)
|
|
798
|
+
# vals = df_gi[col].values.astype(float)
|
|
799
|
+
# vp = np.full(len(vals), np.nan)
|
|
800
|
+
# for i in range(n_probs - 1, 0, -1):
|
|
801
|
+
# vp[vals > cutoffs[i]] = prob_vals[i]
|
|
802
|
+
# df_gp[col] = vp
|
|
803
|
+
|
|
804
|
+
# return df_gp
|
|
394
805
|
|
|
395
806
|
def py_getglp(df_gi: pd.DataFrame) -> pd.DataFrame:
|
|
396
|
-
"""Global indices probability values
|
|
397
|
-
|
|
807
|
+
"""Global indices probability values."""
|
|
808
|
+
|
|
809
|
+
nv = _nv()
|
|
398
810
|
lut_dict = nv["glp_lut"]
|
|
399
|
-
probs
|
|
400
|
-
idxm_0
|
|
401
|
-
idxs_0
|
|
811
|
+
probs = nv["glp_probs"]
|
|
812
|
+
idxm_0 = [i - 1 for i in (nv["glp_idxm"] or [])]
|
|
813
|
+
idxs_0 = [i - 1 for i in (nv["glp_idxs"] or [])]
|
|
814
|
+
|
|
815
|
+
gl_cols = ["msens", "ssens", "tmd", "tsd", "pmd", "psd", "gh", "vfi"]
|
|
402
816
|
|
|
403
|
-
gl_cols = ["msens", "ssens", "tmd", "tsd", "pmd", "psd", "gh", "vfi"]
|
|
404
817
|
mean_cols = [gl_cols[i] for i in idxm_0 if i < len(gl_cols)]
|
|
405
|
-
std_cols
|
|
406
|
-
|
|
818
|
+
std_cols = [gl_cols[i] for i in idxs_0 if i < len(gl_cols)]
|
|
819
|
+
|
|
820
|
+
n_probs = len(probs)
|
|
821
|
+
|
|
822
|
+
prob_vals = np.array([
|
|
823
|
+
_parse_lut_value(p) if isinstance(p, str) else float(p)
|
|
824
|
+
for p in probs
|
|
825
|
+
])
|
|
407
826
|
|
|
408
|
-
prob_vals = np.array([_parse_lut_value(p) if isinstance(p, str) else float(p)
|
|
409
|
-
for p in probs])
|
|
410
827
|
df_gp = df_gi.copy()
|
|
411
828
|
|
|
829
|
+
def _prob_col_name(col):
|
|
830
|
+
return f"{col}prob"
|
|
831
|
+
|
|
412
832
|
def _build_col_lut(col):
|
|
413
833
|
raw = lut_dict.get(col, [np.nan] * n_probs)
|
|
414
|
-
return np.array([
|
|
415
|
-
|
|
834
|
+
return np.array([
|
|
835
|
+
_parse_lut_value(v) if isinstance(v, str) else float(v)
|
|
836
|
+
for v in raw
|
|
837
|
+
])
|
|
416
838
|
|
|
839
|
+
# Left-tailed: lower values are abnormal
|
|
417
840
|
for col in mean_cols:
|
|
418
841
|
if col not in df_gi.columns:
|
|
419
842
|
continue
|
|
843
|
+
|
|
420
844
|
cutoffs = _build_col_lut(col)
|
|
421
845
|
vals = df_gi[col].values.astype(float)
|
|
422
|
-
vp
|
|
846
|
+
vp = np.full(len(vals), np.nan)
|
|
847
|
+
|
|
423
848
|
for i in range(n_probs - 1, 0, -1):
|
|
424
849
|
vp[vals < cutoffs[i]] = prob_vals[i]
|
|
425
|
-
df_gp[col] = vp
|
|
426
850
|
|
|
851
|
+
df_gp[_prob_col_name(col)] = vp
|
|
852
|
+
|
|
853
|
+
# Right-tailed: higher values are abnormal
|
|
427
854
|
for col in std_cols:
|
|
428
855
|
if col not in df_gi.columns:
|
|
429
856
|
continue
|
|
857
|
+
|
|
430
858
|
cutoffs = _build_col_lut(col)
|
|
431
859
|
vals = df_gi[col].values.astype(float)
|
|
432
|
-
vp
|
|
860
|
+
vp = np.full(len(vals), np.nan)
|
|
861
|
+
|
|
433
862
|
for i in range(n_probs - 1, 0, -1):
|
|
434
863
|
vp[vals > cutoffs[i]] = prob_vals[i]
|
|
435
|
-
|
|
864
|
+
|
|
865
|
+
df_gp[_prob_col_name(col)] = vp
|
|
436
866
|
|
|
437
867
|
return df_gp
|
|
438
868
|
|
|
@@ -446,7 +876,11 @@ def py_getallvalues(df_vf: pd.DataFrame):
|
|
|
446
876
|
df_tdp = py_gettdp(df_td)
|
|
447
877
|
df_pd = py_getpd(df_td)
|
|
448
878
|
df_pdp = py_getpdp(df_pd)
|
|
449
|
-
df_gi = py_getgl(df_vf)
|
|
450
|
-
df_gip = py_getglp(df_gi)
|
|
879
|
+
df_gi = np.nan # py_getgl(df_vf)
|
|
880
|
+
df_gip = np.nan # py_getglp(df_gi)
|
|
451
881
|
gh = py_getgh(df_td)
|
|
452
882
|
return df_td, df_tdp, df_gi, df_gip, df_pd, df_pdp, gh
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
|