├── LICENSE ├── constants ├── __init__.py └── labels.py ├── descriptors ├── __init__.py ├── dynamic.py ├── frequentist.py ├── indices_corresp.py ├── positional.py └── stochastic.py ├── main.ipynb ├── main.py ├── stabilogram ├── __init__.py ├── stato.py └── swarii.py └── test.csv /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jythen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jythen/code_descriptors_postural_control/c66a0e4759708c4a4e63c28850d9b5243b197aa2/constants/__init__.py -------------------------------------------------------------------------------- /constants/labels.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ML = "ML" 6 | AP = "AP" 7 | SPD_AP = "SPD_AP" 8 | SPD_ML = "SPD_ML" 9 | SPD_MLAP = "SPD_MLAP" 10 | RADIUS = "Radius" 11 | SWAY_DENSITY = "Sway_Density" 12 | PSD_ML = "Power_Spectrum_Density_ML" 13 | PSD_AP = "Power_Spectrum_Density_AP" 14 | MLAP = "ML_AND_AP" 15 | DIFF_ML = "Diffusion_ML" 16 | DIFF_AP = "Diffusion_AP" 17 | DIFF_MLAP = "Diffusion_ML_AND_AP" 18 | 19 | 20 | 21 | all_labels = [ ML, AP, SPD_ML, SPD_AP, RADIUS, SWAY_DENSITY, PSD_ML, PSD_AP, MLAP, DIFF_ML, DIFF_AP, DIFF_MLAP] -------------------------------------------------------------------------------- /descriptors/__init__.py: -------------------------------------------------------------------------------- 1 | from code_descriptors_postural_control.descriptors import positional, dynamic, frequentist, stochastic 2 | from code_descriptors_postural_control.constants import labels 3 | 4 | 5 | 6 | functions_with_params = {"swd_peaks": ["sway_density_radius"]} 7 | 8 | 9 | default_param_dic = {"sway_density_radius":0.3} 10 | 11 | def compute_all_features(signal, params_dic=default_param_dic): 12 | 13 | 14 | domains = [positional, dynamic, frequentist, stochastic] 15 | 16 | all_labels = labels.all_labels 17 | 18 | features = {} 19 | 20 | 21 | for domain in domains: 22 | 23 | for function in domain.all_features: 24 | 25 | params = None 26 | 27 | for key in functions_with_params: 28 | 29 | if key in str(function): 30 | 31 | params = {param: params_dic[param] for param in functions_with_params[key]} 32 | 33 | for label in all_labels: 34 | 35 | 36 | if params is not None: 37 | 38 | result = function(signal, **params) 39 | 40 | else: 41 | result = function(signal, axis=label) 42 | 43 | features.update(result) 44 | 45 | 46 | 47 | return features 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /descriptors/dynamic.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from code_descriptors_postural_control.constants import labels 3 | import code_descriptors_postural_control.descriptors.positional as positional 4 | 5 | 6 | 7 | def sway_length(signal, axis = labels.ML,only_value = False, normalized=False): 8 | if not (axis in [labels.ML, labels.AP,labels.MLAP]): 9 | return {} 10 | feature_name = "sway_length" 11 | 12 | sig = signal.get_signal(axis) 13 | 14 | dif = np.diff(sig, n=1, axis=0) 15 | dif = np.linalg.norm(dif, axis=1) 16 | feature = np.sum(dif) 17 | 18 | if normalized: 19 | feature = feature * (signal.frequency / len(sig)) 20 | 21 | if only_value: 22 | return feature 23 | 24 | return { feature_name+"_"+axis : feature} 25 | 26 | 27 | 28 | def mean_velocity(signal, axis = labels.ML, only_value = False): 29 | if not (axis in [labels.ML, labels.AP, labels.MLAP]): 30 | return {} 31 | feature_name = "mean_velocity" 32 | 33 | sig = signal.get_signal(axis) 34 | 35 | sway = sway_length(signal, axis, only_value=True) 36 | 37 | feature = sway * (signal.frequency / len(sig)) 38 | 39 | if only_value: 40 | return feature 41 | 42 | return { feature_name+"_"+axis : feature} 43 | 44 | 45 | 46 | def sway_area_per_second(signal, axis = labels.MLAP): 47 | if not (axis in [labels.MLAP]): 48 | return {} 49 | feature_name = "sway_area_per_second" 50 | 51 | sig = signal.get_signal(axis) 52 | 53 | dt = 1/ signal.frequency 54 | 55 | duration = (len(sig) -1) * dt 56 | assert duration>0 57 | 58 | triangles = np.abs(sig[1:,0] * sig[:-1,1] - sig[1:,1] * sig[:-1,0]) 59 | 60 | feature = np.sum(triangles) / (2*duration) 61 | 62 | return { feature_name+"_"+axis : feature} 63 | 64 | 65 | 66 | def phase_plane_parameter(signal, axis = labels.ML): 67 | if not (axis in [labels.ML, labels.AP]): 68 | return {} 69 | feature_name = "phase_plane_parameter" 70 | 71 | std_sig = positional.rms(signal, axis=axis, only_value=True) 72 | 73 | if axis == labels.ML: 74 | spd = signal.get_signal(labels.SPD_ML) 75 | elif axis == labels.AP: 76 | spd = signal.get_signal(labels.SPD_AP) 77 | 78 | feature = np.sqrt(std_sig**2 + np.var(spd)) 79 | 80 | return { feature_name+"_"+axis : feature} 81 | 82 | 83 | 84 | def vfy(signal, axis = labels.SPD_MLAP): 85 | if not (axis in [labels.SPD_MLAP]): 86 | return {} 87 | feature_name = "vfy" 88 | 89 | std = signal.get_signal(axis) 90 | 91 | vdxy = np.var(std) 92 | 93 | muy = signal.mean_value[1] 94 | 95 | if muy == 0: 96 | muy = 0.0001 97 | 98 | feature = vdxy / muy 99 | 100 | return {feature_name+"_"+axis : feature} 101 | 102 | 103 | 104 | def length_over_area(signal, axis = labels.MLAP, normalized=False): 105 | if not (axis in [labels.MLAP]): 106 | return {} 107 | feature_name = "LFS" 108 | 109 | length = sway_length(signal, axis = labels.MLAP, only_value = True) 110 | area = positional.confidence_ellipse_area(signal, axis = labels.MLAP, \ 111 | only_value = True) 112 | 113 | feature = length/area 114 | 115 | if normalized: 116 | 117 | sig = signal.get_signal(axis) 118 | 119 | feature = feature * (signal.frequency / len(sig)) 120 | 121 | return { feature_name+"_"+axis : feature} 122 | 123 | 124 | 125 | def fractal_dimension_ce(signal, axis = labels.MLAP, normalized=False): 126 | if not (axis in [labels.MLAP]): 127 | return {} 128 | feature_name = "fractal_dimension" 129 | 130 | area = positional.confidence_ellipse_area(signal, axis=labels.MLAP, \ 131 | only_value=True) 132 | 133 | d = np.sqrt((area * 4) / np.pi) 134 | 135 | N = len(signal) 136 | 137 | sway = sway_length(signal,axis=axis,only_value = True) 138 | 139 | fd = np.log(N) / (np.log(N) + np.log(d) - np.log(sway)) 140 | 141 | feature = fd 142 | 143 | 144 | if normalized: 145 | feature = feature / np.log(N) 146 | 147 | 148 | return { feature_name+"_"+axis : feature} 149 | 150 | 151 | 152 | def velocity_peaks(signal, axis=labels.SPD_ML, normalized=False): 153 | if not (axis in [labels.SPD_ML, labels.SPD_AP]): 154 | return {} 155 | 156 | sig = signal.get_signal(axis) 157 | 158 | current_peak = 0 159 | current_peak_index = 0 160 | past_value = 0 161 | zero_crossing_index = [] 162 | negative_peaks_index = [] 163 | positive_peaks_index = [] 164 | current_side = np.sign(sig[sig!=0][0]) 165 | 166 | for index,value in enumerate(sig) : 167 | 168 | is_crossing_point = ( (value)*past_value <= 0 ) \ 169 | and (index != 0) \ 170 | and ( value != 0 ) \ 171 | and ( np.sign(value) != current_side ) 172 | 173 | if is_crossing_point: 174 | 175 | if len(zero_crossing_index)>0: 176 | 177 | if value < 0: 178 | positive_peaks_index.append(current_peak_index) 179 | 180 | elif value > 0: 181 | negative_peaks_index.append(current_peak_index) 182 | 183 | zero_crossing_index += [index-1, index] 184 | current_side = np.sign(value) 185 | 186 | current_peak = 0 187 | 188 | if np.abs(value) > np.abs(current_peak) : 189 | current_peak = value 190 | current_peak_index = index 191 | 192 | past_value=value 193 | 194 | positive_peaks = sig[np.array(positive_peaks_index)] 195 | negative_peaks = np.abs(sig[np.array(negative_peaks_index)]) 196 | all_peaks = np.abs(sig[np.array(positive_peaks_index + negative_peaks_index)]) 197 | 198 | 199 | zero_crossing = int(len(zero_crossing_index)/2) 200 | 201 | if normalized: 202 | zero_crossing = zero_crossing * (signal.frequency / len(sig)) 203 | 204 | return {'zero_crossing'+'_'+axis : zero_crossing, 205 | 'peak_velocity_pos'+'_'+axis : np.mean(positive_peaks), 206 | 'peak_velocity_neg'+'_'+axis : np.mean(negative_peaks), 207 | 'peak_velocity_all'+'_'+axis : np.mean(all_peaks)} 208 | 209 | 210 | 211 | def swd_peaks(signal, axis=labels.SWAY_DENSITY, sway_density_radius=0.3): 212 | 213 | 214 | if not (axis in [labels.SWAY_DENSITY]): 215 | return {} 216 | 217 | sig = signal.get_signal(axis, **{"sway_density_radius":sway_density_radius}) 218 | 219 | rsig = signal.get_signal(labels.MLAP) 220 | 221 | # crossing_border = np.median(sig) 222 | # 223 | # #to avoid bugs to crossing_border = 0, when individual moves too much 224 | # if crossing_border == 0: 225 | # crossing_border = 0.0001 226 | # 227 | # sig = sig - crossing_border 228 | # 229 | # current_peak = 0 230 | # current_peak_index = 0 231 | # past_value = 0 232 | # zero_crossing_index = [] 233 | # positive_peaks_index = [] 234 | # current_side = np.sign(sig[sig!=0][0]) 235 | # 236 | # for index,value in enumerate(sig) : 237 | # 238 | # is_crossing_point = ( (value)*past_value <= 0 ) and (index != 0)\ 239 | # and ( value != 0 ) and ( np.sign(value) != current_side ) 240 | # 241 | # if is_crossing_point: 242 | # 243 | # if len(zero_crossing_index)>0: 244 | # 245 | # if value < 0: 246 | # 247 | # positive_peaks_index.append(current_peak_index) 248 | # 249 | # zero_crossing_index += [index-1, index] 250 | # current_side = np.sign(value) 251 | # 252 | # current_peak = 0 253 | # 254 | # if value > current_peak : 255 | # current_peak = value 256 | # current_peak_index = index 257 | # 258 | # past_value=value 259 | 260 | positive_peaks_index = np.where((sig[1:-1] > sig[:-2]) & (sig[1:-1] > sig[2:]))[0] + 1 261 | 262 | 263 | positive_peaks = sig[np.array(positive_peaks_index)] #+ crossing_border 264 | 265 | peak_position = np.array([rsig[u] for u in positive_peaks_index]) 266 | 267 | dist = np.diff(peak_position, n=1, axis=0) 268 | dist = np.linalg.norm(dist, axis=1) 269 | 270 | return {'mean_peak'+'_'+axis : np.mean(positive_peaks), 271 | 'mean_distance_peak'+'_'+axis : np.mean(dist)} 272 | 273 | 274 | 275 | def mean_frequency(signal, axis = labels.ML): 276 | if not (axis in [labels.ML, labels.AP, labels.MLAP]): 277 | return {} 278 | feature_name = "mean_frequency" 279 | 280 | sig = signal.get_signal(axis) 281 | 282 | spd = np.linalg.norm(signal.frequency * ( np.diff(sig, n=1, axis=0)), axis=1,keepdims=True) 283 | 284 | if axis==labels.MLAP: 285 | dist = positional.mean_distance(signal, axis = labels.RADIUS, \ 286 | only_value = True) 287 | feature = (1/(2 * np.pi)) * ( np.mean(spd)/dist) 288 | 289 | else: 290 | dist = positional.mean_distance(signal, axis = axis, only_value = True) 291 | feature = (1/(4*np.sqrt(2))) * ( np.mean(spd)/dist) 292 | 293 | return { feature_name+"_"+axis : feature} 294 | 295 | 296 | 297 | all_features = [mean_velocity, sway_area_per_second, phase_plane_parameter, 298 | vfy, length_over_area, fractal_dimension_ce, velocity_peaks, \ 299 | swd_peaks, mean_frequency] 300 | 301 | 302 | to_normalize = [sway_length, fractal_dimension_ce, velocity_peaks, length_over_area] -------------------------------------------------------------------------------- /descriptors/frequentist.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from code_descriptors_postural_control.constants import labels 3 | 4 | 5 | 6 | def total_power(signal, axis = labels.PSD_AP, only_feature=False): 7 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 8 | return {} 9 | feature_name = "total_power" 10 | 11 | fmin = 0.15 12 | fmax = 5 13 | freqs, powers = signal.get_signal(axis) 14 | 15 | selected_powers = powers[((freqs>=fmin) & (freqs<=fmax))] 16 | 17 | feature = np.sum(selected_powers) 18 | 19 | if only_feature: 20 | return feature 21 | else: 22 | return { feature_name+"_"+axis : feature} 23 | 24 | 25 | 26 | def power_frequency_50(signal, axis = labels.PSD_AP): 27 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 28 | return {} 29 | feature_name = "power_frequency_50" 30 | 31 | freqs, powers = signal.get_signal(axis) 32 | 33 | fmin = 0.15 34 | fmax = 5 35 | 36 | selected_freqs = freqs[ ((freqs>=fmin) & (freqs<=fmax)) ] 37 | selected_powers = powers[ ((freqs>=fmin) & (freqs<=fmax)) ] 38 | 39 | cum_power = np.cumsum(selected_powers) 40 | 41 | feature = selected_freqs[ (cum_power >= (cum_power[-1]*0.5)) ][0] 42 | 43 | return { feature_name+"_"+axis : feature} 44 | 45 | 46 | 47 | def power_frequency_95(signal, axis = labels.PSD_AP): 48 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 49 | return {} 50 | feature_name = "power_frequency_95" 51 | 52 | freqs, powers = signal.get_signal(axis) 53 | 54 | fmin = 0.15 55 | fmax = 5 56 | 57 | selected_freqs = freqs[ ((freqs>=fmin) & (freqs<=fmax)) ] 58 | selected_powers = powers[ ((freqs>=fmin) & (freqs<=fmax)) ] 59 | 60 | cum_power = np.cumsum(selected_powers) 61 | 62 | feature = selected_freqs[ (cum_power >= (cum_power[-1]*0.95)) ][0] 63 | 64 | return { feature_name+"_"+axis : feature} 65 | 66 | 67 | 68 | def power_mode(signal, axis = labels.PSD_AP): 69 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 70 | return {} 71 | feature_name = "frequency_mode" 72 | 73 | freqs, powers = signal.get_signal(axis) 74 | 75 | fmin = 0.15 76 | fmax = 5 77 | 78 | selected_freqs = freqs[(freqs>=fmin) & (freqs<=fmax)] 79 | selected_powers = powers[(freqs>=fmin) & (freqs<=fmax)] 80 | 81 | mode = np.argmax(selected_powers) 82 | 83 | feature = selected_freqs[mode] 84 | 85 | return { feature_name+"_"+axis : feature} 86 | 87 | 88 | 89 | def _spectral_moment(signal, axis = labels.PSD_AP, moment=1): 90 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 91 | return {} 92 | 93 | fmin = 0.15 94 | fmax = 5 95 | 96 | freqs, powers = signal.get_signal(axis) 97 | 98 | selected_freqs = freqs[((freqs>=fmin) & (freqs<=fmax))] 99 | 100 | selected_powers = powers[((freqs>=fmin) & (freqs<=fmax))] 101 | 102 | feature = np.sum( (selected_freqs**moment) * selected_powers ) 103 | 104 | return feature 105 | 106 | 107 | 108 | def centroid_frequency(signal, axis = labels.PSD_AP): 109 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 110 | return {} 111 | feature_name = "centroid_frequency" 112 | 113 | m2 = _spectral_moment(signal, axis=axis, moment=2) 114 | m0 = _spectral_moment(signal, axis=axis, moment=0) 115 | 116 | feature = np.sqrt( m2 / m0 ) 117 | 118 | return { feature_name+"_"+axis : feature} 119 | 120 | 121 | 122 | def frequency_dispersion(signal, axis = labels.PSD_AP): 123 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 124 | return {} 125 | feature_name = "frequency_dispersion" 126 | 127 | m2 = _spectral_moment(signal, axis=axis, moment=2) 128 | m1 = _spectral_moment(signal, axis=axis, moment=1) 129 | m0 = _spectral_moment(signal, axis=axis, moment=0) 130 | 131 | feature = np.sqrt( 1 - ( (m1**2) / (m0*m2) ) ) 132 | 133 | return { feature_name+"_"+axis : feature} 134 | 135 | 136 | 137 | def energy_content_05(signal, axis = labels.PSD_AP): 138 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 139 | return {} 140 | feature_name = "energy_content_below_05" 141 | 142 | fmin = 0.15 143 | fmax = 5 144 | 145 | freqs, powers = signal.get_signal(axis) 146 | 147 | selected_powers = powers[ (freqs>0.) & (freqs<=0.5) & (freqs>=fmin) & (freqs<=fmax) ] 148 | 149 | feature = np.sum(selected_powers) 150 | 151 | return { feature_name+"_"+axis : feature} 152 | 153 | 154 | 155 | def energy_content_05_2(signal, axis = labels.PSD_AP): 156 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 157 | return {} 158 | feature_name = "energy_content_05_2" 159 | 160 | fmin = 0.15 161 | fmax = 5 162 | 163 | freqs, powers = signal.get_signal(axis) 164 | 165 | selected_powers = powers[ (freqs>0.5) & (freqs<=2) & (freqs>=fmin) & (freqs<=fmax) ] 166 | 167 | feature = np.sum(selected_powers) 168 | 169 | return { feature_name+"_"+axis : feature} 170 | 171 | 172 | 173 | def energy_content_2(signal, axis = labels.PSD_AP): 174 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 175 | return {} 176 | feature_name = "energy_content_above_2" 177 | 178 | fmin = 0.15 179 | fmax = 5 180 | 181 | freqs, powers = signal.get_signal(axis) 182 | 183 | selected_powers = powers[ (freqs > 2) & (freqs>=fmin) & (freqs<=fmax) ] 184 | 185 | feature = np.sum(selected_powers) 186 | 187 | return { feature_name+"_"+axis : feature} 188 | 189 | 190 | 191 | def frequency_quotient(signal, axis = labels.PSD_AP): 192 | if not (axis in [labels.PSD_ML, labels.PSD_AP]): 193 | return {} 194 | feature_name = "frequency_quotient" 195 | 196 | fmin = 0.15 197 | fmax = 5 198 | 199 | freqs, powers = signal.get_signal(axis) 200 | 201 | selected_powers_up = powers[ (freqs>2) & (freqs<=5) & (freqs>=fmin) & (freqs<=fmax) ] 202 | selected_powers_down = powers[ (freqs>0) & (freqs<=2) & (freqs>=fmin) & (freqs<=fmax) ] 203 | 204 | feature = np.sum(selected_powers_up) / np.sum(selected_powers_down) 205 | 206 | return { feature_name+"_"+axis : feature} 207 | 208 | 209 | 210 | all_features = [total_power, power_frequency_50, power_frequency_95, \ 211 | power_mode, centroid_frequency, frequency_dispersion, \ 212 | energy_content_05, energy_content_05_2, energy_content_2, \ 213 | frequency_quotient] 214 | 215 | 216 | 217 | to_normalize = [] -------------------------------------------------------------------------------- /descriptors/indices_corresp.py: -------------------------------------------------------------------------------- 1 | import pandas 2 | import numpy as np 3 | 4 | dic_groups = {} 5 | 6 | dic_groups["Positional"] = ["mean_distance", "maximal_distance", "rms", "amplitude", 7 | "quotient_both_direction", "planar_deviation", "coefficient_sway_direction", 8 | "confidence_ellipse_area", "principal_sway_direction"] 9 | 10 | 11 | dic_groups["Dynamic"] = ["sway_length", "mean_velocity", "LFS", "sway_area_per_second", "phase_plane_parameter", 12 | "length_over_area", "fractal_dimension", "zero_crossing", "peak_velocity_pos", 13 | "peak_velocity_neg", "peak_velocity_all", "mean_peak", "mean_distance_peak", "mean_frequency", 14 | ] 15 | 16 | dic_groups["Frequentist"] = ["total_power", "power_frequency_50", "power_frequency_95", "frequency_mode", 17 | "centroid_frequency", "frequency_dispersion", "energy_content_below_05", "energy_content_05_2", 18 | "energy_content_above_2", "frequency_quotient"] 19 | 20 | dic_groups["Stochastic"] = ["short_time_diffusion", "long_time_diffusion", 21 | "critical_time", "critical_displacement", "critical_displacement", "short_time_scaling", 22 | "long_time_scaling"] 23 | 24 | 25 | 26 | def get_corresp(df): 27 | 28 | dic_group = {} 29 | 30 | for group in ["Positional","Dynamic","Frequentist","Stochastic"]: 31 | 32 | features_group = [f for f in df.columns if len([u for u in dic_groups[group] \ 33 | if f.replace("_opened_eyes","").replace("_closed_eyes","").replace("_Closed","") \ 34 | .replace("_Open","").replace("_Foam","").replace("_Firm","").replace("_Radius","") \ 35 | .replace("_Power_Spectrum_Density","").replace("_Diffusion","") \ 36 | .replace("_Sway_Density","").replace("_SPD","").replace("_ML_AND_AP","") \ 37 | .replace("_ML","").replace("_AP","").replace("FEATURE_","") == u])>0] 38 | 39 | for feature in features_group: 40 | dic_group[feature] = group 41 | 42 | for feature in df.columns: 43 | if "GENERATIVE_MODEL" in feature: 44 | dic_group[feature] = "Generative" 45 | 46 | dic_axis = {} 47 | 48 | for feature in df.columns: 49 | 50 | if "_ML" in feature and not "_AP" in feature: 51 | dic_axis[feature] = "ML" 52 | elif "_AP" in feature and not "_ML" in feature: 53 | dic_axis[feature] = "AP" 54 | else: 55 | dic_axis[feature] = "ML_AND_AP" 56 | 57 | 58 | for f in df.columns: 59 | 60 | if f not in dic_group: 61 | # print(f, "metadata") 62 | dic_group[f] = "Morphological characteristics" 63 | 64 | if f not in dic_axis: 65 | dic_axis[f] = "Morphological characteristics" 66 | 67 | dic_names = {} 68 | for f in df.columns: 69 | dic_names[f] = f.replace("FEATURE_","").replace("GENERATIVE_MODEL_","").replace("opened_eyes","OE") \ 70 | .replace("closed_eyes","CE").replace("_Power_Spectrum_Density","") \ 71 | .replace("_Diffusion","").replace("_"," ") 72 | 73 | 74 | return {"dic_names":dic_names, 75 | "dic_groups":dic_group, 76 | "dic_axis":dic_axis 77 | } 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /descriptors/positional.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import stats 3 | from sklearn.decomposition import PCA 4 | from code_descriptors_postural_control.constants import labels 5 | 6 | 7 | 8 | def mean_value(signal, axis = labels.ML, only_value = False): 9 | if not (axis in [labels.ML, labels.AP]): 10 | return {} 11 | feature_name = "mean_value" 12 | 13 | if axis==labels.ML: 14 | feature = signal.mean_value[0] 15 | else: 16 | feature = signal.mean_value[1] 17 | 18 | if only_value : 19 | return feature 20 | 21 | return { feature_name+"_"+axis : feature} 22 | 23 | 24 | 25 | def mean_distance(signal, axis = labels.ML ,only_value = False): 26 | if not (axis in [labels.ML, labels.AP, labels.RADIUS]): 27 | return {} 28 | feature_name = "mean_distance" 29 | 30 | sig = signal.get_signal(axis) 31 | 32 | dif = np.abs(sig) 33 | 34 | feature = np.mean(dif) 35 | 36 | if only_value : 37 | return feature 38 | 39 | return { feature_name+"_"+axis : feature} 40 | 41 | 42 | 43 | def maximal_distance(signal, axis = labels.ML): 44 | if not (axis in [labels.ML, labels.AP,labels.RADIUS]): 45 | return {} 46 | feature_name = "maximal_distance" 47 | 48 | sig = signal.get_signal(axis) 49 | feature = np.max(np.abs((sig))) 50 | 51 | return { feature_name+"_"+axis : feature} 52 | 53 | 54 | 55 | def rms(signal, axis = labels.ML, only_value = False): 56 | if not (axis in [labels.ML, labels.AP, labels.RADIUS]): 57 | return {} 58 | feature_name = "rms" 59 | 60 | sig = signal.get_signal(axis) 61 | 62 | feature = np.sqrt(np.mean(sig**2)) 63 | 64 | if only_value : 65 | return feature 66 | 67 | return { feature_name+"_"+axis : feature} 68 | 69 | 70 | 71 | def amplitude(signal, axis = labels.ML,only_value = False): 72 | if not (axis in [labels.ML, labels.AP, labels.MLAP]): 73 | return {} 74 | feature_name = "range" 75 | 76 | sig = signal.get_signal(axis) 77 | 78 | r = 0 79 | for i in range(len(sig)): 80 | d = sig - sig[i] 81 | dist= 0 82 | 83 | if len(sig.shape)==1: 84 | dist = np.abs(d) 85 | elif len(sig.shape)>1: 86 | dist = np.linalg.norm(d, axis=1) 87 | 88 | r = max(r, np.max(dist)) 89 | 90 | feature = r 91 | 92 | if only_value: 93 | return feature 94 | return { feature_name+"_"+axis : feature} 95 | 96 | 97 | 98 | def quotient_both_direction(signal, axis = labels.MLAP): 99 | if not (axis in [labels.MLAP]): 100 | return {} 101 | feature_name = "range_ratio" 102 | 103 | amplitude_ml = amplitude(signal,axis=labels.ML, only_value=True) 104 | amplitude_ap = amplitude(signal,axis=labels.AP, only_value=True) 105 | 106 | feature = amplitude_ml/amplitude_ap 107 | 108 | return { feature_name+"_"+axis : feature} 109 | 110 | 111 | 112 | def planar_deviation(signal, axis = labels.MLAP): 113 | if not (axis in [labels.MLAP]): 114 | return {} 115 | feature_name = "planar_deviation" 116 | 117 | s_ml = rms(signal, axis=labels.ML, only_value=True) 118 | s_ap = rms(signal, axis=labels.AP, only_value=True) 119 | 120 | feature = np.sqrt(s_ml**2 + s_ap**2) 121 | 122 | return { feature_name+"_"+axis : feature} 123 | 124 | 125 | 126 | def coeff_sway_direction(signal, axis = labels.MLAP): 127 | if not (axis in [labels.MLAP]): 128 | return {} 129 | feature_name = "coefficient_sway_direction" 130 | 131 | sig = signal.get_signal(axis) 132 | 133 | cov = (1/len(sig)*np.sum(sig[:,0]*sig[:,1])) 134 | s_ml = rms(signal, axis=labels.ML, only_value=True) 135 | s_ap = rms(signal, axis=labels.AP, only_value=True) 136 | 137 | 138 | feature = cov / (s_ml * s_ap) 139 | 140 | return { feature_name+"_"+axis : feature} 141 | 142 | 143 | 144 | 145 | def confidence_ellipse_area(signal, axis = labels.MLAP, only_value = False): 146 | if not (axis in [labels.MLAP]): 147 | return {} 148 | feature_name = "confidence_ellipse_area" 149 | 150 | sig = signal.get_signal(axis) 151 | 152 | cov = (1/len(sig))*np.sum(sig[:,0]*sig[:,1]) 153 | 154 | s_ml = rms(signal, axis=labels.ML, only_value=True) 155 | s_ap = rms(signal, axis=labels.AP, only_value=True) 156 | 157 | confidence = 0.95 158 | 159 | quant = stats.f.ppf(confidence, 2, len(sig)-2) 160 | 161 | n = len(sig) 162 | coeff = ((n+1)*(n-1)) / (n*(n-2)) 163 | 164 | det = (s_ml**2)*(s_ap**2) - cov**2 165 | feature = 2 * np.pi * quant * np.sqrt(det) * coeff 166 | 167 | if only_value: 168 | return feature 169 | 170 | return { feature_name+"_"+axis : feature} 171 | 172 | 173 | 174 | 175 | def principal_sway_direction(signal, axis = labels.MLAP): 176 | if not (axis in [labels.MLAP]): 177 | return {} 178 | feature_name = "principal_sway_direction" 179 | 180 | sig = signal.get_signal(axis) 181 | 182 | pca = PCA(n_components= 2) 183 | pca.fit(sig) 184 | main_direction = pca.components_[0] 185 | 186 | angle_rad = np.arccos(np.abs(main_direction[1])/np.linalg.norm(main_direction)) 187 | 188 | feature = angle_rad*(180/np.pi) 189 | 190 | return { feature_name+"_"+axis : feature} 191 | 192 | 193 | 194 | all_features = [mean_value, mean_distance, maximal_distance, rms, amplitude, \ 195 | quotient_both_direction, planar_deviation, \ 196 | coeff_sway_direction, confidence_ellipse_area, \ 197 | principal_sway_direction] 198 | 199 | 200 | to_normalize = [] -------------------------------------------------------------------------------- /descriptors/stochastic.py: -------------------------------------------------------------------------------- 1 | 2 | import statsmodels.api as sm 3 | import numpy as np 4 | from code_descriptors_postural_control.constants import labels 5 | 6 | 7 | 8 | def SDA(signal, axis=labels.DIFF_ML): 9 | 10 | if not (axis in [labels.DIFF_ML, labels.DIFF_AP]): 11 | return {} 12 | 13 | time, msd = signal.get_signal(axis) 14 | frequency = signal.frequency 15 | 16 | log_time = np.log(time[1:]) 17 | log_msd = np.log(msd[1:]) 18 | 19 | ind_start = int(0.3*frequency) + 1 20 | ind_stop = int(2.5*frequency) 21 | 22 | best_rmse = np.inf 23 | best_ind = None 24 | best_params = None 25 | 26 | for i in range(ind_start, ind_stop+1): 27 | 28 | Y_s = log_msd[:i] 29 | X_s = sm.add_constant(log_time[:i]) 30 | 31 | model_s = sm.OLS(Y_s,X_s) 32 | result_s = model_s.fit() 33 | 34 | rmse = np.sqrt( np.mean((result_s.resid)**2) ) 35 | 36 | if rmse <= best_rmse: 37 | best_rmse = rmse 38 | best_params = result_s.params 39 | best_ind = i 40 | 41 | ind_end_first_region = best_ind 42 | params_log_s = best_params 43 | 44 | best_rmse = np.inf 45 | best_ind = None 46 | best_params = None 47 | 48 | for i in range(ind_start, ind_stop+1): 49 | 50 | Y_l = log_msd[i-1:] 51 | X_l = sm.add_constant(log_time[i-1:]) 52 | 53 | model_l = sm.OLS(Y_l,X_l) 54 | result_l = model_l.fit() 55 | 56 | rmse = np.sqrt( np.mean((result_l.resid)**2) ) 57 | 58 | if rmse <= best_rmse: 59 | best_rmse = rmse 60 | best_params = result_l.params 61 | best_ind = i 62 | 63 | 64 | ind_begin_second_region = best_ind 65 | params_log_l = best_params 66 | 67 | log_critical_time = (params_log_l[0] - params_log_s[0]) / (params_log_s[1] - params_log_l[1]) 68 | 69 | 70 | if log_critical_time > log_time[-1]: 71 | log_critical_time = log_time[-1] 72 | 73 | critical_time = np.exp(log_critical_time) 74 | 75 | 76 | 77 | log_critical_displacement = params_log_s[0] + params_log_s[1] * log_critical_time 78 | critical_displacement = np.exp(log_critical_displacement) 79 | 80 | 81 | 82 | short_time_diffusion = np.exp(params_log_s[0]) 83 | long_time_diffusion = np.exp(params_log_l[0]) 84 | short_time_scaling = params_log_s[1]/2 85 | long_time_scaling = params_log_l[1]/2 86 | 87 | # 88 | return {'short_time_diffusion'+'_'+axis : short_time_diffusion, 89 | 'long_time_diffusion'+'_'+axis : long_time_diffusion, 90 | 'critical_time'+'_'+axis : critical_time, 91 | 'critical_displacement'+'_'+axis : critical_displacement, 92 | 'short_time_scaling'+'_'+axis: short_time_scaling, 93 | 'long_time_scaling'+'_'+axis : long_time_scaling} 94 | 95 | all_features = [SDA] 96 | 97 | 98 | to_normalize = [] 99 | -------------------------------------------------------------------------------- /main.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import pandas as pd\n", 11 | "import os\n", 12 | "import matplotlib.pyplot as plt\n", 13 | "\n", 14 | "from stabilogram.stato import Stabilogram\n", 15 | "from descriptors import compute_all_features\n", 16 | "\n" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "forceplate_file_selected = \"test.csv\"" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 3, 31 | "metadata": {}, 32 | "outputs": [ 33 | { 34 | "data": { 35 | "text/html": [ 36 | "
\n", 37 | "\n", 50 | "\n", 51 | " \n", 52 | " \n", 53 | " \n", 54 | " \n", 55 | " \n", 56 | " \n", 57 | " \n", 58 | " \n", 59 | " \n", 60 | " \n", 61 | " \n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | "
MocapTimeDeviceFrameFxFyFzMxMyMzCxCyCz
MocapFrame
00.0001.622131-8.233887581.4375-37.412109-12.1845703.4602050.020956-0.0643440.0
10.0110.0000000.0000000.00000.0000000.0000000.0000000.0000000.0000000.0
20.0220.0000000.0000000.00000.0000000.0000000.0000000.0000000.0000000.0
30.0331.216919-7.369629582.3125-38.582031-12.5971683.2469480.021633-0.0662570.0
40.0440.894653-7.661621582.3125-38.800781-12.5073243.1556400.021479-0.0666320.0
\n", 154 | "
" 155 | ], 156 | "text/plain": [ 157 | " MocapTime DeviceFrame Fx Fy Fz Mx \\\n", 158 | "MocapFrame \n", 159 | "0 0.00 0 1.622131 -8.233887 581.4375 -37.412109 \n", 160 | "1 0.01 1 0.000000 0.000000 0.0000 0.000000 \n", 161 | "2 0.02 2 0.000000 0.000000 0.0000 0.000000 \n", 162 | "3 0.03 3 1.216919 -7.369629 582.3125 -38.582031 \n", 163 | "4 0.04 4 0.894653 -7.661621 582.3125 -38.800781 \n", 164 | "\n", 165 | " My Mz Cx Cy Cz \n", 166 | "MocapFrame \n", 167 | "0 -12.184570 3.460205 0.020956 -0.064344 0.0 \n", 168 | "1 0.000000 0.000000 0.000000 0.000000 0.0 \n", 169 | "2 0.000000 0.000000 0.000000 0.000000 0.0 \n", 170 | "3 -12.597168 3.246948 0.021633 -0.066257 0.0 \n", 171 | "4 -12.507324 3.155640 0.021479 -0.066632 0.0 " 172 | ] 173 | }, 174 | "execution_count": 3, 175 | "metadata": {}, 176 | "output_type": "execute_result" 177 | } 178 | ], 179 | "source": [ 180 | "data_forceplatform = pd.read_csv(forceplate_file_selected,header=[31],sep=\",\",index_col=0)\n", 181 | "data_forceplatform.head()" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 4, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "dft = data_forceplatform\n", 191 | "X = dft.get(\" My\")/dft.get(\" Fz\")\n", 192 | "Y = dft.get(' Mx')/ dft.get(' Fz')\n", 193 | "X = X - np.mean(X)\n", 194 | "Y = Y - np.mean(Y)\n", 195 | "X = 100*X\n", 196 | "Y = 100*Y\n", 197 | "\n", 198 | "X = X.to_numpy()[4000:7000]\n", 199 | "Y= Y.to_numpy()[4000:7000]" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 5, 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "data": { 209 | "text/plain": [ 210 | "[]" 211 | ] 212 | }, 213 | "execution_count": 5, 214 | "metadata": {}, 215 | "output_type": "execute_result" 216 | }, 217 | { 218 | "data": { 219 | "image/png": "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\n", 220 | "text/plain": [ 221 | "
" 222 | ] 223 | }, 224 | "metadata": { 225 | "needs_background": "light" 226 | }, 227 | "output_type": "display_data" 228 | } 229 | ], 230 | "source": [ 231 | "plt.plot(X)\n", 232 | "plt.plot(Y)" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": 6, 238 | "metadata": {}, 239 | "outputs": [], 240 | "source": [ 241 | "data = np.array([X,Y]).T\n" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": 7, 247 | "metadata": {}, 248 | "outputs": [ 249 | { 250 | "name": "stdout", 251 | "output_type": "stream", 252 | "text": [ 253 | "3000 3000\n" 254 | ] 255 | } 256 | ], 257 | "source": [ 258 | "valid_index = (np.sum(np.isnan(data),axis=1) == 0)\n", 259 | "print(np.sum(valid_index),len(data))\n" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": 8, 265 | "metadata": {}, 266 | "outputs": [], 267 | "source": [ 268 | "stato = Stabilogram()\n", 269 | "stato.from_array(array=data, original_frequency=100)" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 9, 275 | "metadata": {}, 276 | "outputs": [ 277 | { 278 | "data": { 279 | "text/plain": [ 280 | "[]" 281 | ] 282 | }, 283 | "execution_count": 9, 284 | "metadata": {}, 285 | "output_type": "execute_result" 286 | }, 287 | { 288 | "data": { 289 | "image/png": "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\n", 290 | "text/plain": [ 291 | "
" 292 | ] 293 | }, 294 | "metadata": { 295 | "needs_background": "light" 296 | }, 297 | "output_type": "display_data" 298 | } 299 | ], 300 | "source": [ 301 | "plt.plot(stato.medio_lateral)\n", 302 | "plt.plot(stato.antero_posterior)" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": 11, 308 | "metadata": {}, 309 | "outputs": [], 310 | "source": [ 311 | "sway_density_radius = 0.3 # 3 mm\n", 312 | "\n", 313 | "params_dic = {\"sway_density_radius\": sway_density_radius}\n", 314 | "\n", 315 | "features = compute_all_features(stato, params_dic=params_dic)\n" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 12, 321 | "metadata": {}, 322 | "outputs": [ 323 | { 324 | "data": { 325 | "text/plain": [ 326 | "{'mean_value_ML': 0.37408211618441994,\n", 327 | " 'mean_value_AP': -0.23424815039801114,\n", 328 | " 'mean_distance_ML': 0.1795810955311253,\n", 329 | " 'mean_distance_AP': 0.34804323313894586,\n", 330 | " 'mean_distance_Radius': 0.4254325412234754,\n", 331 | " 'maximal_distance_ML': 1.0934116824820324,\n", 332 | " 'maximal_distance_AP': 1.0760854903981556,\n", 333 | " 'maximal_distance_Radius': 1.1715638528570649,\n", 334 | " 'rms_ML': 0.26540984939429485,\n", 335 | " 'rms_AP': 0.41648441647296885,\n", 336 | " 'rms_Radius': 0.4938640069091203,\n", 337 | " 'range_ML': 1.6807988285326032,\n", 338 | " 'range_AP': 2.1256765044334616,\n", 339 | " 'range_ML_AND_AP': 2.127255063910078,\n", 340 | " 'range_ratio_ML_AND_AP': 0.7907124273270227,\n", 341 | " 'planar_deviation_ML_AND_AP': 0.4938640069091203,\n", 342 | " 'coefficient_sway_direction_ML_AND_AP': -0.23294898489296692,\n", 343 | " 'confidence_ellipse_area_ML_AND_AP': 2.034277923412154,\n", 344 | " 'principal_sway_direction_ML_AND_AP': 13.280808808239561,\n", 345 | " 'mean_velocity_ML': 0.3581913643625223,\n", 346 | " 'mean_velocity_AP': 0.5203914302706945,\n", 347 | " 'mean_velocity_ML_AND_AP': 0.6953667708089489,\n", 348 | " 'sway_area_per_second_ML_AND_AP': 0.09587003176410186,\n", 349 | " 'phase_plane_parameter_ML': 0.5737780683328979,\n", 350 | " 'phase_plane_parameter_AP': 0.8232743925680995,\n", 351 | " 'LFS_ML_AND_AP': 10.22739987646638,\n", 352 | " 'fractal_dimension_ML_AND_AP': 1.6306876842986446,\n", 353 | " 'zero_crossing_SPD_ML': 96,\n", 354 | " 'peak_velocity_pos_SPD_ML': 0.4436348825935376,\n", 355 | " 'peak_velocity_neg_SPD_ML': 0.4419487741618079,\n", 356 | " 'peak_velocity_all_SPD_ML': 0.4428007026325764,\n", 357 | " 'zero_crossing_SPD_AP': 119,\n", 358 | " 'peak_velocity_pos_SPD_AP': 0.5450626275866571,\n", 359 | " 'peak_velocity_neg_SPD_AP': 0.5607541773355091,\n", 360 | " 'peak_velocity_all_SPD_AP': 0.5529084024610833,\n", 361 | " 'mean_peak_Sway_Density': 2.1265552009131103,\n", 362 | " 'mean_distance_peak_Sway_Density': 0.29705469931389783,\n", 363 | " 'mean_frequency_ML': 0.353069743030671,\n", 364 | " 'mean_frequency_AP': 0.2646689220465907,\n", 365 | " 'mean_frequency_ML_AND_AP': 0.26048598103306375,\n", 366 | " 'total_power_Power_Spectrum_Density_ML': 1.9701029214613246,\n", 367 | " 'total_power_Power_Spectrum_Density_AP': 2.0989758261604448,\n", 368 | " 'power_frequency_50_Power_Spectrum_Density_ML': 0.267379679144385,\n", 369 | " 'power_frequency_50_Power_Spectrum_Density_AP': 0.267379679144385,\n", 370 | " 'power_frequency_95_Power_Spectrum_Density_ML': 0.4679144385026737,\n", 371 | " 'power_frequency_95_Power_Spectrum_Density_AP': 0.8689839572192513,\n", 372 | " 'frequency_mode_Power_Spectrum_Density_ML': 0.16711229946524062,\n", 373 | " 'frequency_mode_Power_Spectrum_Density_AP': 0.16711229946524062,\n", 374 | " 'centroid_frequency_Power_Spectrum_Density_ML': 0.3729753079987071,\n", 375 | " 'centroid_frequency_Power_Spectrum_Density_AP': 0.45534971736668983,\n", 376 | " 'frequency_dispersion_Power_Spectrum_Density_ML': 0.5838227123730866,\n", 377 | " 'frequency_dispersion_Power_Spectrum_Density_AP': 0.6227456505967819,\n", 378 | " 'energy_content_below_05_Power_Spectrum_Density_ML': 1.8752359018791513,\n", 379 | " 'energy_content_below_05_Power_Spectrum_Density_AP': 1.7820102956900112,\n", 380 | " 'energy_content_05_2_Power_Spectrum_Density_ML': 0.08965619802324465,\n", 381 | " 'energy_content_05_2_Power_Spectrum_Density_AP': 0.3113113942067609,\n", 382 | " 'energy_content_above_2_Power_Spectrum_Density_ML': 0.0052108215589284105,\n", 383 | " 'energy_content_above_2_Power_Spectrum_Density_AP': 0.005654136263672136,\n", 384 | " 'frequency_quotient_Power_Spectrum_Density_ML': 0.002651963209169222,\n", 385 | " 'frequency_quotient_Power_Spectrum_Density_AP': 0.002701035531691719,\n", 386 | " 'short_time_diffusion_Diffusion_ML': 0.15090846958680665,\n", 387 | " 'long_time_diffusion_Diffusion_ML': 0.17848366042769717,\n", 388 | " 'critical_time_Diffusion_ML': 1.0948561247013364,\n", 389 | " 'critical_displacement_Diffusion_ML': 0.17789552569946945,\n", 390 | " 'short_time_scaling_Diffusion_ML': 0.9077332572699823,\n", 391 | " 'long_time_scaling_Diffusion_ML': -0.018210699820200377,\n", 392 | " 'short_time_diffusion_Diffusion_AP': 0.296984629846704,\n", 393 | " 'long_time_diffusion_Diffusion_AP': 0.8407079713632779,\n", 394 | " 'critical_time_Diffusion_AP': 1.5307497820151665,\n", 395 | " 'critical_displacement_Diffusion_AP': 0.6432588727723381,\n", 396 | " 'short_time_scaling_Diffusion_AP': 0.9076370373880287,\n", 397 | " 'long_time_scaling_Diffusion_AP': -0.31437732622853565}" 398 | ] 399 | }, 400 | "execution_count": 12, 401 | "metadata": {}, 402 | "output_type": "execute_result" 403 | } 404 | ], 405 | "source": [ 406 | "features" 407 | ] 408 | } 409 | ], 410 | "metadata": { 411 | "kernelspec": { 412 | "display_name": "Python 3 (ipykernel)", 413 | "language": "python", 414 | "name": "python3" 415 | }, 416 | "language_info": { 417 | "codemirror_mode": { 418 | "name": "ipython", 419 | "version": 3 420 | }, 421 | "file_extension": ".py", 422 | "mimetype": "text/x-python", 423 | "name": "python", 424 | "nbconvert_exporter": "python", 425 | "pygments_lexer": "ipython3", 426 | "version": "3.8.10" 427 | } 428 | }, 429 | "nbformat": 4, 430 | "nbformat_minor": 4 431 | } 432 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # In[1]: 5 | 6 | 7 | import numpy as np 8 | import pandas as pd 9 | import os 10 | import matplotlib.pyplot as plt 11 | 12 | from stabilogram.stato import Stabilogram 13 | from descriptors import compute_all_features 14 | 15 | 16 | # In[2]: 17 | 18 | 19 | forceplate_file_selected = "test.csv" 20 | 21 | 22 | # In[3]: 23 | 24 | 25 | data_forceplatform = pd.read_csv(forceplate_file_selected,header=[31],sep=",",index_col=0) 26 | data_forceplatform.head() 27 | 28 | 29 | # In[4]: 30 | 31 | 32 | dft = data_forceplatform 33 | X = dft.get(" My")/dft.get(" Fz") 34 | Y = dft.get(' Mx')/ dft.get(' Fz') 35 | X = X - np.mean(X) 36 | Y = Y - np.mean(Y) 37 | X = 100*X 38 | Y = 100*Y 39 | 40 | X = X.to_numpy()[4000:7000] 41 | Y= Y.to_numpy()[4000:7000] 42 | 43 | 44 | # In[5]: 45 | 46 | fig, ax = plt.subplots(1) 47 | ax.plot(X) 48 | ax.plot(Y) 49 | 50 | 51 | # In[6]: 52 | 53 | 54 | data = np.array([X,Y]).T 55 | 56 | 57 | # In[7]: 58 | 59 | # Verif if NaN data 60 | valid_index = (np.sum(np.isnan(data),axis=1) == 0) 61 | 62 | if np.sum(valid_index) != len(data): 63 | raise ValueError("Clean NaN values first") 64 | 65 | 66 | # In[8]: 67 | 68 | 69 | stato = Stabilogram() 70 | stato.from_array(array=data, original_frequency=100) 71 | 72 | 73 | # In[9]: 74 | 75 | 76 | fig, ax = plt.subplots(1) 77 | ax.plot(stato.medio_lateral) 78 | ax.plot(stato.antero_posterior) 79 | 80 | 81 | # In[10]: 82 | 83 | sway_density_radius = 0.3 # 3 mm 84 | 85 | params_dic = {"sway_density_radius": sway_density_radius} 86 | 87 | features = compute_all_features(stato, params_dic=params_dic) 88 | 89 | 90 | # In[11]: 91 | 92 | 93 | print(features) 94 | 95 | -------------------------------------------------------------------------------- /stabilogram/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jythen/code_descriptors_postural_control/c66a0e4759708c4a4e63c28850d9b5243b197aa2/stabilogram/__init__.py -------------------------------------------------------------------------------- /stabilogram/stato.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | import numpy as np 6 | from numpy.core.defchararray import upper 7 | from scipy.signal import butter, filtfilt, periodogram, savgol_filter, welch 8 | 9 | from code_descriptors_postural_control.stabilogram.swarii import SWARII 10 | 11 | from scipy.fft import rfft, rfftfreq 12 | 13 | from code_descriptors_postural_control.constants import labels 14 | 15 | class Stabilogram(): 16 | def __init__(self): 17 | 18 | 19 | self.raw_signal = None # contains the raw signal 20 | self.signal = None # contain the processed signal 21 | self.frequency = None # frequency of the signal. Only if uniformly sampled 22 | 23 | self._sampling_ok = None # is the signal uniformly sampled ? 24 | 25 | 26 | # Store the values of signal transformation, to avoid multiple computations 27 | self._radius = None 28 | self._power_spectrum = None 29 | self._sway_density = None 30 | self._diffusion_plot = None 31 | self._speed = None 32 | 33 | 34 | 35 | def from_array(self, array, center = True, original_frequency = None, time = None, resample = True, resample_frequency = 25, filter_ = True, filter_lower_bound=0, filter_upper_bound=10, filter_order = 4 ): 36 | """ 37 | Import an array as a stabilogram. 38 | 39 | array should be a N x d ndarray. N is the number of sample, d is the dimension (d=2 or 3) 40 | d = 2 : The columns are ML (cm) and AP (cm). the signal is supposed to be already uniformly sampled. original_frequency should be provided. 41 | d = 3 : The columns are Time (s), ML (cm) and AP (cm). the signal can be non uniformly sampled. 42 | 43 | center : center the signal. Necessary for the correct computation of the features 44 | 45 | resample : resample the signal to the values defined in the paper. See the function resample for more details 46 | filter_ : resample the signal to the values defined in the paper. See the function filter_ for more details 47 | 48 | """ 49 | 50 | signal = np.array(array) 51 | 52 | self.raw_signal = signal 53 | 54 | n_columns = signal.shape[1] 55 | 56 | assert n_columns in [2,3], "invalid number of columns in the array, should be 2 or 3" 57 | 58 | 59 | 60 | 61 | 62 | 63 | if n_columns == 2 : 64 | assert original_frequency is not None or time is not None, "Need to provide a frequency for the signal (parameter original frequency), or timestamps" 65 | 66 | if original_frequency is not None: 67 | 68 | time = np.arange(len(signal))/original_frequency 69 | time = time[:,None] 70 | 71 | valid_index = (np.sum(np.isnan(signal),axis=1) == 0) 72 | time = time[valid_index] 73 | signal = signal[valid_index] 74 | 75 | mean = np.mean(signal, axis=0, keepdims=True) 76 | self.mean_value = mean[0] 77 | 78 | if center : 79 | signal = signal - mean 80 | 81 | 82 | signal = np.concatenate([time, signal], axis = 1) 83 | 84 | else : 85 | # time start from 0 86 | time = signal[:,0] 87 | time = time - time[0] 88 | time = time[:,None] 89 | signal[:,0] = time 90 | 91 | mean = np.mean(signal[:,1:], axis=0, keepdims=True) 92 | self.mean_value = mean 93 | 94 | #center signal 95 | if center : 96 | csignal = signal[:,1:] 97 | csignal = csignal - np.mean(csignal, axis=0, keepdims=True) 98 | signal[:,1:] = csignal 99 | 100 | 101 | self.signal = signal 102 | assert not np.isnan(signal).any(), "error, NaN values" 103 | if resample : 104 | self.resample(target_frequency=resample_frequency) 105 | else : 106 | assert original_frequency is not None, "Need to provide a frequency for the signal (parameter original frequency) when resample is set to False" 107 | self._sampling_ok = True 108 | self.frequency = original_frequency 109 | self.signal = self.signal[:,1:] 110 | 111 | if filter_ : 112 | self.filter_(lower_bound=filter_lower_bound, upper_bound=filter_upper_bound, order= filter_order) 113 | 114 | 115 | 116 | def resample(self, target_frequency=25)-> None: 117 | 118 | """ 119 | Resample the stabilogram using SWARII, using the parameters recommended in the paper 120 | 121 | """ 122 | 123 | assert self.signal is not None, "Please provide a signal first" 124 | 125 | signal = np.array(self.signal) 126 | n_columns = signal.shape[1] 127 | 128 | assert n_columns in [2,3], "invalid number of columns in the array, should be 2 or 3" 129 | 130 | if n_columns == 3 : 131 | signal = SWARII.resample(data = signal, desired_frequency=target_frequency) 132 | 133 | self.signal = signal 134 | self._sampling_ok = True 135 | self.frequency = target_frequency 136 | 137 | 138 | def filter_(self, lower_bound=0, upper_bound=10, order = 4) -> None: 139 | """ 140 | Filter the stabilogram using a Butterworth filter. Default parameters are the one used in the paper. 141 | """ 142 | 143 | 144 | assert self.raw_signal is not None, "Please provide a signal first" 145 | assert self._sampling_ok, "Please resample the signal first, using the function resample " 146 | assert self.signal is not None, "Error, please resample the signal again" 147 | 148 | 149 | 150 | 151 | signal = np.array(self.signal) 152 | nyq = 0.5 * self.frequency 153 | low = lower_bound / nyq 154 | high = upper_bound / nyq 155 | 156 | if low == 0 : 157 | b, a = butter(order, high, btype='lowpass') 158 | elif high == np.inf : 159 | b, a = butter(order, low, btype='highpass') 160 | else : 161 | b, a = butter(order, (low,high), btype='bandpass') 162 | 163 | y = filtfilt(b, a, signal,axis=0) 164 | self.signal = y 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | # =================================================================== 177 | # Methods to compute the transformations of the signals. 178 | # Should not be called directly, and instead accessed through properties 179 | # =================================================================== 180 | 181 | 182 | 183 | 184 | 185 | def _compute_radius(self)-> None: 186 | """ 187 | Compute the radius of the stabilogram (signal is supposed centered). 188 | """ 189 | self._radius = np.linalg.norm(self.signal, axis=1, keepdims=True) 190 | 191 | 192 | 193 | def _compute_power_spectrum(self)-> None: 194 | """ 195 | Compute the PSD of the stabilogram using the Welch method. 196 | """ 197 | 198 | freqs, psd = welch(self.signal, fs=self.frequency, \ 199 | detrend="linear", nperseg=10*self.frequency, \ 200 | noverlap=0.5*10*self.frequency, axis=0, \ 201 | nfft=len(self.signal)) 202 | 203 | power_fft = np.concatenate( [freqs[:,None], psd], axis=1) 204 | self._power_spectrum = power_fft 205 | 206 | 207 | 208 | def _compute_sway_density(self, radius=0.3)-> None: 209 | 210 | """ 211 | Sway Density is computed by default for a 3 mm radius. 212 | """ 213 | signal = np.array(self.signal) 214 | sway = np.zeros(len(signal)-1) 215 | 216 | for t in range(len(signal)-1): 217 | 218 | 219 | stopping_point = t+1 220 | while stopping_pointradius: 222 | break 223 | stopping_point+=1 224 | 225 | starting_point = t-1 226 | while starting_point>=0: 227 | if np.linalg.norm(signal[starting_point] - signal[t])>radius: 228 | break 229 | starting_point-=1 230 | 231 | start = starting_point+1 232 | stop = stopping_point-1 233 | 234 | sway[t] = stop-start 235 | 236 | 237 | sway = sway / self.frequency 238 | 239 | nyq = 0.5 * self.frequency 240 | 241 | high = 2.5 / nyq 242 | 243 | b, a = butter(N=4, Wn=high, btype='lowpass') 244 | 245 | sway = filtfilt(b, a, sway, axis=0) 246 | 247 | self._sway_density = sway 248 | 249 | 250 | 251 | def _compute_diffusion_plot(self, duration_ratio=1/3)-> None: 252 | """ 253 | Compute the diffusion plot of the stabilogram. duration_ratio parameter set the limit for the computation, and should only be modified by experts familiar with the diffusion plot 254 | """ 255 | 256 | n = len(self.signal) 257 | max_ind = int(n * duration_ratio) 258 | time = np.arange(n)/self.frequency 259 | msd = [np.array([0,0])] + [np.mean((self.signal[i:,:] - self.signal[:(n-i),:])**2,axis=0) for i in range(1,max_ind+1)] 260 | diffusion_plot = np.concatenate([time[:max_ind+1,None], np.array(msd)], axis=1) 261 | self._diffusion_plot = diffusion_plot 262 | 263 | 264 | def _compute_speed(self, window_length=5, polyorder=3) -> None: 265 | """ 266 | Speed is computed using savgol filter. Default parameters are the one used in the paper. 267 | """ 268 | cop = self.signal 269 | spd_savgol = savgol_filter( x = cop, window_length=window_length, polyorder=polyorder, deriv= 1, axis=0, delta=1/self.frequency ) 270 | self._speed = spd_savgol 271 | 272 | 273 | def _test_correct_format(self) -> None: 274 | assert self.raw_signal is not None, "Please provide a signal first" 275 | assert self._sampling_ok, "Please resample the signal first, using the function resample " 276 | assert self.signal is not None, "Error, please resample and filter the signal again" 277 | 278 | 279 | 280 | # =================================================================== 281 | # Defines the properties to access the transformations of the signal 282 | # =================================================================== 283 | 284 | def __len__(self)-> int: 285 | self._test_correct_format 286 | return(len(self.signal)) 287 | 288 | 289 | @property 290 | def medio_lateral(self) -> np.ndarray: 291 | self._test_correct_format() 292 | return self.signal[:,0:1] 293 | 294 | @property 295 | def antero_posterior(self) -> np.ndarray: 296 | self._test_correct_format() 297 | return self.signal[:,1:2] 298 | 299 | @property 300 | def sway_density(self) -> np.ndarray: 301 | self._test_correct_format() 302 | if self._sway_density is None: 303 | self._compute_sway_density(self.sway_density_radius) 304 | return self._sway_density 305 | 306 | 307 | @property 308 | def speed(self) -> np.ndarray: 309 | self._test_correct_format() 310 | if self._speed is None: 311 | self._compute_speed() 312 | return self._speed 313 | 314 | 315 | @property 316 | def power_spectrum(self) -> np.ndarray: 317 | self._test_correct_format() 318 | if self._power_spectrum is None: 319 | self._compute_power_spectrum() 320 | return self._power_spectrum 321 | 322 | 323 | @property 324 | def radius(self) -> np.ndarray: 325 | self._test_correct_format() 326 | if self._radius is None: 327 | self._compute_radius() 328 | return self._radius 329 | 330 | @property 331 | def diffusion_plot(self) -> np.ndarray: 332 | self._test_correct_format() 333 | if self._diffusion_plot is None: 334 | self._compute_diffusion_plot() 335 | return self._diffusion_plot 336 | 337 | 338 | 339 | def get_signal(self, name, **kwargs) -> np.ndarray: 340 | 341 | 342 | 343 | if name == labels.ML: 344 | return self.medio_lateral 345 | if name == labels.AP : 346 | return self.antero_posterior 347 | if name == labels.MLAP : 348 | return self.signal 349 | if name == labels.RADIUS : 350 | return self.radius 351 | if name == labels.SWAY_DENSITY : 352 | self.sway_density_radius = kwargs["sway_density_radius"] 353 | return self.sway_density 354 | if name == labels.PSD_ML : 355 | return self.power_spectrum[:,0], self.power_spectrum[:,1] 356 | if name == labels.PSD_AP : 357 | return self.power_spectrum[:,0], self.power_spectrum[:,2] 358 | if name == labels.SPD_ML: 359 | return self.speed[:,0:1] 360 | if name == labels.SPD_AP: 361 | return self.speed[:,1:2] 362 | if name == labels.SPD_MLAP: 363 | return np.linalg.norm(self.speed,axis=1) 364 | if name == labels.DIFF_ML: 365 | return self.diffusion_plot[:,0], self.diffusion_plot[:,1] 366 | if name == labels.DIFF_AP: 367 | return self.diffusion_plot[:,0], self.diffusion_plot[:,2] 368 | if name == labels.DIFF_MLAP: 369 | return self.diffusion_plot[:,0], self.diffusion_plot[:,1]+self.diffusion_plot[:,2] # is it a sum really ? 370 | raise NotImplementedError 371 | 372 | 373 | 374 | 375 | 376 | -------------------------------------------------------------------------------- /stabilogram/swarii.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Fri Apr 15 10:25:45 2016 4 | 5 | @author: audiffren 6 | """ 7 | 8 | import numpy as np 9 | from scipy.interpolate import interp1d 10 | #´from parsers import parse_wbb_acq_data 11 | 12 | 13 | 14 | 15 | 16 | class Local_SWARII: 17 | """ 18 | Implementation of the Sliding Windows Weighted Averaged Interpolation method 19 | 20 | How To use : 21 | First instantiate the class with the desired parameters 22 | Then call resample on the desired signal 23 | 24 | """ 25 | 26 | def __init__(self, window_size=1, desired_frequency=25, verbose=0,**kwargs): 27 | """ 28 | Instantiate SWARII 29 | 30 | Parameters : 31 | desired_frequency : The frequency desired for the output signal, 32 | after the resampling. 33 | window_size : The size of the sliding window, in seconds. 34 | """ 35 | self.desired_frequency = desired_frequency 36 | self.window_size = window_size 37 | self.verbose= verbose 38 | self.options = kwargs 39 | 40 | 41 | 42 | def resample(self, time, signal,interpolate=1): 43 | """ 44 | Apply the SWARII to resample a given signal. 45 | 46 | Input : 47 | time: The time stamps of the data point in the signal. A 1-d 48 | array of shape n, where n is the number of points in the 49 | signal. The unit is seconds. 50 | signal: The data points representing the signal. A k-d array of 51 | shape (n,k), where n is the number of points in the signal, 52 | and k is the dimension of the signal (e.g. 2 for a 53 | statokinesigram). 54 | skip_if_missing : will raise an exception if the number of empty windows is larger than 55 | this value (default : + infty) 56 | interpolate : 0 - last point interpolation 57 | 1 - linear interpolation 58 | -1 - no interpolation, delete missing times (experimental) 59 | 60 | options : 61 | count_interpolations : if True, will return the number of interpolated poitns 62 | 63 | 64 | Output: 65 | resampled_time : The time stamps of the signal after the resampling 66 | resampled_signal : The resampled signal. 67 | """ 68 | 69 | a_signal=np.array(signal) 70 | current_time = max(0.,time[0]) 71 | #print current_time 72 | output_time=[] 73 | output_signal = [] 74 | missing_windows=0 75 | 76 | while current_time < time[-1]: 77 | 78 | relevant_times = [t for t in range(len(time)) if abs( 79 | time[t] - current_time) < self.window_size * 0.5] 80 | if len(relevant_times) == 0 : 81 | missing_windows +=1 82 | if self.verbose == 2: 83 | print("Trying to interpolate an empty window ! at time ", current_time) 84 | else : 85 | if len(relevant_times) == 1: 86 | value = a_signal[relevant_times[0]] 87 | 88 | else : 89 | value = 0 90 | weight = 0 91 | 92 | for i, t in enumerate(relevant_times): 93 | if i == 0 or t==0: 94 | left_border = max( 95 | time[0], (current_time - self.window_size * 0.5)) 96 | 97 | else: 98 | left_border = 0.5 * (time[t] + time[t - 1]) 99 | 100 | 101 | 102 | if i == len(relevant_times) - 1: 103 | right_border = min( 104 | time[-1], current_time + self.window_size * 0.5) 105 | else: 106 | right_border = 0.5 * (time[t + 1] + time[t]) 107 | 108 | w = right_border - left_border 109 | 110 | 111 | value += a_signal[t] * w 112 | weight += w 113 | 114 | 115 | 116 | value /= weight 117 | output_time.append(current_time) 118 | output_signal.append(value) 119 | current_time += 1. / self.desired_frequency 120 | if missing_windows>0: 121 | if self.verbose>0: 122 | print("There was {} empty windows".format(missing_windows)) 123 | if interpolate>=0: 124 | interpolation_kind = "linear" if interpolate ==1 else 'previous' 125 | if self.verbose>0: 126 | print("interpolating") 127 | desired_times = np.arange(output_time[0],output_time[-1],1. / self.desired_frequency) 128 | func = interp1d(output_time,output_signal,kind=interpolation_kind,axis=0,bounds_error=False) 129 | desired_signal = func(desired_times) 130 | output_time, output_signal = desired_times, desired_signal 131 | else : 132 | if self.verbose>0 : 133 | print("no interpolation") 134 | 135 | if interpolate>=0 and self.options["count_interpolations"]: 136 | return np.array(output_time),np.array(output_signal), missing_windows 137 | else : 138 | return np.array(output_time),np.array(output_signal) 139 | 140 | 141 | @staticmethod 142 | def purge_artefact(time, signal, threshold_up=2, threshold_down=0.5,verbose=0): 143 | asignal = np.array(signal) 144 | nsignal=[] 145 | ntime=[] 146 | n_artefact=0 147 | 148 | for t in range(1,len(time)-1): 149 | if time[t]<0.1: 150 | pass 151 | elif ((len(ntime)>0) and (t>0) and (t threshold_up)): 154 | n_artefact+=1 155 | pass 156 | elif ( (len(ntime)>0) and (t>1) and (t threshold_up)): 157 | n_artefact+=1 158 | pass 159 | else : 160 | ntime.append(time[t]) 161 | nsignal.append(signal[t]) 162 | if n_artefact >0: 163 | if verbose >0: 164 | print("skipped", n_artefact, "artefacts" ) 165 | return ntime,nsignal 166 | 167 | 168 | 169 | 170 | class SWARII : 171 | @staticmethod 172 | def resample(data,window_size=0.08,desired_frequency=25,interpolate = True, verbose=0, count_interpolations=False): 173 | """ 174 | time should be in second 175 | 176 | """ 177 | swarii = Local_SWARII(window_size=window_size-1e-6,desired_frequency=desired_frequency, verbose=verbose, count_interpolations=count_interpolations) 178 | t = data[:,0] 179 | signal = data[:,1:] 180 | #y = data.T[2] 181 | nt,nsignal = Local_SWARII.purge_artefact(time=t,signal=signal, verbose=verbose) 182 | 183 | #t_close,x_close = swarii.resample(t,x) 184 | #t_close,y_close = swarii.resample(t,y) 185 | 186 | if count_interpolations : 187 | nnt,nnsignal, missing_windows= swarii.resample( time =nt, signal= nsignal, interpolate=interpolate) 188 | return nnsignal[:,:2], missing_windows 189 | else : 190 | nnt, nnsignal= swarii.resample( time =nt, signal= nsignal, interpolate=interpolate) 191 | return nnsignal[:,:2] 192 | 193 | --------------------------------------------------------------------------------