[EDA] 포켓몬으로 살펴보는 피처 엔지니어링 2

Ethan·2022년 5월 27일
0

전편에 이어서 작성한다.


데이터 전처리하기

EDA를 통해 어느 정도 데이터를 살펴봤다면, 얻은 정보나 가설 등을 기반으로 데이터를 모델에 사용하기 좋게 전처리를 해주어야 한다.


Name >= 10 ?

앞서 이름의 길이가 10 이상인 포켓몬의 비율을 살펴봤는데, 모델은 문자열을 읽지 못하므로 이를 수치형 데이터로 변환해 준다.

# 이름 길이 정보를 가진 컬럼 생성
pokemon["name_count"] = pokemon["Name"].apply(lambda i: len(i))

# 이름의 길이가 10 이상인지를 알려주는 컬럼 생성
pokemon["long_name"] = pokemon["name_count"] >= 10

이름 토큰 추출

Name은 unique하지만, 공유하는 특정 단어들이 있다.
따라서 특정 토큰을 가지고 있으면 전설 포켓몬을 판별할 수 있을지도 모른다.

포켓몬 이름 데이터는 크게 4가지가 있다.

  • 1개 단어로 이뤄진 경우 (e.g. Venusaur)
  • 2개 단어 + 대문자로 구분 (e.g. VenusaurMega Venusaur)
  • 2개 단어 + 맨 뒤에서 성별 표시 (e.g. CharizardMega Charizard X)
  • 알파벳이 아닌 문자 포함 (e.g. Zygarde50% Forme)

알파벳이 아닌 문자를 포함하는 경우

import re

# 알파벳이 아닌 문자 포함
# 띄어쓰기 제거
pokemon["Name_nospace"] = pokemon["Name"].apply(lambda i: i.replace(" ", ""))
pokemon["name_isalpha"] = pokemon["Name_nospace"].apply(lambda i: i.isalpha())

# 나머진 직접 처리
pokemon = pokemon.replace(to_replace="Nidoran♀", value="Nidoran X")
pokemon = pokemon.replace(to_replace="Nidoran♂", value="Nidoran Y")
pokemon = pokemon.replace(to_replace="Farfetch'd", value="Farfetchd")
pokemon = pokemon.replace(to_replace="Mr. Mime", value="Mr Mime")
pokemon = pokemon.replace(to_replace="Porygon2", value="Porygon")
pokemon = pokemon.replace(to_replace="Ho-oh", value="Ho Oh")
pokemon = pokemon.replace(to_replace="Mime Jr.", value="Mime Jr")
pokemon = pokemon.replace(to_replace="Porygon-Z", value="Porygon Z")
pokemon = pokemon.replace(to_replace="Zygarde50% Forme", value="Zygarde Forme")

# nospace 컬럼 생성
pokemon["Name_nospace"] = pokemon["Name"].apply(lambda i: i.replace(" ", ""))
pokemon["name_isalpha"] = pokemon["Name_nospace"].apply(lambda i: i.isalpha())

2개 단어 + 대문자로 구분된 경우

def tokenize(name):
    name_split = name.split(" ")
    
    tokens = []
    for part_name in name_split:
        a = re.findall('[A-Z][a-z]*', part_name)
        tokens.extend(a)
        
    return np.array(tokens)

all_tokens = list(legendary["Name"].apply(tokenize).values)

token_set = []
for token in all_tokens:
    token_set.extend(token)

print(len(set(token_set)))
>>> 65

총 65개의 토큰을 확인할 수 있다. 이 중에서 자주 사용된 토큰을 추출한다.

# TOP 10
most_common = Counter(token_set).most_common(10)
most_common
>>> [('Forme', 15),
     ('Mega', 6),
     ('Mewtwo', 5),
     ('Kyurem', 5),
     ('Deoxys', 4),
     ('Hoopa', 4),
     ('Latias', 3),
     ('Latios', 3),
     ('Kyogre', 3),
     ('Groudon', 3)]

Forme, Mega 등이 가장 자주 쓰이는 단어임을 확인할 수 있다.
전설 포켓몬은 기존 포켓몬에서 추가적인 진화나 형태변화한 경우가 많은 듯하다.

# 각 토큰별로 컬럼 생성
for token, _ in most_common:
    # pokemon[token] = ... 형식으로 사용하면 뒤에서 warning이 발생
    pokemon[f"{token}"] = pokemon["Name"].str.contains(token)

이렇게 하면 토큰별로 True / False를 판별할 수 있다.


Type 1 & 2 변환

해당 컬럼들은 속성 정보를 갖는 범주형 데이터들이므로, 원핫인코딩을 진행해준다.

for t in types:
    pokemon[t] = (pokemon["Type 1"] == t) | (pokemon["Type 2"] == t)

훈련 & 테스트 데이터 준비

# 원본 데이터 확인
original_data.columns
>>> Index(['#', 'Name', 'Type 1', 'Type 2', 'Total', 'HP', 'Attack', 'Defense',
       	   'Sp. Atk', 'Sp. Def', 'Speed', 'Generation', 'Legendary'],
           dtype='object')
           
# target, 불필요한 컬럼 제거
features = ['Total', 'HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed', 'Generation']
target = 'Legendary'

# X에 사용할 데이터를 담음
X = original_data[features]
print(X.shape)
>>> (800, 8)

# Y에 타겟 데이터를 담음
y = original_data[target]
print(y.shape)
>>> (800, )

# Train, Test 데이터 분리
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=15)

print(X_train.shape, y_train.shape)
>>> (640, 8) (640,)

print(X_test.shape, y_test.shape)
>>> (160, 8) (160,)

이상으로 전처리가 완료되었다.


베이스라인 모델 생성

베이스라인(Baseline): 별도의 처리를 하지 않은 데이터로 만들어진 모델.

현재 생성/사용중인 모델의 성능 가이드라인 역할을 함
= 구현한 모델의 성능이 적어도 베이스라인보다는 잘 나와야 정상

이번 포켓몬 문제에서는 decision tree 모델을 사용한다.

from sklearn.tree import DecisionTreeClassifier as dtc
from sklearn.metrics import confusion_matrix

# 재현을 위한 랜덤값 고정
model = dtc(random_state=25)

# 모델 훈련
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# 모델 평가
confusion_matrix(y_test, y_pred)
>>> array([[144,   3],
		   [  5,   8]])
  • Confusion Matrix Docs
  • 오차행렬 개념 설명

confusion_matrix 는 왼쪽 위부터 순서대로 TN, FP, FN, TP 값을 리턴한다.
이 경우 Positive는 전설 포켓몬, Negative는 일반 포켓몬에 해당한다.

Accuracy = (TN + TP) / N

이므로, 이 경우에는 152/160 = 95%의 정확도를 보인다.


Accuracy 95%면 대단한 거 아님?

print(len(legendary))
>>> 65

print(len(ordinary))
>>> 735

전설 포켓몬의 수가 일반 포켓몬에 비해 매우 적다.
즉, 800마리를 전부 일반 포켓몬이라고 판정해도 정확도가 91.8%가 나온다.

데이터의 수가 불균형하면 이런 문제가 생긴다.

from sklearn.metrics import classification_report as cr

print(cr(y_test, y_pred))
>>>              precision    recall  f1-score   support

       False       0.97      0.98      0.97       147
        True       0.73      0.62      0.67        13

    accuracy                           0.95       160
   macro avg       0.85      0.80      0.82       160
weighted avg       0.95      0.95      0.95       160

Recall = (TP) / (FN + TP)

recall 값이 0.62로 낮은 편이다. 이는 FN(=전설 포켓몬인데 일반이라고 판정)인 경우의 수가 많다는 의미가 된다.

따라서 이런 불균형 데이터에서는 Positive를 얼마나 잘 찾는지를 척도로 잡을 수 있다.

profile
재미있게 살고 싶은 대학원생

0개의 댓글