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.
@@ -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("l") and c[1:].isdigit()]
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
- nv = _nv()
197
+
198
+ nv = _nv()
198
199
  coeff = nv["agem_coeff"]
199
200
 
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)
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
- 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)}.")
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
- vf_cols = _get_vf_cols(df_td)
227
- use_cols = [c for c in vf_cols if c not in bs_cols]
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
- vf_cols = _get_vf_cols(df_td)
255
+ td_cols = _get_vf_cols(df_td, colname='td')
241
256
  gh = py_getgh(df_td)
242
- df_pd = df_td.copy()
243
- df_pd[vf_cols] = df_td[vf_cols].values - gh[:, np.newaxis]
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 _apply_prob_lut(df: pd.DataFrame, lut_dict: dict, probs: list) -> pd.DataFrame:
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
- vf_cols = _get_vf_cols(df)
301
+
302
+ in_cols = _get_point_cols(df, input_prefix)
303
+
262
304
  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)
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
- for j, col in enumerate(vf_cols):
269
- raw = lut_dict.get(col) or [np.nan] * n_probs
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([_parse_lut_value(p) if isinstance(p, str) else float(p)
274
- for p in probs])
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
- df_p = df.copy()
281
- df_p[vf_cols] = valsp
282
- return df_p
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(df_td, nv["td_lut"], nv["td_probs"])
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(df_pd, nv["pd_lut"], nv["pd_probs"])
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
- """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}
592
+ """
593
+ Compute missing global indices only.
341
594
 
342
- vf_cols = _get_vf_cols(df_vf)
343
- use_cols = [c for c in vf_cols if c not in bs_cols]
595
+ Assumes df_vf is canonicalized and contains:
596
+ l1-l54, td1-td54, pd1-pd54, tdp1-tdp54, pdp1-pdp54
597
+ """
344
598
 
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)
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 (left-tailed for means, right for SDs)."""
397
- nv = _nv()
807
+ """Global indices probability values."""
808
+
809
+ nv = _nv()
398
810
  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 [])]
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 = [gl_cols[i] for i in idxs_0 if i < len(gl_cols)]
406
- n_probs = len(probs)
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([_parse_lut_value(v) if isinstance(v, str) else float(v)
415
- for v in raw])
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 = np.full(len(vals), np.nan)
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 = np.full(len(vals), np.nan)
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
- df_gp[col] = vp
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
+