SAX 기법 코드 예제

시계열 데이터의 이상치 처리를 위해서 SAX 기법을 이용하고자 합니다.

이상치 제거를 위한 좋은 방법이 없을까 논문을 찾았는데 관련된 SAX 관련 논문이 재밌더군요.

바로 시작하죠.

SAX가 뭔데?

위 그림이 SAX 입니다.

SAX 는 “Symbolic Aggregate Approximation” 이라는 의미를 가지고 있습니다.

데이터 집합을 기호화로 접근하는 방법입니다.

이를 위해서는 우선적으로 PAA 방법이 사용됩니다.

PAA 는 “Piecewise Aggregate Approximation” 인데요.

데이터를 쪼개서 단순화합니다. 

위 그림처럼 원래는 파란색이 원본이에요.

그런데 파란색 뒤에 계단 형태로 듬성듬성 데이터를 표현했지요?

저렇게 마치 계단식으로 데이터를 단순화 하는 것이 PAA의 핵심입니다. 

이후에는 데이터를 다음과 같이 부호화를 통해 나타냅니다.

평균과 비슷한 부분을 “B” 로 평균 이하의 값들을 “A” 평균 초과의 값들을 “C”로 구분하는 것이지요.

그렇다면 이 데이터들을 부호화로 나열하면 “BAABCCBC” 와 같이 문자열을 통해 데이터를 단순화 할 수 있습니다. 

바로 이것이 SAX의 핵심입니다. 

우리가 “B” 라는 부분의 값만 남기고 싶다면, 부호화 표현에서 “B”를 제외한 “A” “C” 와 같은 부분만 날려 버린다면…

쉽게 이상치를 제거할 수 있지 않을까요?

한 번 해보죠.

간단하게 코드를 짜보자.

				
					import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# 테스트를 위한 데이터 로드
path = r"VitalStudy\Data\TestSet.csv"
df = pd.read_csv(path)

# 분석을 하고자 하는 시계열 데이터의 데이터입니다.
data = df["data"].tolist()

# 데이터의 길이를 N으로 지정합니다.
N = len(data)

# 데이터를 출력해봅시다. 

x = np.linspace(0, 1, N)
y = data 

plt.plot(x, y, c = "k")
plt.show()

				
			

우선은 위와 같은 코드를 통해서 내가 정제하고 싶은 원본 데이터를 보겠습니다. 

정제하고 싶은 원본 데이터입니다.

이 데이터에서 0.1 과 0.2 사이 부근 정도에 갑자기 위로 튄 부분이 있지요?

저 부분을 탐지해서 깔끔하게 없에고 싶습니다.

이를 위해서 우선 데이터를 정규화 해볼까요?

정규화를 위해 사용한 공식입니다.

각각의 데이터 포인트를 Z-score 정규화를 할 겁니다. 

코드로 나타내자면…

				
					import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# 테스트를 위한 데이터 로드
path = r"VitalStudy\Data\TestSet.csv"
df = pd.read_csv(path)

# 분석을 하고자 하는 시계열 데이터의 데이터입니다.
data = df["data"].tolist()

# 데이터의 길이를 N으로 지정합니다.
N = len(data)

# 데이터를 정규화 합시다. 
NormData = []
Xmean = np.nanmean(data)
std_dev = np.nanstd(data)

# 각각의 데이터 포인트를 Z-정규화 
for x in data:
    Xnorm = (x - Xmean) / std_dev
    NormData.append(Xnorm)
    
    
# 데이터를 출력해봅시다. 
x = np.linspace(0, 1, N)
y = NormData 

plt.plot(x, y, c = "k")
plt.show()

				
			

위와 같이 Z-Score 정규화를 통해서 data 를 정규화 합니다.

그리고 출력을 해볼까요?

그럼 이런 식으로 데이터들이 정규화가 되어 나타납니다.

이 상태에서 이제 PAA를 시도할 겁니다. 

PAA를 시도하자

앞서 정규화를 이용하여 NormData 로 기존 데이터를 변경하는 것도 공식이 있었죠?

마찬가지로 PAA를 시도하기 위해서도 사용하는 공식이 있습니다.

위와 같은 공식입니다.

N이 전체 데이터의 길이이고, W는 이 N을 몇 등분을 할 것인지에 대한 값입니다. 

쉽게 대충 그림과 글로 설명하자면…

위와 같은 내용입니다.

이걸 그럼 실제 코드로 해봐야겠죠?

				
					import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# 테스트를 위한 데이터 로드
path = r"VitalStudy\Data\TestSet.csv"
df = pd.read_csv(path)

# 분석을 하고자 하는 시계열 데이터의 데이터입니다.
data = df["data"].tolist()

# 데이터의 길이를 N으로 지정합니다.
N = len(data)

# 데이터를 정규화합시다.
NormData = []
Xmean = np.nanmean(data)
std_dev = np.nanstd(data)

# 각각의 데이터 포인트를 Z-정규화
for x in data:
    Xnorm = (x - Xmean) / std_dev
    NormData.append(Xnorm)

# PAA 적용을 위한 구간 개수 W를 설정합니다.
W = 20  # 대충 아무거나 넣어봄 일단 

# PAA 변환: 데이터를 W 개의 구간으로 나누어 각 구간의 평균을 계산
paa_values = []
segment_size = N // W  # 각 구간의 길이

for i in range(W):
    # 구간의 시작과 끝 인덱스를 계산합니다.
    start_idx = i * segment_size
    end_idx = start_idx + segment_size
    
    # 구간 내 모든 값을 더함 
    segment_sum = np.sum(NormData[start_idx:end_idx])
    # 구간 내 모든 값을 W/N 와 곱함 
    segment_value = W/N * segment_sum
    paa_values.append(segment_value)

# PAA 데이터를 원본 데이터의 길이와 일치시키기 위해 각 구간을 단순화된 값으로 대체함 
paa_data_expanded = np.repeat(paa_values, segment_size)

# 시각화
x = np.linspace(0, 1, N)

plt.figure(figsize=(12, 6))
# 원본 정규화된 데이터 그래프
plt.plot(x, NormData, label="Normalized Data", color="blue", linewidth=1)
# PAA 변환 데이터 그래프
plt.step(x[:len(paa_data_expanded)], paa_data_expanded, where="mid", label="PAA Data", color="red", linewidth=2)

plt.xlabel("Time")
plt.ylabel("Normalized Value")
plt.title("Normalized Data vs. PAA Transformed Data")
plt.legend()
plt.show()

				
			

자, 이제 위처럼 코드를 조작했습니다.

이를 통해서 원본 데이터와, PAA 진행된 데이터를 함께 그래프로 나타내면…

위와 같이 나타납니다.

기존에 파란색 원본 데이터를 위와 같이 단순화를 시킬 수 있었습니다. 

만약 W의 값을 더 많~이 쪼개면 쪼갤수록 구간이 많이 생기니까, 더 많은 구간을 단순화 할 수 있을 겁니다. 

W값을 늘려볼까요?

그럼 이런 식으로 데이터가 변하게 됩니다.

대신이 전체적으로 길이가 줄어든 형태로 나타나게 되지요.

이건 구간을 N//W 으로 끊다 보니까, 마지막까지 포함을 다 할 수가 없어서 그런 겁니다.

아래와 같은 코드로 해결 가능하지요.

				
					for i in range(W):
    # 구간의 시작과 끝 인덱스를 계산합니다.
    start_idx = int(i * segment_size)
    end_idx = int((i + 1) * segment_size) if i < W - 1 else N  # 마지막 구간은 끝까지 포함
    
    # 구간 내 모든 값을 더하고, W/N을 곱한 평균값 계산
    segment_sum = np.sum(NormData[start_idx:end_idx])
    segment_value = (W / N) * segment_sum
    paa_values.append(segment_value)
				
			

부호화를 해보자

이제 PAA가 이뤄진 데이터를 이용해서 부호화를 할 차례입니다.

우선 예제 코드를 볼까요?

				
					import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# 테스트를 위한 데이터 로드
path = r"VitalStudy\Data\TestSet.csv"
df = pd.read_csv(path)

# 분석을 하고자 하는 시계열 데이터의 데이터입니다.
data = df["data"].tolist()

# 데이터의 길이를 N으로 지정합니다.
N = len(data)

# 데이터를 정규화합시다.
NormData = []
Xmean = np.nanmean(data)
std_dev = np.nanstd(data)

# 각각의 데이터 포인트를 Z-정규화
for x in data:
    Xnorm = (x - Xmean) / std_dev
    NormData.append(Xnorm)

# PAA 적용을 위한 구간 개수 W를 설정합니다.
W = 100  # 예: 100개의 구간으로 나누기

# PAA 변환: 데이터를 W 개의 구간으로 나누어 각 구간의 평균을 계산
paa_values = []
segment_size = N / W  # 각 구간의 길이 (소수점 포함)

for i in range(W):
    # 구간의 시작과 끝 인덱스를 계산합니다.
    start_idx = int(i * segment_size)
    end_idx = int((i + 1) * segment_size) if i < W - 1 else N  # 마지막 구간은 끝까지 포함
    
    # 구간 내 모든 값을 더하고, W/N을 곱한 평균값 계산
    segment_sum = np.sum(NormData[start_idx:end_idx])
    segment_value = (W / N) * segment_sum
    paa_values.append(segment_value)

# PAA 데이터를 원본 데이터의 길이와 일치시키기 위해 각 구간을 평균값으로 대체함
paa_data_expanded = np.repeat(paa_values, np.ceil(segment_size).astype(int))[:N]  # 원본 길이에 맞춰 잘라냄

# 부호화 단계
alphabet = ["a", "b", "c", "d"]

# 분위수 계산  분위수 경계: [-0.6745, 0, 0.6745]
breakpoints = norm.ppf(np.linspace(0,1,len(alphabet) + 1)[1:-1])

# SAX 변환
sax_symbols = []
for value in paa_values:
    if value < breakpoints[0]:
        sax_symbols.append("a")
    elif value < breakpoints[1]:
        sax_symbols.append("b")
    elif value < breakpoints[2]:
        sax_symbols.append("c")
    else:
        sax_symbols.append("d")

# 시각화
x = np.linspace(0, 1, N)

plt.figure(figsize=(12, 6))
# 원본 정규화된 데이터 그래프
plt.plot(x, NormData, label="Normalized Data", color="blue", linewidth=1)
# PAA 변환 데이터 그래프
plt.step(x[:len(paa_data_expanded)], paa_data_expanded, where="mid", label="PAA Data", color="red", linewidth=2)

# SAX 기호 추가
for i, symbol in enumerate(sax_symbols):
    # 각 PAA 구간의 중앙에 텍스트로 기호 표시
    plt.text((i + 0.5) * (1 / W), paa_values[i], symbol, ha="center", va="bottom", fontsize=12, color="black")

plt.xlabel("Time")
plt.ylabel("Normalized Value")
plt.title("Normalized Data vs. PAA Transformed Data with SAX Symbols")
plt.legend()
plt.show()
				
			

위와 같은 코드를 통해서 우리는 PAA 된 구간 데이터를 

특정 부호를 통해 구분이 가능합니다.

시각화를 해볼까요?

다른 모든 구간의 값들은 “B” 로 표시가 되지요?

하지만 이상치가 있는 구간의 값들은 “D” 로 표시가 되는 것을 볼 수가 있습니다.

만약 이상치 제거를 한다면, 저는 “B”에 해당하는 구간만 남기고 나머지를 삭제하는 로직을 적용한다면 

이 데이터를 기대에 알맞게 정리할 수 있겠습니다. 

수학적으로 이해해보자

그렇다면 SAX 가 도대체 어떻게 가능해지는 것인지 차근차근 알아볼까요?

우리는 원본 데이터 전체를 Z-Score 정규화를 하였습니다. 

Z-Score 정규화를 거치면 데이터는 평균이 0, 표준편차를 1로 맞추기 때문에 대부분의 값은 -1에서 1사이에 있을 확률이 높습니다. 

물론 정규 분포의 특성상 데이터의 일부는 이 범위를 벗어날 수 있습니다. 

다만, PAA는 앞서 Z-Score 정규화를 거친 데이터를 이용하여 구간 별로 값을 압축한 것입니다.

때문에 PAA 역시 마찬가지로 평균이 0, 표준편차가 1인 데이터와 매우 가깝습니다. 

즉 PAA를 통해 설정된 각  구간의 값들도 대부분 -1~1 사이의 값이며 이것들은 정규 분포를 따르는 값이라는 것이죠. 

 

				
					# 분위수 계산  분위수 경계: [-0.6745, 0, 0.6745]
breakpoints = norm.ppf(np.linspace(0,1,len(alphabet) + 1)[1:-1])
				
			

이제 위의 코드를 이해하면 됩니다.

np.linspace(0, 1, len(alphabet)  +1) 의 값이 무엇을 의미할까요?

0~1 사이를 내가 정하고 싶은 부호들보다 1개 더 많은 개수로 균등하게 나누는 겁니다.

				
					[0, 0.25, 0.5, 0.75, 1]
				
			

균등하게 나눈다면 이제 위와 같겠지요?

여기에서 [1:-1] 이라는 것은 0번째 값과, 가장 끝의 값을 뺀다는 뜻입니다. 

				
					[0.25, 0.5, 0.75]
				
			

이렇게 바꾼 거죠.

앞에 norm.ppf 를 씌우면 정규분포에서 해당 퍼센트의 해당하는 값이 몇인지를 알려줍니다. 

				
					 [-0.6745, 0, 0.6745]
				
			

이렇게 되는 겁니다. 

정규 분포를 따르는 값들 중에서 아래에서 25%에 해당하는 값은 “-0.6745” 였구나~

딱 중앙에 있는 50%에 해당하는 값은 “0” 이구나~ 

하는 거죠…

				
					# SAX 변환
sax_symbols = []
for value in paa_values:
    if value < breakpoints[0]:
        sax_symbols.append("a")
    elif value < breakpoints[1]:
        sax_symbols.append("b")
    elif value < breakpoints[2]:
        sax_symbols.append("c")
    else:
        sax_symbols.append("d")
				
			

그럼 뭐 이 변환식도 다시 보면 간단하죠?

PAA 의 구간값이 정규 분포도의 0~25% 사이에 해당하는 값이면 “A”를 부여 받는 겁니다

25~50% 사이에 해당하는 값이면 “B”를 부여 받는 거고요.

0~75% 내부 어디에도 해당하지 않는 값으로 엄청 튀는 값이라면 “D”를 부여 받게 되겠지요? 

마치며...

오늘은 이렇게 SAX 를 이용해서 데이터의 이상치를 잡아내느 방법을 알아 봤습니다.

실제 간단한 예시 코드를 이용하여 이해를 더 깊이 해보고자 했습니다. 

다음에도 또 비루한 주제를 가지고 포스팅을 해보겠습니다.

여기까지 봐주신 분들 정말 감사합니다.^^