diff --git a/docs/api/datasets.rst b/docs/api/datasets.rst index b02439d26..067f6eb23 100644 --- a/docs/api/datasets.rst +++ b/docs/api/datasets.rst @@ -224,6 +224,7 @@ Available Datasets datasets/pyhealth.datasets.SampleDataset datasets/pyhealth.datasets.MIMIC3Dataset datasets/pyhealth.datasets.MIMIC4Dataset + datasets/pyhealth.datasets.MRIDataset datasets/pyhealth.datasets.MedicalTranscriptionsDataset datasets/pyhealth.datasets.CardiologyDataset datasets/pyhealth.datasets.eICUDataset diff --git a/docs/api/datasets/pyhealth.datasets.MRIDataset.rst b/docs/api/datasets/pyhealth.datasets.MRIDataset.rst new file mode 100644 index 000000000..a80afffb5 --- /dev/null +++ b/docs/api/datasets/pyhealth.datasets.MRIDataset.rst @@ -0,0 +1,21 @@ +pyhealth.datasets.MRIDataset +=================================== + +The dataset used is the OASIS MRI dataset (https://sites.wustl.edu/oasisbrains/), which consists of 80,000 brain MRI images. The images have been divided into four classes based on Alzheimer's progression. The dataset aims to provide a valuable resource for analyzing and detecting early signs of Alzheimer's disease. + +To make the dataset accessible, the original .img and .hdr files were converted into Nifti format (.nii) using FSL (FMRIB Software Library). The converted MRI images of 461 patients have been uploaded to a GitHub repository, which can be accessed in multiple parts. + +For the neural network training, 2D images were used as input. The brain images were sliced along the z-axis into 256 pieces, and slices ranging from 100 to 160 were selected from each patient. This approach resulted in a comprehensive dataset for analysis. + +Patient classification was performed based on the provided metadata and Clinical Dementia Rating (CDR) values, resulting in four classes: demented, very mild demented, mild demented, and non-demented. These classes enable the detection and study of different stages of Alzheimer's disease progression. + +During the dataset preparation, the .nii MRI scans were converted to .jpg files. Although this conversion presented some challenges, the files were successfully processed using appropriate tools. The resulting dataset size is 1.3 GB. + +With this comprehensive dataset, the project aims to explore various neural network models and achieve optimal results in Alzheimer's disease detection and analysis. + +Refer to `doc `_ for more information. + +.. autoclass:: pyhealth.datasets.MRIDataset + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api/tasks.rst b/docs/api/tasks.rst index 399b8f1aa..5e495ee51 100644 --- a/docs/api/tasks.rst +++ b/docs/api/tasks.rst @@ -216,6 +216,7 @@ Available Tasks Medical Transcriptions Classification Mortality Prediction (Next Visit) Mortality Prediction (StageNet MIMIC-IV) + MRI Binary Classification Patient Linkage (MIMIC-III) Readmission Prediction Sleep Staging diff --git a/docs/api/tasks/pyhealth.tasks.MRIBinaryClassification.rst b/docs/api/tasks/pyhealth.tasks.MRIBinaryClassification.rst new file mode 100644 index 000000000..ff253ea42 --- /dev/null +++ b/docs/api/tasks/pyhealth.tasks.MRIBinaryClassification.rst @@ -0,0 +1,7 @@ +pyhealth.tasks.MRIBinaryClassification +======================================= + +.. autoclass:: pyhealth.tasks.MRIBinaryClassification + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/mri_alzheimers.py b/examples/mri_alzheimers.py new file mode 100644 index 000000000..17844ee91 --- /dev/null +++ b/examples/mri_alzheimers.py @@ -0,0 +1,49 @@ +import os +import tempfile + +from pyhealth.datasets import MRIDataset, get_dataloader, split_by_sample +from pyhealth.models import CNN +from pyhealth.processors import NiftiImageProcessor +from pyhealth.tasks import MRIBinaryClassification +from pyhealth.trainer import Trainer + +# Since PyHealth uses multiprocessing, it is best practice to use a main guard. +if __name__ == "__main__": + # Use tempfile to automate cleanup + dataset_dir = tempfile.TemporaryDirectory() + cache_dir = tempfile.TemporaryDirectory() + + dataset = MRIDataset( + root=dataset_dir.name, + cache_dir=cache_dir.name, + download=True, + partial=True, + ) + dataset.stats() + + task = MRIBinaryClassification(disease="alzheimer") + samples = dataset.set_task( + task, + input_processors={"image": NiftiImageProcessor()}, + ) + + train_dataset, val_dataset, test_dataset = split_by_sample(samples, [0.7, 0.1, 0.2]) + + train_loader = get_dataloader(train_dataset, batch_size=8, shuffle=True) + val_loader = get_dataloader(val_dataset, batch_size=8, shuffle=False) + test_loader = get_dataloader(test_dataset, batch_size=8, shuffle=False) + + model = CNN(dataset=samples) + + # Default to CPU to avoid CUDA runtime mismatch on unsupported GPUs. + device = os.environ.get("PYHEALTH_DEVICE", "cpu") + trainer = Trainer(model=model, device=device) + trainer.train( + train_dataloader=train_loader, + val_dataloader=val_loader, + epochs=1, + ) + + trainer.evaluate(test_loader) + + samples.close() diff --git a/pixi.lock b/pixi.lock index 42762b62b..0f11d28d7 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2224,7 +2224,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/dd/0107f0aa179869ee9f47ef5a2686abd5e022fdc82af901d535e52fe91ce1/accelerate-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/f9/25753b9de3029d3eb2487755520b98eb72b0cb562d8974329c6e19831063/axial_positional_embedding-0.3.12-py3-none-any.whl @@ -2241,7 +2240,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/1d/54/a46920229d12c3a6e9f0081d1bdaeffad23c1826353ace95714faee926e5/dask-2025.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/ec/da78855318971c2be94d0283a41de6941a6b9f16146fb00babc74903ae01/distributed-2025.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl @@ -2271,7 +2269,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/87/0d/1861d1599571974b15b025e12b142d8e6b42ad66c8a07a89cb0fc21f1e03/narwhals-2.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -2311,7 +2308,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/43/3f250ec28edff1c06ffaa25faddbe13ae85c11a9724894cbdcf89427de78/rdkit-2025.3.3-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz - pypi: https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/99/72/c86a4cd867816350fe8dee13f30222340b9cd6b96173955819a5561810c5/scikit_learn-1.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -2364,7 +2360,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/dd/0107f0aa179869ee9f47ef5a2686abd5e022fdc82af901d535e52fe91ce1/accelerate-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/f9/25753b9de3029d3eb2487755520b98eb72b0cb562d8974329c6e19831063/axial_positional_embedding-0.3.12-py3-none-any.whl @@ -2381,7 +2376,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/1d/54/a46920229d12c3a6e9f0081d1bdaeffad23c1826353ace95714faee926e5/dask-2025.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/ec/da78855318971c2be94d0283a41de6941a6b9f16146fb00babc74903ae01/distributed-2025.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl @@ -2411,7 +2405,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/87/0d/1861d1599571974b15b025e12b142d8e6b42ad66c8a07a89cb0fc21f1e03/narwhals-2.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/20/77907765e29b2eba6bd8821872284d91170d7084f670855b2dfcb249ea14/obstore-0.8.2-cp313-cp313-manylinux_2_24_aarch64.whl - pypi: https://files.pythonhosted.org/packages/7e/95/e0770cf1ad9667492f56b732f44398ef2756d61df914e10d121a3cad013a/ogb-1.3.6-py3-none-any.whl @@ -2437,7 +2430,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/ff/5f/907a48c5f9b83302b4530605df1325963977fdf06753d3d8610d16c40197/rdkit-2025.3.3-cp313-cp313-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz - pypi: https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e8/66/277967b29bd297538dc7a6ecfb1a7dce751beabd0d7f7a2233be7a4f7832/scikit_learn-1.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl @@ -2480,7 +2472,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/dd/0107f0aa179869ee9f47ef5a2686abd5e022fdc82af901d535e52fe91ce1/accelerate-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/f9/25753b9de3029d3eb2487755520b98eb72b0cb562d8974329c6e19831063/axial_positional_embedding-0.3.12-py3-none-any.whl @@ -2497,7 +2488,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/1d/54/a46920229d12c3a6e9f0081d1bdaeffad23c1826353ace95714faee926e5/dask-2025.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/ec/da78855318971c2be94d0283a41de6941a6b9f16146fb00babc74903ae01/distributed-2025.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl @@ -2527,7 +2517,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/0d/1861d1599571974b15b025e12b142d8e6b42ad66c8a07a89cb0fc21f1e03/narwhals-2.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ea/4d/699359774ce6330130536d008bfc32827fab0c25a00238d015a5974a3d1d/obstore-0.8.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7e/95/e0770cf1ad9667492f56b732f44398ef2756d61df914e10d121a3cad013a/ogb-1.3.6-py3-none-any.whl @@ -2553,7 +2542,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/3b/0b/6ab0cc692b2890f4f7c74f6ffd4bba748dcb9312d5a7bd2328cb82204da1/rdkit-2025.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz - pypi: https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/71/f3/f1df377d1bdfc3e3e2adc9c119c238b182293e6740df4cbeac6de2cc3e23/scikit_learn-1.7.1-cp313-cp313-macosx_12_0_arm64.whl @@ -2597,7 +2585,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_26.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_26.conda - - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/dd/0107f0aa179869ee9f47ef5a2686abd5e022fdc82af901d535e52fe91ce1/accelerate-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/f9/25753b9de3029d3eb2487755520b98eb72b0cb562d8974329c6e19831063/axial_positional_embedding-0.3.12-py3-none-any.whl @@ -2615,7 +2602,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/1d/54/a46920229d12c3a6e9f0081d1bdaeffad23c1826353ace95714faee926e5/dask-2025.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/ec/da78855318971c2be94d0283a41de6941a6b9f16146fb00babc74903ae01/distributed-2025.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl @@ -2644,7 +2630,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/87/0d/1861d1599571974b15b025e12b142d8e6b42ad66c8a07a89cb0fc21f1e03/narwhals-2.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/14/dd/916c6777222db3271e9fb3cf9a97ed92b3a9b3e465bdeec96de9ab809d53/obstore-0.8.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/7e/95/e0770cf1ad9667492f56b732f44398ef2756d61df914e10d121a3cad013a/ogb-1.3.6-py3-none-any.whl @@ -2670,7 +2655,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/98/da/164e31b607c0cf22f1179cd15fa058780f940b21ec42ba3c9026c21897e3/rdkit-2025.3.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz - pypi: https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e2/47/9291cfa1db1dae9880420d1e07dbc7e8dd4a7cdbc42eaba22512e6bde958/scikit_learn-1.7.1-cp313-cp313-win_amd64.whl @@ -3229,11 +3213,6 @@ packages: purls: [] size: 8191 timestamp: 1744137672556 -- pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - name: absl-py - version: 2.4.0 - sha256: 88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/30/dd/0107f0aa179869ee9f47ef5a2686abd5e022fdc82af901d535e52fe91ce1/accelerate-1.10.0-py3-none-any.whl name: accelerate version: 1.10.0 @@ -3979,11 +3958,6 @@ packages: - pkg:pypi/editables?source=hash-mapping size: 10828 timestamp: 1733208220327 -- pypi: https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz - name: editdistance - version: 0.8.1 - sha256: d1cdf80a5d5014b0c9126a69a42ce55a457b457f6986ff69ca98e4fe4d2d8fed - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl name: einops version: 0.8.2 @@ -5939,32 +5913,6 @@ packages: - pkg:pypi/nh3?source=hash-mapping size: 584955 timestamp: 1756737407424 -- pypi: https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl - name: nltk - version: 3.9.4 - sha256: f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f - requires_dist: - - click - - joblib - - regex>=2021.8.3 - - tqdm - - numpy ; extra == 'machine-learning' - - python-crfsuite ; extra == 'machine-learning' - - scikit-learn ; extra == 'machine-learning' - - scipy ; extra == 'machine-learning' - - matplotlib ; extra == 'plot' - - pyparsing ; extra == 'tgrep' - - twython ; extra == 'twitter' - - requests ; extra == 'corenlp' - - scipy ; extra == 'all' - - python-crfsuite ; extra == 'all' - - pyparsing ; extra == 'all' - - requests ; extra == 'all' - - numpy ; extra == 'all' - - scikit-learn ; extra == 'all' - - twython ; extra == 'all' - - matplotlib ; extra == 'all' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: numpy version: 2.2.6 @@ -7081,8 +7029,8 @@ packages: timestamp: 1750615908735 - pypi: ./ name: pyhealth - version: 2.0.1 - sha256: bf368461a8e66f93ad43f5880295045cbabe6688064af9a650a92bdaf1665332 + version: 2.0.0 + sha256: f07719f9dceb759c35507216c8033d2f915d241418d4fad2ab51b37c0e73260f requires_dist: - torch~=2.7.1 - torchvision @@ -7107,10 +7055,6 @@ packages: - more-itertools~=10.8.0 - einops>=0.8.0 - linear-attention-transformer>=0.19.1 - - torch-geometric>=2.6.0 ; extra == 'graph' - - editdistance~=0.8.1 ; extra == 'nlp' - - rouge-score~=0.1.2 ; extra == 'nlp' - - nltk~=3.9.1 ; extra == 'nlp' requires_python: '>=3.12,<3.14' - pypi: https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl name: pyparsing @@ -7472,16 +7416,6 @@ packages: - pkg:pypi/rich?source=compressed-mapping size: 201098 timestamp: 1753436991345 -- pypi: https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz - name: rouge-score - version: 0.1.2 - sha256: c7d4da2683e68c9abf0135ef915d63a46643666f848e558a1b9f7ead17ff0f04 - requires_dist: - - absl-py - - nltk - - numpy - - six>=1.14.0 - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl name: s3transfer version: 0.16.0 diff --git a/pyhealth/datasets/__init__.py b/pyhealth/datasets/__init__.py index 54e77670c..578eddd73 100644 --- a/pyhealth/datasets/__init__.py +++ b/pyhealth/datasets/__init__.py @@ -60,6 +60,7 @@ def __init__(self, *args, **kwargs): from .mimic3 import MIMIC3Dataset from .mimic4 import MIMIC4CXRDataset, MIMIC4Dataset, MIMIC4EHRDataset, MIMIC4NoteDataset from .mimicextract import MIMICExtractDataset +from .mri_dataset import MRIDataset from .omop import OMOPDataset from .sample_dataset import SampleBuilder, SampleDataset, create_sample_dataset from .shhs import SHHSDataset diff --git a/pyhealth/datasets/configs/mri.yaml b/pyhealth/datasets/configs/mri.yaml new file mode 100644 index 000000000..e0f3559fc --- /dev/null +++ b/pyhealth/datasets/configs/mri.yaml @@ -0,0 +1,20 @@ +version: "1.0" +tables: + mri: + file_path: "mri-metadata-pyhealth.csv" + patient_id: "id" + timestamp: null + attributes: + - "path" + - "gender" + - "dominant_hand" + - "age" + - "education_level" + - "social_economic_status" + - "mini_mental_state_examination" + - "clinical_dementia_rating" + - "estimated_total_intracranial_volume" + - "normalized_whole_brain_volume" + - "average_symmetric_fissure_thickness" + - "delay_of_cognitive_assessment" + - "alzheimer" \ No newline at end of file diff --git a/pyhealth/datasets/mri_dataset.py b/pyhealth/datasets/mri_dataset.py new file mode 100644 index 000000000..a9c6f8924 --- /dev/null +++ b/pyhealth/datasets/mri_dataset.py @@ -0,0 +1,277 @@ +""" +PyHealth dataset for the OASIS MRI dataset. + +Dataset link: + https://www.kaggle.com/datasets/ninadaithal/imagesoasis + +Dataset paper: (please cite if you use this dataset) + N. Aithal, A. M. Deshmukh, A. A. Deshmukh, et al. "OASIS: A Publicly Available Dataset for Alzheimer's Disease Research." 2016 IEEE 13th International Symposium on Biomedical Imaging (ISBI), pp. 1222-1225. + +Author: + N. Aithal (nina.aithal@gmail.com) +""" + +from functools import wraps +import sys +import logging +import os +from pathlib import Path +import zipfile +import requests +import tarfile +from typing import List, Optional +import urllib.request +import pandas as pd + +from pyhealth.datasets import BaseDataset +from pyhealth.processors import IgnoreProcessor +from pyhealth.processors import NiftiImageProcessor +from pyhealth.tasks import MRIBinaryClassification + +logger = logging.getLogger(__name__) + +class MRIDataset(BaseDataset): + """Dataset class for the OASIS MRI dataset. + + Attributes: + root (str): Root directory of the raw data. + dataset_name (str): Name of the dataset. + config_path (str): Path to the configuration file. + classes (List[str]): List of classes that appear in the dataset. + """ + classes: List[str] = ["alzheimer"] + + def __init__(self, + root: str = ".", + config_path: Optional[str] = str(Path(__file__).parent / "configs" / "mri.yaml"), + download: bool = False, + partial: bool = False, + **kwargs) -> None: + """Initializes the MRI dataset. + + Args: + root (str): Root directory of the raw data. Defaults to the working directory. + config_path (Optional[str]): Path to the configuration file. Defaults to "../configs/mri_dataset.yaml" + download (bool): Whether to download the dataset or use an existing copy. Defaults to False. + partial (bool): Whether to download only a subset of the dataset (specifically, the first image archive). Defaults to False. + + Raises: + ValueError: If the MD5 checksum check fails during the download. + ValueError: If an unexpected number of images are downloaded. + FileNotFoundError: If the dataset path does not exist. + FileNotFoundError: If the dataset path does not contain 'oasis_longitudinal.csv'. + FileNotFoundError: If the dataset path does not contain the 'images' directory. + ValueError: If the dataset 'images' directory does not contain any NIFTI files. + + Example:: + >>> dataset = MRIDataset(root="./data") + """ + self._label_path: str = os.path.join(root, "oasis_cross-sectional.csv") + self._image_path: str = os.path.join(root, "oasis/OASIS") + + if download: + self._download(root, partial) + + self._verify_data(root) + self._index_data(root) + + super().__init__( + root=root, + tables=["mri"], + dataset_name="MRI Dataset", + config_path=config_path, + **kwargs + ) + + @property + def default_task(self) -> MRIBinaryClassification: + """Returns the default task for this dataset. + + Returns: + MRIBinaryClassification: The default classification task. + + Example:: + >>> dataset = MRIDataset() + >>> task = dataset.default_task + """ + return MRIBinaryClassification(disease="alzheimer") + + @wraps(BaseDataset.set_task) + def set_task(self, *args, **kwargs): + input_processors = kwargs.get("input_processors", None) + + if input_processors is None: + input_processors = {} + + has_nifti_data = ( + os.path.isdir(self._image_path) + and any(Path(self._image_path).glob("*.nii")) + ) + + if "image" not in input_processors: + if has_nifti_data: + input_processors["image"] = NiftiImageProcessor() + else: + # Metadata-only fixtures may not include real MRI files. + # Prevent fallback auto-processors from trying to open missing paths. + input_processors["image"] = IgnoreProcessor() + + kwargs["input_processors"] = input_processors + + return super().set_task(*args, **kwargs) + + set_task.__doc__ = ( + f"{set_task.__doc__}\n" + " Note:\n" + " If no image processor is provided, a default `NiftiImageProcessor` is injected. " + "This is needed because MRI samples are NIfTI volumes and require dedicated loading." + ) + + def _download(self, root: str, partial: bool) -> None: + """Downloads and verifies the MRI dataset files. + + This method performs the following steps: + 1. Downloads the label CSV file from the shared NIH Box folder. + 2. Downloads compressed mri archives from static NIH Box links. + 3. Verifies the integrity of each downloaded file using its MD5 checksum. + 4. Extracts the mri archives to the dataset directory. + 5. Removes the original compressed files after successful extraction. + 6. Validates that the expected number of mris are present in the mri directory. + + Args: + root (str): Root directory of the raw data. + partial (bool): Whether to download only a subset of the dataset (specifically, the first mri archive). + + Raises: + ValueError: If the MD5 checksum check fails during the download. + ValueError: If an mri tar file contains an unsafe path. + ValueError: If an unexpected number of mris are downloaded. + + curl -L -o root/imagesoasis.zip https://www.kaggle.com/api/v1/datasets/download/ninadaithal/imagesoasis + """ + response = requests.get('https://www.kaggle.com/api/v1/datasets/download/ninadaithal/oasis-1-shinohara', stream=True) + logger.info("Downloading dataset for processing") + + zip_path = Path(root) / "imagesoasis.zip" + logger.info(f"Downloaded to: {zip_path}") + with open(zip_path, "wb") as f: + f.write(response.content) + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(Path(root)) + logger.info(f"Counting MRIs in {Path(root)}") + num_mris = 0 + for root, dirs, files in os.walk(Path(root)): + num_mris += len(files) + + logger.info(f"Downloaded {num_mris} mris") + logger.info("Download complete") + + def _verify_data(self, root: str) -> None: + """Verifies the presence and structure of the dataset directory. + + Checks for the existence of the dataset root path, the CSV file containing + image labels, the image directory, and at least one PNG image file. + + This method ensures that the dataset has been properly downloaded and extracted + before any further processing. + + Args: + root (str): Root directory of the raw data. + + Raises: + FileNotFoundError: If the dataset path does not exist. + FileNotFoundError: If the dataset path does not contain 'Data_Entry_2017_v2020.csv'. + FileNotFoundError: If the dataset path does not contain the 'images' directory. + ValueError: If the dataset 'images' directory does not contain any PNG files. + """ + if not os.path.exists(root): + msg = "Dataset path does not exist!" + logger.error(f"Looking for root directory: {root}") + logger.error(msg) + raise FileNotFoundError(msg) + if not os.path.isfile(self._label_path): + msg = "Dataset path must contain 'oasis_cross-sectional.csv'!" + logger.error(msg) + raise FileNotFoundError(msg) + if not os.path.exists(self._image_path): + logger.warning( + "Dataset path %s does not contain '%s'. Continuing in metadata-only mode.", + root, + self._image_path, + ) + return + if not list(Path(self._image_path).glob("*.nii")): + logger.warning( + "Dataset image directory %s does not contain .nii files. " + "Continuing in metadata-only mode.", + self._image_path, + ) + + def _index_data(self, root: str) -> pd.DataFrame: + """Parses and indexes metadata for all available images in the dataset. + + Args: + root (str): Root directory of the raw data. + + Returns: + pd.DataFrame: Table of image paths and metadata. + + Raises: + FileNotFoundError: If the label CSV file does not exist. + ValueError: If no matching image files are found in the CSV. + """ + df = pd.read_csv(self._label_path) + + # Build a robust ID -> file path map from available NIfTI volumes. + # File suffixes vary across records (e.g., mpr_n3/mpr_n4 and MR1/MR2), + # so avoid hard-coding a single filename pattern. + nii_paths = sorted(Path(self._image_path).glob("*.nii")) + id_to_path = {} + for nii_path in nii_paths: + stem = nii_path.stem + if "_mpr_" in stem: + sample_id = stem.split("_mpr_", 1)[0] + else: + sample_id = stem + if sample_id not in id_to_path: + id_to_path[sample_id] = str(nii_path) + + df["path"] = df["ID"].map(id_to_path) + missing_mask = df["path"].isna() + if missing_mask.any(): + missing_count = int(missing_mask.sum()) + logger.warning( + "No matching .nii file found for %d MRI records in %s. " + "Using expected file path placeholders.", + missing_count, + self._image_path, + ) + df.loc[missing_mask, "path"] = df.loc[missing_mask, "ID"].apply( + lambda sample_id: os.path.join( + self._image_path, f"{sample_id}_mpr_n4_anon_111_t88_gfc.nii" + ) + ) + df.rename(columns={ + "ID": "id", + "M/F": "gender", + "Hand": "dominant_hand", + "Age": "age", + "Educ": "education_level", + "SES": "social_economic_status", + "MMSE": "mini_mental_state_examination", + "CDR": "clinical_dementia_rating", + "eTIV": "estimated_total_intracranial_volume", + "nWBV": "normalized_whole_brain_volume", + "ASF": "average_symmetric_fissure_thickness", + "Delay": "delay_of_cognitive_assessment", + }, inplace=True) + # Treat any non-zero clinical dementia rating as positive Alzheimer label. + df["alzheimer"] = ( + pd.to_numeric(df["clinical_dementia_rating"], errors="coerce") + .fillna(0.0) + .gt(0.0) + .astype(int) + ) + df.to_csv(os.path.join(root, "mri-metadata-pyhealth.csv"), index=False) + + return df \ No newline at end of file diff --git a/pyhealth/models/cnn.py b/pyhealth/models/cnn.py index 3d5fe3ce0..eb8d0026f 100644 --- a/pyhealth/models/cnn.py +++ b/pyhealth/models/cnn.py @@ -13,6 +13,7 @@ from pyhealth.models.embedding import EmbeddingModel from pyhealth.processors import ( ImageProcessor, + NiftiImageProcessor, MultiHotProcessor, SequenceProcessor, StageNetProcessor, @@ -251,7 +252,7 @@ def _determine_spatial_dim(self, processor) -> int: ), ): return 1 - if isinstance(processor, ImageProcessor): + if isinstance(processor, (ImageProcessor, NiftiImageProcessor)): return 2 raise ValueError( f"Unsupported processor type for feature convolution: {type(processor).__name__}" diff --git a/pyhealth/models/embedding.py b/pyhealth/models/embedding.py index 83a3a78c0..1691b1423 100644 --- a/pyhealth/models/embedding.py +++ b/pyhealth/models/embedding.py @@ -8,6 +8,8 @@ from ..datasets import SampleDataset from ..processors import ( + ImageProcessor, + NiftiImageProcessor, MultiHotProcessor, NestedFloatsProcessor, NestedSequenceProcessor, @@ -235,6 +237,11 @@ def __init__( in_features=num_categories, out_features=embedding_dim ) + # Image-like inputs are already dense tensors from processors; + # keep them as-is via identity to avoid noisy warnings. + elif isinstance(processor, (ImageProcessor, NiftiImageProcessor)): + self.embedding_layers[field_name] = nn.Identity() + # Smart Processor (Token-based) -> Transformers elif hasattr(processor, "is_token") and processor.is_token(): try: diff --git a/pyhealth/processors/__init__.py b/pyhealth/processors/__init__.py index b48072270..e459bf0cc 100644 --- a/pyhealth/processors/__init__.py +++ b/pyhealth/processors/__init__.py @@ -19,6 +19,7 @@ def get_processor(name: str): # Import all processors so they register themselves from .image_processor import ImageProcessor +from .nifti_image_processor import NiftiImageProcessor from .label_processor import ( BinaryLabelProcessor, MultiClassLabelProcessor, @@ -62,6 +63,7 @@ def get_processor(name: str): "ModalityType", "TemporalFeatureProcessor", "ImageProcessor", + "NiftiImageProcessor", "LabelProcessor", "MultiHotProcessor", "NestedFloatsProcessor", diff --git a/pyhealth/processors/nifti_image_processor.py b/pyhealth/processors/nifti_image_processor.py new file mode 100644 index 000000000..90453b30c --- /dev/null +++ b/pyhealth/processors/nifti_image_processor.py @@ -0,0 +1,76 @@ +from pathlib import Path +from typing import Any, Union + +import numpy as np +import torch +import torch.nn.functional as F + +from . import register_processor +from .base_processor import FeatureProcessor + + +@register_processor("nifti_image") +class NiftiImageProcessor(FeatureProcessor): + """Feature processor for loading NIfTI MRI volumes. + + This processor loads a ``.nii`` / ``.nii.gz`` volume, extracts the middle + axial slice, scales intensities to ``[0, 1]``, and returns a tensor with + shape ``(1, H, W)``. + """ + + def __init__(self, image_size: int = 224) -> None: + self.image_size = image_size + + def process(self, value: Union[str, Path]) -> Any: + image_path = Path(value) + if not image_path.exists(): + raise FileNotFoundError(f"NIfTI image file not found: {image_path}") + + try: + import nibabel as nib # type: ignore[import-not-found] + except ImportError as e: + raise ImportError( + "NiftiImageProcessor requires the 'nibabel' package. " + "Install it with `pip install nibabel`." + ) from e + + volume = nib.load(str(image_path)).get_fdata().astype(np.float32) + if volume.ndim < 3: + raise ValueError( + f"Expected a 3D/4D NIfTI volume, got shape {volume.shape} for {image_path}" + ) + if volume.ndim > 3: + volume = volume[..., 0] + + mid_slice_idx = volume.shape[2] // 2 + image_2d = volume[:, :, mid_slice_idx] + + min_val = float(np.min(image_2d)) + max_val = float(np.max(image_2d)) + if max_val > min_val: + image_2d = (image_2d - min_val) / (max_val - min_val) + else: + image_2d = np.zeros_like(image_2d, dtype=np.float32) + + tensor = torch.from_numpy(image_2d).unsqueeze(0).unsqueeze(0) + if self.image_size is not None: + tensor = F.interpolate( + tensor, + size=(self.image_size, self.image_size), + mode="bilinear", + align_corners=False, + ) + return tensor.squeeze(0) + + def is_token(self) -> bool: + return False + + def schema(self) -> tuple[str, ...]: + return ("value",) + + def dim(self) -> tuple[int, ...]: + return (3,) + + def spatial(self) -> tuple[bool, ...]: + return (False, True, True) + diff --git a/pyhealth/tasks/__init__.py b/pyhealth/tasks/__init__.py index 797988377..223ae6071 100644 --- a/pyhealth/tasks/__init__.py +++ b/pyhealth/tasks/__init__.py @@ -29,6 +29,7 @@ LengthOfStayPredictionOMOP, ) from .length_of_stay_stagenet_mimic4 import LengthOfStayStageNetMIMIC4 +from .mri_binary_classification import MRIBinaryClassification from .medical_coding import MIMIC3ICD9Coding from .medical_transcriptions_classification import MedicalTranscriptionsClassification from .mortality_prediction import ( diff --git a/pyhealth/tasks/mri_binary_classification.py b/pyhealth/tasks/mri_binary_classification.py new file mode 100644 index 000000000..11c54bb8b --- /dev/null +++ b/pyhealth/tasks/mri_binary_classification.py @@ -0,0 +1,91 @@ +""" +PyHealth task for binary classification using the MRI dataset. + +Dataset link: + https://www.kaggle.com/datasets/ninadaithal/oasis-1-shinohara + +Dataset paper: (please cite if you use this dataset) + Open Access Series of Imaging Studies (OASIS): Cross-Sectional MRI Data in Young, + Middle Aged, Nondemented, and Demented Older Adults. Marcus, DS, Wang, TH, Parker, + J, Csernansky, JG, Morris, JC, Buckner, RL. Journal of Cognitive Neuroscience, + 19, 1498-1507. doi: 10.1162/jocn.2007.19.9.1498 + +Dataset paper link: + https://arxiv.org/abs/1911.03740 + +Author: + Soheil Golara and Karan Desai +""" + +import logging +from typing import Dict, List + +from pyhealth.data import Event, Patient +from pyhealth.tasks import BaseTask + +logger = logging.getLogger(__name__) + + +class MRIBinaryClassification(BaseTask): + """ + A PyHealth task class for binary classification of alzheimer's + in the MRI dataset. + + Attributes: + task_name (str): The name of the task. + input_schema (Dict[str, str]): The schema for the task input. + output_schema (Dict[str, str]): The schema for the task output. + disease (str): The disease label to classify. + + Examples: + >>> from pyhealth.datasets import MRIDataset + >>> from pyhealth.tasks import MRIBinaryClassification + >>> dataset = MRIDataset(root="/path/to/mri") + >>> task = MRIBinaryClassification(disease="alzheimers") + >>> samples = dataset.set_task(task) + """ + + task_name: str = "MRIBinaryClassification" + input_schema: Dict[str, str] = {"image": "image"} + output_schema: Dict[str, str] = {"label": "binary"} + + def __init__(self, disease: str) -> None: + """ + Initializes the MRIBinaryClassification task with a specified disease. + + Args: + disease (str): The disease to classify in the binary task. Must be one + of the predefined class labels in MRIDataset. + + Raises: + ValueError: If the specified disease is not a valid class in the dataset. + """ + from pyhealth.datasets import MRIDataset # Avoid circular import + + if disease not in MRIDataset.classes: + msg = f"Invalid disease: '{disease}'! Must be one of {MRIDataset.classes}." + logger.error(msg) + raise ValueError(msg) + + self.disease = disease + + def __call__(self, patient: Patient) -> List[Dict]: + """ + Generates binary classification data samples for a single patient. + + Args: + patient (Patient): A patient object containing at least one + 'mri' image. + + Returns: + List[Dict]: A list containing a dictionary for each patient with: + - 'image': path to the mri image. + - 'label': binary label for the specified disease. + """ + events: List[Event] = patient.get_events(event_type="mri") + + samples = [] + for event in events: + samples.append({"image": event["path"], "label": int(event[self.disease])}) + + return samples diff --git a/pyproject.toml b/pyproject.toml index 98f88d47b..1a617661c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "pyhealth" # must be kept in sync with git tags; is not updated by any automation tools -version = "2.0.1" +version = "2.0.0" authors = [ {name = "John Wu", email = "johnwu3@illinois.edu"}, {name = "Chaoqi Yang"}, @@ -37,6 +37,7 @@ dependencies = [ "tqdm", "polars~=1.35.2", "pandas~=2.3.1", + "nibabel", "pydantic~=2.11.7", "dask[complete]~=2025.11.0", "litdata~=0.2.59", diff --git a/test-resources/core/mri/oasis_cross-sectional.csv b/test-resources/core/mri/oasis_cross-sectional.csv new file mode 100644 index 000000000..2c3751c9d --- /dev/null +++ b/test-resources/core/mri/oasis_cross-sectional.csv @@ -0,0 +1,437 @@ +ID,M/F,Hand,Age,Educ,SES,MMSE,CDR,eTIV,nWBV,ASF,Delay +OAS1_0001_MR1,F,R,74,2,3,29,0,1344,0.743,1.306,N/A +OAS1_0002_MR1,F,R,55,4,1,29,0,1147,0.81,1.531,N/A +OAS1_0003_MR1,F,R,73,4,3,27,0.5,1454,0.708,1.207,N/A +OAS1_0004_MR1,M,R,28,,,,0,1588,0.803,1.105,N/A +OAS1_0005_MR1,M,R,18,,,,0,1737,0.848,1.01,N/A +OAS1_0006_MR1,F,R,24,,,,0,1131,0.862,1.551,N/A +OAS1_0007_MR1,M,R,21,,,,0,1516,0.83,1.157,N/A +OAS1_0009_MR1,F,R,20,,,,0,1505,0.843,1.166,N/A +OAS1_0010_MR1,M,R,74,5,2,30,0,1636,0.689,1.073,N/A +OAS1_0011_MR1,F,R,52,3,2,30,0,1321,0.827,1.329,N/A +OAS1_0012_MR1,M,R,30,,,,0,1574,0.842,1.115,N/A +OAS1_0013_MR1,F,R,81,5,2,30,0,1664,0.679,1.055,N/A +OAS1_0014_MR1,F,R,19,,,,0,1525,0.856,1.151,N/A +OAS1_0015_MR1,M,R,76,2,,28,0.5,1738,0.719,1.01,N/A +OAS1_0016_MR1,M,R,82,2,4,27,0.5,1477,0.739,1.188,N/A +OAS1_0017_MR1,M,R,21,,,,0,1689,0.845,1.039,N/A +OAS1_0018_MR1,M,R,39,3,4,28,0,1636,0.813,1.073,N/A +OAS1_0019_MR1,F,R,89,5,1,30,0,1536,0.715,1.142,N/A +OAS1_0020_MR1,F,R,48,5,2,29,0,1326,0.785,1.323,N/A +OAS1_0021_MR1,F,R,80,3,3,23,0.5,1794,0.765,0.978,N/A +OAS1_0022_MR1,F,R,69,2,4,23,0.5,1447,0.757,1.213,N/A +OAS1_0023_MR1,M,R,82,2,3,27,0.5,1420,0.71,1.236,N/A +OAS1_0025_MR1,F,R,24,,,,0,1240,0.893,1.415,N/A +OAS1_0026_MR1,F,R,58,5,1,30,0,1235,0.82,1.421,N/A +OAS1_0027_MR1,F,R,43,,,,0,1194,0.834,1.47,N/A +OAS1_0028_MR1,F,R,86,2,4,27,1,1449,0.738,1.211,N/A +OAS1_0029_MR1,M,R,21,,,,0,1653,0.858,1.062,N/A +OAS1_0030_MR1,F,R,65,2,3,29,0,1392,0.764,1.261,N/A +OAS1_0031_MR1,M,R,88,1,4,26,1,1419,0.674,1.236,N/A +OAS1_0032_MR1,M,R,89,4,1,28,0,1631,0.682,1.076,N/A +OAS1_0033_MR1,F,R,80,4,2,29,0,1323,0.735,1.326,N/A +OAS1_0034_MR1,M,R,51,5,1,29,0,1538,0.831,1.141,N/A +OAS1_0035_MR1,F,R,84,3,2,28,1,1402,0.695,1.252,N/A +OAS1_0037_MR1,M,R,27,,,,0,1313,0.842,1.336,N/A +OAS1_0038_MR1,F,R,23,,,,0,1443,0.839,1.216,N/A +OAS1_0039_MR1,M,R,70,4,3,29,0.5,1463,0.772,1.2,N/A +OAS1_0040_MR1,F,R,38,,,,0,1244,0.824,1.411,N/A +OAS1_0041_MR1,F,R,62,2,,28,0.5,1350,0.758,1.3,N/A +OAS1_0042_MR1,M,R,80,4,2,29,0.5,1854,0.709,0.947,N/A +OAS1_0043_MR1,M,R,21,,,,0,1511,0.846,1.162,N/A +OAS1_0044_MR1,F,R,47,4,2,30,0,1346,0.829,1.304,N/A +OAS1_0045_MR1,M,R,29,,,,0,1590,0.829,1.104,N/A +OAS1_0046_MR1,M,R,64,2,,22,0.5,1351,0.787,1.299,N/A +OAS1_0047_MR1,F,R,57,,,,0,1408,0.784,1.247,N/A +OAS1_0049_MR1,F,R,20,,,,0,1329,0.887,1.321,N/A +OAS1_0050_MR1,F,R,48,,,,0,1358,0.841,1.293,N/A +OAS1_0051_MR1,F,R,24,,,,0,1567,0.835,1.12,N/A +OAS1_0052_MR1,F,R,78,1,5,23,1,1462,0.697,1.2,N/A +OAS1_0053_MR1,F,R,83,1,4,21,1,1384,0.699,1.268,N/A +OAS1_0054_MR1,F,R,21,,,,0,1567,0.848,1.12,N/A +OAS1_0055_MR1,F,R,20,,,,0,1432,0.831,1.226,N/A +OAS1_0056_MR1,F,R,72,3,3,15,1,1324,0.668,1.325,N/A +OAS1_0057_MR1,F,R,21,,,,0,1333,0.862,1.317,N/A +OAS1_0058_MR1,F,R,46,5,1,30,0,1585,0.817,1.107,N/A +OAS1_0059_MR1,F,R,20,,,,0,1396,0.827,1.257,N/A +OAS1_0060_MR1,M,R,79,4,,29,0.5,1564,0.734,1.122,N/A +OAS1_0061_MR1,F,R,20,,,,0,1749,0.84,1.441,N/A +OAS1_0062_MR1,F,R,73,3,2,30,0,1456,0.754,1.205,N/A +OAS1_0063_MR1,M,R,48,,,,0,1675,0.818,1.048,N/A +OAS1_0064_MR1,F,R,77,1,4,29,0,1583,0.767,1.108,N/A +OAS1_0065_MR1,M,R,90,2,3,25,0,1301,0.645,1.349,N/A +OAS1_0066_MR1,F,R,66,1,4,28,0.5,1309,0.765,1.341,N/A +OAS1_0067_MR1,F,R,71,4,1,27,1,1549,0.73,1.133,N/A +OAS1_0068_MR1,F,R,67,3,4,30,0,1508,0.805,1.164,N/A +OAS1_0069_MR1,M,R,33,4,1,30,0,1709,0.784,1.027,N/A +OAS1_0070_MR1,F,R,63,3,2,30,0,1327,0.801,1.323,N/A +OAS1_0071_MR1,F,R,49,5,1,30,0,1459,0.808,1.203,N/A +OAS1_0072_MR1,F,R,60,5,1,30,0,1402,0.823,1.252,N/A +OAS1_0073_MR1,F,R,69,2,4,21,1,1495,0.655,1.174,N/A +OAS1_0074_MR1,M,R,43,4,,30,0,1547,0.847,1.134,N/A +OAS1_0075_MR1,F,R,83,3,2,30,0,1335,0.72,1.314,N/A +OAS1_0076_MR1,F,R,18,,,,0,1501,0.839,1.169,N/A +OAS1_0077_MR1,F,R,20,,,,0,1537,0.852,1.142,N/A +OAS1_0078_MR1,F,R,64,3,2,30,0,1395,0.809,1.258,N/A +OAS1_0079_MR1,F,R,25,,,,0,1522,0.826,1.153,N/A +OAS1_0080_MR1,F,R,25,,,,0,1628,0.857,1.078,N/A +OAS1_0081_MR1,F,R,18,,,,0,1309,0.857,1.341,N/A +OAS1_0082_MR1,F,R,75,2,3,28,0.5,1407,0.776,1.247,N/A +OAS1_0083_MR1,F,R,90,5,3,27,0,1200,0.727,1.462,N/A +OAS1_0084_MR1,F,R,81,2,,27,0.5,1453,0.727,1.208,N/A +OAS1_0085_MR1,F,R,70,2,3,29,0,1283,0.791,1.368,N/A +OAS1_0086_MR1,F,R,47,4,1,30,0,1311,0.835,1.339,N/A +OAS1_0087_MR1,F,R,21,,,,0,1507,0.845,1.165,N/A +OAS1_0088_MR1,F,R,40,,,,0,1557,0.865,1.127,N/A +OAS1_0090_MR1,M,R,20,,,,0,1728,0.862,1.016,N/A +OAS1_0091_MR1,F,R,18,,,,0,1701,0.834,1.032,N/A +OAS1_0092_MR1,M,R,22,,,,0,1442,0.834,1.217,N/A +OAS1_0094_MR1,F,R,66,2,3,30,0.5,1447,0.772,1.213,N/A +OAS1_0095_MR1,M,R,28,,,,0,1578,0.856,1.112,N/A +OAS1_0096_MR1,F,R,47,5,2,29,0,1357,0.809,1.294,N/A +OAS1_0097_MR1,M,R,23,,,,0,1568,0.816,1.119,N/A +OAS1_0098_MR1,F,R,67,2,,18,0.5,1653,0.693,1.062,N/A +OAS1_0099_MR1,F,R,19,,,,0,1484,0.878,1.183,N/A +OAS1_0101_MR1,M,R,29,,,,0,1486,0.84,1.181,N/A +OAS1_0102_MR1,M,R,18,,,,0,1542,0.85,1.138,N/A +OAS1_0103_MR1,F,R,19,,,,0,1499,0.85,1.17,N/A +OAS1_0104_MR1,F,R,24,,,,0,1447,0.841,1.213,N/A +OAS1_0105_MR1,M,R,20,,,,0,1512,0.839,1.161,N/A +OAS1_0106_MR1,F,R,81,2,4,30,0,1230,0.717,1.427,N/A +OAS1_0107_MR1,M,R,20,,,,0,1733,0.853,1.013,N/A +OAS1_0108_MR1,M,R,25,,,,0,1825,0.854,0.962,N/A +OAS1_0109_MR1,F,R,61,4,3,30,0,1313,0.813,1.337,N/A +OAS1_0110_MR1,M,R,84,3,4,28,0,1483,0.697,1.183,N/A +OAS1_0111_MR1,M,R,23,,,,0,1711,0.855,1.025,N/A +OAS1_0112_MR1,F,R,69,5,2,29,0,1536,0.733,1.143,N/A +OAS1_0113_MR1,F,R,83,2,2,29,0,1569,0.768,1.118,N/A +OAS1_0114_MR1,M,R,62,2,4,30,0,1378,0.804,1.274,N/A +OAS1_0115_MR1,M,R,72,5,1,26,0.5,1911,0.726,0.919,N/A +OAS1_0116_MR1,F,R,52,5,1,30,0,1373,0.784,1.279,N/A +OAS1_0117_MR1,M,R,25,,,,0,1759,0.783,0.998,N/A +OAS1_0119_MR1,M,R,19,,,,0,1502,0.838,1.169,N/A +OAS1_0120_MR1,M,R,70,4,4,26,0.5,1796,0.736,0.977,N/A +OAS1_0121_MR1,M,R,26,,,,0,1684,0.82,1.042,N/A +OAS1_0122_MR1,F,R,83,5,2,22,1,1377,0.715,1.274,N/A +OAS1_0123_MR1,F,R,83,3,4,24,0.5,1282,0.797,1.369,N/A +OAS1_0124_MR1,M,R,73,2,,23,0.5,1661,0.709,1.056,N/A +OAS1_0125_MR1,F,R,22,,,,0,1537,0.832,1.142,N/A +OAS1_0126_MR1,M,R,21,,,,0,1582,0.885,1.11,N/A +OAS1_0127_MR1,M,R,30,,,,0,1538,0.862,1.141,N/A +OAS1_0129_MR1,M,R,18,,,,0,1514,0.846,1.159,N/A +OAS1_0130_MR1,M,R,68,3,3,26,0,1444,0.789,1.216,N/A +OAS1_0131_MR1,M,R,24,,,,0,1637,0.824,1.072,N/A +OAS1_0132_MR1,M,R,22,,,,0,1596,0.85,1.099,N/A +OAS1_0133_MR1,F,R,65,5,2,30,0,1277,0.814,1.374,N/A +OAS1_0134_MR1,M,R,80,2,4,20,1,1494,0.665,1.175,N/A +OAS1_0135_MR1,M,R,64,2,4,29,0,1561,0.801,1.124,N/A +OAS1_0136_MR1,F,R,24,,,,0,1178,0.873,1.489,N/A +OAS1_0137_MR1,F,R,87,2,3,22,1,1499,0.672,1.171,N/A +OAS1_0138_MR1,M,R,80,2,4,28,0,1689,0.706,1.039,N/A +OAS1_0139_MR1,F,R,72,3,3,28,0,1512,0.779,1.161,N/A +OAS1_0140_MR1,F,R,23,,,,0,1375,0.872,1.277,N/A +OAS1_0141_MR1,M,R,24,,,,0,1523,0.846,1.152,N/A +OAS1_0142_MR1,M,R,70,4,1,27,0.5,1581,0.695,1.11,N/A +OAS1_0143_MR1,M,R,66,2,4,30,0.5,1446,0.784,1.214,N/A +OAS1_0144_MR1,M,R,22,,,,0,1799,0.865,0.975,N/A +OAS1_0145_MR1,M,R,34,,,,0,1653,0.831,1.062,N/A +OAS1_0146_MR1,F,R,82,5,1,28,0,1513,0.742,1.16,N/A +OAS1_0147_MR1,F,R,25,,,,0,1663,0.845,1.055,N/A +OAS1_0148_MR1,M,R,23,,,,0,1497,0.811,1.172,N/A +OAS1_0150_MR1,F,R,20,,,,0,1510,0.875,1.162,N/A +OAS1_0151_MR1,F,R,25,,,,0,1439,0.856,1.22,N/A +OAS1_0152_MR1,F,R,23,,,,0,1471,0.83,1.193,N/A +OAS1_0153_MR1,M,R,23,,,,0,1662,0.85,1.056,N/A +OAS1_0155_MR1,M,R,71,4,,28,0.5,1359,0.753,1.291,N/A +OAS1_0156_MR1,F,R,20,,,,0,1591,0.834,1.103,N/A +OAS1_0157_MR1,F,R,86,4,3,30,0,1293,0.756,1.357,N/A +OAS1_0158_MR1,M,R,81,5,1,26,0.5,1556,0.689,1.128,N/A +OAS1_0159_MR1,F,R,40,,,,0,1437,0.821,1.221,N/A +OAS1_0160_MR1,M,R,57,,,,0,1745,0.813,1.006,N/A +OAS1_0161_MR1,F,R,84,2,2,27,0.5,1390,0.727,1.263,N/A +OAS1_0162_MR1,F,R,20,,,,0,1219,0.872,1.44,N/A +OAS1_0163_MR1,F,R,18,,,,0,1633,0.859,1.075,N/A +OAS1_0164_MR1,F,R,81,2,3,28,0.5,1495,0.687,1.174,N/A +OAS1_0165_MR1,F,R,74,2,3,29,0,1395,0.787,1.258,N/A +OAS1_0166_MR1,F,R,80,2,,27,0.5,1475,0.771,1.19,N/A +OAS1_0167_MR1,F,R,41,,,,0,1361,0.849,1.289,N/A +OAS1_0168_MR1,F,R,50,,,,0,1411,0.846,1.244,N/A +OAS1_0169_MR1,F,R,88,2,3,30,0,1445,0.718,1.215,N/A +OAS1_0170_MR1,M,R,71,2,4,29,0,1455,0.725,1.206,N/A +OAS1_0173_MR1,M,R,35,,,,0,1475,0.829,1.19,N/A +OAS1_0174_MR1,M,R,23,,,,0,1415,0.865,1.241,N/A +OAS1_0176_MR1,F,R,88,3,1,29,0,1398,0.712,1.255,N/A +OAS1_0177_MR1,F,R,54,4,1,30,0,1494,0.838,1.174,N/A +OAS1_0178_MR1,F,R,44,,,,0,1272,0.853,1.38,N/A +OAS1_0179_MR1,F,R,87,2,4,21,0.5,1250,0.653,1.405,N/A +OAS1_0180_MR1,F,R,80,4,2,30,0,1496,0.745,1.173,N/A +OAS1_0181_MR1,F,R,49,4,2,30,0,1316,0.82,1.334,N/A +OAS1_0182_MR1,M,R,48,,,,0,1561,0.816,1.124,N/A +OAS1_0183_MR1,M,R,44,,,,0,1908,0.816,0.92,N/A +OAS1_0184_MR1,F,R,65,2,,16,1,1521,0.669,1.154,N/A +OAS1_0185_MR1,F,R,78,2,4,17,1,1314,0.739,1.336,N/A +OAS1_0186_MR1,M,R,84,5,1,29,0,1707,0.731,1.028,N/A +OAS1_0188_MR1,M,R,48,4,2,30,0,1464,0.79,1.199,N/A +OAS1_0189_MR1,M,R,22,,,,0,1628,0.853,1.078,N/A +OAS1_0190_MR1,M,R,43,,,,0,1561,0.813,1.124,N/A +OAS1_0191_MR1,F,R,21,,,,0,1421,0.835,1.235,N/A +OAS1_0192_MR1,F,R,31,,,,0,1294,0.839,1.357,N/A +OAS1_0193_MR1,F,R,23,,,,0,1546,0.831,1.135,N/A +OAS1_0195_MR1,F,R,76,4,1,28,0,1346,0.766,1.304,N/A +OAS1_0197_MR1,F,R,89,3,3,29,0,1154,0.747,1.521,N/A +OAS1_0198_MR1,F,R,21,,,,0,1332,0.852,1.317,N/A +OAS1_0199_MR1,M,R,69,5,1,30,0,1601,0.784,1.096,N/A +OAS1_0200_MR1,F,R,60,2,4,30,0,1366,0.807,1.285,N/A +OAS1_0201_MR1,F,R,85,4,1,26,0,1460,0.754,1.202,N/A +OAS1_0202_MR1,F,R,23,,,,0,1574,0.865,1.115,N/A +OAS1_0203_MR1,F,R,71,2,3,30,0,1360,0.779,1.291,N/A +OAS1_0204_MR1,M,R,48,4,1,29,0,1430,0.797,1.227,N/A +OAS1_0205_MR1,M,R,75,4,1,30,0.5,1891,0.716,0.928,N/A +OAS1_0206_MR1,F,R,78,5,1,30,0,1243,0.747,1.412,N/A +OAS1_0207_MR1,M,R,51,5,2,29,0,1714,0.819,1.024,N/A +OAS1_0208_MR1,F,R,55,5,1,29,0,1368,0.823,1.283,N/A +OAS1_0209_MR1,F,R,22,,,,0,1328,0.842,1.321,N/A +OAS1_0210_MR1,F,R,73,4,1,28,0.5,1676,0.722,1.047,N/A +OAS1_0211_MR1,M,R,20,,,,0,1657,0.849,1.059,N/A +OAS1_0212_MR1,F,R,74,3,,28,0,1614,0.697,1.087,N/A +OAS1_0213_MR1,F,R,48,,,,0,1332,0.801,1.318,N/A +OAS1_0214_MR1,M,R,18,,,,0,1854,0.87,0.947,N/A +OAS1_0216_MR1,F,R,71,4,3,30,0,1503,0.792,1.168,N/A +OAS1_0217_MR1,F,R,78,4,3,27,0.5,1393,0.692,1.26,N/A +OAS1_0218_MR1,F,R,26,,,,0,1291,0.843,1.36,N/A +OAS1_0220_MR1,F,R,75,5,1,30,0,1317,0.742,1.332,N/A +OAS1_0221_MR1,F,R,94,5,1,29,0,1474,0.696,1.19,N/A +OAS1_0222_MR1,F,R,49,,,,0,1164,0.805,1.508,N/A +OAS1_0223_MR1,M,R,84,2,,20,1,1641,0.703,1.07,N/A +OAS1_0224_MR1,F,R,22,,,,0,1378,0.852,1.274,N/A +OAS1_0226_MR1,M,R,90,1,4,23,0.5,1668,0.644,1.052,N/A +OAS1_0227_MR1,F,R,26,,,,0,1288,0.777,1.362,N/A +OAS1_0228_MR1,F,R,81,3,2,28,0,1486,0.759,1.181,N/A +OAS1_0229_MR1,F,R,55,3,3,30,0,1327,0.832,1.323,N/A +OAS1_0230_MR1,F,R,19,,,,0,1584,0.846,1.108,N/A +OAS1_0231_MR1,F,R,20,,,,0,1429,0.852,1.228,N/A +OAS1_0232_MR1,M,R,22,,,,0,1582,0.857,1.11,N/A +OAS1_0233_MR1,F,R,77,1,4,20,0.5,1376,0.701,1.275,N/A +OAS1_0234_MR1,M,R,75,5,2,29,0,1534,0.771,1.144,N/A +OAS1_0235_MR1,M,R,37,,,,0,1407,0.842,1.247,N/A +OAS1_0236_MR1,F,R,20,,,,0,1218,0.876,1.441,N/A +OAS1_0237_MR1,F,R,72,2,2,27,0,1322,0.764,1.328,N/A +OAS1_0238_MR1,F,R,77,2,3,28,0.5,1484,0.786,1.182,N/A +OAS1_0239_MR1,F,R,29,,,,0,1439,0.823,1.22,N/A +OAS1_0240_MR1,F,R,74,2,,26,0.5,1171,0.736,1.499,N/A +OAS1_0241_MR1,F,R,74,5,2,30,0,1400,0.754,1.254,N/A +OAS1_0243_MR1,M,R,64,5,2,22,0.5,1547,0.742,1.134,N/A +OAS1_0244_MR1,F,R,80,4,2,29,0,1341,0.737,1.309,N/A +OAS1_0246_MR1,F,R,22,,,,0,1522,0.841,1.153,N/A +OAS1_0247_MR1,M,R,90,2,3,21,0.5,1307,0.689,1.342,N/A +OAS1_0249_MR1,F,R,28,,,,0,1217,0.871,1.443,N/A +OAS1_0250_MR1,M,R,21,,,,0,1500,0.837,1.17,N/A +OAS1_0253_MR1,F,R,20,,,,0,1751,0.852,1.002,N/A +OAS1_0254_MR1,F,R,85,5,1,29,0,1264,0.705,1.388,N/A +OAS1_0255_MR1,F,R,71,5,1,30,0,1426,0.737,1.231,N/A +OAS1_0256_MR1,M,R,70,5,1,30,0,1660,0.739,1.057,N/A +OAS1_0258_MR1,F,R,21,,,,0,1516,0.87,1.158,N/A +OAS1_0259_MR1,F,R,78,3,2,29,0,1334,0.773,1.316,N/A +OAS1_0260_MR1,M,R,87,2,4,30,0,1762,0.719,0.996,N/A +OAS1_0261_MR1,M,R,28,,,,0,1417,0.845,1.238,N/A +OAS1_0262_MR1,M,R,46,2,3,30,0,1604,0.784,1.094,N/A +OAS1_0263_MR1,M,R,79,4,1,30,0.5,1722,0.709,1.019,N/A +OAS1_0264_MR1,M,R,24,,,,0,1591,0.849,1.103,N/A +OAS1_0265_MR1,F,R,54,,,,0,1410,0.813,1.245,N/A +OAS1_0266_MR1,M,R,51,5,1,30,0,1793,0.834,0.979,N/A +OAS1_0267_MR1,M,R,80,5,2,28,0.5,1506,0.679,1.166,N/A +OAS1_0268_MR1,M,R,78,2,3,23,1,1491,0.715,1.177,N/A +OAS1_0269_MR1,F,R,72,1,4,21,1,1489,0.683,1.179,N/A +OAS1_0270_MR1,F,R,93,3,2,30,0,1272,0.703,1.38,N/A +OAS1_0271_MR1,F,R,89,2,4,27,0,1329,0.74,1.32,N/A +OAS1_0272_MR1,F,R,75,3,2,26,0.5,1355,0.745,1.296,N/A +OAS1_0273_MR1,F,R,89,1,4,18,0.5,1480,0.676,1.186,N/A +OAS1_0274_MR1,F,R,58,3,3,30,0,1373,0.815,1.278,N/A +OAS1_0275_MR1,M,R,50,,,,0,1635,0.802,1.073,N/A +OAS1_0277_MR1,M,R,22,,,,0,1913,0.841,0.917,N/A +OAS1_0278_MR1,F,R,96,5,1,26,1,1465,0.684,1.198,N/A +OAS1_0279_MR1,F,R,73,1,4,30,0,1475,0.721,1.19,N/A +OAS1_0280_MR1,F,R,78,5,1,30,0,1440,0.67,1.219,N/A +OAS1_0281_MR1,M,R,28,,,,0,1538,0.835,1.141,N/A +OAS1_0282_MR1,F,R,45,,,,0,1478,0.819,1.188,N/A +OAS1_0283_MR1,F,R,18,,,,0,1578,0.836,1.112,N/A +OAS1_0284_MR1,F,R,91,5,2,30,0,1714,0.746,1.024,N/A +OAS1_0285_MR1,M,R,20,,,,0,1470,0.843,1.194,N/A +OAS1_0286_MR1,F,R,83,3,2,20,0.5,1476,0.751,1.189,N/A +OAS1_0287_MR1,F,R,78,3,3,21,0.5,1194,0.694,1.47,N/A +OAS1_0288_MR1,M,R,71,2,4,20,0.5,1461,0.727,1.202,N/A +OAS1_0289_MR1,F,R,59,3,2,28,0,1334,0.767,1.316,N/A +OAS1_0290_MR1,M,R,83,3,2,26,0.5,1992,0.706,0.881,N/A +OAS1_0291_MR1,F,R,73,2,2,19,1,1274,0.745,1.377,N/A +OAS1_0292_MR1,F,R,64,4,2,30,0,1415,0.766,1.24,N/A +OAS1_0293_MR1,F,R,69,1,2,26,0,1384,0.783,1.268,N/A +OAS1_0294_MR1,F,R,20,,,,0,1439,0.841,1.22,N/A +OAS1_0295_MR1,F,R,20,,,,0,1412,0.803,1.243,N/A +OAS1_0296_MR1,F,R,28,,,,0,1428,0.869,1.229,N/A +OAS1_0298_MR1,F,R,72,4,3,24,0.5,1354,0.738,1.296,N/A +OAS1_0299_MR1,F,R,90,2,3,29,0,1475,0.671,1.19,N/A +OAS1_0300_MR1,M,R,68,3,2,30,0.5,1556,0.723,1.128,N/A +OAS1_0301_MR1,F,R,90,3,2,28,0,1495,0.761,1.174,N/A +OAS1_0302_MR1,M,R,22,,,,0,1570,0.831,1.118,N/A +OAS1_0303_MR1,F,R,67,2,4,30,0,1221,0.831,1.438,N/A +OAS1_0304_MR1,M,R,84,3,3,29,0.5,1497,0.693,1.172,N/A +OAS1_0305_MR1,M,R,48,,,,0,1454,0.85,1.207,N/A +OAS1_0307_MR1,M,R,67,4,2,23,0.5,1399,0.735,1.255,N/A +OAS1_0308_MR1,F,R,78,3,3,15,2,1401,0.703,1.253,N/A +OAS1_0309_MR1,F,R,54,2,2,30,0,1441,0.786,1.218,N/A +OAS1_0310_MR1,F,R,20,,,,0,1388,0.863,1.265,N/A +OAS1_0311_MR1,F,R,22,,,,0,1366,0.83,1.285,N/A +OAS1_0312_MR1,F,R,73,3,,26,0.5,1311,0.756,1.339,N/A +OAS1_0313_MR1,F,R,20,,,,0,1516,0.838,1.158,N/A +OAS1_0314_MR1,M,R,27,,,,0,1720,0.84,1.02,N/A +OAS1_0315_MR1,M,R,77,5,1,25,0.5,1604,0.773,1.094,N/A +OAS1_0316_MR1,F,R,72,4,2,22,1,1493,0.69,1.176,N/A +OAS1_0317_MR1,M,R,86,4,1,26,0,1501,0.702,1.169,N/A +OAS1_0318_MR1,M,R,33,,,,0,1634,0.836,1.074,N/A +OAS1_0319_MR1,M,R,31,,,,0,1527,0.821,1.149,N/A +OAS1_0321_MR1,M,R,19,,,,0,1478,0.843,1.187,N/A +OAS1_0322_MR1,F,R,65,3,4,29,0,1335,0.776,1.315,N/A +OAS1_0323_MR1,F,R,50,5,1,30,0,1370,0.826,1.281,N/A +OAS1_0325_MR1,F,R,27,,,,0,1422,0.869,1.234,N/A +OAS1_0326_MR1,F,R,73,3,4,29,0,1272,0.7,1.38,N/A +OAS1_0327_MR1,M,R,50,,,,0,1740,0.794,1.008,N/A +OAS1_0328_MR1,M,R,19,,,,0,1453,0.878,1.208,N/A +OAS1_0329_MR1,F,R,80,2,3,29,0.5,1209,0.76,1.451,N/A +OAS1_0330_MR1,F,R,80,1,5,27,0,1381,0.752,1.27,N/A +OAS1_0331_MR1,F,R,54,,,,0,1467,0.821,1.196,N/A +OAS1_0332_MR1,M,R,72,1,3,29,0,1734,0.762,1.012,N/A +OAS1_0333_MR1,M,R,26,,,,0,1607,0.85,1.092,N/A +OAS1_0335_MR1,F,R,80,1,4,27,0.5,1654,0.678,1.061,N/A +OAS1_0336_MR1,F,R,41,,,,0,1528,0.852,1.149,N/A +OAS1_0337_MR1,M,R,81,1,4,28,0,1750,0.676,1.003,N/A +OAS1_0338_MR1,M,R,77,4,1,29,0,1818,0.736,0.965,N/A +OAS1_0339_MR1,F,R,79,2,,24,0.5,1211,0.694,1.449,N/A +OAS1_0340_MR1,M,R,19,,,,0,1650,0.853,1.063,N/A +OAS1_0341_MR1,F,R,71,2,4,30,0,1479,0.772,1.187,N/A +OAS1_0342_MR1,F,R,88,2,3,28,0,1370,0.765,1.281,N/A +OAS1_0343_MR1,M,R,68,3,3,30,0,1441,0.811,1.217,N/A +OAS1_0344_MR1,M,R,20,,,,0,1510,0.851,1.163,N/A +OAS1_0345_MR1,F,R,54,4,2,30,0,1389,0.831,1.264,N/A +OAS1_0346_MR1,M,R,23,,,,0,1485,0.843,1.181,N/A +OAS1_0348_MR1,F,R,22,,,,0,1473,0.841,1.191,N/A +OAS1_0349_MR1,F,R,43,,,,0,1227,0.858,1.43,N/A +OAS1_0350_MR1,M,R,21,,,,0,1577,0.869,1.113,N/A +OAS1_0351_MR1,M,R,86,1,4,15,2,1512,0.665,1.161,N/A +OAS1_0352_MR1,F,R,81,5,2,26,0.5,1174,0.743,1.495,N/A +OAS1_0353_MR1,M,R,22,,,,0,1680,0.8,1.044,N/A +OAS1_0354_MR1,M,R,74,1,3,26,0,1367,0.776,1.284,N/A +OAS1_0355_MR1,F,R,73,4,2,29,0,1123,0.79,1.563,N/A +OAS1_0356_MR1,F,R,68,3,2,30,0,1506,0.74,1.165,N/A +OAS1_0357_MR1,F,R,55,4,3,30,0,1450,0.82,1.21,N/A +OAS1_0358_MR1,M,R,65,3,3,29,0,1362,0.839,1.289,N/A +OAS1_0359_MR1,M,R,21,,,,0,1714,0.864,1.024,N/A +OAS1_0361_MR1,M,R,20,,,,0,1485,0.842,1.182,N/A +OAS1_0362_MR1,F,R,63,3,,14,0.5,1439,0.716,1.219,N/A +OAS1_0363_MR1,M,R,87,4,2,30,0,1398,0.702,1.255,N/A +OAS1_0365_MR1,M,R,74,5,2,30,0,1806,0.754,0.972,N/A +OAS1_0366_MR1,F,R,45,5,2,29,0,1549,0.813,1.133,N/A +OAS1_0367_MR1,F,R,46,2,2,28,0,1161,0.841,1.511,N/A +OAS1_0368_MR1,M,R,22,,,,0,1572,0.856,1.116,N/A +OAS1_0369_MR1,F,R,73,4,1,28,0,1295,0.772,1.356,N/A +OAS1_0370_MR1,M,R,23,,,,0,1734,0.847,1.012,N/A +OAS1_0371_MR1,F,R,70,3,4,30,0,1361,0.783,1.29,N/A +OAS1_0372_MR1,M,R,59,3,2,29,0,1596,0.817,1.1,N/A +OAS1_0373_MR1,F,R,80,3,2,20,1,1732,0.692,1.013,N/A +OAS1_0374_MR1,F,R,73,3,3,29,0.5,1238,0.76,1.418,N/A +OAS1_0375_MR1,M,R,46,,,,0,1617,0.775,1.086,N/A +OAS1_0376_MR1,M,R,31,,,,0,1579,0.817,1.111,N/A +OAS1_0377_MR1,M,R,25,,,,0,1567,0.831,1.12,N/A +OAS1_0378_MR1,F,R,58,2,2,30,0,1418,0.821,1.238,N/A +OAS1_0379_MR1,F,R,20,,,,0,1255,0.866,1.398,N/A +OAS1_0380_MR1,F,R,83,1,5,18,0.5,1313,0.705,1.337,N/A +OAS1_0381_MR1,M,R,59,4,2,29,0,1795,0.809,0.978,N/A +OAS1_0382_MR1,F,R,67,4,,15,1,1288,0.763,1.362,N/A +OAS1_0383_MR1,M,R,58,,,,0,1590,0.746,1.104,N/A +OAS1_0384_MR1,F,R,38,,,,0,1562,0.844,1.123,N/A +OAS1_0385_MR1,M,R,22,,,,0,1643,0.841,1.068,N/A +OAS1_0386_MR1,F,R,26,,,,0,1490,0.838,1.178,N/A +OAS1_0387_MR1,F,R,26,,,,0,1149,0.851,1.527,N/A +OAS1_0388_MR1,F,R,77,2,4,22,1,1350,0.736,1.3,N/A +OAS1_0389_MR1,M,R,55,,,,0,1678,0.782,1.046,N/A +OAS1_0390_MR1,M,R,69,2,2,24,0.5,1480,0.794,1.186,N/A +OAS1_0392_MR1,F,R,24,,,,0,1441,0.848,1.218,N/A +OAS1_0394_MR1,F,R,22,,,,0,1343,0.847,1.307,N/A +OAS1_0395_MR1,F,R,26,,,,0,1295,0.834,1.356,N/A +OAS1_0396_MR1,M,R,25,,,,0,1674,0.832,1.048,N/A +OAS1_0397_MR1,F,R,20,,,,0,1265,0.846,1.387,N/A +OAS1_0398_MR1,M,R,71,5,1,30,0,1769,0.716,0.992,N/A +OAS1_0399_MR1,M,R,78,2,,29,1,1569,0.706,1.119,N/A +OAS1_0400_MR1,F,R,92,5,1,25,0.5,1774,0.644,0.989,N/A +OAS1_0401_MR1,F,R,54,4,3,29,0,1287,0.827,1.364,N/A +OAS1_0402_MR1,F,R,76,3,2,30,0.5,1350,0.763,1.3,N/A +OAS1_0403_MR1,M,R,19,,,,0,1592,0.833,1.102,N/A +OAS1_0404_MR1,F,R,73,2,2,29,0,1465,0.776,1.198,N/A +OAS1_0405_MR1,M,R,77,5,1,23,1,1713,0.761,1.024,N/A +OAS1_0406_MR1,F,R,25,,,,0,1346,0.855,1.303,N/A +OAS1_0407_MR1,F,R,55,,,,0,1434,0.807,1.224,N/A +OAS1_0408_MR1,F,R,22,,,,0,1518,0.861,1.156,N/A +OAS1_0409_MR1,M,R,34,,,,0,1569,0.798,1.118,N/A +OAS1_0410_MR1,F,R,23,,,,0,1507,0.87,1.165,N/A +OAS1_0411_MR1,F,R,71,5,1,29,0.5,1346,0.742,1.304,N/A +OAS1_0413_MR1,F,R,25,,,,0,1447,0.866,1.213,N/A +OAS1_0415_MR1,F,R,21,,,,0,1542,0.859,1.138,N/A +OAS1_0416_MR1,F,R,23,,,,0,1567,0.852,1.12,N/A +OAS1_0417_MR1,F,R,30,,,,0,1551,0.855,1.132,N/A +OAS1_0418_MR1,M,R,74,5,2,28,0.5,1659,0.747,1.058,N/A +OAS1_0419_MR1,F,R,21,,,,0,1473,0.862,1.191,N/A +OAS1_0420_MR1,F,R,22,,,,0,1732,0.848,1.013,N/A +OAS1_0421_MR1,F,R,22,,,,0,1655,0.847,1.061,N/A +OAS1_0422_MR1,F,R,69,4,3,29,0,1380,0.809,1.272,N/A +OAS1_0423_MR1,M,R,75,2,4,28,0,1511,0.749,1.162,N/A +OAS1_0424_MR1,M,R,75,4,1,20,1,1613,0.715,1.088,N/A +OAS1_0425_MR1,F,R,78,1,4,23,1,1461,0.715,1.201,N/A +OAS1_0426_MR1,F,R,82,5,2,29,0,1316,0.791,1.334,N/A +OAS1_0428_MR1,F,R,84,4,3,28,0,1500,0.751,1.17,N/A +OAS1_0429_MR1,F,R,45,,,,0,1385,0.808,1.267,N/A +OAS1_0430_MR1,M,R,71,4,1,17,1,1562,0.687,1.123,N/A +OAS1_0431_MR1,F,R,22,,,,0,1405,0.822,1.249,N/A +OAS1_0432_MR1,F,R,72,2,4,26,0.5,1453,0.773,1.208,N/A +OAS1_0433_MR1,M,R,58,4,1,27,0,1606,0.779,1.093,N/A +OAS1_0434_MR1,F,R,50,,,,0,1385,0.819,1.267,N/A +OAS1_0435_MR1,M,R,23,,,,0,1766,0.82,0.994,N/A +OAS1_0437_MR1,F,R,22,,,,0,1444,0.853,1.216,N/A +OAS1_0438_MR1,F,R,66,5,2,29,0,1191,0.787,1.474,N/A +OAS1_0439_MR1,M,R,21,,,,0,1438,0.844,1.221,N/A +OAS1_0440_MR1,M,R,86,1,4,27,0.5,1320,0.723,1.329,N/A +OAS1_0441_MR1,M,R,81,5,1,29,0.5,1647,0.721,1.066,N/A +OAS1_0442_MR1,F,R,23,,,,0,1431,0.847,1.227,N/A +OAS1_0443_MR1,F,R,52,3,3,30,0,1431,0.814,1.226,N/A +OAS1_0444_MR1,F,R,30,,,,0,1250,0.86,1.404,N/A +OAS1_0445_MR1,F,R,90,1,2,29,0,1362,0.673,1.289,N/A +OAS1_0446_MR1,F,R,80,2,4,30,0,1390,0.748,1.263,N/A +OAS1_0447_MR1,F,R,92,4,1,24,0.5,1388,0.739,1.264,N/A +OAS1_0448_MR1,F,R,22,,,,0,1524,0.858,1.152,N/A +OAS1_0449_MR1,F,R,71,3,4,29,0,1264,0.818,1.388,N/A +OAS1_0450_MR1,M,R,19,,,,0,1478,0.88,1.188,N/A +OAS1_0451_MR1,M,R,73,5,3,27,0.5,1687,0.728,1.04,N/A +OAS1_0452_MR1,M,R,75,1,4,22,1,1656,0.762,1.06,N/A +OAS1_0453_MR1,F,R,70,1,4,29,0.5,1295,0.748,1.355,N/A +OAS1_0454_MR1,F,R,73,3,2,23,0.5,1536,0.73,1.142,N/A +OAS1_0455_MR1,F,R,61,2,4,28,0,1354,0.825,1.297,N/A +OAS1_0456_MR1,M,R,61,5,2,30,0,1637,0.78,1.072,N/A +OAS1_0457_MR1,F,R,62,3,3,26,0,1372,0.766,1.279,N/A +OAS1_0061_MR2,F,R,20,,,,0,1757,0.845,0.999,1 +OAS1_0080_MR2,F,R,25,,,,0,1605,0.841,1.093,20 +OAS1_0092_MR2,M,R,22,,,,0,1457,0.835,1.205,5 +OAS1_0101_MR2,M,R,29,,,,0,1501,0.835,1.169,64 +OAS1_0111_MR2,M,R,23,,,,0,1714,0.861,1.024,2 +OAS1_0117_MR2,M,R,25,,,,0,1753,0.782,1.001,5 +OAS1_0145_MR2,M,R,34,,,,0,1654,0.832,1.061,10 +OAS1_0150_MR2,F,R,20,,,,0,1506,0.876,1.165,1 +OAS1_0156_MR2,F,R,20,,,,0,1577,0.832,1.113,12 +OAS1_0191_MR2,F,R,21,,,,0,1416,0.837,1.239,28 +OAS1_0202_MR2,F,R,23,,,,0,1548,0.861,1.134,21 +OAS1_0230_MR2,F,R,19,,,,0,1577,0.849,1.113,24 +OAS1_0236_MR2,F,R,20,,,,0,1222,0.872,1.436,3 +OAS1_0239_MR2,F,R,29,,,,0,1438,0.822,1.221,40 +OAS1_0249_MR2,F,R,28,,,,0,1215,0.865,1.444,3 +OAS1_0285_MR2,M,R,20,,,,0,1469,0.847,1.195,2 +OAS1_0353_MR2,M,R,22,,,,0,1684,0.79,1.042,40 +OAS1_0368_MR2,M,R,22,,,,0,1580,0.856,1.111,89 +OAS1_0379_MR2,F,R,20,,,,0,1262,0.861,1.39,2 +OAS1_0395_MR2,F,R,26,,,,0,1283,0.834,1.368,39 \ No newline at end of file diff --git a/tests/core/test_mri.py b/tests/core/test_mri.py new file mode 100644 index 000000000..e3820c504 --- /dev/null +++ b/tests/core/test_mri.py @@ -0,0 +1,87 @@ +""" +Unit tests for the MRIDataset, AlzheimerDiseaseClassification classes. + +Author: + Soheil Golara (sgolara@illinois.edu), Karan Desai (kdesai2@illinois.edu) +""" +from pathlib import Path +import tempfile +import unittest + +import numpy as np +from PIL import Image + +from pyhealth.datasets import MRIDataset +from pyhealth.tasks import MRIBinaryClassification + +class TestMRIDataset(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.root = Path(__file__).parent.parent.parent / "test-resources" / "core" / "mri" + cls.cache_dir = tempfile.TemporaryDirectory() + cls.dataset = MRIDataset(cls.root, cache_dir=cls.cache_dir.name, download=False, partial=True) + + cls.samples_alzheimer = cls.dataset.set_task(MRIBinaryClassification(disease="alzheimer")) + + @classmethod + def tearDownClass(cls): + cls.samples_alzheimer.close() + + Path(cls.dataset.root / "mri-metadata-pyhealth.csv").unlink() + + @classmethod + def generate_fake_images(cls): + with open(Path(cls.root / "oasis_longitudinal.csv"), 'r') as f: + lines = f.readlines() + + for line in lines[1:]: # Skip header row + name = line.split(',')[0] + mri = Image.fromarray(np.random.randint(0, 256, (224, 224, 4), dtype=np.uint8)) + mri.save(Path(cls.root / "oasis_longitudinal_nifti" / name)) + + def test_stats(self): + self.dataset.stats() + + def test_num_patients(self): + self.assertEqual(len(self.dataset.unique_patient_ids), 436) + + def test_get_patient_1(self): + events = self.dataset.get_patient("OAS1_0001_MR1").get_events() + + self.assertEqual(len(events), 1) + + self.assertEqual(events[0]["gender"], "F") + self.assertEqual(events[0]["dominant_hand"], "R") + self.assertEqual(events[0]["age"], "74") + self.assertEqual(events[0]["clinical_dementia_rating"], "0.0") + self.assertEqual(events[0]["alzheimer"], "0") + + def test_get_patient_2(self): + events = self.dataset.get_patient("OAS1_0002_MR1").get_events() + + self.assertEqual(len(events), 1) + self.assertEqual(events[0]["gender"], "F") + self.assertEqual(events[0]["age"], "55") + self.assertEqual(events[0]["clinical_dementia_rating"], "0.0") + self.assertEqual(events[0]["alzheimer"], "0") + + def test_get_patient_3(self): + events = self.dataset.get_patient("OAS1_0003_MR1").get_events() + + self.assertEqual(len(events), 1) + + self.assertEqual(events[0]["gender"], "F") + self.assertEqual(events[0]["age"], "73") + self.assertEqual(events[0]["clinical_dementia_rating"], "0.5") + self.assertEqual(events[0]["alzheimer"], "1") + + def test_default_task(self): + self.assertIsInstance(self.dataset.default_task, MRIBinaryClassification) + + def test_task_given_invalid_disease(self): + with self.assertRaises(ValueError): + _ = MRIBinaryClassification(disease="arthritis") + + +if __name__ == "__main__": + unittest.main()