HRVAS 소스코드로 HRV 분석하기

HRVAS (Heart Rate Variability Analysis Software)는 Ramshur J.가 개발한 HRV 분석을 위한 MATLAB 기반 소프트웨어입니다.

이 소프트웨어는 시간 영역(Time Domain), 주파수 영역(Frequency Domain), 비선형 분석(Nonlinear Analysis) 등 다양한 HRV 지표를 분석합니다.

이를 MatLab 에서 코드를 통해서 HRVAS 가 제공하는 다양한 지표를 분석을 해볼 것입니다.

저는 ECG 신호에서 R피크와 R피크 간격인 RRI를 이용하여 HRV 분석에 사용할 예정입니다. 

HRVAS 의 GitHub 소스코드 복사

				
					$ git clone https://github.com/jramshur/HRVAS.git

				
			

git bash를 엽니다.

위 명령어를 입력해서 HRVAS 의 소스코드를 복사가 가능합니다.

				
					$ cd HRVAS


				
			

복사가 완료되면 위 코드를 통해서 HRVAS 프로젝트로 이동이 가능합니다.

				
					$ pwd

				
			

 위 명령어를 통해서 현재 HRVAS 프로젝트의 경로를 파악 가능합니다. 

MatLAB 으로 프로젝트 열기

위와 같이 MatLab 에서 상단을 보면 새로 만들기가 있습니다. 

새로 만들기를 누르고 “프로젝트 >> 폴더에서” 를 눌러줍니다.

그럼 경로를 설정 가능한데요.

앞서 pwd 명령어로 확인을 했던 HRVAS 프로젝트 경로를 지정하면 됩니다. 

하위 경로들을 포함하여 모든 파일들이 MatLab 경로 내부에 포함이 되었을 겁니다.

이번에는 HRVAS 디렉토리 바로 아래로 “my_hrr_analysis” 라는 스크립트 파일을 생성합시다.

새로만들기 >> 스크립트를 눌러주세요.

이후 컨트롤+S 를 눌러서 해당 스크립트의 이름을 “my_hrr_analysis” 이라는 이름으로 저장하면 됩니다. 

Signal Processing Toolbox 다운로드

소스코드 동작을 위한 스크립트를 만들기 위해서 필요한 애드온을 다운로드 해보겠습니다.

MatLAB 상단 Bar에서 홈으로 이동을 하면 “애드온” 이 있습니다.

여기서 애드온 추가기를 선택해줍시다.

그러면 위와 같이 여러 애드온을 검색해서 탐색할 수 있는 창이 열립니다.

Signal Processing Toolbox 을 검색해서 다음의 애드온을 찾읍시다.

오른쪽 위에 검새창이 있으니 참고해주세요.

찾아낸 애드온을 클릭하면 다음과 같이 상세 페이지로 넘어갑니다.

상세 페이지에서 보시면 “설치” 버튼이 있습니다. 

설치를 눌러서 진행을 끝까지 해주시면 됩니다. 

결과적으로 애드온이 설치가 되며, MatLAB이 재실행 된다면 성공입니다. 

샘플 데이터 분석

깃허브에서 복사한 프로젝트를 보면 다음과 같이 구성이 되어 있습니다.

이때 “SampleData” 폴더가 있습니다.

SampleData 내부로 들어가면 우리가 분석에 사용하기 위한 여러 데이터들이 존재합니다. 

위와 같이 여러 개의 SampleData가 들어 있는 것을 볼 수 있습니다.

여기서 103.ibi 를 제외한 다른 모든 데이터는 1개의 열로 이뤄진 숫자열입니다. 

103.ibi 의 경우 누적 시간에 따른 RR간격을 초로 나타낸 것이며 MIT-BIH Arrhythmia Database에서 가져온 데이터입니다.

나머지 데이터들은 모두 1개의 열을 이룬 데이터이며, 그 값들은 모두 RR 간격들을 순차적으로 기입한 것입니다.

rat.ibi 는 쥐의 심박을 통해 RR 간격을 정리한 자료입니다. 

저는 103.ibi 를 이용하여 HRV 분석을 진행하겠습니다. 

 

여기서 개념들에 대한 부분들은 HRVAS 내부에 있는 Documents 내부에 논문이 있습니다.

논문을 참고하여 개념들을 정리하여 진행하면 더욱이 이해가 쉽습니다. 

 

여기서 IBI 라고 하는 것이 R Peak 와 R Peak 간의 시간 간격을 뜻합니다. 

수식을 보게 되면 다음과 같은데요…

IBI(1) 은 Beat(2) ~ Beat(1) 의 시간 간격이라고 보면 됩니다.

이러한 IBI가 IBI(1), IBI(2), …. IBI(N) 까지 쭉 숫자로 적혀 있는 파일들이 SampleData 내부에 있다고 생각하면 됩니다. 

HRV 스크립트 분석

				
					timeDomainHRV.m
freqDomainHRV.m
nonlinearHRV.m
				
			

HRV 분석을 위해서 주요적으로 사용하게 되는 스크립트는 위 3개의 스크립트입니다.

다음의 스크립트들이 HRVAS 폴더 하위에 존재할 것입니다.

이번에는 이 스크립트들이 각각 어떤 분석을 위해서 사용이 되는 스크립트인지 알아보겠습니다. 

Time Domain HRV

				
					function output = timeDomainHRV(ibi,win,xx)
%timeDomainHRV: calculates time-domain hrv of ibi interval series
% ibi = 2dim ibi array
% win = window size to use for SDNNi (in seconds)
% xx = value to use for NNx and pNNx (in milliseconds)

				
			

스크립트의 시작은 다음과 같습니다.

 ibi 는 2차원 배열로, 첫번째 열이 시간, 두 번째 열이 IBI값인 데이터를 인자로 받을 것을 상정합니다.

win 은 시간 창 크기로 초 단위로 설정을 합니다.

xx 는 NNx/pNNx 계산에 사용되는 임계값입니다. 

여기서 NNx 란 연속된 두 심박 간격(IBI) 사이의 차이가 xx 이상인 경우의 개수를 뜻합니다.

즉, 심장 박동 간격이 특정 값 이상으로 달라지는 횟수를 셉니다.

pNNx 값은 전체 심박 간격 개수로 나누어 백분율로 표현한 값입니다. 

즉, 전체 심장 박동 간격 중에서 특정 값 이상으로 달라지는 횟수가 몇 퍼센트인지 나타냅니다.

 

				
					t = ibi(:,1) - ibi(1,1); % IBI의 상대 시간 계산
ibi = ibi(:,2); % IBI 값만 가져옴
ibi = ibi .* 1000; % 초 단위에서 밀리초(ms)로 변환

				
			

첫 번째 열(시간)에서 시작 시간을 기준으로 상대 시간으로 변환합니다.

두 번째 열 (IBI값)만 추철하여, 이것을 ms 단위로 변환하여 ibi 에 넣습니다. 

				
					output.max = round(max(ibi) * 10) / 10;
output.min = round(min(ibi) * 10) / 10;
output.mean = round(mean(ibi) * 10) / 10;
output.median = round(median(ibi) * 10) / 10;

				
			

이후 다음과 같이 ibi 값들의 최대값, 최소값, 평균값, 중앙값을 계산하여 저장합니다. 

결과는 소수점 첫째 자리까지 반올림하여 저장합니다. 

				
					output.SDNN = round(std(ibi) * 10) / 10;
output.SDANN = round(SDANN(ibi, win * 1000) * 10) / 10;
[p, n] = pNNx(ibi, xx);
output.NNx = round(n * 10) / 10;
output.pNNx = round(p * 10) / 10;
output.RMSSD = round(RMSSD(ibi) * 10) / 10;
output.SDNNi = round(SDANN(ibi, win * 1000) * 10) / 10;

				
			
  • SDNN : 전체 IBI 표준편차를 계산.
  • SDANN: win 크기의 시간 창별 평균 IBI의 표준편차를 계산.
  • pNNx / NNx: 특정 임계값 이상으로 차이 나는 IBI 비율과 개수를 계산.
  • RMSSD: IBI 차이의 제곱 평균 루트를 계산.
  • SDNNi: 창별 IBI 표준편차의 평균.
 
계산을 통해서 각각의 지표들을 위와 같은 이름으로 저장을 하게 됩니다. 
				
					hr = 60 ./ (ibi ./ 1000); % HR = 60 / (IBI in seconds)
output.meanHR = round(mean(hr) * 10) / 10;
output.sdHR = round(std(hr) * 10) / 10;

				
			

hr 에는 60을 초로 환산한 ibi 배열로 나눈 값들로 배열을 넣어줍니다.

이 hr 배열의 평균값을 소수 첫째 자리까지 반올림하여 meanHR에 넣습니다.

이 hr 배열의 표준편차를 계산하여 소수 첫째 자리까지 반올림하여 sdHR에 넣습니다. 

				
					dt = max(ibi) - min(ibi); % IBI의 범위
binWidth = 1 / 128 * 1000; % 히스토그램 bin 크기
nBins = round(dt / binWidth);
nBins = 32; % 히스토그램 bin 수 고정

output.HRVTi = round(hrvti(ibi, nBins) * 10) / 10;
output.TINN = round(tinn(ibi, nBins) * 10) / 10;

				
			

dt 는 IBI 값들의 범위가 얼마나 벌어졌는지 체크합니다.

전체 IBI 값들 중에서 가장 긴 값과, 가장 작은 값의 차이를 저장합니다.

이 범위가 넓을수록 심장 박동 간 간격이 더 불규칙함을 나타냅니다. 

히스토그램 bin의 수를 32로 고정하는 방식을 취하고 있습니다.

HRV 분석에서 bin 수를 일정하게 고정함으로써, 데이터가 적은 경우에도 안정적인 결과를 보장할 수 있도록 합니다.

데이터가 많거나, 적어도 일관된 비교를 위해서 사용합니다. 

HRVTi 의 값은 위 수식을 이용한 값에서 소수 첫번째 값까지 반올림을 합니다.

높은 HRVTi 값은 HRV가 낮고, 심장 박동이 규칙점임을 뜻합니다.

낮은 HRVTi 값은 HRV가 높고, 심장 박동 간격이 불규칙함을 의미합니다. 

				
					function output = hrvti(ibi, nbin)
    % 히스토그램 생성
    [n, xout] = hist(ibi, nbin); 
    
    % HRVTi 계산
    output = length(ibi) / max(n); 
end

				
			

hrviti 의 함수를 보면 다음과 같습니다.

전체 ibi 배열을 nbin 개수의 범위로 히스토그램을 생성합니다.

				
					^
|                ■
|        ■       ■
|        ■       ■       ■
| ■      ■       ■       ■
+-------------------------
  800   850     900     950

				
			

히스토그램의 각 개수 중에서 가장 큰 개수가 max(n) 입니다.

xout 은 각 bin 의 포함되는 값들의 중심값을 뜻합니다. 

이 값을 전체 ibi 개수에 나눔으로써 hrvti 값을 구합니다. 

다음은 TINN을 구할 때 사용되는 보조 함수입니다. 

				
					function output = tinn(ibi, nbin)
%tinn: Triangular Interpolation of NN interval histogram
%Reference: Standards of Measurement, Physiological Interpretation, 
%and Clinical Use
%           Circulation. 1996; 93(5):1043-1065.

    % Step 1: IBI 데이터의 히스토그램 계산
    [nout, xout] = hist(ibi, nbin); % 히스토그램 계산 (nbin 크기)
    
    % Step 2: 히스토그램에서 최대 bin 찾기
    D = nout; % 히스토그램 빈도
    peaki = find(D == max(D)); % 최대 빈도 값을 가지는 bin의 인덱스
    if length(peaki) > 1 % 최대 bin이 여러 개일 경우
        peaki = round(mean(peaki)); % 평균 위치를 사용
    end

    % Step 3: 최대 bin이 첫 번째에 위치할 경우 처리
    if peaki == 1
        output = NaN; % 삼각형을 생성할 수 없으므로 NaN 반환
        return;
    end
    
    % Step 4: 양쪽 끝점 탐색을 위한 반복문
    i = 1; % 차이를 저장할 인덱스 초기화
    d = zeros((peaki - 1) * (nbin - peaki), 3); % 차이값 저장 배열 초기화

    for m = (peaki - 1):-1:1 % 왼쪽 탐색
        for n = (peaki + 1):nbin % 오른쪽 탐색
            % 히스토그램 삼각형 생성
            q = zeros(1, length(D));
            q(1:m) = 0; % 왼쪽 끝까지 0
            q(n:end) = 0; % 오른쪽 끝까지 0
            q(m:peaki) = linspace(0, D(peaki), peaki - m + 1); % 왼쪽 선형 보간
            q(peaki:n) = linspace(D(peaki), 0, n - peaki + 1); % 오른쪽 선형 보간

            % Step 5: 삼각형과 히스토그램의 차이 계산
            d(i, 1) = trapz((D - q).^2); % 차이의 제곱 적분
            d(i, 2:3) = [m, n]; % 양 끝점 저장
            i = i + 1;
        end
    end

    % Step 6: 최소 차이를 가지는 삼각형 찾기
    i = find(d(:, 1) == min(d(:, 1))); % 최소 차이를 가지는 인덱스
    i = i(1); % 다수일 경우 첫 번째 값 사용
    m = d(i, 2); % 삼각형의 왼쪽 끝점
    n = d(i, 3); % 삼각형의 오른쪽 끝점

    % Step 7: TINN 계산
    output = abs(xout(n) - xout(m)); % 밑변의 길이 계산
end

				
			

위 보조 함수의 내용을 차례대로 따라가겠습니다. 

				
					[nout, xout] = hist(ibi, nbin);

				
			

앞서 설명한 hrviti 함수에 사용한 히스토그램 변환 함수입니다. 

nout 에는 각 bin의 빈도 수.

xout 에는 각 bin의 중심값이 들어갑니다. 

				
					peaki = find(D == max(D));
if length(peaki) > 1
    peaki = round(mean(peaki));
end

				
			

최대 빈도를 가지는 bin을 찾습니다.

최대 bin이 여러 개일 경우에는 평균 위치를 사용합니다. 

총 5개를 포함한 bin이 최대 빈도를 가지는 bin이고, 이러한 bin이 2, 6 번째 index에 위치한다면

4를 사용합니다. 

				
					if peaki == 1
    output = NaN;
    return;
end

				
			

최대 bin이 첫 번째 bin에 위치한다면, 삼각형을 생성할 수 없으므로 NAN을 반환합니다. 

				
					for m = (peaki - 1):-1:1
    for n = (peaki + 1):nbin
        % 히스토그램 삼각형 생성
        q = zeros(1, length(D));
        q(1:m) = 0; % 왼쪽 끝까지 0
        q(n:end) = 0; % 오른쪽 끝까지 0
        q(m:peaki) = linspace(0, D(peaki), peaki - m + 1); % 왼쪽 보간
        q(peaki:n) = linspace(D(peaki), 0, n - peaki + 1); % 오른쪽 보간

        % 차이의 제곱 적분
        d(i, 1) = trapz((D - q).^2);
        d(i, 2:3) = [m, n]; % 양 끝점 저장
        i = i + 1;
    end
end

				
			

m은 삼각형의 왼쪽 끝점을 나타냅니다.

n은 삼각형의 오른쪽 끝점을 나타냅니다.

m은 최대 bin의 왼쪽 어딘가의 점이고, n은 최대 bin의 오른쪽 어딘가의 점입니다.

이 점들을 이어서 삼각형을 만들고 제곱 적분을 시행합니다. 

i가 0회차라고 가정했을 때,

d(0, 1) 에는 제곱 적분의 값이 저장됩니다. 

d(0, 2:3) 에는 해당 적분에 사용된 양 끝점이 저장됩니다. 

				
					i = find(d(:, 1) == min(d(:, 1)));
m = d(i, 2); % 왼쪽 끝점
n = d(i, 3); % 오른쪽 끝점

				
			

모든 i를 순회한 이후, 

가장 제곱 적분의 값이 작았던 양 끝점의 값을 가져옵니다. 

				
					output = abs(xout(n) - xout(m));

				
			

두 점의 차이의 절댓값은 삼각형 밑변의 길이가 됩니다.

TINN의 값이 해당 값으로 설정이 됩니다.

TINN이 높을수록 히스토그램이 넓게 퍼져 있음을 나타냅니다.

즉, IBI 값이 더 불규칙적이고 HRV가 높음을 뜻합니다.

TINN 이 낮을 수록, 히스토 그램이 좁고 집중되어 있음을 나타냅니다.

이는 IBI 값이 규칙적이며 HRV가 낮음을 나타냅니다. 

Freq Domain HRV

				
					function output = freqDomainHRV(ibi, VLF, LF, HF, AR_order, window, ...
    noverlap, nfft, fs, methods, flagPlot)

				
			

함수의 시작은 다음과 같습니다.

IBI 데이터를 사용하여 주파수 영역 HRV 를 수행하게 됩니다.

해당 함수에서 사용되는 입력 인자를 다음과 같이 정리할 수 있습니다. 

				
					% 입력 변수 설명
% ibi       - 2차원 배열: 시간(s) 및 IBI 간격(s)
% VLF       - Very Low Frequency 범위 (Hz)
% LF        - Low Frequency 범위 (Hz)
% HF        - High Frequency 범위 (Hz)
% AR_order  - AR 모델 차수
% window    - Welch 분석에 사용할 샘플 수
% noverlap  - Welch 분석 윈도우 중첩 수
% nfft      - FFT 계산에 사용할 점 수
% fs        - IBI 데이터를 리샘플링할 주파수(Hz)
% methods   - 사용할 주파수 분석 방법 ('welch', 'ar', 'lomb')
% flagPlot  - 플래그: 분석 결과를 시각화할지 여부 (1 = 시각화)

				
			
				
					% 분석 방법 플래그 설정
flagWelch = false; flagAR = false; flagLomb = false;

for m = 1:length(methods)
    if strcmpi(methods{m}, 'welch')
        flagWelch = true;
    elseif strcmpi(methods{m}, 'ar')
        flagAR = true;
    elseif strcmpi(methods{m}, 'lomb')
        flagLomb = true;
    end
end

				
			

methods 에 어떤 값을 입력했는지 따라서 flagWelch, flagAR, flagLomb 분석 실행 여부가 결정이 됩니다. 

				
					% 데이터 준비
t = ibi(:,1); % 시간 데이터 (s)
y = ibi(:,2); % IBI 데이터 (s)

y = y .* 1000; % IBI를 밀리초(ms)로 변환
y = detrend(y, 'linear'); % 선형 추세 제거
y = y - mean(y); % 평균값 제거

				
			

데이터의 전처리는 다음과 같은 과정으로 진행합니다.

y = detrend(y, ‘linear’);

주파수 분석 시, 신호의 진동 성분만을 분석하기 위해서 장기적인 변화 추세를 제거하여 단기적인 변동성에 집중하게 합니다. 

y = y – mean(y);

데이터의 평균값을 배열에서 모두 감산합니다.

이를 통해서 신호는 평균 0을 중심으로 대칭적으로 변환하여, 진동 성분만을 분석에 활용합니다. 

시각화를 하자면 다음과 같습니다.

전체적으로 데이터가 점차 우상향을 하고 있기에, 이러한 성분을 제거하고

이후 신호가 0을 중심으로 대칭적으로 나타나게 됩니다.

Welch 분석

				
					if flagWelch
    [output.welch.psd, output.welch.f] = calcWelch(t, y, window, noverlap, nfft, fs);
    output.welch.hrv = calcAreas(output.welch.f, output.welch.psd, VLF, LF, HF);
else
    output.welch = emptyData(nfft, fs / 2);
end

				
			

보조함수를 이용하여 2가지 값을 계산합니다.

calcWelch 는 Welch FFT 방법으로 주파수 스펙트럼 밀도(PSD)를 계산합니다.

calcAreas 는 PSD를 이용하여 VLF, LF, HF 대역의 파워를 계산합니다. 

				
					function [PSD, F] = calcWelch(t, y, window, noverlap, nfft, fs)
    % 시간 보간 (리샘플링)
    t2 = t(1):1/fs:t(end); % fs 주파수에 맞게 새 시간 데이터 생성
    y = interp1(t, y, t2', 'spline')'; % 스플라인 보간법 적용
    y = y - mean(y); % 중앙 정렬

    % Welch 방법으로 PSD 계산
    [PSD, F] = pwelch(y, window, noverlap, (nfft * 2) - 1, fs, 'onesided');
end

				
			

IBI 데이터를 fs 통해 리샘플링 합니다.

인접 데이터 포인트를 부드러운 곡선으로 연결하여 중앙값을 설정하는 방식을 취합니다. 

y=[y1,y2,…,yN]

과 같이 데이터 배열이 있다고 가정하겠습니다. 

window = 256, noverlap = 128일 경우…

  • 첫 번째 윈도우: y[1:256]
  • 두 번째 윈도우: y[129:384]
  • 세 번째 윈도우: y[257:512]

위와 같이 128 포인트씩 겹치면서 window 크기 만큼 나뉘게 됩니다. 

각 윈도우에 가중치 함수를 적용하여 윈도우 경계에서 발생하는 신호 불연속성을 줄입니다. 

이렇게 되면 윈도우 가장자리의 신호보다 윈도우 중심부의 신호에 집중하여 주파수 성분을 구할 수 있습니다. 

L은 윈도우 길이, Fs 는 샘플링 주파수 입니다.

다음과 같이 특정 윈도우의 스펙트럼 밀도를 구할 수 있습니다.

k 번째의 PSD의 값입니다. 

해당 작업을 모든 윈도우에 대해서 실행하여 더하고, 평균을 냅니다.

결과적으로 F 에는 주파수 값들이, PSD 에는 스펠트럼 밀도가 저장됩니다. 

				
					function output = calcAreas(F, PSD, VLF, LF, HF)
    % 주파수 대역 필터링
    iVLF = (F >= VLF(1)) & (F <= VLF(2)); % VLF 대역 필터
    iLF  = (F >= LF(1)) & (F <= LF(2));  % LF 대역 필터
    iHF  = (F >= HF(1)) & (F <= HF(2));  % HF 대역 필터

    % 대역별 에너지 계산 (적분)
    aVLF = trapz(F(iVLF), PSD(iVLF)); % VLF 에너지
    aLF  = trapz(F(iLF), PSD(iLF));   % LF 에너지
    aHF  = trapz(F(iHF), PSD(iHF));   % HF 에너지
    aTotal = aVLF + aLF + aHF;

    % 결과 저장
    output.aVLF = aVLF; output.aLF = aLF; output.aHF = aHF;
    output.aTotal = aTotal;
    output.pVLF = (aVLF / aTotal) * 100; % VLF 에너지 비율(%)
    output.pLF = (aLF / aTotal) * 100;   % LF 에너지 비율(%)
    output.pHF = (aHF / aTotal) * 100;   % HF 에너지 비율(%)
end

				
			

calcAreas 에는 앞서 저장된 F, PSD 를 이용합니다.

Welch 분석에서 생성된 주파수 값인 F를 대역 필터링 합니다.

VLF, LF, HF 는 인자가 2개로 설정된 배열입니다. 

각 대역을 정의하자면 다음과 같습니다. 

  • VLF (Very Low Frequency): 0.003~0.04 Hz (보통 자율신경계 활동의 저주파수 성분).
  • LF (Low Frequency): 0.04~0.15 Hz (교감/부교감 신경의 균형과 관련된 성분).
  • HF (High Frequency): 0.15~0.4 Hz (부교감 신경 활성의 주요 지표).
				
					aVLF = trapz(F(iVLF), PSD(iVLF)); % VLF 에너지
aLF  = trapz(F(iLF), PSD(iLF));   % LF 에너지
aHF  = trapz(F(iHF), PSD(iHF));   % HF 에너지
aTotal = aVLF + aLF + aHF;

				
			

각 주파수 대역에 해당하는 PSD를 더합니다.

이것은 각 대역이 에너지를 나타내게 됩니다. 

				
					output.aVLF = aVLF; % VLF 에너지 (ms²)
output.aLF = aLF;   % LF 에너지 (ms²)
output.aHF = aHF;   % HF 에너지 (ms²)
output.aTotal = aTotal; % 총 에너지 (ms²)

output.pVLF = (aVLF / aTotal) * 100; % VLF 비율 (%)
output.pLF = (aLF / aTotal) * 100;   % LF 비율 (%)
output.pHF = (aHF / aTotal) * 100;   % HF 비율 (%)

				
			

전체 면적에서 특정 대역의 PSD를 이용해서 각 대역의 에너지 비율을 알 수 있습니다.

HF 비율이 감소하며, LF/HF 비율이 증가시 스트레스 반응, 피로 상태가 증가합니다.

HF 비율이 증가하면 심리적으로 안정 상태를 유지하게 됩니다. 

Nonlinear HRV

				
					function output = nonlinearHRV(ibi, m, r, n1, n2, breakpoint)

				
			

함수의 시작은 다음과 같습니다.

  • ibi: Inter-Beat Interval 데이터(초 단위).
  • m: SampEn 계산 시 템플릿 길이.
  • r: SampEn 계산 시 허용 오차(표준편차의 비율).
  • n1, n2: DFA 계산 시 최소/최대 윈도우 크기.
  • breakpoint: DFA에서 단기/장기 구분 경계점.
				
					    ibi(:,2) = ibi(:,2) .* 1000; % IBI 데이터를 ms 단위로 변환

				
			
				
					    output.sampen = sampen(ibi(:,2), m, r, 0, 0); % SampEn 계산
    output.dfa = DFA(ibi(:,2), n1, n2, breakpoint); % DFA 계산

				
			

sampen 은 IBI의 불규칙성을 나타내는 엔트로피를 계산합니다. 

해당 값이 낮을수록 데이터가 반복적이고 예측 가능함을 의미합니다. 

DFA는 HRV 데이터의 장기적/단거직 스케일링 거동을 분석하며  alpha1 (단기 스케일링) alpha2 (장기 스케일링) 을 구합니다. 

alpha1 이 0.5 미만인 경우, 심박동이 무작위라는 뜻이며 

alpha2 가 1.2 이상인 경우 장기 패턴이 과도하게 변화하며, 자율신경계 이상이 있을 수 있습니다. 

				
					function [e, se, A, B] = sampen(y, m, r, sflag, vflag)

				
			
  • 입력:

    • y: 분석 대상 데이터.
    • m: 템플릿 길이.
    • r: 허용 오차(데이터 표준편차의 비율).
    • sflag: 데이터 표준화 여부 (1 = 표준화를 하겠다는 뜻).
    • vflag: 표준오차 계산 여부.
  • 출력:

    • e: 각 템플릿 길이(m)에 따른 샘플 엔트로피.
    • se: 엔트로피 표준오차.
    • A, B: 매칭된 템플릿 수.
				
					    if sflag > 0
        y = y - mean(y); % 평균 제거
        s = sqrt(mean(y.^2)); % RMS 계산
        y = y / s; % 표준화
    end

				
			

sflag 가 1일 경우, 표준화를 수행하며 데이터의 상대적인 변동성을 분석할 수 있게 합니다. 

sflag 가 0일 경우, 표준화를 수행하지 않아 절대적인 크기를 유지한 상태로 분석이 실행됩니다. 

시각화 하자면 다음과 같습니다. 

데이터가 RMS 로 나누어져, 진폭은 줄어들었지만, 주기와 패턴은 동일하게 유지가 됩니다. 

				
					    r = r * std(y); % 허용 오차를 데이터 표준편차 기반으로 계산

				
			
				
					    [e, A, B] = sampenc(y, m, r);

				
			
				
					    for i = 1:(n-1)
        nj = n - i;
        y1 = y(i);
        for jj = 1:nj
            j = jj + i;
            if abs(y(j) - y1) < r
                run(jj) = lastrun(jj) + 1;
                M1 = min(M, run(jj));
                for m = 1:M1
                    A(m) = A(m) + 1;
                    if j < n
                        B(m) = B(m) + 1;
                    end
                end
            else
                run(jj) = 0;
            end
        end
        lastrun = run;
    end

				
			

y 데이터가 배열되어 있다면, 탬플릿 길이인 m씩 값을 묶어 나갑니다.

탬플릿들 간의 거리를 계산하고 차이가 r 이하인 매칭을 카운트합니다. 

두 샘플 간 차이가 r 이하인 경우 매칭을 확인하여 A, B 카운트를 업데이트 합니다. 

파이썬 코드로 예제를 들어보겠습니다. 

				
					# 예제 데이터
y = np.array([0.1, 0.15, 0.2, 0.12, 0.18])  # 간단한 IBI 데이터

templates_M = np.array([y[i:i+M] for i in range(len(y) - M + 1)])
templates_M1 = np.array([y[i:i+M+1] for i in range(len(y) - M)])

				
			

길이 M 템플릿과 길이 M+1 템플릿을 생성합니다.

M=2인 경우, 길이 2의 템플릿은 [[0.1, 0.15], [0.15, 0.2], [0.2, 0.12], [0.12, 0.18]]입니다.

길이 3의 템플릿은 [[0.1, 0.15, 0.2], [0.15, 0.2, 0.12], [0.2, 0.12, 0.18]]입니다.

				
					matches_M = 0
for i in range(len(templates_M)):
    for j in range(len(templates_M)):
        if i != j:  # 자기 자신과의 비교 제외
            distance = np.max(np.abs(templates_M[i] - templates_M[j]))
            if distance <= r:
                matches_M += 1
				
			

[[0.1, 0.15], [0.15, 0.2], [0.2, 0.12], [0.12, 0.18]]

에서 

탬플릿 1 : [0.1, 0.15]

탬플릿 2 : [0.15, 0.2]

입니다.

이때 투 탬플릿의 distance 는abs([0.1, 0.15] – [0.15, 0.2] ) 배열에서 가장 큰 요소를 가져오는 것이므로, 0.05가 됩니다. 

				
					    for i = 1:(n-1)
        nj = n - i;
        y1 = y(i);
        for jj = 1:nj
            j = jj + i;
            if abs(y(j) - y1) < r
                run(jj) = lastrun(jj) + 1;
                M1 = min(M, run(jj));
                for m = 1:M1
                    A(m) = A(m) + 1;
                    if j < n
                        B(m) = B(m) + 1;
                    end
                end
            else
                run(jj) = 0;
            end
        end
        lastrun = run;
    end

				
			

다시 기존 함수 코드로 돌아가서 확인하면..

B는 탬플릿 길이 M에서 두 탬플릿 간의 거리가 허용 임계값 이하인 경우의 모든 수입니다. 

두 탬플릿의 값이 서로 유사할수록 허용 임계값 이하인 경우가 많이 나타나므로, 탬플릿 쌍 중에서 서로 비슷하다고 판단되는 쌍의 개수입니다. 

A는 탬플릿은 길이가 M+1 개의 매창 개수이며, 탬플릿이 유사하다고 판단되는 쌍의 개수로 볼 수 있습니다. 

				
					    e = -log(A ./ B); % 엔트로피 계산

				
			

A/B는 길이 M+1 때의 매치율이 M일 때의 매치율에 비해 얼마나 작은지를 나타낸다.

비율이 작을수록 데이터는 더 불규칙하다고 해석이 된다. 

				
					function output = DFA(data, n1, n2, breakpoint)

				
			

Detrended Fluctuation Analysis (DFA) 를 구하는 로직입니다. 

data 는 전체 IBI 배열입니다. n1, n2 는 DFA 분석을 위한 윈도우 크기 범위입니다.

breakpoint 는 단기/장기 스케일링 구분 경계입니다. 

예시를 들어서 보겠습니다. 

				
					ibi_data = [0.8, 0.82, 0.81, 0.85, 0.88, 0.87, 0.9, 0.92, 0.89, 0.91]; % 단위: 초
n1 = 2; % 최소 윈도우 크기
n2 = 5; % 최대 윈도우 크기
breakpoint = 3; % 단기-장기 경계점

				
			

다음과 같이 인자를 설정했다고 가정하겠습니다. 

				
					n = n1:n2; % 윈도우 크기 배열
nWin = floor(length(yk) ./ n); % 각 크기에서 가능한 윈도우 개수
disp(n);
% 출력: [2, 3, 4, 5]
disp(nWin);
% 출력: [5, 3, 2, 2]

				
			

2~5 윈도우 배열이 [2, 3, 4, 5] 이고 IBI 데이터 개수가 총 10개라면..

데이터 2개씩 윈도우를 설정하면 총 5개..

데이터 3개씩 윈도우를 설정하면 총 3개… 와 같은 패턴으로 

nWin 은 [5, 3, 2, 2] 가 됩니다. 

				
					F_n = zeros(size(n)); % 윈도우 크기별 RMS 저장
for i = 1:length(n)
    w_size = n(i); % 현재 윈도우 크기
    rms_sum = 0;
    for j = 1:nWin(i)
        % 현재 윈도우 데이터 선택
        idx_start = (j-1)*w_size + 1;
        idx_end = j*w_size;
        segment = yk(idx_start:idx_end);
        
        % 1차 다항식 추세 제거
        p = polyfit(1:w_size, segment, 1);
        trend = polyval(p, 1:w_size);
        detrended = segment - trend;
        
        % RMS 계산
        rms_sum = rms_sum + mean(detrended.^2);
    end
    F_n(i) = sqrt(rms_sum / nWin(i));
end
disp(F_n);
% 출력: [0.012, 0.015, 0.02, 0.03] (예시 값)

				
			

각 윈도우 별로 데이터를 훑습니다.

이때 추세를 제거하여, 데이터의 직선으로 변화하는 것을 제거합니다.

이후 RMS 계산으로 각 윈도우 내에서 신호의 진폭이 얼마나 변동했는지를 나타냅니다.

이렇게 각 윈도우 마다의 변동성을 F_n 배열에 저장 가능합니다. 

				
					log_n = log10(n); % 윈도우 크기의 로그
log_F_n = log10(F_n); % RMS의 로그
a = polyfit(log_n, log_F_n, 1); % 로그-로그 플롯에서 기울기 계산
alpha = a(1); % 스케일링 지수
disp(alpha);
% 출력: 0.85 (예시 값)

				
			

polyfit 은 로그 변환된 두 데이터의 경향성을 잘 표현하는 직선을 찾아냅니다. 

데이터를 직선으로 근사할 때 오차가 최소화 되는 직선을 찾습니다. 

				
					bp_idx = find(n == breakpoint);
alpha1 = polyfit(log_n(1:bp_idx), log_F_n(1:bp_idx), 1);
alpha2 = polyfit(log_n(bp_idx+1:end), log_F_n(bp_idx+1:end), 1);

disp(alpha1(1)); % 단기 스케일링 지수
disp(alpha2(1)); % 장기 스케일링 지수
% 출력: alpha1 = 0.7, alpha2 = 1.1 (예시 값)

output.alpha = alpha; % 전체 스케일링 지수
output.alpha1 = alpha1(1); % 단기 스케일링 지수
output.alpha2 = alpha2(1); % 장기 스케일링 지수
output.F_n = F_n'; % 각 윈도우 크기의 RMS 값
output.n = n'; % 윈도우 크기


				
			

breakpoint 로 윈도우의 크기가 비교적 작았을 때의 기울기와

윈도우의 크기가 비교적 클 때의 기울기들을 구하게 됩니다.

윈도우 크기를 작게 비교한 값의 alpha1 에, 그 반대의 경우가 alpha2 에 해당하게 됩니다. 

스크립트 작동

				
					% HRVAS HRV Analysis Script

% 1. HRVAS 경로 추가
addpath(genpath(pwd)); % 현재 디렉토리와 하위 폴더 경로 추가

% 2. IBI 데이터 로드
ibi_data = load('SampleData/103.ibi'); % 2D 배열: [시간(s), IBI(s)]
disp('Loaded IBI Data:');
disp(ibi_data);

% 3. Time-Domain 분석
win = 300; % 5분 창
xx = 50;   % NNx, pNNx 임계값
time_results = timeDomainHRV(ibi_data, win, xx);
disp('Time-Domain Results:');
disp(time_results);

% 4. Frequency-Domain 분석
VLF = [0, 0.04]; % Very Low Frequency (Hz)
LF = [0.04, 0.15]; % Low Frequency (Hz)
HF = [0.15, 0.4]; % High Frequency (Hz)
AR_order = 16; % Auto-Regressive 모델 차수
window = 256; % Welch 윈도우 크기
noverlap = 128; % 윈도우 중첩 샘플 수
nfft = 512; % FFT에서 사용하는 점 수
fs = 4; % 리샘플링 주파수 (Hz)
methods = {'welch', 'ar', 'lomb'}; % 주파수 분석 방법
freq_results = freqDomainHRV(ibi_data, VLF, LF, HF, AR_order, window, noverlap, nfft, fs, methods, 1);
disp('Frequency-Domain Results:');
disp(freq_results);

% 5. Nonlinear 분석
m = 2; % 샘플 엔트로피 템플릿 길이
r = 0.2; % 샘플 엔트로피 허용 오차
n1 = 4; % DFA 최소 윈도우 크기
n2 = 300; % DFA 최대 윈도우 크기
breakpoint = 13; % DFA 단기/장기 경계점
nonlinear_results = nonlinearHRV(ibi_data, m, r, n1, n2, breakpoint);
disp('Nonlinear Results:');
disp(nonlinear_results);

% 6. 결과 저장
results_file = 'HRVAS_Results_103.mat';
save(results_file, 'time_results', 'freq_results', 'nonlinear_results');
disp(['HRV Analysis Results saved to ', results_file]);

				
			

위에서 분석한 코드를 다음과 같이 활용하여 분석 결과를 HRVAS 루트 경로로 저장 가능합니다.

간단한 시각화 또한 적용이 가능했습니다. 

시각화의 경우는 PSD 분포가 주파수에 따라 얼마나 분포하고 있는지를 확인 가능했습니다. 

마치며...

이상으로 HRVAS 의 소스코드를 분석 및 Matlab 스크립트로 분석을 실행할 수 있었습니다.