전편에 이어서 작성한다.
EDA를 통해 어느 정도 데이터를 살펴봤다면, 얻은 정보나 가설 등을 기반으로 데이터를 모델에 사용하기 좋게 전처리를 해주어야 한다.
앞서 이름의 길이가 10 이상인 포켓몬의 비율을 살펴봤는데, 모델은 문자열을 읽지 못하므로 이를 수치형 데이터로 변환해 준다.
# 이름 길이 정보를 가진 컬럼 생성
pokemon["name_count"] = pokemon["Name"].apply(lambda i: len(i))
# 이름의 길이가 10 이상인지를 알려주는 컬럼 생성
pokemon["long_name"] = pokemon["name_count"] >= 10
Name
은 unique하지만, 공유하는 특정 단어들이 있다.
따라서 특정 토큰을 가지고 있으면 전설 포켓몬을 판별할 수 있을지도 모른다.
포켓몬 이름 데이터는 크게 4가지가 있다.
Venusaur
)VenusaurMega Venusaur
)CharizardMega Charizard X
)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())
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를 판별할 수 있다.
해당 컬럼들은 속성 정보를 갖는 범주형 데이터들이므로, 원핫인코딩을 진행해준다.
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
는 왼쪽 위부터 순서대로 TN, FP, FN, TP 값을 리턴한다.
이 경우 Positive는 전설 포켓몬, Negative는 일반 포켓몬에 해당한다.
Accuracy = (TN + TP) / N
이므로, 이 경우에는 152/160 = 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를 얼마나 잘 찾는지를 척도로 잡을 수 있다.