├── requirements.txt ├── Dockerfile ├── train_model.py ├── run_12ECG_classifier.py ├── LICENSE.txt ├── README.md ├── .gitignore ├── driver.py ├── train_12ECG_classifier.py └── get_12ECG_features.py /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.17.4 2 | scipy==1.2.1 3 | joblib==0.13.2 4 | Cython==0.29.13 5 | pandas==0.25.3 6 | scikit-learn==0.21.3 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.3-slim 2 | 3 | ## The MAINTAINER instruction sets the Author field of the generated images 4 | MAINTAINER author@sample.com 5 | ## DO NOT EDIT THESE 3 lines 6 | RUN mkdir /physionet 7 | COPY ./ /physionet 8 | WORKDIR /physionet 9 | 10 | ## Install your dependencies here using apt-get etc. 11 | 12 | ## Do not edit if you have a requirements.txt 13 | RUN pip install -r requirements.txt 14 | 15 | -------------------------------------------------------------------------------- /train_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, sys 4 | from train_12ECG_classifier import train_12ECG_classifier 5 | 6 | if __name__ == '__main__': 7 | # Parse arguments. 8 | input_directory = sys.argv[1] 9 | output_directory = sys.argv[2] 10 | 11 | if not os.path.isdir(output_directory): 12 | os.mkdir(output_directory) 13 | 14 | print('Running training code...') 15 | 16 | train_12ECG_classifier(input_directory, output_directory) 17 | 18 | print('Done.') 19 | -------------------------------------------------------------------------------- /run_12ECG_classifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np, os, sys 4 | import joblib 5 | from get_12ECG_features import get_12ECG_features 6 | 7 | def run_12ECG_classifier(data,header_data,loaded_model): 8 | 9 | 10 | # Use your classifier here to obtain a label and score for each class. 11 | model = loaded_model['model'] 12 | imputer = loaded_model['imputer'] 13 | classes = loaded_model['classes'] 14 | 15 | features=np.asarray(get_12ECG_features(data,header_data)) 16 | feats_reshape = features.reshape(1, -1) 17 | feats_reshape = imputer.transform(feats_reshape) 18 | current_label = model.predict(feats_reshape)[0] 19 | current_label=current_label.astype(int) 20 | current_score = model.predict_proba(feats_reshape) 21 | current_score=np.asarray(current_score) 22 | current_score=current_score[:,0,1] 23 | 24 | return current_label, current_score,classes 25 | 26 | def load_12ECG_model(input_directory): 27 | # load the model from disk 28 | f_out='finalized_model.sav' 29 | filename = os.path.join(input_directory,f_out) 30 | 31 | loaded_model = joblib.load(filename) 32 | 33 | return loaded_model 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, PhysioNet 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example classifier code for Python for the PhysioNet/CinC Challenge 2020 2 | 3 | ## Contents 4 | 5 | This code uses two main scripts to train the model and classify the data: 6 | 7 | * `train_model.py` Train your model. Add your model code to the `train_12ECG_model` function. It also performs all file input and output. **Do not** edit this script or we will be unable to evaluate your submission. 8 | * `driver.py` is the classifier which calls the output from your `train_model` script. It also performs all file input and output. **Do not** edit this script or we will be unable to evaluate your submission. 9 | 10 | Check the code in these files for the input and output formats for the `train_model` and `driver` scripts. 11 | 12 | To create and save your model, you should edit `train_12ECG_classifier.py` script. Note that you should not change the input arguments of the `train_12ECG_classifier` function or add output arguments. The needed models and parameters should be saved in a separated file. In the sample code, an additional script, `get_12ECG_features.py`, is used to extract hand-crafted features. 13 | 14 | To run your classifier, you should edit the `run_12ECG_classifier.py` script, which takes a single recording as input and outputs the predicted classes and probabilities. Please, keep the formats of both outputs as they are shown in the example. You should not change the inputs and outputs of the `run_12ECG_classifier` function. 15 | 16 | ## Use 17 | 18 | You can run this classifier code by installing the requirements and running 19 | 20 | python train_model.py training_data model 21 | python driver.py model test_data test_outputs 22 | 23 | where `training_data` is a directory of training data files, `model` is a directory of files for the model, `test_data` is the directory of test data files, and `test_outputs` is a directory of classifier outputs. The [PhysioNet/CinC 2020 webpage](https://physionetchallenges.github.io/2020/) provides a training database with data files and a description of the contents and structure of these files. 24 | 25 | ## Submission 26 | 27 | The `driver.py`, `get_12ECG_score.py`, and `get_12ECG_features.py` scripts must be in the root path of your repository. If they are inside a folder, then the submission will be unsuccessful. 28 | 29 | ## Details 30 | 31 | See the [PhysioNet/CinC 2020 webpage](https://physionetchallenges.github.io/2020/) for more details, including instructions for the other files in this repository. 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /driver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np, os, sys 4 | from scipy.io import loadmat 5 | from run_12ECG_classifier import load_12ECG_model, run_12ECG_classifier 6 | 7 | def load_challenge_data(filename): 8 | 9 | x = loadmat(filename) 10 | data = np.asarray(x['val'], dtype=np.float64) 11 | 12 | new_file = filename.replace('.mat','.hea') 13 | input_header_file = os.path.join(new_file) 14 | 15 | with open(input_header_file,'r') as f: 16 | header_data=f.readlines() 17 | 18 | 19 | return data, header_data 20 | 21 | 22 | def save_challenge_predictions(output_directory,filename,scores,labels,classes): 23 | 24 | recording = os.path.splitext(filename)[0] 25 | new_file = filename.replace('.mat','.csv') 26 | output_file = os.path.join(output_directory,new_file) 27 | 28 | # Include the filename as the recording number 29 | recording_string = '#{}'.format(recording) 30 | class_string = ','.join(classes) 31 | label_string = ','.join(str(i) for i in labels) 32 | score_string = ','.join(str(i) for i in scores) 33 | 34 | with open(output_file, 'w') as f: 35 | f.write(recording_string + '\n' + class_string + '\n' + label_string + '\n' + score_string + '\n') 36 | 37 | 38 | 39 | if __name__ == '__main__': 40 | # Parse arguments. 41 | if len(sys.argv) != 4: 42 | raise Exception('Include the model, input and output directories as arguments, e.g., python driver.py model input output.') 43 | 44 | model_input = sys.argv[1] 45 | input_directory = sys.argv[2] 46 | output_directory = sys.argv[3] 47 | 48 | # Find files. 49 | input_files = [] 50 | for f in os.listdir(input_directory): 51 | if os.path.isfile(os.path.join(input_directory, f)) and not f.lower().startswith('.') and f.lower().endswith('mat'): 52 | input_files.append(f) 53 | 54 | if not os.path.isdir(output_directory): 55 | os.mkdir(output_directory) 56 | 57 | # Load model. 58 | print('Loading 12ECG model...') 59 | model = load_12ECG_model(model_input) 60 | 61 | # Iterate over files. 62 | print('Extracting 12ECG features...') 63 | num_files = len(input_files) 64 | 65 | for i, f in enumerate(input_files): 66 | print(' {}/{}...'.format(i+1, num_files)) 67 | tmp_input_file = os.path.join(input_directory,f) 68 | data,header_data = load_challenge_data(tmp_input_file) 69 | current_label, current_score,classes = run_12ECG_classifier(data,header_data, model) 70 | # Save results. 71 | save_challenge_predictions(output_directory,f,current_score,current_label,classes) 72 | 73 | 74 | print('Done.') 75 | -------------------------------------------------------------------------------- /train_12ECG_classifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np, os, sys, joblib 4 | from scipy.io import loadmat 5 | from sklearn.impute import SimpleImputer 6 | from sklearn.ensemble import RandomForestClassifier 7 | from get_12ECG_features import get_12ECG_features 8 | 9 | def train_12ECG_classifier(input_directory, output_directory): 10 | # Load data. 11 | print('Loading data...') 12 | 13 | header_files = [] 14 | for f in os.listdir(input_directory): 15 | g = os.path.join(input_directory, f) 16 | if not f.lower().startswith('.') and f.lower().endswith('hea') and os.path.isfile(g): 17 | header_files.append(g) 18 | 19 | classes = get_classes(input_directory, header_files) 20 | num_classes = len(classes) 21 | num_files = len(header_files) 22 | recordings = list() 23 | headers = list() 24 | 25 | for i in range(num_files): 26 | recording, header = load_challenge_data(header_files[i]) 27 | recordings.append(recording) 28 | headers.append(header) 29 | 30 | # Train model. 31 | print('Training model...') 32 | 33 | features = list() 34 | labels = list() 35 | 36 | for i in range(num_files): 37 | recording = recordings[i] 38 | header = headers[i] 39 | 40 | tmp = get_12ECG_features(recording, header) 41 | features.append(tmp) 42 | 43 | for l in header: 44 | if l.startswith('#Dx:'): 45 | labels_act = np.zeros(num_classes) 46 | arrs = l.strip().split(' ') 47 | for arr in arrs[1].split(','): 48 | class_index = classes.index(arr.rstrip()) # Only use first positive index 49 | labels_act[class_index] = 1 50 | labels.append(labels_act) 51 | 52 | features = np.array(features) 53 | labels = np.array(labels) 54 | 55 | # Replace NaN values with mean values 56 | imputer=SimpleImputer().fit(features) 57 | features=imputer.transform(features) 58 | 59 | # Train the classifier 60 | model = RandomForestClassifier().fit(features,labels) 61 | 62 | # Save model. 63 | print('Saving model...') 64 | 65 | final_model={'model':model, 'imputer':imputer,'classes':classes} 66 | 67 | filename = os.path.join(output_directory, 'finalized_model.sav') 68 | joblib.dump(final_model, filename, protocol=0) 69 | 70 | # Load challenge data. 71 | def load_challenge_data(header_file): 72 | with open(header_file, 'r') as f: 73 | header = f.readlines() 74 | mat_file = header_file.replace('.hea', '.mat') 75 | x = loadmat(mat_file) 76 | recording = np.asarray(x['val'], dtype=np.float64) 77 | return recording, header 78 | 79 | # Find unique classes. 80 | def get_classes(input_directory, filenames): 81 | classes = set() 82 | for filename in filenames: 83 | with open(filename, 'r') as f: 84 | for l in f: 85 | if l.startswith('#Dx'): 86 | tmp = l.split(': ')[1].split(',') 87 | for c in tmp: 88 | classes.add(c.strip()) 89 | return sorted(classes) 90 | -------------------------------------------------------------------------------- /get_12ECG_features.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | from scipy.signal import butter, lfilter 5 | from scipy import stats 6 | 7 | def detect_peaks(ecg_measurements,signal_frequency,gain): 8 | 9 | """ 10 | Method responsible for extracting peaks from loaded ECG measurements data through measurements processing. 11 | 12 | This implementation of a QRS Complex Detector is by no means a certified medical tool and should not be used in health monitoring. 13 | It was created and used for experimental purposes in psychophysiology and psychology. 14 | You can find more information in module documentation: 15 | https://github.com/c-labpl/qrs_detector 16 | If you use these modules in a research project, please consider citing it: 17 | https://zenodo.org/record/583770 18 | If you use these modules in any other project, please refer to MIT open-source license. 19 | 20 | If you have any question on the implementation, please refer to: 21 | 22 | Michal Sznajder (Jagiellonian University) - technical contact (msznajder@gmail.com) 23 | Marta lukowska (Jagiellonian University) 24 | Janko Slavic peak detection algorithm and implementation. 25 | https://github.com/c-labpl/qrs_detector 26 | https://github.com/jankoslavic/py-tools/tree/master/findpeaks 27 | 28 | MIT License 29 | Copyright (c) 2017 Michal Sznajder, Marta Lukowska 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. 46 | 47 | """ 48 | 49 | 50 | filter_lowcut = 0.001 51 | filter_highcut = 15.0 52 | filter_order = 1 53 | integration_window = 30 # Change proportionally when adjusting frequency (in samples). 54 | findpeaks_limit = 0.35 55 | findpeaks_spacing = 100 # Change proportionally when adjusting frequency (in samples). 56 | refractory_period = 240 # Change proportionally when adjusting frequency (in samples). 57 | qrs_peak_filtering_factor = 0.125 58 | noise_peak_filtering_factor = 0.125 59 | qrs_noise_diff_weight = 0.25 60 | 61 | 62 | # Detection results. 63 | qrs_peaks_indices = np.array([], dtype=int) 64 | noise_peaks_indices = np.array([], dtype=int) 65 | 66 | 67 | # Measurements filtering - 0-15 Hz band pass filter. 68 | filtered_ecg_measurements = bandpass_filter(ecg_measurements, lowcut=filter_lowcut, highcut=filter_highcut, signal_freq=signal_frequency, filter_order=filter_order) 69 | 70 | filtered_ecg_measurements[:5] = filtered_ecg_measurements[5] 71 | 72 | # Derivative - provides QRS slope information. 73 | differentiated_ecg_measurements = np.ediff1d(filtered_ecg_measurements) 74 | 75 | # Squaring - intensifies values received in derivative. 76 | squared_ecg_measurements = differentiated_ecg_measurements ** 2 77 | 78 | # Moving-window integration. 79 | integrated_ecg_measurements = np.convolve(squared_ecg_measurements, np.ones(integration_window)/integration_window) 80 | 81 | # Fiducial mark - peak detection on integrated measurements. 82 | detected_peaks_indices = findpeaks(data=integrated_ecg_measurements, 83 | limit=findpeaks_limit, 84 | spacing=findpeaks_spacing) 85 | 86 | detected_peaks_values = integrated_ecg_measurements[detected_peaks_indices] 87 | 88 | return detected_peaks_values,detected_peaks_indices 89 | 90 | 91 | def bandpass_filter(data, lowcut, highcut, signal_freq, filter_order): 92 | """ 93 | Method responsible for creating and applying Butterworth filter. 94 | :param deque data: raw data 95 | :param float lowcut: filter lowcut frequency value 96 | :param float highcut: filter highcut frequency value 97 | :param int signal_freq: signal frequency in samples per second (Hz) 98 | :param int filter_order: filter order 99 | :return array: filtered data 100 | """ 101 | nyquist_freq = 0.5 * signal_freq 102 | low = lowcut / nyquist_freq 103 | high = highcut / nyquist_freq 104 | b, a = butter(filter_order, [low, high], btype="band") 105 | y = lfilter(b, a, data) 106 | return y 107 | 108 | def findpeaks(data, spacing=1, limit=None): 109 | """ 110 | Janko Slavic peak detection algorithm and implementation. 111 | https://github.com/jankoslavic/py-tools/tree/master/findpeaks 112 | Finds peaks in `data` which are of `spacing` width and >=`limit`. 113 | :param ndarray data: data 114 | :param float spacing: minimum spacing to the next peak (should be 1 or more) 115 | :param float limit: peaks should have value greater or equal 116 | :return array: detected peaks indexes array 117 | """ 118 | len = data.size 119 | x = np.zeros(len + 2 * spacing) 120 | x[:spacing] = data[0] - 1.e-6 121 | x[-spacing:] = data[-1] - 1.e-6 122 | x[spacing:spacing + len] = data 123 | peak_candidate = np.zeros(len) 124 | peak_candidate[:] = True 125 | for s in range(spacing): 126 | start = spacing - s - 1 127 | h_b = x[start: start + len] # before 128 | start = spacing 129 | h_c = x[start: start + len] # central 130 | start = spacing + s + 1 131 | h_a = x[start: start + len] # after 132 | peak_candidate = np.logical_and(peak_candidate, np.logical_and(h_c > h_b, h_c > h_a)) 133 | 134 | ind = np.argwhere(peak_candidate) 135 | ind = ind.reshape(ind.size) 136 | if limit is not None: 137 | ind = ind[data[ind] > limit] 138 | return ind 139 | 140 | 141 | def get_12ECG_features(data, header_data): 142 | 143 | tmp_hea = header_data[0].split(' ') 144 | ptID = tmp_hea[0] 145 | num_leads = int(tmp_hea[1]) 146 | sample_Fs= int(tmp_hea[2]) 147 | gain_lead = np.zeros(num_leads) 148 | 149 | for ii in range(num_leads): 150 | tmp_hea = header_data[ii+1].split(' ') 151 | gain_lead[ii] = int(tmp_hea[2].split('/')[0]) 152 | 153 | # for testing, we included the mean age of 57 if the age is a NaN 154 | # This value will change as more data is being released 155 | for iline in header_data: 156 | if iline.startswith('#Age'): 157 | tmp_age = iline.split(': ')[1].strip() 158 | age = int(tmp_age if tmp_age != 'NaN' else 57) 159 | elif iline.startswith('#Sex'): 160 | tmp_sex = iline.split(': ')[1] 161 | if tmp_sex.strip()=='Female': 162 | sex =1 163 | else: 164 | sex=0 165 | # elif iline.startswith('#Dx'): 166 | # label = iline.split(': ')[1].split(',')[0] 167 | 168 | 169 | 170 | # We are only using data from lead1 171 | peaks,idx = detect_peaks(data[0],sample_Fs,gain_lead[0]) 172 | 173 | # mean 174 | mean_RR = np.mean(idx/sample_Fs*1000) 175 | mean_Peaks = np.mean(peaks*gain_lead[0]) 176 | 177 | # median 178 | median_RR = np.median(idx/sample_Fs*1000) 179 | median_Peaks = np.median(peaks*gain_lead[0]) 180 | 181 | # standard deviation 182 | std_RR = np.std(idx/sample_Fs*1000) 183 | std_Peaks = np.std(peaks*gain_lead[0]) 184 | 185 | # variance 186 | var_RR = stats.tvar(idx/sample_Fs*1000) 187 | var_Peaks = stats.tvar(peaks*gain_lead[0]) 188 | 189 | # Skewness 190 | skew_RR = stats.skew(idx/sample_Fs*1000) 191 | skew_Peaks = stats.skew(peaks*gain_lead[0]) 192 | 193 | # Kurtosis 194 | kurt_RR = stats.kurtosis(idx/sample_Fs*1000) 195 | kurt_Peaks = stats.kurtosis(peaks*gain_lead[0]) 196 | 197 | features = np.hstack([age,sex,mean_RR,mean_Peaks,median_RR,median_Peaks,std_RR,std_Peaks,var_RR,var_Peaks,skew_RR,skew_Peaks,kurt_RR,kurt_Peaks]) 198 | 199 | 200 | return features 201 | 202 | 203 | --------------------------------------------------------------------------------