PyGlaucoMetrics 0.2.0__tar.gz
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.
Potentially problematic release.
This version of PyGlaucoMetrics might be problematic. Click here for more details.
- pyglaucometrics-0.2.0/PKG-INFO +64 -0
- pyglaucometrics-0.2.0/README.md +47 -0
- pyglaucometrics-0.2.0/pyproject.toml +31 -0
- pyglaucometrics-0.2.0/setup.cfg +4 -0
- pyglaucometrics-0.2.0/src/PyGlaucoMetrics.egg-info/PKG-INFO +64 -0
- pyglaucometrics-0.2.0/src/PyGlaucoMetrics.egg-info/SOURCES.txt +10 -0
- pyglaucometrics-0.2.0/src/PyGlaucoMetrics.egg-info/dependency_links.txt +1 -0
- pyglaucometrics-0.2.0/src/PyGlaucoMetrics.egg-info/requires.txt +8 -0
- pyglaucometrics-0.2.0/src/PyGlaucoMetrics.egg-info/top_level.txt +1 -0
- pyglaucometrics-0.2.0/src/PyVisualFields/vf_core.py +228 -0
- pyglaucometrics-0.2.0/src/PyVisualFields/vfprogression.py +585 -0
- pyglaucometrics-0.2.0/src/PyVisualFields/visualFields.py +1044 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: PyGlaucoMetrics
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Visual field analysis for glaucoma without R dependencies
|
|
5
|
+
Author-email: Mousa Moradi <mmoradi2@meei.harvard.edu>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Mousamoradi/PyGlaucoMetrics/tree/ver-2026
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: pandas>=1.2
|
|
11
|
+
Requires-Dist: matplotlib>=3.7
|
|
12
|
+
Requires-Dist: scipy>=1.10
|
|
13
|
+
Requires-Dist: PyQt5>=5.15
|
|
14
|
+
Requires-Dist: Pillow>=8.0
|
|
15
|
+
Requires-Dist: seaborn>=0.13
|
|
16
|
+
Requires-Dist: pingouin>=0.5
|
|
17
|
+
|
|
18
|
+
# Description: PyGlaucoMetrics is designed to classify visual field data to glaucomatous or non-glaucomatous classes. It can accept Humphrey Field Analysis 24-2 and 10-2 test patterns. PyGlaucometrics, has been tested with below data types:
|
|
19
|
+
# (A) data_vfpwgRetest24d2: Short-term retest static automated perimetry data, collected from 30 glaucoma patients at the Queen Elizabeth Health Sciences Centre in Halifax, Nova Scotia. This dataset includes 12 visual field tests conducted over 12 weekly sessions.
|
|
20
|
+
# (B) data_vfpwgSunyiu24d2: 24-2 static automated perimetry data from a patient with glaucoma. This dataset consists of real patient data, with age modified for anonymity.
|
|
21
|
+
# (C) data_vfctrSunyiu24d2: A dataset of healthy eyes for 24-2 static automated perimetry, used to generate normative values. This dataset (sunyiu_24d2 and related sets) is provided courtesy of William H. Swanson and Mitch W. Dul.
|
|
22
|
+
# (D) data_vfctrSunyiu10d2: A dataset of healthy eyes for 10-2 static automated perimetry, also contributed by William H. Swanson.
|
|
23
|
+
|
|
24
|
+
# Instruction:
|
|
25
|
+
|
|
26
|
+
***This instruction assumes the required library correctly installed. If not, please install exact version of libraries/packages as stated in the Requirements.
|
|
27
|
+
|
|
28
|
+
1- Open Anaconda PowerShell Prompt:
|
|
29
|
+
|
|
30
|
+
>> cd "set path to folder"
|
|
31
|
+
|
|
32
|
+
2- conda activate env_pyVF
|
|
33
|
+
|
|
34
|
+
#python test_rpy2.py
|
|
35
|
+
|
|
36
|
+
3- Open Jupyter notebook
|
|
37
|
+
|
|
38
|
+
>> jupyter notebook
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# General overview:
|
|
44
|
+
# 1- Import raw VF data
|
|
45
|
+
df_VFs = pd.read_csv('VF_Data.csv')
|
|
46
|
+
# 2- Get td, tdp, pd, and pdp from PyVisualField Package.
|
|
47
|
+
df_td, df_tdp, df_pdp = visualFields.getallvalues(df_VFs)
|
|
48
|
+
# 3- Obtain required columns from each dataframe
|
|
49
|
+
raw_data_pdp = df_pdp.loc[:, 'l1':'l54']
|
|
50
|
+
raw_data_td = df_td.loc[:, 'l1':'l54']
|
|
51
|
+
raw_data_tdp = df_tdp.loc[:, 'l1':'l54']
|
|
52
|
+
# 4- Call each function and save resulted diagnosis
|
|
53
|
+
df_diag_HAP2 = Fn_HAP2_part2(raw_data_pdp) # it needs pdp values. will compute if necessary
|
|
54
|
+
df_diag_UKG = Fn_UKGTS(raw_data_td) #it needs tdp values, will compute if necessary
|
|
55
|
+
df_diag_logts = Fn_LoGTS(raw_data_tdp) # it need TD values, will compute if necessary
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# References:
|
|
60
|
+
1- Moradi, M., Hashemabad, S.K., Vu, D.M., Soneru, A.R., Fujita, A., Wang, M., Elze, T., Eslami, M. and Zebardast, N., 2025. PyGlaucoMetrics: a stacked weight-based machine learning approach for glaucoma detection using visual field data. Medicina, 61(3), p.541. https://www.mdpi.com/1648-9144/61/3/541
|
|
61
|
+
|
|
62
|
+
2- Moradi, Mousa, Mohammad Eslami, Saber Kazeminasab Hashemabad, David S. Friedman, Michael V. Boland, Mengyu Wang, Tobias Elze, and Nazlee Zebardast. "PyGlaucoMetrics: An Open-Source Multi-Criteria Glaucoma Defect Evaluation." Investigative Ophthalmology & Visual Science 65, no. 7 (2024): OD38-OD38. https://iovs.arvojournals.org/article.aspx?articleid=2800368
|
|
63
|
+
|
|
64
|
+
3- Eslami, Mohammad, Saber Kazeminasab, Vishal Sharma, Yangjiani Li, Mojtaba Fazli, Mengyu Wang, Nazlee Zebardast, and Tobias Elze. "PyVisualFields: A Python Package for Visual Field Analysis." Translational Vision Science & Technology 12, no. 2 (2023): 6-6. https://tvst.arvojournals.org/article.aspx?articleid=2785341)https://tvst.arvojournals.org/article.aspx?articleid=2785341
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Description: PyGlaucoMetrics is designed to classify visual field data to glaucomatous or non-glaucomatous classes. It can accept Humphrey Field Analysis 24-2 and 10-2 test patterns. PyGlaucometrics, has been tested with below data types:
|
|
2
|
+
# (A) data_vfpwgRetest24d2: Short-term retest static automated perimetry data, collected from 30 glaucoma patients at the Queen Elizabeth Health Sciences Centre in Halifax, Nova Scotia. This dataset includes 12 visual field tests conducted over 12 weekly sessions.
|
|
3
|
+
# (B) data_vfpwgSunyiu24d2: 24-2 static automated perimetry data from a patient with glaucoma. This dataset consists of real patient data, with age modified for anonymity.
|
|
4
|
+
# (C) data_vfctrSunyiu24d2: A dataset of healthy eyes for 24-2 static automated perimetry, used to generate normative values. This dataset (sunyiu_24d2 and related sets) is provided courtesy of William H. Swanson and Mitch W. Dul.
|
|
5
|
+
# (D) data_vfctrSunyiu10d2: A dataset of healthy eyes for 10-2 static automated perimetry, also contributed by William H. Swanson.
|
|
6
|
+
|
|
7
|
+
# Instruction:
|
|
8
|
+
|
|
9
|
+
***This instruction assumes the required library correctly installed. If not, please install exact version of libraries/packages as stated in the Requirements.
|
|
10
|
+
|
|
11
|
+
1- Open Anaconda PowerShell Prompt:
|
|
12
|
+
|
|
13
|
+
>> cd "set path to folder"
|
|
14
|
+
|
|
15
|
+
2- conda activate env_pyVF
|
|
16
|
+
|
|
17
|
+
#python test_rpy2.py
|
|
18
|
+
|
|
19
|
+
3- Open Jupyter notebook
|
|
20
|
+
|
|
21
|
+
>> jupyter notebook
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# General overview:
|
|
27
|
+
# 1- Import raw VF data
|
|
28
|
+
df_VFs = pd.read_csv('VF_Data.csv')
|
|
29
|
+
# 2- Get td, tdp, pd, and pdp from PyVisualField Package.
|
|
30
|
+
df_td, df_tdp, df_pdp = visualFields.getallvalues(df_VFs)
|
|
31
|
+
# 3- Obtain required columns from each dataframe
|
|
32
|
+
raw_data_pdp = df_pdp.loc[:, 'l1':'l54']
|
|
33
|
+
raw_data_td = df_td.loc[:, 'l1':'l54']
|
|
34
|
+
raw_data_tdp = df_tdp.loc[:, 'l1':'l54']
|
|
35
|
+
# 4- Call each function and save resulted diagnosis
|
|
36
|
+
df_diag_HAP2 = Fn_HAP2_part2(raw_data_pdp) # it needs pdp values. will compute if necessary
|
|
37
|
+
df_diag_UKG = Fn_UKGTS(raw_data_td) #it needs tdp values, will compute if necessary
|
|
38
|
+
df_diag_logts = Fn_LoGTS(raw_data_tdp) # it need TD values, will compute if necessary
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# References:
|
|
43
|
+
1- Moradi, M., Hashemabad, S.K., Vu, D.M., Soneru, A.R., Fujita, A., Wang, M., Elze, T., Eslami, M. and Zebardast, N., 2025. PyGlaucoMetrics: a stacked weight-based machine learning approach for glaucoma detection using visual field data. Medicina, 61(3), p.541. https://www.mdpi.com/1648-9144/61/3/541
|
|
44
|
+
|
|
45
|
+
2- Moradi, Mousa, Mohammad Eslami, Saber Kazeminasab Hashemabad, David S. Friedman, Michael V. Boland, Mengyu Wang, Tobias Elze, and Nazlee Zebardast. "PyGlaucoMetrics: An Open-Source Multi-Criteria Glaucoma Defect Evaluation." Investigative Ophthalmology & Visual Science 65, no. 7 (2024): OD38-OD38. https://iovs.arvojournals.org/article.aspx?articleid=2800368
|
|
46
|
+
|
|
47
|
+
3- Eslami, Mohammad, Saber Kazeminasab, Vishal Sharma, Yangjiani Li, Mojtaba Fazli, Mengyu Wang, Nazlee Zebardast, and Tobias Elze. "PyVisualFields: A Python Package for Visual Field Analysis." Translational Vision Science & Technology 12, no. 2 (2023): 6-6. https://tvst.arvojournals.org/article.aspx?articleid=2785341)https://tvst.arvojournals.org/article.aspx?articleid=2785341
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "PyGlaucoMetrics"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Visual field analysis for glaucoma without R dependencies"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {file = "LICENSE"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{name = "Mousa Moradi", email = "mmoradi2@meei.harvard.edu"}]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy>=1.24",
|
|
15
|
+
"pandas>=1.2",
|
|
16
|
+
"matplotlib>=3.7",
|
|
17
|
+
"scipy>=1.10",
|
|
18
|
+
"PyQt5>=5.15",
|
|
19
|
+
"Pillow>=8.0",
|
|
20
|
+
"seaborn>=0.13",
|
|
21
|
+
"pingouin>=0.5",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-data]
|
|
28
|
+
"PyGlaucoMetrics" = ["data/*.csv"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Mousamoradi/PyGlaucoMetrics/tree/ver-2026"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: PyGlaucoMetrics
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Visual field analysis for glaucoma without R dependencies
|
|
5
|
+
Author-email: Mousa Moradi <mmoradi2@meei.harvard.edu>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Mousamoradi/PyGlaucoMetrics/tree/ver-2026
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: pandas>=1.2
|
|
11
|
+
Requires-Dist: matplotlib>=3.7
|
|
12
|
+
Requires-Dist: scipy>=1.10
|
|
13
|
+
Requires-Dist: PyQt5>=5.15
|
|
14
|
+
Requires-Dist: Pillow>=8.0
|
|
15
|
+
Requires-Dist: seaborn>=0.13
|
|
16
|
+
Requires-Dist: pingouin>=0.5
|
|
17
|
+
|
|
18
|
+
# Description: PyGlaucoMetrics is designed to classify visual field data to glaucomatous or non-glaucomatous classes. It can accept Humphrey Field Analysis 24-2 and 10-2 test patterns. PyGlaucometrics, has been tested with below data types:
|
|
19
|
+
# (A) data_vfpwgRetest24d2: Short-term retest static automated perimetry data, collected from 30 glaucoma patients at the Queen Elizabeth Health Sciences Centre in Halifax, Nova Scotia. This dataset includes 12 visual field tests conducted over 12 weekly sessions.
|
|
20
|
+
# (B) data_vfpwgSunyiu24d2: 24-2 static automated perimetry data from a patient with glaucoma. This dataset consists of real patient data, with age modified for anonymity.
|
|
21
|
+
# (C) data_vfctrSunyiu24d2: A dataset of healthy eyes for 24-2 static automated perimetry, used to generate normative values. This dataset (sunyiu_24d2 and related sets) is provided courtesy of William H. Swanson and Mitch W. Dul.
|
|
22
|
+
# (D) data_vfctrSunyiu10d2: A dataset of healthy eyes for 10-2 static automated perimetry, also contributed by William H. Swanson.
|
|
23
|
+
|
|
24
|
+
# Instruction:
|
|
25
|
+
|
|
26
|
+
***This instruction assumes the required library correctly installed. If not, please install exact version of libraries/packages as stated in the Requirements.
|
|
27
|
+
|
|
28
|
+
1- Open Anaconda PowerShell Prompt:
|
|
29
|
+
|
|
30
|
+
>> cd "set path to folder"
|
|
31
|
+
|
|
32
|
+
2- conda activate env_pyVF
|
|
33
|
+
|
|
34
|
+
#python test_rpy2.py
|
|
35
|
+
|
|
36
|
+
3- Open Jupyter notebook
|
|
37
|
+
|
|
38
|
+
>> jupyter notebook
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# General overview:
|
|
44
|
+
# 1- Import raw VF data
|
|
45
|
+
df_VFs = pd.read_csv('VF_Data.csv')
|
|
46
|
+
# 2- Get td, tdp, pd, and pdp from PyVisualField Package.
|
|
47
|
+
df_td, df_tdp, df_pdp = visualFields.getallvalues(df_VFs)
|
|
48
|
+
# 3- Obtain required columns from each dataframe
|
|
49
|
+
raw_data_pdp = df_pdp.loc[:, 'l1':'l54']
|
|
50
|
+
raw_data_td = df_td.loc[:, 'l1':'l54']
|
|
51
|
+
raw_data_tdp = df_tdp.loc[:, 'l1':'l54']
|
|
52
|
+
# 4- Call each function and save resulted diagnosis
|
|
53
|
+
df_diag_HAP2 = Fn_HAP2_part2(raw_data_pdp) # it needs pdp values. will compute if necessary
|
|
54
|
+
df_diag_UKG = Fn_UKGTS(raw_data_td) #it needs tdp values, will compute if necessary
|
|
55
|
+
df_diag_logts = Fn_LoGTS(raw_data_tdp) # it need TD values, will compute if necessary
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# References:
|
|
60
|
+
1- Moradi, M., Hashemabad, S.K., Vu, D.M., Soneru, A.R., Fujita, A., Wang, M., Elze, T., Eslami, M. and Zebardast, N., 2025. PyGlaucoMetrics: a stacked weight-based machine learning approach for glaucoma detection using visual field data. Medicina, 61(3), p.541. https://www.mdpi.com/1648-9144/61/3/541
|
|
61
|
+
|
|
62
|
+
2- Moradi, Mousa, Mohammad Eslami, Saber Kazeminasab Hashemabad, David S. Friedman, Michael V. Boland, Mengyu Wang, Tobias Elze, and Nazlee Zebardast. "PyGlaucoMetrics: An Open-Source Multi-Criteria Glaucoma Defect Evaluation." Investigative Ophthalmology & Visual Science 65, no. 7 (2024): OD38-OD38. https://iovs.arvojournals.org/article.aspx?articleid=2800368
|
|
63
|
+
|
|
64
|
+
3- Eslami, Mohammad, Saber Kazeminasab, Vishal Sharma, Yangjiani Li, Mojtaba Fazli, Mengyu Wang, Nazlee Zebardast, and Tobias Elze. "PyVisualFields: A Python Package for Visual Field Analysis." Translational Vision Science & Technology 12, no. 2 (2023): 6-6. https://tvst.arvojournals.org/article.aspx?articleid=2785341)https://tvst.arvojournals.org/article.aspx?articleid=2785341
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/PyGlaucoMetrics.egg-info/PKG-INFO
|
|
4
|
+
src/PyGlaucoMetrics.egg-info/SOURCES.txt
|
|
5
|
+
src/PyGlaucoMetrics.egg-info/dependency_links.txt
|
|
6
|
+
src/PyGlaucoMetrics.egg-info/requires.txt
|
|
7
|
+
src/PyGlaucoMetrics.egg-info/top_level.txt
|
|
8
|
+
src/PyVisualFields/vf_core.py
|
|
9
|
+
src/PyVisualFields/vfprogression.py
|
|
10
|
+
src/PyVisualFields/visualFields.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyVisualFields
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vf_core.py
|
|
3
|
+
Pure-Python replacement for the R `visualFields` package.
|
|
4
|
+
Covers: normative lookup, TD/PD, MD/PSD/VFI, progression regression.
|
|
5
|
+
|
|
6
|
+
Dependencies: numpy, scipy, pandas — no R or rpy2 required.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from scipy import stats
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# 1. Normative values
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Auto-load real Sunyiu 24-2 normatives computed from vfctrSunyiu24d2 dataset.
|
|
19
|
+
# Falls back to Heijl 1987 approximation if CSV not found.
|
|
20
|
+
|
|
21
|
+
def _load_default_normdb() -> np.ndarray:
|
|
22
|
+
"""Load normvals_sunyiu24d2.csv if available, else use Heijl 1987 approximation."""
|
|
23
|
+
csv_path = Path(__file__).parent / 'data' / 'normvals_sunyiu24d2.csv'
|
|
24
|
+
if csv_path.exists():
|
|
25
|
+
df = pd.read_csv(csv_path)
|
|
26
|
+
return df[['intercept', 'slope']].values.astype(np.float32)
|
|
27
|
+
# Heijl 1987 approximation fallback (54 points)
|
|
28
|
+
return np.array([
|
|
29
|
+
[33.5, -0.082], [34.2, -0.082], [33.8, -0.083], [33.1, -0.082],
|
|
30
|
+
[32.9, -0.081], [33.0, -0.081], [33.2, -0.082], [32.8, -0.081],
|
|
31
|
+
[31.5, -0.080], [32.0, -0.081], [32.5, -0.080], [32.1, -0.079],
|
|
32
|
+
[30.8, -0.079], [31.3, -0.079], [31.6, -0.079], [31.0, -0.078],
|
|
33
|
+
[30.1, -0.078], [30.5, -0.078], [30.9, -0.078], [30.3, -0.077],
|
|
34
|
+
[29.5, -0.077], [29.8, -0.077], [30.2, -0.077], [29.6, -0.076],
|
|
35
|
+
[28.9, -0.076], [29.1, -0.076], [29.5, -0.076], [28.8, -0.075],
|
|
36
|
+
[28.0, -0.075], [28.4, -0.075], [28.7, -0.075], [28.1, -0.074],
|
|
37
|
+
[27.2, -0.074], [27.6, -0.074], [27.9, -0.074], [27.3, -0.073],
|
|
38
|
+
[26.5, -0.073], [26.8, -0.073], [27.1, -0.073], [26.5, -0.072],
|
|
39
|
+
[25.8, -0.072], [26.0, -0.072], [26.3, -0.072], [25.7, -0.071],
|
|
40
|
+
[25.0, -0.071], [25.2, -0.071], [25.5, -0.071], [24.9, -0.070],
|
|
41
|
+
[24.2, -0.070], [24.4, -0.070], [24.7, -0.070], [24.1, -0.069],
|
|
42
|
+
[23.5, -0.069], [23.7, -0.069],
|
|
43
|
+
], dtype=np.float32)
|
|
44
|
+
|
|
45
|
+
_NORM_24_2 = _load_default_normdb()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_normative_db(csv_path: Optional[str] = None) -> np.ndarray:
|
|
49
|
+
"""
|
|
50
|
+
Load normative table (54 × 2: intercept, slope).
|
|
51
|
+
Pass a CSV path with columns ['intercept', 'slope'] to override defaults.
|
|
52
|
+
"""
|
|
53
|
+
if csv_path and Path(csv_path).exists():
|
|
54
|
+
df = pd.read_csv(csv_path)
|
|
55
|
+
return df[['intercept', 'slope']].values.astype(np.float32)
|
|
56
|
+
return _NORM_24_2
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def age_matched_norms(age: float, norm_db: Optional[np.ndarray] = None) -> np.ndarray:
|
|
60
|
+
"""Expected threshold per test point for a given age (years). Returns shape (54,)."""
|
|
61
|
+
db = norm_db if norm_db is not None else _NORM_24_2
|
|
62
|
+
return db[:, 0] + db[:, 1] * age # intercept + slope × age
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# 2. Pointwise deviations
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def total_deviation(sensitivity: np.ndarray, age: float,
|
|
70
|
+
norm_db: Optional[np.ndarray] = None) -> np.ndarray:
|
|
71
|
+
if age is None or np.isnan(age): # ← ADD
|
|
72
|
+
age = 60.0 # ← fallback to population mean
|
|
73
|
+
norms = age_matched_norms(age, norm_db)
|
|
74
|
+
return sensitivity - norms
|
|
75
|
+
|
|
76
|
+
def pattern_deviation(td: np.ndarray, percentile: float = 85.0) -> np.ndarray:
|
|
77
|
+
valid = td[~np.isnan(td)]
|
|
78
|
+
if len(valid) == 0: # ← ADD THIS GUARD
|
|
79
|
+
return np.full_like(td, np.nan)
|
|
80
|
+
general_height = np.percentile(valid, percentile)
|
|
81
|
+
return td - general_height
|
|
82
|
+
|
|
83
|
+
def compute_indices(sensitivity: np.ndarray, age: float,
|
|
84
|
+
weights=None, norm_db=None,
|
|
85
|
+
norm_percentile: float = 85.0) -> dict:
|
|
86
|
+
td = total_deviation(sensitivity, age, norm_db)
|
|
87
|
+
if np.sum(~np.isnan(td)) == 0: # all-NaN row — return safe nulls
|
|
88
|
+
nan54 = np.full(len(td), np.nan)
|
|
89
|
+
return {"td": nan54, "pd": nan54,
|
|
90
|
+
"MD": np.nan, "PSD": np.nan, "VFI": np.nan}
|
|
91
|
+
pd_vals = pattern_deviation(td, norm_percentile)
|
|
92
|
+
return {
|
|
93
|
+
"td": td,
|
|
94
|
+
"pd": pd_vals,
|
|
95
|
+
"MD": mean_deviation(td, weights),
|
|
96
|
+
"PSD": pattern_std(pd_vals, weights),
|
|
97
|
+
"VFI": vfi(pd_vals, weights),
|
|
98
|
+
}
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# 3. Global indices
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
# Eccentricity-based weights for 24-2 (simplified Heijl 1987 Gaussian weights).
|
|
104
|
+
# In production, load the exact per-point weights from the normative package.
|
|
105
|
+
_WEIGHTS_24_2 = np.ones(54, dtype=np.float32) # uniform fallback
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def mean_deviation(td: np.ndarray, weights: Optional[np.ndarray] = None) -> float:
|
|
109
|
+
w = weights if weights is not None else _WEIGHTS_24_2
|
|
110
|
+
mask = ~np.isnan(td)
|
|
111
|
+
if mask.sum() == 0: # ← ADD
|
|
112
|
+
return np.nan # ← nothing to average
|
|
113
|
+
return float(np.average(td[mask], weights=w[mask]))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pattern_std(pd_vals: np.ndarray, weights: Optional[np.ndarray] = None) -> float:
|
|
117
|
+
"""
|
|
118
|
+
Pattern Standard Deviation (PSD) in dB.
|
|
119
|
+
Weighted root-mean-square of pattern deviation values.
|
|
120
|
+
"""
|
|
121
|
+
w = weights if weights is not None else _WEIGHTS_24_2
|
|
122
|
+
mask = ~np.isnan(pd_vals)
|
|
123
|
+
pd_m = pd_vals[mask]
|
|
124
|
+
w_m = w[mask]
|
|
125
|
+
w_m = w_m / w_m.sum()
|
|
126
|
+
weighted_mean = np.dot(w_m, pd_m)
|
|
127
|
+
weighted_var = np.dot(w_m, (pd_m - weighted_mean) ** 2)
|
|
128
|
+
return float(np.sqrt(weighted_var))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def vfi(pd_vals: np.ndarray, weights: Optional[np.ndarray] = None) -> float:
|
|
132
|
+
"""
|
|
133
|
+
Visual Field Index (VFI) — approximate (Bengtsson & Heijl 2008).
|
|
134
|
+
Returns value in [0, 100] where 100 = normal.
|
|
135
|
+
"""
|
|
136
|
+
w = weights if weights is not None else _WEIGHTS_24_2
|
|
137
|
+
mask = ~np.isnan(pd_vals)
|
|
138
|
+
pd_m = pd_vals[mask]
|
|
139
|
+
w_m = w[mask] / w[mask].sum()
|
|
140
|
+
# clamp PD contribution to [−30, 0] then scale to percentage
|
|
141
|
+
contrib = np.clip(pd_m, -30.0, 0.0) / 30.0 # 0 = normal, −1 = fully depressed
|
|
142
|
+
return float(100.0 * (1.0 + np.dot(w_m, contrib)))
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# 4. Progression analysis
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def vf_progression(dates: list, md_series: list) -> dict:
|
|
149
|
+
"""
|
|
150
|
+
Linear regression of MD over time (OLS — mirrors `vfprogression` PLR).
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
dates : list of date-like objects or float years (e.g. 2020.5)
|
|
155
|
+
md_series : list/array of MD values (dB)
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
dict: slope (dB/year), intercept, r2, p_value, se, progression_flag
|
|
160
|
+
"""
|
|
161
|
+
if isinstance(dates[0], (int, float)):
|
|
162
|
+
t = np.array(dates, dtype=float)
|
|
163
|
+
else:
|
|
164
|
+
t0 = pd.to_datetime(dates[0])
|
|
165
|
+
t = np.array([(pd.to_datetime(d) - t0).days / 365.25 for d in dates])
|
|
166
|
+
|
|
167
|
+
md = np.array(md_series, dtype=float)
|
|
168
|
+
mask = ~np.isnan(md)
|
|
169
|
+
if mask.sum() < 3:
|
|
170
|
+
return {"slope": np.nan, "intercept": np.nan,
|
|
171
|
+
"r2": np.nan, "p_value": np.nan,
|
|
172
|
+
"se": np.nan, "progression_flag": False}
|
|
173
|
+
|
|
174
|
+
slope, intercept, r, p, se = stats.linregress(t[mask], md[mask])
|
|
175
|
+
# Flag as progressing: slope < −1.0 dB/year AND p < 0.05 (common clinical threshold)
|
|
176
|
+
flag = bool(slope < -1.0 and p < 0.05)
|
|
177
|
+
return {
|
|
178
|
+
"slope": round(slope, 4),
|
|
179
|
+
"intercept": round(intercept, 4),
|
|
180
|
+
"r2": round(r ** 2, 4),
|
|
181
|
+
"p_value": round(p, 4),
|
|
182
|
+
"se": round(se, 4),
|
|
183
|
+
"progression_flag": flag,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# 5. Probability maps (p < 0.05 / 0.02 / 0.01 / 0.005 flags)
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Normal distribution approximation; replace with empirical quantiles if available.
|
|
191
|
+
|
|
192
|
+
def _load_empirical_cutoffs():
|
|
193
|
+
"""Load per-location empirical probability cutoffs if available."""
|
|
194
|
+
csv_path = Path(__file__).parent / 'data' / 'normvals_cutoffs.csv'
|
|
195
|
+
if csv_path.exists():
|
|
196
|
+
return pd.read_csv(csv_path).values # shape (54, 4): p0.005, p0.01, p0.02, p0.05
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
_EMPIRICAL_CUTOFFS = _load_empirical_cutoffs() # (54,4) or None
|
|
200
|
+
|
|
201
|
+
_TD_PROB_CUTOFFS = {0.05: -2.0, 0.02: -2.5, 0.01: -3.0, 0.005: -3.5}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def probability_map(deviation: np.ndarray,
|
|
205
|
+
cutoffs: Optional[dict] = None) -> np.ndarray:
|
|
206
|
+
"""
|
|
207
|
+
Assign probability level per test point.
|
|
208
|
+
Uses per-location empirical cutoffs if available (matches R exactly),
|
|
209
|
+
otherwise falls back to normal approximation.
|
|
210
|
+
Returns float array: 0.05, 0.02, 0.01, 0.005, or 1.0 (normal).
|
|
211
|
+
"""
|
|
212
|
+
dev = np.array(deviation, dtype=float)
|
|
213
|
+
levels = np.ones(len(dev))
|
|
214
|
+
|
|
215
|
+
if _EMPIRICAL_CUTOFFS is not None and cutoffs is None:
|
|
216
|
+
n = min(len(dev), _EMPIRICAL_CUTOFFS.shape[0])
|
|
217
|
+
for j, p in zip([3, 2, 1, 0], [0.05, 0.02, 0.01, 0.005]):
|
|
218
|
+
col = _EMPIRICAL_CUTOFFS[:n, j]
|
|
219
|
+
for i in range(n):
|
|
220
|
+
if not np.isnan(dev[i]) and dev[i] <= col[i]:
|
|
221
|
+
levels[i] = p
|
|
222
|
+
else:
|
|
223
|
+
# Fallback: uniform dB cutoffs
|
|
224
|
+
cuts = cutoffs or _TD_PROB_CUTOFFS
|
|
225
|
+
for p_level in sorted(cuts.keys(), reverse=True):
|
|
226
|
+
levels[deviation <= cuts[p_level]] = p_level
|
|
227
|
+
|
|
228
|
+
return levels
|