8장 KNN 분류#
자료 출처: ISLP (An Introduction to Statistical Learning with Applications in Python)
K-최근접이웃(K-nearest neighbors: KNN)은 1951년 Fix와 Hodges에 의해 처음 개발된 비파라미터적 지도학습(non-parametric supervised learning) 방법이다. 원리가 매우 직관적인데, 어떤 관측의 출력(반응)을 예측함에 있어서 해당 관측의 입력값과 가장 가까운 \(K\)개의 관측 데이터를 이용한다는 아이디어이다. 분류와 회귀 모두에 사용된다. 분류(classification)의 경우에 출력은 어떤 범주이기 때문에 이때는 \(K\)개의 가장 가까운 이웃(\(K\)는 양의 정수로 일반적으로 작은 숫자임) 중에서 어떤 범주가 가장 많은지 다수결 투표 방식으로 분류된다. 가령 \(K\)가 3이면, 가장 가까운 3개 관측이 어떤 범주이냐에 따라 주어진 관측의 반응 범주가 결정된다. 반면 회귀(regression)의 경우에는 출력이 범주가 아니라 어떤 값이기 때문에 이때는 \(K\)개의 최근접이웃 값의 평균을 예측값으로 삼는다. 본 장에서는 KNN 회귀는 다루지 않고, KNN 분류에 대해서만 살펴보기로 한다.
8.1 개요#
분류 문제에서 모형의 정확도를 생각해보자. 훈련 데이터 \({(x_1, y_1), (x_2, y_2),..., (x_n, y_n)}\)으로 \(f\)를 추정하는데, \(y_1, y_2,..., y_n\)이 정성적 변수다. 이럴 때 \(f\) 추정의 정확도를 측정하는 대표적인 방법은 오류율(error rate)이다.
여기서 \(\hat y_i\)는 \(\hat f\)으로 예측한 \(i\)번째 관측에 대한 예측 범주이고, \( I\left( y_i \neq \hat y_i \right)\)은 \(y_i \neq \hat y_i\)이면 값이 1이고, \(y_i = \hat y_i \)이면 값이 0인 소위 지표변수(indicator variable)이다. \( I\left( y_i \neq \hat y_i \right)=0 \)이면, \(i\)번째 관측이 올바르게 분류된 것을 의미하고, 그렇지 않으면 잘못 분류된 것을 의미한다.
결국 위 식은 관측 중에서 분류가 잘못된 것이 몇 개인지를 세어 전체 관측 개수로 나눈 것, 즉 잘못된 분류의 비율이다. 위 식은 훈련 데이터를 기반으로 계산된다는 점에서 훈련 오류율이다. 테스트 데이터에 대한 오류율을 테스트 오류율이라 한다.
베이즈 분류기#
어떤 식으로 분류해야 오류율을 낮출 수 있을까? 어떤 주어진 관측의 범주를 분류함에 있어서 가장 가능성이 높은 범주로 분류해야만 테스트 오류율을 평균적으로 최소화할 수 있다는 것을 이론적으로 증명할 수 있다. 이 분류 방식을 식으로 표현하면, 테스트 관측 \(x_0\)이 주어질 때, 다음의 조건부 확률(conditional probability)이 가장 큰 범주 \(j\)로 테스트 관측의 범주를 예측한다는 것이다.
이러한 분류 방식을 베이즈 분류기(Bayes classifier)라고 한다. 베이즈 분류기는 가능한 가장 낮은 테스트 오류율을 생성하며, 이를 베이즈 오류율이라고 한다.
K-최근접이웃#
베이즈 분류기를 사용하여 정성적 반응을 예측하고 싶지만, 실제 데이터의 경우 \(X\)가 주어졌을 때 \(Y\)의 조건부 분포를 알지 못하므로 베이즈 분류기를 사용하는 것은 불가능하다. 이에 따라 많은 접근법들은 주어진 \(X\)에 대한 \(Y\)의 조건부 분포를 추정한 다음, 추정 확률이 가장 높은 범주로 해당 관측을 분류하는 방식을 취한다. 이러한 방법 중 하나가 K-최근접이웃(KNN) 분류기로서 다음과 같은 방식을 취한다.
양의 정수 \(K\)와 테스트 관측 \(x_0\)가 주어지면 먼저 훈련 데이터에서 \(x_0\)에 가장 가까운 \(K\)개의 포인트를 식별한다.
이를 \(\mathcal N_0\)로 표시하면, \(\mathcal N_0\)의 여러 반응에서 \(j\) 범주가 차지하는 비율을 \(j\)에 대한 조건부 확률로 추정한다.
최종적으로 베이즈 분류 규칙에 따라 각 범주 중 가장 큰 확률을 가진 범주에 테스트 관측 \(x_0\)을 분류한다.
KNN 그림 예시#
아래 그림 8.1은 KNN 접근법을 예시적으로 보여준다. 왼쪽 그림에서 우리의 목표는 검정색 X로 표시된 점의 색깔을 예측하는 것이다. 가령 \(K=3\)을 선택한다면, KNN은 먼저 X에 가장 가까운 3개의 관측을 식별한다. 이 경우 원으로 표시돼있는 X의 이웃(neighborhood)에 파란색 동그라미가 두 개, 그리고 오렌지색이 하나로 구성돼있기 때문에 파란색 범주 확률이 2/3, 오렌지색 범주 확률이 1/3로 추정된다. 따라서 KNN은 X가 파란색 범주에 속할 것으로 예측한다.
그림 8.1의 오른쪽 그림은 \(K=3\)인 KNN 기법을 사용하여 주어진 평면의 모든 격자 점들에 대해 포인트 X를 예측했던 것과 똑같은 방식으로 예측한 결과를 보여준다. 파란색 격자 점들은 해당 포인트가 파란색으로 예측된 것이고, 오렌지색 격자 점들은 오렌지색으로 예측된 것이다. 두 영역을 구분해 놓은 검정색 실선을 결정 경계(decision boundary)라고 부른다. 매우 간단한 접근 방식임에도 불구하고 KNN은 종종 최적의 베이즈 분류에 가까운 성과를 보인다.
그림 8.1. 6개의 파란색 관측과 6개의 오렌지색 관측이 있는 간단한 상황에서 \(K=3\)을 사용하는 KNN 접근 방식.

그림 출처: ISLP, FIGURE 2.14
KNN 분류는 \(K\)를 어떻게 선택하느냐에 따라 결과가 크게 달라질 수 있다. \(K=1\)인 경우에는 결정 경계가 지나치게 꼬불꼬불할 것이고, \(K\)가 커짐에 따라 유연성(flexibility)이 떨어지고 선형에 가까운 결정 경계를 생성한다. \(K=1\)이면 테스트 데이터에서 가장 가까운 관측의 반응값을 따른다는 것이기 때문에 테스트 데이터의 위치가 조금만 바뀌어도 반응값이 달라질 수 있다. 반면 \(K\)값이 아주 커지면 테스트 데이터 위치가 조금씩 달라지더라도 부근 관측에 의한 다수결의 결과가 달라지지 않을 가능성이 크다. 따라서 이 경우 결정 경계가 선형에 더 가까워지게 된다.
훈련 오류율과 테스트 오류율이 같은 방향으로 움직이지는 않는다. \(K=1\)인 경우 훈련 오류율은 (항상 자신의 올바른 반응 범주를 사용하기 때문에) 0이지만 테스트 오류율은 상당히 높을 수 있다. 일반적으로 유연한 분류 방법을 사용하면 훈련 오류율은 감소하지만 테스트 오류율은 높을 수 있다. 따라서 적절한 수준의 유연성을 선택하는 것이 중요하다.
KNN 기법 숫자 예시#
아래 훈련 데이터는 6개의 관측, 3개의 예측변수, 1개의 정성적 반응변수로 이루어져 있다. 이 데이터에 대해 \(K\)-최근접이웃 기법을 사용하여 \(X_1=X_2=X_3=0\)일 때 \(Y\)의 범주가 Red인지 Green인지를 예측하려고 한다.(출처: ISLP, 2장 연습문제 Q7)
먼저 테스트 데이터 \(X_1=X_2=X_3=0\)에 대해 주어진 각 관측과의 유클리드 거리를 계산해보자. 두 개의 점 \(\mathbf p =(p_1,p_2,...,p_n)\)와 \(\mathbf q =(q_1,q_2,...,q_n)\)의 유클리드 거리(Euclidean distance)는 다음과 같이 정의된다.
따라서 각 관측 \((x_1,x_2,x_3)\)와 문제에서 주어진 테스트 관측 \((0,0,0)\) 사이의 유클리드 거리는 다음과 같다.
여기에서 주어진 예시는 관측이 6개밖에 되지 않아 손으로도 계산이 가능하지만, 연습삼아 코딩으로 계산을 수행해보자. 먼저 데이터를 입력하고 데이터프레임 형태로 저장한다.
import pandas as pd
import numpy as np
d = {'X1': pd.Series([0,2,0,0,-1,1]),
'X2': pd.Series([3,0,1,1,0,1]),
'X3': pd.Series([0,0,3,2,1,1]),
'Y': pd.Series(['Red','Red','Red','Green','Green','Red'])}
df = pd.DataFrame(d)
df.index = np.arange(1, len(df) + 1)
df
| X1 | X2 | X3 | Y | |
|---|---|---|---|---|
| 1 | 0 | 3 | 0 | Red |
| 2 | 2 | 0 | 0 | Red |
| 3 | 0 | 1 | 3 | Red |
| 4 | 0 | 1 | 2 | Green |
| 5 | -1 | 0 | 1 | Green |
| 6 | 1 | 1 | 1 | Red |
각 관측과 \((0,0,0)\) 사이의 유클리드 거리를 계산하여 distance라는 이름으로 저장한다.
df['distance']=np.sqrt(df['X1']**2+df['X2']**2+df['X3']**2)
df
| X1 | X2 | X3 | Y | distance | |
|---|---|---|---|---|---|
| 1 | 0 | 3 | 0 | Red | 3.000000 |
| 2 | 2 | 0 | 0 | Red | 2.000000 |
| 3 | 0 | 1 | 3 | Red | 3.162278 |
| 4 | 0 | 1 | 2 | Green | 2.236068 |
| 5 | -1 | 0 | 1 | Green | 1.414214 |
| 6 | 1 | 1 | 1 | Red | 1.732051 |
이제 \(K=1\)일 때, 테스트 데이터 \((0, 0, 0)\)의 반응값을 예측해보자. \(K=1\)은 가장 가까운 관측 1개를 이용한다는 것으로서 테스트 데이터와 가장 가까운 관측을 찾으면, 5번(Green) 관측이다. 따라서 테스트 데이터의 \(Y\) 값을 Green으로 예측하게 된다.
# sort_values() 메서드를 이용하여 'distance' 기준으로 정렬시킴
df.sort_values(['distance'])
| X1 | X2 | X3 | Y | distance | |
|---|---|---|---|---|---|
| 5 | -1 | 0 | 1 | Green | 1.414214 |
| 6 | 1 | 1 | 1 | Red | 1.732051 |
| 2 | 2 | 0 | 0 | Red | 2.000000 |
| 4 | 0 | 1 | 2 | Green | 2.236068 |
| 1 | 0 | 3 | 0 | Red | 3.000000 |
| 3 | 0 | 1 | 3 | Red | 3.162278 |
이번에는 \(K=3\)일 때, 테스트 데이터 \((0, 0, 0)\)의 반응값을 예측해보자. \(K=3\)은 가장 가까운 관측 3개를 이용한다는 것으로서 테스트 데이터와 가장 가까운 관측 3개를 찾으면, 5번(Green) 6번(Red) 2번(Red) 관측이기 때문에 다수결 원칙에 따라 테스트 데이터의 \(Y\)값을 Red로 예측한다.
ChatGPT Q&A#
ChatGPT의 연습문제 풀이(ISLP 2장 연습문제 Q7)

다음 문제를 풀어줘.(한글로)
The table below provides a training data set containing six observations,
three predictors, and one qualitative response variable.Obs. X1 X2 X3 Y 1 0 3 0 Red 2 2 0 0 Red 3 0 1 3 Red 4 0 1 2 Green 5 −1 0 1 Green 6 1 1 1 RedSuppose we wish to use this data set to make a prediction for Y when
X1 = X2 = X3 = 0 using K-nearest neighbors.(a) Compute the Euclidean distance between each observation and
the test point, X1 = X2 = X3 = 0.(b) What is our prediction with K = 1? Why?
(c) What is our prediction with K = 3? Why?
(d) If the Bayes decision boundary in this problem is highly nonlinear,
then would we expect the best value for K to be large or
small? Why?
이 문제는 K-최근접 이웃(K-Nearest Neighbors, KNN) 알고리즘을 사용하여 주어진 데이터 세트를 바탕으로 새로운 관측값에 대한 예측을 하는 것입니다. 주어진 데이터 세트는 6개의 관측치와 3개의 예측변수(X1, X2, X3) 및 1개의 질적 반응 변수(Y)를 가지고 있습니다. 새로운 관측값은 X1 = X2 = X3 = 0입니다.
(a) 유클리디안 거리 계산: 유클리디안 거리는 두 점 사이의 직선 거리를 계산하는 방법입니다. 각 관측치와 X1 = X2 = X3 = 0 사이의 거리는 다음과 같이 계산됩니다.
관측치 1: \(\sqrt{(0-0)^2 + (3-0)^2 + (0-0)^2} = \sqrt{9} = 3\)
관측치 2: \(\sqrt{(2-0)^2 + (0-0)^2 + (0-0)^2} = \sqrt{4} = 2\)
관측치 3: \(\sqrt{(0-0)^2 + (1-0)^2 + (3-0)^2} = \sqrt{10}\)
관측치 4: \(\sqrt{(0-0)^2 + (1-0)^2 + (2-0)^2} = \sqrt{5}\)
관측치 5: \(\sqrt{(-1-0)^2 + (0-0)^2 + (1-0)^2} = \sqrt{2}\)
관측치 6: \(\sqrt{(1-0)^2 + (1-0)^2 + (1-0)^2} = \sqrt{3}\)
(b) K = 1일 때의 예측: K=1일 때, 가장 가까운 하나의 이웃을 기반으로 예측합니다. 위의 계산에 따르면, 가장 가까운 이웃은 관측치 5입니다(거리 = \(\sqrt{2}\)). 관측치 5의 Y 값은 Green이므로, 예측값은 Green입니다.
(c) K = 3일 때의 예측: K=3일 때, 가장 가까운 세 개의 이웃을 기반으로 예측합니다. 가장 가까운 세 관측치는 5번(\(\sqrt{2}\)), 6번(\(\sqrt{3}\)), 그리고 2번(\(\sqrt{4}\))입니다. 이들의 Y 값은 각각 Green, Red, Red입니다. 다수결에 의해, 예측값은 Red가 됩니다.
(d) 베이즈 결정 경계가 매우 비선형인 경우, K의 최적값: 베이즈 결정 경계가 매우 비선형인 경우, 작은 K 값을 사용하는 것이 일반적으로 더 좋습니다. 비선형 결정 경계는 로컬한 변화가 많은 복잡한 패턴을 가지고 있기 때문에, 작은 K 값은 이러한 로컬한 패턴을 더 잘 캡처할 수 있습니다. 반면에 큰 K 값은 경계를 너무 단순화하여 복잡한 패턴을 무시할 수 있습니다.
8.2 데이터 분석 예제#
자료: gperaza/ISLR-Python-Labs 및 emredjan/ISL-python
주식시장 데이터#
ISLP에서 제공하는 Smarket 데이터세트는 2001년 초부터 2005년 말까지 1,250일 동안 S&P 500 주가지수의 수익률(%)을 기록한 것이다.
Today는 각 날짜의 수익률이고, 각 날짜에 대해 이전 5개 거래일의 수익률을 Lag1부터 Lag5까지의 이름으로 기록했다. 또한 Volume은 전날 거래된 주식 수(단위: 십억)이고, Direction은 해당 날짜에 시장이 상승(Up) 또는 하락(Down)했는지를 나타낸다. Year는 각 날짜가 속한 연도이다. 앞 장의 로지스틱 회귀에 의한 분류에서 이 데이터세트를 분석했기 때문에 아래 KNN 분류의 성과를 그것과 비교해보기 바란다.
import pandas as pd
import numpy as np
Smarket = pd.read_csv('../Data/Smarket.csv')
Smarket.head()
| Year | Lag1 | Lag2 | Lag3 | Lag4 | Lag5 | Volume | Today | Direction | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2001 | 0.381 | -0.192 | -2.624 | -1.055 | 5.010 | 1.1913 | 0.959 | Up |
| 1 | 2001 | 0.959 | 0.381 | -0.192 | -2.624 | -1.055 | 1.2965 | 1.032 | Up |
| 2 | 2001 | 1.032 | 0.959 | 0.381 | -0.192 | -2.624 | 1.4112 | -0.623 | Down |
| 3 | 2001 | -0.623 | 1.032 | 0.959 | 0.381 | -0.192 | 1.2760 | 0.614 | Up |
| 4 | 2001 | 0.614 | -0.623 | 1.032 | 0.959 | 0.381 | 1.2057 | 0.213 | Up |
Smarket.shape
(1250, 9)
KNN 분류#
KNN 접근의 예측 정확도를 제대로 평가하기 위해서는 데이터의 일부만을 사용하여 모델을 피팅한 다음, 훈련에 사용하지 않은 나머지 데이터에 대해 얼마나 잘 예측하는지를 조사해 봐야 한다. 즉 모델을 피팅하는 데 사용한 데이터가 아니라 시장의 움직임을 알 수 없는 미래의 날짜에 대해 모델의 성능을 평가해 볼 필요가 있다.
이를 위해 2001년부터 2004년까지의 관측을 훈련 세트로 하고, 2005년의 관측을 테스트 세트로 한다. 또한 예측변수로는 앞 장에서와 마찬가지로 Lag1, Lag2 변수만 사용해보자.
train_df = Smarket[Smarket.Year < 2005]
test_df = Smarket[Smarket.Year == 2005]
X_train = train_df[['Lag1','Lag2']]
y_train = train_df['Direction']
X_test = test_df[['Lag1','Lag2']]
y_test = test_df['Direction']
KNeighborsClassifier 함수
이제 sklearn 모듈의 KNeighborsClassifier 함수를 사용하여 KNN을 수행해보자. 그런 다음, 테스트 세트의 관측 중 얼마만큼이 정확하게 분류되었고 얼마만큼이 잘못 분류되었는지를 혼동행렬(confusion matrix)로 파악한다.(혼동행렬에 대해서는 7장 부록 참조.)
우선 \(K=1\)을 수행한다. 모델을 피팅하려면 예측변수(X_train)와 출력변수(y_train)를 입력해야 한다. 그 피팅 결과를 바탕으로 테스트 세트 관측(X_test)에 대해 범주를 예측하여, 그것을 테스트 세트 실제 범주(y_test)와 비교하는 절차를 거치게 된다.
from sklearn.metrics import confusion_matrix, classification_report, precision_score
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=1)
pred = knn.fit(X_train, y_train).predict(X_test)
ChatGPT Q&A#
다음 코드를 설명해줘.
knn = KNeighborsClassifier(n_neighbors=1) pred = knn.fit(X_train, y_train).predict(X_test)
이 코드는 K-최근접 이웃(K-Nearest Neighbors, KNN) 알고리즘을 사용하여 분류 문제를 해결하기 위한 파이썬 코드의 일부입니다. 코드는
scikit-learn라이브러리의KNeighborsClassifier클래스를 사용하고 있습니다. KNN은 지도 학습의 일종으로, 주어진 데이터 포인트가 주변의 가장 가까운 k개의 이웃 데이터 포인트의 레이블을 기반으로 분류되는 알고리즘입니다.코드의 각 부분을 살펴보겠습니다:
knn = KNeighborsClassifier(n_neighbors=1):KNeighborsClassifier클래스의 인스턴스를 생성하고 있습니다.n_neighbors=1매개변수는 가장 가까운 이웃 하나만을 사용하여 분류를 결정하겠다는 것을 의미합니다. 즉, 주어진 데이터 포인트에 가장 가까운 하나의 이웃을 찾고, 그 이웃의 레이블로 데이터 포인트를 분류합니다.
pred = knn.fit(X_train, y_train).predict(X_test): 이 코드 라인은 두 가지 작업을 수행합니다.
knn.fit(X_train, y_train):fit메서드를 사용하여 모델을 학습 데이터X_train과 해당 레이블y_train에 맞춥니다. 이 과정에서 모델은 학습 데이터의 각 데이터 포인트와 그 레이블 간의 관계를 학습합니다.
.predict(X_test): 학습된 모델을 사용하여 테스트 데이터X_test에 대한 예측을 수행합니다. 이 메서드는X_test의 각 데이터 포인트에 대해 가장 가까운 이웃을 찾고, 그 이웃의 레이블을 기반으로 예측 레이블을 생성합니다.결과적으로, 이 코드는 학습 데이터 세트를 사용하여 KNN 모델을 학습시키고, 학습된 모델을 테스트 데이터 세트에 적용하여 예측을 수행합니다. 여기서는 K=1, 즉 가장 가까운 하나의 이웃을 사용하여 예측을 결정합니다.
pred변수에는X_test데이터 포인트 각각에 대한 예측된 레이블이 저장됩니다.
혼동행렬#
cm = confusion_matrix(y_test, pred) # 혼동행렬 작성
cm_df = pd.DataFrame(cm, index=['Down','Up'], columns=['Down','Up'])
cm_df.index.name = 'True'
cm_df.columns.name = 'Predicted'
print(cm_df)
print()
print(classification_report(y_test, pred, digits=3)) # 정밀도 등
Predicted Down Up
True
Down 43 68
Up 58 83
precision recall f1-score support
Down 0.426 0.387 0.406 111
Up 0.550 0.589 0.568 141
accuracy 0.500 252
macro avg 0.488 0.488 0.487 252
weighted avg 0.495 0.500 0.497 252
\(K = 1\)을 사용한 결과는 그다지 좋지 않다. 관측의 50.0%만 올바르게 예측되었기 때문이다. \(K = 1\)이 데이터에 지나치게 유연하게 피팅할 수 있기 때문에 이번에는 \(K = 3\)을 사용하여 분석해보자.
knn = KNeighborsClassifier(n_neighbors=3)
pred = knn.fit(X_train, y_train).predict(X_test)
print(confusion_matrix(y_test, pred))
print(classification_report(y_test, pred, digits=3))
[[48 63]
[55 86]]
precision recall f1-score support
Down 0.466 0.432 0.449 111
Up 0.577 0.610 0.593 141
accuracy 0.532 252
macro avg 0.522 0.521 0.521 252
weighted avg 0.528 0.532 0.529 252
정확도가 53.2%로 약간 개선되었다. 그러나 \(K\)를 더 늘려도 결과가 더 이상 개선되지 않는 것으로 나타났다.
캐러밴 보험 데이터#
ISLP에서 제공하는 Caravan 데이터세트는 캐러밴 보험(caravan insurance) 가입과 관련된 데이터이다. 조사대상은 총 5,822명이고, 이들에 대해 인구통계적(demographic) 특성과 관련된 85개의 예측변수가 들어있다. 반응변수는 Purchase로서 각 개인이 캐러밴 보험을 구매했는지 여부를 나타낸다.
앞 장의 로지스틱 회귀에 의한 분류에서 이 데이터세트를 분석했기 때문에 아래 KNN 분류의 성과를 그것과 비교해보기 바란다.
Caravan = pd.read_csv('../Data/Caravan.csv')
Caravan.Purchase = Caravan.Purchase.astype('category')
Caravan.head()
| MOSTYPE | MAANTHUI | MGEMOMV | MGEMLEEF | MOSHOOFD | MGODRK | MGODPR | MGODOV | MGODGE | MRELGE | ... | APERSONG | AGEZONG | AWAOREG | ABRAND | AZEILPL | APLEZIER | AFIETS | AINBOED | ABYSTAND | Purchase | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 33 | 1 | 3 | 2 | 8 | 0 | 5 | 1 | 3 | 7 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | No |
| 1 | 37 | 1 | 2 | 2 | 8 | 1 | 4 | 1 | 4 | 6 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | No |
| 2 | 37 | 1 | 2 | 2 | 8 | 0 | 4 | 2 | 4 | 3 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | No |
| 3 | 9 | 1 | 3 | 3 | 3 | 2 | 3 | 2 | 4 | 5 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | No |
| 4 | 40 | 1 | 4 | 2 | 10 | 1 | 4 | 1 | 4 | 7 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | No |
5 rows × 86 columns
Caravan.Purchase.value_counts()
Purchase
No 5474
Yes 348
Name: count, dtype: int64
데이터 표준화#
KNN 분류는 가장 가까운 관측을 식별하여 주어진 테스트 데이터의 범주를 예측하기 때문에 변수의 척도(scale)가 중요하다. 척도가 큰 변수는 척도가 작은 변수보다 관측 사이의 거리가 더 크기 때문에 KNN 분류에 더 큰 영향을 미친다.
예를 들어, 월급(달러로 측정)과 나이(연도로 측정)라는 두 가지 변수가 포함된 데이터세트를 생각해보자. 이 경우 어떤 두 사람간에 월급이 1,000달러 차이나는 것은 나이가 50살 차이나는 것에 비해 KNN 분류 작업에서 훨씬 크게 받아들여진다. 결과적으로 월급은 KNN 분류에 큰 영향을 미치는 반면, 나이는 거의 영향을 미치지 않게 된다. 이것은 50살의 나이 차이에 비해 월급 차이 1,000달러는 별로 크지 않다는 우리의 직관과는 상반된다.
KNN 분류에서 척도의 중요성은 또 다른 문제로 이어진다. 즉 월급을 일본 엔으로 측정하거나 나이를 분 단위로 측정하면 이 두 변수를 달러와 년으로 측정하여 얻은 것과는 상당히 다른 결과로 이어질 수 있다. 이 문제를 처리하는 좋은 방법은 모든 변수에 대해 평균이 0이고 표준편차가 1이 되도록 데이터를 표준화(standardization) 하는 것이다. 그러면 모든 변수가 비슷한 척도를 갖게 된다.
데이터세트에서 처음 85개 변수를 예측변수(X)로 하고, Purchase 변수를 반응변수(y)로 설정한다. 이와 함께, X 변수에 대해 apply(zscore)를 사용하여 데이터를 표준화시켰다. 이때 반응변수이자 정성적 변수인 Purchase 변수(86번째 열)는 표준화를 시키지 않았다.
from scipy.stats import zscore
X = Caravan.iloc[:, 0:85].apply(zscore) # standardize for KNN
y = Caravan.Purchase
처음 1,000개의 관측을 테스트 세트로 하고 나머지 관측을 훈련 세트로 분할한다.
# X와 y에서 처음 1,000개를 테스트 세트로 지정
X_test = X.iloc[:1000].values
y_test = y.iloc[:1000]
# X와 y에서 나머지를 훈련 세트로 지정
X_train = X.iloc[1000:].values
y_train = y.iloc[1000:]
테스트 관측 1,000명 중 캐러밴 보험에 가입한 비율은 5.9%임을 확인할 수 있다.
y_test.value_counts()['Yes']/len(y_test)
0.059
KNN 분류#
\(K = 1\)을 사용하여 훈련 세트에 KNN 모델을 피팅하고 테스트 세트에 대해 추정 성과를 평가한다.
knn_model = KNeighborsClassifier(n_neighbors=1)
knn_fit1 = knn_model.fit(X_train, y_train)
cm = confusion_matrix(y_test, knn_fit1.predict(X_test))
cm_df = pd.DataFrame(cm, index=['No','Yes'], columns=['No','Yes'])
cm_df.index.name = 'True'
cm_df.columns.name = 'Predicted'
print(cm_df)
print()
print(classification_report(y_test, knn_fit1.predict(X_test), digits=3))
Predicted No Yes
True
No 873 68
Yes 50 9
precision recall f1-score support
No 0.946 0.928 0.937 941
Yes 0.117 0.153 0.132 59
accuracy 0.882 1000
macro avg 0.531 0.540 0.535 1000
weighted avg 0.897 0.882 0.889 1000
1,000명의 테스트 관측에 대한 KNN 오류율은 11.8%이다. 괜찮은 성과로 보일 수 있지만 테스트 관측 고객의 5.9%만이 보험을 구매했기 때문에 예측변수의 값에 상관없이 항상 No로 예측하면 오류율을 5.9%까지 낮출 수 있어 이것은 좋은 성과라 할 수 없다.
그런데 보험을 판매하는 데 어느 정도 비용이 든다고 생각해보자. 예를 들어, 영업 사원이 각 잠재 고객을 방문하는 데는 비용이 든다. 회사가 무작위로 선택한 고객에게 보험을 판매하려고 하면 성공률은 5.9%에 불과하다. 관련 비용을 고려하면 이 정도의 성공률은 너무 낮은 편이다. 이러한 상황에서 보험을 구매할 가능성이 있는 고객에게만 보험을 판매하는 전략을 생각해보자. 이 경우 전체 오류율은 중요하지 않고, 보험을 구매할 것으로 예측된 사람들에 대한 오류율이 중요하다.
이렇게 생각한다면 \(K=1\)인 KNN은 보험을 구매할 것으로 예상되는 고객에 대해서는 무작위 추측(random guessing)보다는 결과가 훨씬 낫다. 위 결과를 보면, 77명의 보험가입 예상 고객 중 9명(11.7%)이 실제로 보험에 가입했기 때문이다. 이것은 무작위 추측으로 얻을 수 있는 성과(즉 5.9%)의 약 두 배에 달한다.
print('K=1일 때, \'Yes\'에 대한 정밀도는 %0.1f%%이다.'
% (100*precision_score(y_test, knn_fit1.predict(X_test), pos_label='Yes')))
K=1일 때, 'Yes'에 대한 정밀도는 11.7%이다.
knn_model = KNeighborsClassifier(n_neighbors=3)
knn_fit3 = knn_model.fit(X_train, y_train)
print('K=3일 때, \'Yes\'에 대한 정밀도는 %0.1f%%이다.'
% (100*precision_score(y_test, knn_fit3.predict(X_test), pos_label='Yes')))
K=3일 때, 'Yes'에 대한 정밀도는 20.0%이다.
knn_model = KNeighborsClassifier(n_neighbors=5)
knn_fit5 = knn_model.fit(X_train, y_train)
print('K=5일 때, \'Yes\'에 대한 정밀도는 %0.1f%%이다.'
% (100*precision_score(y_test, knn_fit5.predict(X_test), pos_label='Yes')))
K=5일 때, 'Yes'에 대한 정밀도는 28.6%이다.
위 결과에서 보듯이 \(K = 3\)을 사용하면, 보험가입 예상 고객 중 실제로 보험에 가입한 비율, 즉 Yes에 대한 정밀도(precision)가 20.0%로 증가하고, \(K = 5\)이면 28.6%가 된다. 이것은 무작위 추측 결과의 5배 가까이 되는 수준이다.