PYTHON/데이터분석

2-2 데이터 전처리

sshhhh 2023. 9. 15.

#넘파이로 데이터 준비하기

 

생선데이터

fish_length = [25.4, 26.3, 26.5, 29.0, 29.0, 29.7, 29.7, 30.0, 30.0, 30.7, 31.0, 31.0, 
                31.5, 32.0, 32.0, 32.0, 33.0, 33.0, 33.5, 33.5, 34.0, 34.0, 34.5, 35.0, 
                35.0, 35.0, 35.0, 36.0, 36.0, 37.0, 38.5, 38.5, 39.5, 41.0, 41.0, 9.8, 
                10.5, 10.6, 11.0, 11.2, 11.3, 11.8, 11.8, 12.0, 12.2, 12.4, 13.0, 14.3, 15.0]
fish_weight = [242.0, 290.0, 340.0, 363.0, 430.0, 450.0, 500.0, 390.0, 450.0, 500.0, 475.0, 500.0, 
                500.0, 340.0, 600.0, 600.0, 700.0, 700.0, 610.0, 650.0, 575.0, 685.0, 620.0, 680.0, 
                700.0, 725.0, 720.0, 714.0, 850.0, 1000.0, 920.0, 955.0, 925.0, 975.0, 950.0, 6.7, 
                7.5, 7.0, 9.7, 9.8, 8.7, 10.0, 9.9, 9.8, 12.2, 13.4, 12.2, 19.7, 19.9]

넘파이를 배웠으니 리스트 쓸 필요 없다.

import numpy as np

넘파이의 column_stack() 함수는 전달받은 리스트를 일렬로 세운 다음 차례대로 나란히 연결한다.

예를 들어 간단한 2개의 리스트를 나란히 붙여본다.

연결할 리스트는 파이썬 튜플로 전달합니다.

 

※ 리스트 : 순서가 있다 

   튜플 : 순서 있고 수정할 수 없다..

np.column_stack(([1,2,3], [4,5,6]))

"""
array([[1, 4],
       [2, 5],
       [3, 6]])
       
"""

 

그럼 이제 fish_length와 fish_weight를 합치자

fish_data = np.column_stack((fish_length, fish_weight))

잘 연결되었는지 처음 5개의 데이터 확인

print(fish_data[:5])
"""
[[ 25.4 242. ]
 [ 26.3 290. ]
 [ 26.5 340. ]
 [ 29.  363. ]
 [ 29.  430. ]]

"""

잘 됐다..

넘파이 배열을 출력하면 리스트처럼 한줄로 길게 출력되지 않고

행과 열을 맞추어 가지런히 정리된 모습으로 보여준다.

5개 행 , 2개 열이 있다는 것을 알 수 있다.

 

 

 

동일한 방법으로 타깃 데이터도 만들자

이전에는 원소가 하나인 리스트 [1],[0]을 여러번 곱해서 타깃 데이터를 만들었다. 넘파이로 더 나은 방법을 써보자

np.ones()와 np.zeros()함수로 각각 원하는 개수의 1과 0을 채운 배열을 만들 수 있다.

print(np.ones(5))
#[1. 1. 1. 1. 1.]

 

1이 35개인 배열과 0이 14개인 배열 만든 후 두 배열을 그대로 연결한다.

np.concatenate()함수를 사용해 타깃데이터를 만든다.

fish_target = np.concatenate((np.ones(35), np.zeros(14)))
print(fish_target)
"""
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0.]
 """

 

 

#사이킷런으로 훈련 세트와 테스트 세트 나누기

앞에서는 넘파이 배열의 인덱스를 직접 섞어서 훈련세트와 테스트세트로 나누었다.

번거롭다.

 

사이킷런은 머신러닝 모델을 위한 알고리즘뿐만 아니라 다양한 유틸리티 도구도 제공한다.

대표적인 도구가 train_test_split() 함수이다.

이 함수는 전달되는 리스트나 배열을 비율에 맞게 훈련세트와 테스트세트로 나누어준다.

물론 나누기전에 알아서 섞어준다.

 

 

 

train_test_split() 함수는 사이킷런의 model_selection 모듈 아래 있다.

from sklearn.model_selection import train_test_split

 

사용법 : 나누고 싶은 리스트나 배열을 원하는 만큼 전달한다.

자체적으로 랜덤 시드를 정할 수 있는 random_state 매개변수가 있다.

다음과 같이 훈련세트와 테스트세트를 나눈다.

 

train_input 훈련 입력데이터 

test_input  테스트 입력데이터

train_target 훈련 타겟데이터

test_target  테스트 타겟데이터

train_input, test_input, train_target, test_target #훈련 , 테스트 
= train_test_split(fish_data, fish_target, random_state=42) #랜덤

fish_data와  fish_target 2개의 배열을 전달했으므로 2개씩 나뉘어 4개의 배열이 반환된다.

기본적으로 25%를 테스트 세트로 떼어낸다.

 

 

잘 나누었는지 넘파이 배열의 shape속성으로 입력 데이터의 크기를 출력하겠다.

print(train_input.shape, test_input.shape)

#(36, 2) (13, 2) (행,열) 2차원배열
print(train_target.shape, test_target.shape)

#(36,) (13,) 1차원배열

 

도미와 빙어가 잘 섞였는지 테스트 데이터 출력

13개의 테스트 세트중 10개가 도미(1) 이고 3개가 빙어(0)이다..

print(test_target)

#[1. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

 

제대로 섞이지 않는 샘플링 편향 발생

이렇게 무작위로 데이터 나누었을때 골고루 섞이지 않는다. 특히 일부 클래스의 개수가 적을때 이런일이 생길 수 있다.

stratify 매개변수에 타깃 데이터를 전달하면 클래스 비율에 맞게 데이터를 나눈다.

훈련 데이터가 작거나 특정 클래스의 샘플개수가 적을때 유용하다.

train_input, test_input, train_target, test_target 
= train_test_split(fish_data, fish_target, stratify=fish_target, random_state=42)

빙어가 늘었다.. 테스트 세트 비율이 2.25:1 완료

print(test_target)

#[0. 0. 1. 0. 1. 0. 1. 1. 1. 1. 1. 1. 1.]

 

 

#수상한 도미 한마리

 

KNN 훈련하기(훈련 데이터를 저장하는 것이 훈련의 전부다)

훈련데이터로 모델을 훈련하고 테스트 데이터로 모델을 평가한다.

 

테스트 세트의 도미와 빙어를 모두 올바르게 분류했다.

from sklearn.neighbors import KNeighborsClassifier

kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)

#1.0

 

이 모델에 도미 데이터를 넣고 결과 확인해보기 

근데 틀렸음

print(kn.predict([[25, 150]]))
#[0.]

 

어찌 된 일인지 산점도로 확인

import matplotlib.pyplot as plt
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

왜 빙어에 가까울까 ?

 

KNN은 주변의 샘플 중에서 다수인 클래스를 예측으로 사용한다.

KNeighborsClassifier클래스는 주어진 샘플에서 가장 가까운 이웃을 찾아주는 kneighbors()메서드를 제공한다.

이 메서드는 이웃까지의 거리와 이웃 샘플의 인덱스를 반환한다.

KNeighborsClassifier클래스의 이웃개수인 n_neighbors의 기본값은 5이므로 5개의 이웃이 반환된다.

distances, indexes = kn.kneighbors([[25, 150]])

 

indexes배열을 사용해 훈련 데이터 중에서 이웃 샘플을 따로 구분해 그린다.

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

삼각형 샘플에 가장 가까운 5개의 샘플이 초록다이아몬드로 표시되었다.

역시 가장 가까운 이웃에 도미가 하나밖에 포함되지 않았다.

데이터를 직접확인하겠다.

print(train_input[indexes])
"""
[[[ 25.4 242. ]
  [ 15.   19.9]
  [ 14.3  19.7]
  [ 13.   12.2]
  [ 12.2  12.2]]]
  """

타겟 데이터로 확인하면 더 명확하다.

print(train_target[indexes])
#[[1. 0. 0. 0. 0.]]

따라서 빙어라고 예측하는것에 무리가 없어보인다.. 왜 가까운 이웃을 빙어라 할까?

산점도를 보면 직관적으로 도미와 가깝게 보이는데..

 

이 문제의 실마리를 찾기위해 kneighbors()에서 반환한 distances 배열을 출력하겠다.

이 배열에는 이웃 샘플까지의 거리가 담겨 있다.

print(distances)
#[[ 92.00086956 130.48375378 130.73859415 138.32150953 138.39320793]]

이상하다!

 

 

 

#기준을 맞춰라

삼각형 샘플에 가장 가까운 샘플까지의 거리는 92이고,

그 외 가장 가까운 샘플은 130,138이다. 근데 그래프에 나타난 거리 비율이 이상하다.

어림짐작으로 보아도 92의 거리보다 족히 몇배는 되어보임..

-> x축은 범위가 좁고(10~40) , y축은 범위가 (0~1000)이기 때문이다. 

    따라서 y축으로 조금만 멀어져도 거리가 아주 큰 값으로 계산될 것이다. 

    이때문에 오른쪽 위의 도미 샘플이 이웃으로 선택되지 못했던 것이다.

 

 

x축의 범위를 동일하게 0~1000으로 맞추겠다.

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker='D')
plt.xlim((0, 1000)) #맞추기
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

산점도가 거의 일직선으로 나타난다.

x축과 y축의 범위를 동일하게 맞추었더니 모든 데이터가 수직으로 늘어선 형태가 되었다.

이런 데이터라면 생선의 길이는 가장 가까운 이웃을 찾는데 크게 영향을 미치지 못한다.

오로지 생선의 무게만 고려대상이 된다.

 

두 특성(길이,무게)의 값이 놓인 범위가 매우 다르다. 스케일이 다르다고 말한다. 흔한일이다.

(생선을 cm로 잴지.. mm로 잴지.....)

데이터를 표현하는 기준이 다르면 알고리즘이 올바르게 예측할 수 없다.

알고리즘이 거리 기반일때 특히 그렇다. KNN도 포함된다.

이런 알고리즘들은 샘플간의 거리에 영향을 많이 받으므로

제대로 사용하려면 특성값을 일정한 기준으로 맞춰주어야한다.

이러한 작업을 데이터 전처리라고한다.

 

가장 널리 사용하는 전처리 방법  - 표준점수

각 특성값이 평균에서 표준편차의 몇배만큼 떨어져있는지 나타낸다.

이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교할 수 있다.

계산법: 평균빼고 표준편차 나누기

 

mean = np.mean(train_input, axis=0)#평균계산 / axis=0 : 각 특성별로 계산해야 하므로 행을 따라 각열의 통계값 계산
std = np.std(train_input, axis=0) #표준편차계산
print(mean, std)
#[ 27.29722222 454.09722222] [  9.98244253 323.29893931]

표준점수

train_scaled = (train_input - mean) / std

 

 

#전처리 데이터로 모델 훈련하기

 

plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

값의 범위가 달라져서 다시

 

new = ([25, 150] - mean) / std
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(new[0], new[1], marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

표준편차로 변환하기 전의 산점도와 거의 동일해졌다.

이 데이터 셋으로 다시 KNN 훈련 하겠다.

 

kn.fit(train_scaled, train_target)

테스트세트도 훈련세트의 평균과 표준편차로 변환해야한다,.

그렇지 않다면 데이터의 스케일이 같아지지 않으므로 훈련한 모델이 쓸모없게 된다.

 

테스트 세트의 스케일을 변환하기

test_scaled = (test_input - mean) / std

모델 평가하기 - good~

kn.score(test_scaled, test_target)
#1.0

드디어 도미로 예측함

print(kn.predict([new]))
#[1.]

 

kneighbors() 함수로 이 샘플의 KNN 구한 다음 산점도로 그린다.

특성을 표준점수로 바꿨기 때문에 올바르게 거리를 측정했을 거고 가장 가까운 이웃이 변화했을 것.

plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(new[0], new[1], marker='^')
plt.scatter(train_scaled[indexes,0], train_scaled[indexes,1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

 

결론

2-1 에서 완벽하게 테스트세트를 분류했다. 근데 엉뚱하게 빙어라고 예측했다..

근데 그래프로 그려보면 샘플은 도미에 가깝다.

 

이는 샘플의 두 특성인 길이와 무게의 스케일이 다르기 때문이다.

그래서 특성을 표준점수로 변환했다.

 

그 후 테스트 세트도 똑같이 변환한다.

'PYTHON > 데이터분석' 카테고리의 다른 글

3-2 선형회귀  (0) 2023.09.15
3-1 KNN 회귀  (0) 2023.09.15
2-1 훈련세트와 테스트 세트  (0) 2023.09.15
1-3 마켓과 머신러닝  (0) 2023.09.15
통계 데이터 분석 -KNN회귀  (0) 2023.09.15

댓글