Browse Source

CorticalParcellationStats: changed type of attribute `structure_measurements` to `pandas.DataFrame`; dropped attribute `structure_measurement_units`

Fabian Peter Hammerle 4 years ago
parent
commit
d9778070fd
3 changed files with 94 additions and 123 deletions
  1. 7 16
      README.rst
  2. 17 39
      freesurfer_stats/__init__.py
  3. 70 68
      tests/test_cortical_parcellation_stats.py

+ 7 - 16
README.rst

@@ -35,19 +35,10 @@ Usage
     (1670487.274486, 'mm^3')
     >>> stats.whole_brain_measurements['White Surface Total Area']
     (98553.0, 'mm^2')
-    >>> stats.structure_measurements['postcentral']
-    {'Structure Name': 'postcentral',
-     'Number of Vertices': 8102,
-     'Surface Area': 5258.0,
-     'Gray Matter Volume': 12037.0,
-     'Average Thickness': 2.109,
-     'Thickness StdDev': 0.568,
-     ...}
-    >>> stats.structure_measurement_units
-    {'Structure Name': None,
-     'Number of Vertices': None,
-     'Surface Area': 'mm^2',
-     'Gray Matter Volume': 'mm^3',
-     'Average Thickness': 'mm',
-     'Thickness StdDev': 'mm',
-     ...}
+    >>> stats.structure_measurements[['Structure Name', 'Surface Area (mm^2)', 'Gray Matter Volume (mm^3)']].head()
+                Structure Name  Surface Area (mm^2)  Gray Matter Volume (mm^3)
+    0  caudalanteriorcingulate                 1472                       4258
+    1      caudalmiddlefrontal                 3039                       8239
+    2                   cuneus                 2597                       6722
+    3               entorhinal                  499                       2379
+    4                 fusiform                 3079                       9064

+ 17 - 39
freesurfer_stats/__init__.py

@@ -18,22 +18,13 @@ https://surfer.nmr.mgh.harvard.edu/
 (1670487.274486, 'mm^3')
 >>> stats.whole_brain_measurements['White Surface Total Area']
 (98553.0, 'mm^2')
->>> stats.structure_measurements['postcentral']
-{'Structure Name': 'postcentral',
- 'Number of Vertices': 8102,
- 'Surface Area': 5258.0,
- 'Gray Matter Volume': 12037.0,
- 'Average Thickness': 2.109,
- 'Thickness StdDev': 0.568,
- ...}
->>> stats.structure_measurement_units
-{'Structure Name': None,
- 'Number of Vertices': None,
- 'Surface Area': 'mm^2',
- 'Gray Matter Volume': 'mm^3',
- 'Average Thickness': 'mm',
- 'Thickness StdDev': 'mm',
- ...}
+>>> stats.structure_measurements[['Structure Name', 'Surface Area (mm^2)', 'Gray Matter Volume (mm^3)']].head()
+            Structure Name  Surface Area (mm^2)  Gray Matter Volume (mm^3)
+0  caudalanteriorcingulate                 1472                       4258
+1      caudalmiddlefrontal                 3039                       8239
+2                   cuneus                 2597                       6722
+3               entorhinal                  499                       2379
+4                 fusiform                 3079                       9064
 """
 
 import datetime
@@ -57,9 +48,7 @@ class CorticalParcellationStats:
         self.whole_brain_measurements \
             = {}  # type: typing.Dict[str, typing.Tuple[float, int]]
         self.structure_measurements \
-            = {}  # type: typing.Dict[str, typing.Dict[str, typing.Union[str, int, float]]]
-        self.structure_measurement_units \
-            = {}  # type: typing.Dict[str, typing.Union[str, None]]
+            = {}  # type: typing.Union[pandas.DataFrame, None]
 
     @property
     def hemisphere(self) -> str:
@@ -103,10 +92,12 @@ class CorticalParcellationStats:
                 self.headers[attr_name] = attr_value
 
     @staticmethod
-    def _filter_unit(unit: str) -> typing.Union[str, None]:
+    def _format_column_name(column_attrs: typing.Dict[str, str]) -> str:
+        name = column_attrs['FieldName']
+        unit = column_attrs['Units']
         if unit in ['unitless', 'NA']:
-            return None
-        return unit
+            return name
+        return '{} ({})'.format(name, unit)
 
     @classmethod
     def _read_column_attributes(cls, num: int, stream: typing.TextIO) \
@@ -143,23 +134,10 @@ class CorticalParcellationStats:
             int(line[len('NTableCols '):]), stream)
         assert self._read_header_line(stream) \
             == 'ColHeaders ' + ' '.join(c['ColHeader'] for c in columns)
-        assert columns[0]['ColHeader'] == 'StructName'
-        column_names = [c['FieldName'] for c in columns]
-        self.structure_measurements = {}
-        for line in stream:
-            values = line.rstrip().split()
-            assert len(values) == len(column_names)
-            struct_name = values[0]
-            assert struct_name not in self.structure_measurements
-            for column_index, column_attrs in enumerate(columns):
-                if column_attrs['ColHeader'] in ['NumVert', 'FoldInd']:
-                    values[column_index] = int(values[column_index])
-                elif column_attrs['ColHeader'] != 'StructName':
-                    values[column_index] = float(values[column_index])
-            self.structure_measurements[struct_name] \
-                = dict(zip(column_names, values))
-        self.structure_measurement_units = {
-            c['FieldName']: self._filter_unit(c['Units']) for c in columns}
+        self.structure_measurements = pandas.DataFrame(
+            (line.rstrip().split() for line in stream),
+            columns=list(map(self._format_column_name, columns))) \
+            .apply(pandas.to_numeric, errors='ignore')
 
     @classmethod
     def read(cls, path: str) -> 'CorticalParcellationStats':

+ 70 - 68
tests/test_cortical_parcellation_stats.py

@@ -1,6 +1,7 @@
 import datetime
 import os
 
+import pandas.util.testing
 import pytest
 
 from conftest import SUBJECTS_DIR, assert_approx_equal
@@ -38,39 +39,36 @@ from freesurfer_stats import CorticalParcellationStats
        'Supratentorial volume': (1172669.548920, 'mm^3'),
        'Supratentorial volume Without Ventricles': (1164180.548920, 'mm^3'),
        'Estimated Total Intracranial Volume': (1670487.274486, 'mm^3')},
-      {'caudalanteriorcingulate': {
-          'Structure Name': 'caudalanteriorcingulate',
-          'Number of Vertices': 2061,
-          'Surface Area': 1472.0,
-          'Gray Matter Volume': 4258.0,
-          'Average Thickness': 2.653,
-          'Thickness StdDev': 0.644,
-          'Integrated Rectified Mean Curvature': 0.135,
-          'Integrated Rectified Gaussian Curvature': 0.020,
-          'Folding Index': 27,
-          'Intrinsic Curvature Index': 1.6},
-       'caudalmiddlefrontal': {
-           'Structure Name': 'caudalmiddlefrontal',
-           'Number of Vertices': 4451,
-           'Surface Area': 3039.0,
-           'Gray Matter Volume': 8239.0,
-           'Average Thickness': 2.456,
-           'Thickness StdDev': 0.486,
-           'Integrated Rectified Mean Curvature': 0.116,
-           'Integrated Rectified Gaussian Curvature': 0.020,
-           'Folding Index': 42,
-           'Intrinsic Curvature Index': 3.7},
-       'insula': {
-           'Structure Name': 'insula',
-           'Number of Vertices': 3439,
-           'Surface Area': 2304.0,
-           'Gray Matter Volume': 7594.0,
-           'Average Thickness': 3.193,
-           'Thickness StdDev': 0.620,
-           'Integrated Rectified Mean Curvature': 0.116,
-           'Integrated Rectified Gaussian Curvature': 0.027,
-           'Folding Index': 33,
-           'Intrinsic Curvature Index': 3.5}}),
+      [{'Structure Name': 'caudalanteriorcingulate',
+        'Number of Vertices': 2061,
+        'Surface Area (mm^2)': 1472,
+        'Gray Matter Volume (mm^3)': 4258,
+        'Average Thickness (mm)': 2.653,
+        'Thickness StdDev (mm)': 0.644,
+        'Integrated Rectified Mean Curvature (mm^-1)': 0.135,
+        'Integrated Rectified Gaussian Curvature (mm^-2)': 0.020,
+        'Folding Index': 27,
+        'Intrinsic Curvature Index': 1.6},
+       {'Structure Name': 'caudalmiddlefrontal',
+        'Number of Vertices': 4451,
+        'Surface Area (mm^2)': 3039,
+        'Gray Matter Volume (mm^3)': 8239,
+        'Average Thickness (mm)': 2.456,
+        'Thickness StdDev (mm)': 0.486,
+        'Integrated Rectified Mean Curvature (mm^-1)': 0.116,
+        'Integrated Rectified Gaussian Curvature (mm^-2)': 0.020,
+        'Folding Index': 42,
+        'Intrinsic Curvature Index': 3.7},
+       {'Structure Name': 'insula',
+        'Number of Vertices': 3439,
+        'Surface Area (mm^2)': 2304,
+        'Gray Matter Volume (mm^3)': 7594,
+        'Average Thickness (mm)': 3.193,
+        'Thickness StdDev (mm)': 0.620,
+        'Integrated Rectified Mean Curvature (mm^-1)': 0.116,
+        'Integrated Rectified Gaussian Curvature (mm^-2)': 0.027,
+        'Folding Index': 33,
+        'Intrinsic Curvature Index': 3.5}]),
      (os.path.join(
          SUBJECTS_DIR, 'fabian', 'stats', 'rh.aparc.pial.stats.short'),
       {'CreationTime': datetime.datetime(2019, 5, 9, 21, 3, 42, tzinfo=datetime.timezone.utc),
@@ -100,28 +98,26 @@ from freesurfer_stats import CorticalParcellationStats
        'Supratentorial volume': (1172669.548920, 'mm^3'),
        'Supratentorial volume Without Ventricles': (1164180.548920, 'mm^3'),
        'Estimated Total Intracranial Volume': (1670487.274486, 'mm^3')},
-      {'bankssts': {
-          'Structure Name': 'bankssts',
-          'Number of Vertices': 1344,
-          'Surface Area': 825.0,
-          'Gray Matter Volume': 2171.0,
-          'Average Thickness': 2.436,
-          'Thickness StdDev': 0.381,
-          'Integrated Rectified Mean Curvature': 0.115,
-          'Integrated Rectified Gaussian Curvature': 0.028,
-          'Folding Index': 19,
-          'Intrinsic Curvature Index': 1.7},
-       'transversetemporal': {
-           'Structure Name': 'transversetemporal',
-           'Number of Vertices': 651,
-           'Surface Area': 545.0,
-           'Gray Matter Volume': 1061.0,
-           'Average Thickness': 2.251,
-           'Thickness StdDev': 0.317,
-           'Integrated Rectified Mean Curvature': 0.110,
-           'Integrated Rectified Gaussian Curvature': 0.021,
-           'Folding Index': 3,
-           'Intrinsic Curvature Index': 0.6}})],
+      [{'Structure Name': 'bankssts',
+        'Number of Vertices': 1344,
+        'Surface Area (mm^2)': 825,
+        'Gray Matter Volume (mm^3)': 2171,
+        'Average Thickness (mm)': 2.436,
+        'Thickness StdDev (mm)': 0.381,
+        'Integrated Rectified Mean Curvature (mm^-1)': 0.115,
+        'Integrated Rectified Gaussian Curvature (mm^-2)': 0.028,
+        'Folding Index': 19,
+        'Intrinsic Curvature Index': 1.7},
+       {'Structure Name': 'transversetemporal',
+        'Number of Vertices': 651,
+        'Surface Area (mm^2)': 545,
+        'Gray Matter Volume (mm^3)': 1061,
+        'Average Thickness (mm)': 2.251,
+        'Thickness StdDev (mm)': 0.317,
+        'Integrated Rectified Mean Curvature (mm^-1)': 0.110,
+        'Integrated Rectified Gaussian Curvature (mm^-2)': 0.021,
+        'Folding Index': 3,
+        'Intrinsic Curvature Index': 0.6}])],
 )
 def test_read(path, headers, hemisphere, whole_brain_measurements, structure_measurements):
     stats = CorticalParcellationStats.read(path)
@@ -129,16 +125,22 @@ def test_read(path, headers, hemisphere, whole_brain_measurements, structure_mea
     assert hemisphere == stats.hemisphere
     assert_approx_equal(whole_brain_measurements,
                         stats.whole_brain_measurements)
-    assert stats.structure_measurement_units == {
-        'Structure Name': None,
-        'Number of Vertices': None,
-        'Surface Area': 'mm^2',
-        'Gray Matter Volume': 'mm^3',
-        'Average Thickness': 'mm',
-        'Thickness StdDev': 'mm',
-        'Integrated Rectified Mean Curvature': 'mm^-1',
-        'Integrated Rectified Gaussian Curvature': 'mm^-2',
-        'Folding Index': None,
-        'Intrinsic Curvature Index': None,
-    }
-    assert_approx_equal(structure_measurements, stats.structure_measurements)
+    assert list(stats.structure_measurements.columns) == [
+        'Structure Name',
+        'Number of Vertices',
+        'Surface Area (mm^2)',
+        'Gray Matter Volume (mm^3)',
+        'Average Thickness (mm)',
+        'Thickness StdDev (mm)',
+        'Integrated Rectified Mean Curvature (mm^-1)',
+        'Integrated Rectified Gaussian Curvature (mm^-2)',
+        'Folding Index',
+        'Intrinsic Curvature Index',
+    ]
+    pandas.util.testing.assert_frame_equal(
+        left=pandas.DataFrame(structure_measurements),
+        right=stats.structure_measurements,
+        check_like=True,  # ignore the order of index & columns
+        check_dtype=True,
+        check_names=True,
+    )