├── 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 | " MocapTime | \n",
55 | " DeviceFrame | \n",
56 | " Fx | \n",
57 | " Fy | \n",
58 | " Fz | \n",
59 | " Mx | \n",
60 | " My | \n",
61 | " Mz | \n",
62 | " Cx | \n",
63 | " Cy | \n",
64 | " Cz | \n",
65 | "
\n",
66 | " \n",
67 | " MocapFrame | \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 | " 0 | \n",
84 | " 0.00 | \n",
85 | " 0 | \n",
86 | " 1.622131 | \n",
87 | " -8.233887 | \n",
88 | " 581.4375 | \n",
89 | " -37.412109 | \n",
90 | " -12.184570 | \n",
91 | " 3.460205 | \n",
92 | " 0.020956 | \n",
93 | " -0.064344 | \n",
94 | " 0.0 | \n",
95 | "
\n",
96 | " \n",
97 | " 1 | \n",
98 | " 0.01 | \n",
99 | " 1 | \n",
100 | " 0.000000 | \n",
101 | " 0.000000 | \n",
102 | " 0.0000 | \n",
103 | " 0.000000 | \n",
104 | " 0.000000 | \n",
105 | " 0.000000 | \n",
106 | " 0.000000 | \n",
107 | " 0.000000 | \n",
108 | " 0.0 | \n",
109 | "
\n",
110 | " \n",
111 | " 2 | \n",
112 | " 0.02 | \n",
113 | " 2 | \n",
114 | " 0.000000 | \n",
115 | " 0.000000 | \n",
116 | " 0.0000 | \n",
117 | " 0.000000 | \n",
118 | " 0.000000 | \n",
119 | " 0.000000 | \n",
120 | " 0.000000 | \n",
121 | " 0.000000 | \n",
122 | " 0.0 | \n",
123 | "
\n",
124 | " \n",
125 | " 3 | \n",
126 | " 0.03 | \n",
127 | " 3 | \n",
128 | " 1.216919 | \n",
129 | " -7.369629 | \n",
130 | " 582.3125 | \n",
131 | " -38.582031 | \n",
132 | " -12.597168 | \n",
133 | " 3.246948 | \n",
134 | " 0.021633 | \n",
135 | " -0.066257 | \n",
136 | " 0.0 | \n",
137 | "
\n",
138 | " \n",
139 | " 4 | \n",
140 | " 0.04 | \n",
141 | " 4 | \n",
142 | " 0.894653 | \n",
143 | " -7.661621 | \n",
144 | " 582.3125 | \n",
145 | " -38.800781 | \n",
146 | " -12.507324 | \n",
147 | " 3.155640 | \n",
148 | " 0.021479 | \n",
149 | " -0.066632 | \n",
150 | " 0.0 | \n",
151 | "
\n",
152 | " \n",
153 | "
\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 |
--------------------------------------------------------------------------------