070 LOF 알고리즘 상세
키워드: LOF, Local Outlier Factor
개요
LOF(Local Outlier Factor)는 데이터 포인트 주변의 지역 밀도를 기반으로 이상치를 탐지하는 알고리즘입니다. 전역 밀도가 아닌 지역 밀도를 사용하여 다양한 밀도의 클러스터에서도 효과적으로 이상치를 찾습니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
pycaret[full]>=3.0
LOF 원리
핵심 아이디어
전역 밀도 방식의 한계:
- 밀도가 다른 클러스터가 있을 때 문제
●●●●● ○ ○
●●●●●●● ○ ○ ○
●●●●●●●●● ○ ○
●●●●●●● ○
●●●●●
고밀도 영역 저밀도 영역
전역 밀도: 저밀도 영역의 정상 데이터를 이상치로 오탐
LOF 방식:
- 각 점의 지역 밀도를 이웃의 밀도와 비교
- 이웃보다 밀도가 현저히 낮으면 이상치
핵심 개념
- k-거리 (k-distance): k번째 최근접 이웃까지의 거리
- 도달 거리 (Reachability Distance): max(k-distance(o), d(p, o))
- 지역 도달 밀도 (Local Reachability Density): 도달 거리의 역수 평균
- LOF: 이웃의 LRD 평균 / 자신의 LRD
LOF ≈ 1: 이웃과 비슷한 밀도 (정상)
LOF > 1: 이웃보다 낮은 밀도 (이상치)
LOF < 1: 이웃보다 높은 밀도 (조밀 영역)
LOF 계산 과정
import numpy as np
from sklearn.neighbors import NearestNeighbors
def calculate_lof(X, k=5):
"""LOF 수동 계산"""
n_samples = len(X)
# 1. k개 최근접 이웃 찾기
nbrs = NearestNeighbors(n_neighbors=k+1) # 자기 자신 제외
nbrs.fit(X)
distances, indices = nbrs.kneighbors(X)
# k-distance (k번째 이웃까지 거리)
k_distances = distances[:, k]
# 2. 도달 거리 계산
# reach_dist(p, o) = max(k_dist(o), d(p, o))
reach_dists = np.zeros((n_samples, k))
for i in range(n_samples):
for j in range(k):
neighbor_idx = indices[i, j+1]
reach_dists[i, j] = max(k_distances[neighbor_idx], distances[i, j+1])
# 3. 지역 도달 밀도 (LRD)
# lrd(p) = 1 / (평균 도달 거리)
lrd = np.zeros(n_samples)
for i in range(n_samples):
avg_reach_dist = reach_dists[i].mean()
lrd[i] = 1 / (avg_reach_dist + 1e-10) # 0 방지
# 4. LOF 계산
# LOF(p) = 이웃들의 평균 LRD / 자신의 LRD
lof_scores = np.zeros(n_samples)
for i in range(n_samples):
neighbor_lrd_sum = sum(lrd[indices[i, j+1]] for j in range(k))
lof_scores[i] = (neighbor_lrd_sum / k) / (lrd[i] + 1e-10)
return lof_scores
# 070 테스트
np.random.seed(42)
X = np.vstack([
np.random.randn(100, 2) * 0.5, # 고밀도
np.random.randn(20, 2) * 2 + [5, 0], # 저밀도
[[3, 3]] # 이상치
])
lof_manual = calculate_lof(X, k=5)
print(f"이상치 LOF: {lof_manual[-1]:.4f}")
print(f"정상 평균 LOF: {lof_manual[:-1].mean():.4f}")
PyCaret에서 LOF
from pycaret.anomaly import *
import pandas as pd
import numpy as np
# 070 데이터 생성
np.random.seed(42)
normal = np.random.randn(1000, 3)
outliers = np.random.uniform(-5, 5, (50, 3))
data = pd.DataFrame(np.vstack([normal, outliers]), columns=['F1', 'F2', 'F3'])
# 070 환경 설정
anomaly = setup(data, session_id=42, verbose=False)
# 070 LOF
lof = create_model('lof')
# 070 fraction 지정
lof_5pct = create_model('lof', fraction=0.05)
주요 하이퍼파라미터
from sklearn.neighbors import LocalOutlierFactor
# 070 n_neighbors: k값 (기본 20)
lof = LocalOutlierFactor(n_neighbors=20)
# 070 algorithm: 이웃 탐색 알고리즘
lof = LocalOutlierFactor(algorithm='auto') # 자동 선택
lof = LocalOutlierFactor(algorithm='ball_tree')
lof = LocalOutlierFactor(algorithm='kd_tree')
lof = LocalOutlierFactor(algorithm='brute')
# 070 metric: 거리 측정 방법
lof = LocalOutlierFactor(metric='minkowski', p=2) # 유클리드
lof = LocalOutlierFactor(metric='manhattan')
# 070 contamination: 이상치 비율
lof = LocalOutlierFactor(contamination=0.05)
n_neighbors 영향
from sklearn.neighbors import LocalOutlierFactor
from sklearn.metrics import f1_score
import numpy as np
import matplotlib.pyplot as plt
# 070 데이터 생성
np.random.seed(42)
normal = np.random.randn(500, 2)
outliers = np.random.uniform(-5, 5, (50, 2))
X = np.vstack([normal, outliers])
y_true = np.array([0] * 500 + [1] * 50)
# 070 k 값에 따른 성능
k_values = [5, 10, 20, 30, 50, 100]
f1_scores = []
for k in k_values:
lof = LocalOutlierFactor(n_neighbors=k, contamination=0.1)
y_pred = lof.fit_predict(X)
y_pred = (y_pred == -1).astype(int)
f1 = f1_score(y_true, y_pred)
f1_scores.append(f1)
plt.figure(figsize=(10, 6))
plt.plot(k_values, f1_scores, 'bo-', linewidth=2, markersize=10)
plt.xlabel('n_neighbors (k)')
plt.ylabel('F1 Score')
plt.title('LOF: Effect of n_neighbors')
plt.grid(True, alpha=0.3)
plt.savefig('lof_k.png', dpi=150)
# 070 보통 20~50 정도가 적절
다중 밀도 클러스터에서의 LOF
import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import LocalOutlierFactor
from sklearn.ensemble import IsolationForest
np.random.seed(42)
# 070 밀도가 다른 두 클러스터
cluster1 = np.random.randn(200, 2) * 0.3 # 고밀도
cluster2 = np.random.randn(100, 2) * 1.5 + [4, 0] # 저밀도
outlier = np.array([[2, 2], [-1, 2]]) # 이상치
X = np.vstack([cluster1, cluster2, outlier])
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 070 원본 데이터
axes[0].scatter(cluster1[:, 0], cluster1[:, 1], c='blue', label='Dense Cluster', alpha=0.5)
axes[0].scatter(cluster2[:, 0], cluster2[:, 1], c='green', label='Sparse Cluster', alpha=0.5)
axes[0].scatter(outlier[:, 0], outlier[:, 1], c='red', marker='*', s=200, label='Outliers')
axes[0].set_title('Original Data')
axes[0].legend()
# 070 LOF
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.02)
lof_pred = lof.fit_predict(X)
axes[1].scatter(X[lof_pred == 1, 0], X[lof_pred == 1, 1], c='blue', alpha=0.5, label='Normal')
axes[1].scatter(X[lof_pred == -1, 0], X[lof_pred == -1, 1], c='red', marker='x', s=100, label='Anomaly')
axes[1].set_title('LOF (Good at multi-density)')
axes[1].legend()
# 070 Isolation Forest
iforest = IsolationForest(contamination=0.02, random_state=42)
iforest_pred = iforest.fit_predict(X)
axes[2].scatter(X[iforest_pred == 1, 0], X[iforest_pred == 1, 1], c='blue', alpha=0.5, label='Normal')
axes[2].scatter(X[iforest_pred == -1, 0], X[iforest_pred == -1, 1], c='red', marker='x', s=100, label='Anomaly')
axes[2].set_title('Isolation Forest')
axes[2].legend()
plt.tight_layout()
plt.savefig('lof_vs_iforest_multidensity.png', dpi=150)
LOF 점수 분석
from sklearn.neighbors import LocalOutlierFactor
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
normal = np.random.randn(500, 2)
outliers = np.random.uniform(-5, 5, (50, 2))
X = np.vstack([normal, outliers])
# 070 LOF (novelty=True로 설정하면 score_samples 사용 가능)
lof = LocalOutlierFactor(n_neighbors=20, novelty=True)
lof.fit(X[:500]) # 정상 데이터만 학습
# 070 LOF 점수 계산
lof_scores = -lof.score_samples(X) # 음수이므로 부호 반전
# 070 분포
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.hist(lof_scores[:500], bins=30, alpha=0.7, label='Normal')
plt.hist(lof_scores[500:], bins=30, alpha=0.7, label='Outliers')
plt.xlabel('LOF Score')
plt.ylabel('Frequency')
plt.title('LOF Score Distribution')
plt.legend()
plt.subplot(1, 2, 2)
plt.scatter(X[:, 0], X[:, 1], c=lof_scores, cmap='RdYlBu_r', alpha=0.7)
plt.colorbar(label='LOF Score')
plt.title('LOF Scores Visualization')
plt.tight_layout()
plt.savefig('lof_scores.png', dpi=150)
새로운 데이터 예측 (novelty=True)
from sklearn.neighbors import LocalOutlierFactor
import numpy as np
np.random.seed(42)
X_train = np.random.randn(500, 2) # 학습 데이터 (정상만)
# 070 novelty=True: 새 데이터 예측 가능
lof = LocalOutlierFactor(n_neighbors=20, novelty=True, contamination=0.05)
lof.fit(X_train)
# 070 새로운 데이터
X_new = np.array([
[0, 0], # 정상
[10, 10], # 이상치
[-0.5, 0.5] # 정상
])
# 070 예측
predictions = lof.predict(X_new)
scores = lof.score_samples(X_new)
print("새 데이터 예측:")
for i, (pred, score) in enumerate(zip(predictions, scores)):
status = "Normal" if pred == 1 else "Anomaly"
print(f" 데이터 {i+1}: {status} (score: {score:.4f})")
LOF vs 다른 알고리즘
from pycaret.anomaly import *
import pandas as pd
import numpy as np
from sklearn.metrics import f1_score, precision_score, recall_score
# 070 다중 밀도 데이터
np.random.seed(42)
cluster1 = np.random.randn(400, 3) * 0.5
cluster2 = np.random.randn(400, 3) * 2 + [5, 0, 0]
outliers = np.random.uniform(-5, 8, (50, 3))
data = pd.DataFrame(np.vstack([cluster1, cluster2, outliers]),
columns=['F1', 'F2', 'F3'])
y_true = np.array([0] * 800 + [1] * 50)
anomaly = setup(data, session_id=42, verbose=False)
# 070 여러 알고리즘 비교
algorithms = ['lof', 'iforest', 'knn', 'svm']
results = []
for algo in algorithms:
model = create_model(algo, fraction=0.05)
result = assign_model(model)
y_pred = result['Anomaly'].values
results.append({
'Algorithm': algo,
'Precision': precision_score(y_true, y_pred),
'Recall': recall_score(y_true, y_pred),
'F1': f1_score(y_true, y_pred)
})
df = pd.DataFrame(results).sort_values('F1', ascending=False)
print("알고리즘 비교 (다중 밀도 데이터):")
print(df.round(4))
장단점
장점:
- 지역 밀도 기반으로 다중 밀도 처리
- 밀도가 다른 클러스터에서 효과적
- 이상 정도 수치화 (LOF 점수)
단점:
- k(n_neighbors) 선택이 중요
- 계산 비용 높음 (O(n²))
- 고차원에서 성능 저하 (차원의 저주)
언제 사용하나?
LOF 추천:
- 밀도가 다른 여러 클러스터가 있을 때
- 지역적 이상치를 찾을 때
- 이상 정도의 수치가 필요할 때
Isolation Forest 추천:
- 대용량 데이터
- 고차원 데이터
- 빠른 처리가 필요할 때
정리
- LOF는 지역 밀도 기반 이상치 탐지
- 이웃의 밀도 대비 자신의 밀도로 판단
- 다중 밀도 클러스터에서 효과적
- n_neighbors(k)가 핵심 파라미터
- 대용량/고차원에는 부적합
다음 글 예고
다음 글에서는 이상치 탐지 실전 - 사기 거래 탐지를 다룹니다.
PyCaret 머신러닝 마스터 시리즈 #070