오늘 펀더멘탈 노드는 어느정도 정리하는게 좋을것 같아서 회고 전에 내용정리를 먼저 하려고 한다.
데이터셋 : Kaggle 포켓몬스터 데이터
import os
csv_path = os.getenv("HOME") +"/aiffel/pokemon_eda/data/Pokemon.csv"
original_data = pd.read_csv(csv_path)
pokemon 변수에 원본 데이터를 복사하고 확인해보기(원본 데이터 훼손 방지)
pokemon = original_data.copy()
print(pokemon.shape)
pokemon.head()
우리는 legendary 변수를 target으로 설정할 것이므로 legendary 포켓몬이면 legendary 변수에, 일반 포켓몬이면 ordinary 변수에 저장
# 전설의 포켓몬 데이터셋
legendary = pokemon[pokemon["Legendary"] == True].reset_index(drop=True)
print(legendary.shape)
legendary.head()
# 일반 포켓몬 데이터셋
ordinary = pokemon[pokemon["Legendary"] == False].reset_index(drop=True)
print(ordinary.shape)
ordinary.head()
결측치 확인
pokemon.isnull().sum()
>> Type 2 의 결측치 : 386
전체 컬럼 확인
print(len(pokemon.columns)) >>13
pokemon.columns
Type1과 Type2의 종류는 몇가지인지 확인
len(list(set(pokemon["Type 1"]))), len(list(set(pokemon["Type 2"])))
>>18, 19
Type2가 1 더 큰데, 차집합을 확인해보자
set(pokemon["Type 2"]) - set(pokemon["Type 1"])
>> {nan}
18가지 속성은 type1과 type2에 동일하게 들어가 있다. type2에는 결측치만 더 있을뿐!
type에는 어떤게 있는지 확인해보자
types = list(set(pokemon["Type 1"]))
print(len(types))
print(types)
일반 포켓몬과 전설의 포켓몬의 type1을 sns.countplot을 이용해 시각화
plt.figure(figsize=(10, 7)) # 그래프 크기를 조정
plt.subplot(211)
sns.countplot(data=ordinary, x="Type 1", order=types).set_xlabel('')
plt.title("[Ordinary Pokemons]")
plt.subplot(212)
sns.countplot(data=legendary, x="Type 1", order=types).set_xlabel('')
plt.title("[Legendary Pokemons]")
plt.show()
pivot table로 각 속성의 전설의 포켓몬이 몇퍼센트나 있는지 확인
# Type1별로 Legendary 의 비율을 보여주는 피벗 테이블
pd.pivot_table(pokemon, index="Type 1", values="Legendary").sort_values(by=["Legendary"], ascending=False)
속성별 Total 값 분포를 plot해보자, 이때, legendary면 색상을 다르게 나타내었다.
fig, ax = plt.subplots()
fig.set_size_inches(12, 6) # 화면 해상도에 따라 그래프 크기를 조정해 주세요.
sns.scatterplot(data=pokemon, x="Type 1", y="Total", hue="Legendary")
plt.show()
확인 결과, 전설의 포켓몬은 스택 값이 주로 상위권에 위치함
각 세대별 포켓몬의 수 확인
plt.figure(figsize=(12, 10)) # 화면 해상도에 따라 그래프 크기를 조정해 주세요.
plt.subplot(211)
sns.countplot(data=ordinary, x="Generation").set_xlabel('')
plt.title("[All Pkemons]")
plt.subplot(212)
sns.countplot(data=legendary, x="Generation").set_xlabel('')
plt.title("[Legendary Pkemons]")
plt.show()
전설의 포켓몬들의 total 값 확인해보기
fig, ax = plt.subplots()
fig.set_size_inches(8, 4)
sns.scatterplot(data=legendary, y="Type 1", x="Total")
plt.show()
결과를 확인했을때, 특정 값이 몰려있는 경우가 많음
print(sorted(list(set(legendary["Total"]))))
정렬해서 확인해보면, 9개의 값이 출력됨.
전설의 포켓몬이 65마리인데 total값이 9개인것은 몰려있다고 해석할 수 있음!
그렇다면 일반 포켓몬은?
len(sorted(list(set(ordinary["Total"])))) >>195
735마리인데 total값이 195개 >> 상대적으로 덜 몰려있음
포켓몬의 total값이 전설의 포켓몬의 total값 집합에 포함된다면 >> 전설의 포켓몬일 확률 높음!!
따라서 total feature은 target을 나누는데 중요한 컬럼이다
이름 길이는 어떨지 궁금하다. 레전더리 데이터셋에 name_count라는 column을 새로 한번 만들어보자
legendary["name_count"] = legendary["Name"].apply(lambda i: len(i))
legendary.head()
ordinary데이터셋도 마찬가지로 만들어보자
ordinary["name_count"] = ordinary["Name"].apply(lambda i: len(i))
ordinary.head()
시각화를 통해 두 그룹을 비교
plt.figure(figsize=(12, 10))
plt.subplot(211)
sns.countplot(data=legendary, x="name_count").set_xlabel('')
plt.title("Legendary")
plt.subplot(212)
sns.countplot(data=ordinary, x="name_count").set_xlabel('')
plt.title("Ordinary")
plt.show()
대체적으로 전설의 포켓몬의 이름이 긴것으로 확인된다. 일반 포켓몬은 10글자 이상도 매우 적음!
그럼 이거를 시각화가 아니라 확률로 알아보자.
print(round(len(legendary[legendary["name_count"] > 9]) / len(legendary) * 100, 2), "%") #41.54%,
print(round(len(ordinary[ordinary["name_count"] > 9]) / len(ordinary) * 100, 2), "%") #15.65%
이름 길이 feature도 전설의 포켓몬을 구분하는데 중요한 변수인것 같다.
name_count column에서 이름의 길이가 10을 넘는지 유무를 categorical column을 생성해서 나타내보자. "long name" column에 새롭게 나타냈다.
pokemon["name_count"] = pokemon["Name"].apply(lambda i: len(i))
pokemon["long_name"] = pokemon["name_count"] >= 10
pokemon.head()
다음으로 전설의 포켓몬 이름에 많이 쓰이는 토큰을 알아보고 새로운 컬럼을 만들어보자.
우리는 isalpha를 통해 포켓몬 이름이 모두 알파벳으로 이루어져있는지 확인할건데, 띄어쓰기가 있는 경우에도 False로 출력되기 때문에, 우선 띄어쓰기가 없는 컬럼을 만들어야 한다.
pokemon["Name_nospace"] = pokemon["Name"].apply(lambda i: i.replace(" ", ""))
이제 isalpha 컬럼을 만들어보자
pokemon["name_isalpha"] = pokemon["Name_nospace"].apply(lambda i: i.isalpha())
이름에 알파벳이 아닌 다른 문자가 포함된 것들의 이름을 바꿔주자.
print(pokemon[pokemon["name_isalpha"] == False].shape) #9마리
pokemon[pokemon["name_isalpha"] == False]
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")
pokemon.loc[[34, 37, 90, 131, 252, 270, 487, 525, 794]] #확인
그럼 이제 정규표현식을 이용해 토큰화를 해보자.
우리는 대문자로 시작해서 소문자로 끝나는 패턴을 찾아서 토큰화를 하려 한다.
따라서 이름을 띄어쓰기 기준으로 나누고, 패턴에 따라 토큰화를 진행하는 함수를 선언하였다.
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)))
print(token_set)
나누어진 토큰중, 많이 나오는 10개의 토큰을 추려보자
이때 우리는 collection 패키지의 counter객체를 이용한다.
most_common = Counter(token_set).most_common(10)
most_common
이제 전설에 포켓몬 이름에 나오는 토큰이 포켓몬 이름에 있는 여부를 나타내는 컬럼을 만ㄷ늘어보자.
for token, _ in most_common:
pokemon[f"{token}"] = pokemon["Name"].str.contains(token)
pokemon.head(10)
이제, 범주형 데이터인 Type을 원핫인코딩을 이용해 전처리해보자.
for t in types:
pokemon[t] = (pokemon["Type 1"] == t) | (pokemon["Type 2"] == t)
pokemon[[["Type 1", "Type 2"] + types][0]].head()
근데 이렇게 안하고 판다스의 getdummy()를 쓰는게 더 효율적일것 같다,,
우선 여태 전처리 했던 컬럼 + 기존 컬럼 중, 필요 없거나 문자형 변수를 가진 컬럼을 제외하고 feature을 정의 해주자.
또, legendary 를 타겟 데이터로 분류해주자.
features = ['Total', 'HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed', 'Generation',
'name_count', 'long_name', 'Forme', 'Mega', 'Mewtwo', 'Kyurem', 'Deoxys', 'Hoopa',
'Latias', 'Latios', 'Kyogre', 'Groudon', 'Poison', 'Water', 'Steel', 'Grass',
'Bug', 'Normal', 'Fire', 'Fighting', 'Electric', 'Psychic', 'Ghost', 'Ice',
'Rock', 'Dark', 'Flying', 'Ground', 'Dragon', 'Fairy']
target = "Legendary"
이제 X,y에 각 데이터를 입력해주고, 데이터를 train set과 test set으로 분리해주자.
X = pokemon[features]
y = pokemon[target]
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)
print(X_test.shape, y_test.shape)
DecisionTree로 우리 모델을 만들고 학습 및 평가를 진행해보자.
model = DecisionTreeClassifier(random_state=25)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
confusion_matrix(y_test, y_pred)
print(classification_report(y_test, y_pred))
recall > 0.92
이전의 베이스라인 모델에서는 recall이 0.62 수준이었는데 새로운 feature을 생성하고 학습시키니 모델의 성능이 매우 향상됨을 알 수 있다.
사실 데이터 분석을 할때 새로운 feature을 넣어봐야겠다는 생각은 명목형 변수를 더미변수로 만들때 말고는 크게 생각을 못해봤던것 같다. 이번 노드를 통해 나에게 데이터가 주어졌을때 무작정 모델을 돌려보는게 아니라, 어떤 순서와 방법으로 전처리해서 모델에 넣을지를 조금은 알게 된 것 같다.
오늘 또 느끼지만 모델 적용하는 코드는 누구나 칠수있게 간단한데, 전처리 코드는 정말 제각각에 어렵게하면 너무 어려워질 수 있는것 같아서 파이썬 실력의 중요성을 또 깨닫는다.
이번주도 어느덧 끝이 났다. 벌써 아이펠 한지 3주나 지났는데 내 실력이 어느정도로 향상됐는지 돌아보면 아직은 잘 모르겠다,, 내가 그만큼 열심히 안했다는 뜻인것 같기도 하다. 3주동안 아이펠을 하면서 계획과 시간을 크게 신경쓰지 않고 했던것 같은데, 앞으로는 노션 페이지를 따로 만들어서 해야할것, 진행중인것, 완료한것을 좀 체계화시켜서 눈에 한번에 보이게끔 만들어봐야겠다.