From b2a11fa97d13cd374c3a9c54eff516fa5220a603 Mon Sep 17 00:00:00 2001 From: omidvarnia Date: Fri, 25 Nov 2022 17:05:02 +0100 Subject: [PATCH] A bug was fixed in complexity.py. One marker per measure is being created now. --- junifer/markers/complexity.py | 47 +++++---- junifer/markers/tests/test_complexity.py | 20 ++-- junifer/markers/utils.py | 128 +++++++++++++---------- 3 files changed, 108 insertions(+), 87 deletions(-) diff --git a/junifer/markers/complexity.py b/junifer/markers/complexity.py index 8f492f4a32..b5d9860ef5 100644 --- a/junifer/markers/complexity.py +++ b/junifer/markers/complexity.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional +# from ptpython.repl import embed from ..api.decorators import register_marker from ..utils import logger from .base import BaseMarker @@ -29,6 +30,19 @@ class Complexity(BaseMarker): aggregation_method : str, optional The method to perform aggregation using. Check valid options in :func:`junifer.stats.get_aggfunc_by_name` (default "mean"). + measure_type: dict + A dictionary including the name of the desired complexity measure + to be extracted and its associated parameters. The measures and + their default values include: + + {"_range_entropy": {"m": 2, "tol": 0.5}} + {"_range_entropy_auc": {"m": 2, "n_r": 10}} + {"_perm_entropy": {"m": 4, "tau": 1}} + {"_weighted_perm_entropy": {"m": 4, "tau": 1}} + {"_sample_entropy": {"m": 2, "tau": 1, "tol": 0.5}} + {"_multiscale_entropy_auc": {"m": 2, "tol": 0.5, "scale": 10}} + {"_hurst_exponent": {"reserved": None}} + name : str, optional The name of the marker. If None, will use the class name (default None). @@ -38,26 +52,18 @@ class Complexity(BaseMarker): def __init__( self, parcellation: str, - measure_types: dict = None, + measure_type: dict = None, aggregation_method: str = "mean", name: Optional[str] = None, ) -> None: self.parcellation = parcellation self.aggregation_method = aggregation_method - # measure_types should be a dctionary with keys as the function names, + # measure_type should be a dctionary with keys as the function names, # and values as another dictionary with function parameters. - if measure_types is None: - self.measure_types = { - "_range_entropy": {"m": 2, "tol": 0.5}, - "_range_entropy_auc": {"m": 2, "n_r": 10}, - "_perm_entropy": {"m": 4, "tau": 1}, - "_weighted_perm_entropy": {"m": 4, "tau": 1}, - "_sample_entropy": {"m": 2, "tau": 1, "tol": 0.5}, - "_multiscale_entropy_auc": {"m": 2, "tol": 0.5, "scale": 10}, - "_hurst_exponent": {"reserved": None}, - } + if measure_type is None: + self.measure_type = {"_range_entropy": {"m": 2, "tol": 0.5}} else: - self.measure_types = measure_types + self.measure_type = measure_type super().__init__(name=name) @@ -142,13 +148,13 @@ def compute( References ---------- - .. [1] A. Omidvarnia et al. + .. [1] Omidvarnia, A., et al. Range Entropy: A Bridge between Signal Complexity and Self-Similarity, Entropy, vol. 20, no. 12, p. 962, 2018. .. [2] Bandt, C., & Pompe, B. Permutation entropy: a natural complexity measure for time - series. Physical review letters, 88(17), 174102, 2002 + series. Physical review letters, 88(17), 174102, 2002. .. [3] Fadlallah, B., Chen, B., Keil, A., & Principe, J. Weighted-permutation entropy: A complexity measure for time @@ -167,10 +173,13 @@ def compute( .. [6] Peng, C.; Havlin, S.; Stanley, H.E.; Goldberger, A.L. Quantification of scaling exponents and crossover phenomena in nonstationary heartbeat time series. - Chaos Interdiscip. J. Nonlinear Sci., 5, 82–87, 1995 + Chaos Interdiscip. J. Nonlinear Sci., 5, 82–87, 1995. """ + # print('Stop: complexity_compute') + # embed(globals(), locals()) + logger.debug("Calculating root sum of squares of edgewise timeseries.") # Initialize a ParcelAggregation parcel_aggregation = ParcelAggregation( @@ -184,10 +193,10 @@ def compute( # Calculate complexity and et correct column/row labels bold_ts = pa_dict["data"] - tmp = _calculate_complexity(bold_ts, self.measure_types) + tmp = _calculate_complexity(bold_ts, self.measure_type) out = {} - out["data"] = tmp - out["col_names"] = self.measure_types.keys() + out["data"] = tmp.T + out["col_names"] = self.measure_type.keys() out["row_names"] = pa_dict["columns"] return out diff --git a/junifer/markers/tests/test_complexity.py b/junifer/markers/tests/test_complexity.py index 9eced0b98f..a04d4d2c90 100644 --- a/junifer/markers/tests/test_complexity.py +++ b/junifer/markers/tests/test_complexity.py @@ -31,18 +31,18 @@ def test_compute() -> None: # Create input data input_dict = {"data": niimg, "path": out["BOLD"]["path"]} # Create input data - measure_types = { - "_range_entropy": {"m": 2, "tol": 0.5}, - "_range_entropy_auc": {"m": 2, "n_r": 10}, - "_perm_entropy": {"m": 4, "tau": 1}, - "_weighted_perm_entropy": {"m": 4, "tau": 1}, - "_sample_entropy": {"m": 2, "tau": 1, "tol": 0.5}, - "_multiscale_entropy_auc": {"m": 2, "tol": 0.5, "scale": 2}, - "_hurst_exponent": {"reserved": None}, - } + measure_type = {"_range_entropy": {"m": 2, "tol": 0.5}} + # measure_type = {"_range_entropy_auc": {"m": 2, "n_r": 10}} + # measure_type = {"_perm_entropy": {"m": 4, "tau": 1}} + # measure_type = {"_weighted_perm_entropy": {"m": 4, "tau": 1}} + # measure_type = {"_sample_entropy": {"m": 2, "tau": 1, "tol": 0.5}} + # measure_type = {"_multiscale_entropy_auc": + # {"m": 2, "tol": 0.5, "scale": 10}} + # measure_type = {"_hurst_exponent": {"reserved": None}} + # Compute the Complexity markers complexity = Complexity( - parcellation=PARCELLATION, measure_types=measure_types + parcellation=PARCELLATION, measure_type=measure_type ) new_out = complexity.compute(input_dict) diff --git a/junifer/markers/utils.py b/junifer/markers/utils.py index 3ed56d3bff..0dbb73620a 100644 --- a/junifer/markers/utils.py +++ b/junifer/markers/utils.py @@ -99,10 +99,20 @@ def _correlate_dataframes( def _calculate_complexity( bold_ts: np.ndarray, - measure_types: dict, + measure_type: dict, ) -> np.ndarray: """Compute the region-wise complexity measures from 2d BOLD time series. + Available options and their default values: + + {"_range_entropy": {"m": 2, "tol": 0.5}} + {"_range_entropy_auc": {"m": 2, "n_r": 10}} + {"_perm_entropy": {"m": 4, "tau": 1}} + {"_weighted_perm_entropy": {"m": 4, "tau": 1}} + {"_sample_entropy": {"m": 2, "tau": 1, "tol": 0.5}} + {"_multiscale_entropy_auc": {"m": 2, "tol": 0.5, "scale": 10}} + {"_hurst_exponent": {"reserved": None}} + - Range entropy: Take a timeseries of brain areas, and calculate range entropy according to the method outlined in [1]. @@ -116,6 +126,17 @@ def _calculate_complexity( - Weighted permutation entropy: Take a timeseries of brain areas, and calculate permutation entropy according to the method outlined in [3]. + - Sample entropy: Take a timeseries of brain areas, and calculate + sample entropy [4]. + + - Multiscale entropy: Take a timeseries of brain areas, calculate + multiscale entropy for each region and calculate the AUC of the entropy + curves leading to a region-wise map of the brain [5]. + + - Hurst exponent: Take a timeseries of brain areas, and calculate + Hurst exponent using the detrended fluctuation analysis method assuming + the data is monofractal (q = 2 in nk.fractal_dfa) [6]. + Parameters ---------- bold_ts : np.ndarray @@ -128,44 +149,52 @@ def _calculate_complexity( References ---------- - .. [1] A. Omidvarnia et al. (2018) + .. [1] Omidvarnia, A., et al. Range Entropy: A Bridge between Signal Complexity and - Self-Similarity, Entropy, vol. 20, no. 12, p. 962. + Self-Similarity, Entropy, vol. 20, no. 12, p. 962, 2018. - .. [2] [1] Bandt, C., & Pompe, B. (2002) + .. [2] Bandt, C., & Pompe, B. Permutation entropy: a natural complexity measure for time - series. Physical review letters, 88(17), 174102. + series. Physical review letters, 88(17), 174102, 2002. - .. [3] B. Fadlallah et al. (2013) + .. [3] Fadlallah, B., et al. Weighted-permutation entropy: A complexity measure for time series incorporating amplitude information. - Physical Review E, 87(2), 022911. + Physical Review E, 87(2), 022911, 2013. + + .. [4] Richman, J., Moorman, J. + Physiological time-series analysis using approximate entropy and + sample entropy, Am. J. Physiol. Heart Circ. Physiol., + 278 (6), pp. H2039-2049, 2000. + + .. [5] Costa, M., Goldberger, A. L., & Peng, C. K. + Multiscale entropy analysis of complex physiologic time series. + Physical review letters, 89(6), 068102, 2002. + + .. [6] Peng, C.; Havlin, S.; Stanley, H.E.; Goldberger, A.L. + Quantification of scaling exponents and crossover phenomena in + nonstationary heartbeat time series. + Chaos Interdiscip. J. Nonlinear Sci., 5, 82–87, 1995. See also --------- https://neuropsychology.github.io/NeuroKit/functions/complexity.html """ - _, n_roi = bold_ts.shape - - # Number of complexity measures to be computed. - n_feat = len(measure_types) - - # Initialize the matrix of all feature maps for bold_ts - complexity_features = np.zeros((n_roi, n_feat)) + # print('Stop: _calculate_complexity') + # embed(globals(), locals()) # Start the analysis - feat_idx = 0 - for compl_measure, params in measure_types.items(): - func_name = globals()[compl_measure] - feature_map = func_name(bold_ts, params) # n_roi x 1 - complexity_features[:, feat_idx] = feature_map.T - feat_idx = feat_idx + 1 + complexity_measure = list(measure_type.keys())[0] + params = list(measure_type.values())[0] + func_name = globals()[complexity_measure] + feature_map = func_name(bold_ts, params) # n_roi x 1 + complexity_features = feature_map.T return complexity_features -def _range_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: +def _range_entropy(bold_ts: np.ndarray, params: dict) -> np.ndarray: """Compute the region-wise range entropy from 2d BOLD time series. - Range entropy: Take a timeseries of brain areas, and calculate @@ -175,9 +204,8 @@ def _range_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: ---------- bold_ts : np.ndarray BOLD time series (time x ROIs) - measure_types : dict - a dctionary with keys as the function names, and values as another - dictionary with function parameters. + params : dict + The dictionary of input parameters. Returns ------- @@ -198,7 +226,6 @@ def _range_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: # print('Stop: _range_entropy') # embed(globals(), locals()) - params = measure_types["_range_entropy"] emb_dim = params["m"] tolerance = params["tol"] _, n_roi = bold_ts.shape @@ -217,7 +244,7 @@ def _range_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: return range_en_roi -def _range_entropy_auc(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: +def _range_entropy_auc(bold_ts: np.ndarray, params: dict) -> np.ndarray: """Compute the region-wise AUC of range entropy from 2d BOLD time series. - AUC of range entropy: Take a timeseries of brain areas, calculate @@ -228,9 +255,8 @@ def _range_entropy_auc(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: ---------- bold_ts : np.ndarray BOLD time series (time x ROIs) - measure_types : dict - a dctionary with keys as the function names, and values as another - dictionary with function parameters. + params : dict + The dictionary of input parameters. Returns ------- @@ -251,7 +277,6 @@ def _range_entropy_auc(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: # print('Stop: _range_entropy_auc') # embed(globals(), locals()) - params = measure_types["_range_entropy_auc"] emb_dim = params["m"] n_r = params["n_r"] r_span = np.arange(1 / n_r, (1 + 1 / n_r), 1 / n_r) # Tolerance r span @@ -286,7 +311,7 @@ def _range_entropy_auc(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: return range_en_auc_roi -def _perm_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: +def _perm_entropy(bold_ts: np.ndarray, params: dict) -> np.ndarray: """Compute the region-wise permutation entropy from 2d BOLD time series. - Permutation entropy: Take a timeseries of brain areas, and calculate @@ -296,9 +321,8 @@ def _perm_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: ---------- bold_ts : np.ndarray BOLD time series (time x ROIs) - measure_types : dict - a dctionary with keys as the function names, and values as another - dictionary with function parameters. + params : dict + The dictionary of input parameters. Returns ------- @@ -319,7 +343,6 @@ def _perm_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: # print('Stop: _perm_entropy') # embed(globals(), locals()) - params = measure_types["_perm_entropy"] emb_dim = params["m"] delay = params["tau"] _, n_roi = bold_ts.shape @@ -343,9 +366,7 @@ def _perm_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: return perm_en_roi -def _weighted_perm_entropy( - bold_ts: np.ndarray, measure_types: dict -) -> np.ndarray: +def _weighted_perm_entropy(bold_ts: np.ndarray, params: dict) -> np.ndarray: """Compute the region-wise weighted permutation entropy from bold_ts. - Weighted permutation entropy: Take a timeseries of brain areas, and @@ -356,9 +377,8 @@ def _weighted_perm_entropy( ---------- bold_ts : np.ndarray BOLD time series (time x ROIs) - measure_types : dict - a dctionary with keys as the function names, and values as another - dictionary with function parameters. + params : dict + The dictionary of input parameters. Returns ------- @@ -380,7 +400,6 @@ def _weighted_perm_entropy( # print('Stop: _weighted_perm_entropy') # embed(globals(), locals()) - params = measure_types["_weighted_perm_entropy"] emb_dim = params["m"] delay = params["tau"] _, n_roi = bold_ts.shape @@ -404,7 +423,7 @@ def _weighted_perm_entropy( return wperm_en_roi -def _sample_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: +def _sample_entropy(bold_ts: np.ndarray, params: dict) -> np.ndarray: """Compute the region-wise weighted permutation entropy from bold_ts. - Sample entropy: Take a timeseries of brain areas, and @@ -414,9 +433,8 @@ def _sample_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: ---------- bold_ts : np.ndarray BOLD time series (time x ROIs) - measure_types : dict - a dctionary with keys as the function names, and values as another - dictionary with function parameters. + params : dict + The dictionary of input parameters. Returns ------- @@ -438,7 +456,6 @@ def _sample_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: # print('Stop: _sample_entropy') # embed(globals(), locals()) - params = measure_types["_sample_entropy"] emb_dim = params["m"] delay = params["tau"] tol = params["tol"] @@ -461,9 +478,7 @@ def _sample_entropy(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: return samp_en_roi -def _multiscale_entropy_auc( - bold_ts: np.ndarray, measure_types: dict -) -> np.ndarray: +def _multiscale_entropy_auc(bold_ts: np.ndarray, params: dict) -> np.ndarray: """Compute the region-wise AUC of multiscale entropy of bold_ts. - Multiscale entropy: Take a timeseries of brain areas, @@ -474,9 +489,8 @@ def _multiscale_entropy_auc( ---------- bold_ts : np.ndarray BOLD time series (time x ROIs) - measure_types : dict - a dctionary with keys as the function names, and values as another - dictionary with function parameters. + params : dict + The dictionary of input parameters. Returns ------- @@ -497,7 +511,6 @@ def _multiscale_entropy_auc( # print('Stop: _multiscale_entropy_auc') # embed(globals(), locals()) - params = measure_types["_multiscale_entropy_auc"] emb_dim = params["m"] tol = params["tol"] scale = params["scale"] @@ -531,7 +544,7 @@ def _multiscale_entropy_auc( return MSEn_auc_roi -def _hurst_exponent(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: +def _hurst_exponent(bold_ts: np.ndarray, params: dict) -> np.ndarray: """Compute the region-wise Hurst exponent of bold_ts. - Hurst exponent: Take a timeseries of brain areas, and @@ -542,9 +555,8 @@ def _hurst_exponent(bold_ts: np.ndarray, measure_types: dict) -> np.ndarray: ---------- bold_ts : np.ndarray BOLD time series (time x ROIs) - measure_types : dict - a dctionary with keys as the function names, and values as another - dictionary with function parameters. + params : dict + The dictionary of input parameters. Returns -------