데이터 분석가:Applied Data Analytics/판다스 데이터분석

전설의 포켓몬 찾아 삼만리

데이터분석 2025. 2. 20. 13:03
320x100
728x90

이번에는 

  • pandas의 문법과 다양한 메서드를 활용해 본 적이 있고, 코드를 보면 어느 정도 이해할 수 있습니다.
  • matplotlib을 활용해서 데이터 시각화를 해본 적이 있고, 코드를 보면 어느 정도 이해할 수 있습니다.
  • 데이터셋을 train/test 데이터셋으로 나누어서 모델을 학습 및 검증해본 경험이 있다.

목표

  • 다양한 피처가 있는 데이터셋을 밑바닥부터 샅샅이 뜯어보고, 전설의 포켓몬을 분류하기 위한 피처에는 무엇이 있는지 생각해 보자.
  • 모델 학습을 시작하기 전 모든 컬럼에 대해 그래프 시각화, 피벗 테이블 등을 활용하며 다양한 방법으로 충분한 EDA를 진행하자.
  • 모델 학습에 넣기 위해서 전처리가 필요한 범주형/문자열 데이터에 대한 전처리를 원-핫 인코딩 등으로 적절하게 진행.
  • 전체 데이터셋을 train/test 데이터셋으로 나누고, 적절한 분류 모델(Decision Tree)을 선택해 학습시키며 베이스라인과 비교하.

목차 :

  1. 포켓몬, 그 데이터는 어디서 구할까
    (1) 안녕, 포켓몬과 인사해!
    (2) 포켓몬, 그 데이터는 어디서 구할까
    (3) 포켓몬 데이터 불러오기
  2. 전설의 포켓몬? 먼저 샅샅이 살펴보자!
    (1) 결측치와 전체 칼럼
    (2) ID와 이름
    (3) 포켓몬의 속성
    (4) 모든 스탯의 총합
    (5) 세부 스탯
    (6) 세대
  3. 전설의 포켓몬과 일반 포켓몬, 그 차이는?
    (1) 전설의 포켓몬의 Total값
    (2) 전설의 포켓몬의 이름
  4. 모델에 넣기 위해! 데이터 전처리하기
    (1) 이름의 길이가 10 이상인가?
    (2) 이름에 자주 쓰이는 토큰 추출
    (3) Type 1 & 2! 범주형 데이터 전처리하기
  5. 가랏, 몬스터볼!
    (1) 가장 기본 데이터로 만드는 베이스라인
    (2) 의사 결정 트리 모델 학습시키기
    (3) 피처 엔지니어링 데이터로 학습시키면 얼마나 차이가 날까?
    (4) 의사 결정 트리 모델 다시 학습시키기

오늘은 흥미로운 포켓몬 데이터셋을 가지고 실습을 해 보겠습니다. 포켓몬의 이름, 속성, 또는 공격력이나 방어력 등과 같은 스탯 값만을 가지고 전설의 포켓몬인지 아닌지를 구별해낼 수 있을까요?

오늘은 이러한 분류 문제를 풀기 위해 데이터를 밑바닥부터 샅샅이 뜯어보는 연습을 할 것입니다.

이러한 과정을 탐색적 데이터 분석(Exploratory Data Analysis, 이하 EDA) 이라고 합니다.

EDA는 더 좋은 데이터 분석과 더 좋은 머신러닝 모델을 만들기 위해 필수적인 과정입니다.

그리고, 더 나은 성능의 모델을 만들어내기 위해서는 무엇을 고민해야 하는지, 이러한 흐름에 익숙해진다면 어떤 데이터셋을 만나더라도 충분히 빠르고 치밀하게 데이터셋을 다룰 수 있게 될 것입니다.

(출처 : 모두연 프로덕트데이터분석가1기 과정중)

import pandas as pd
import numpy as np
import seaborn as sns

import matplotlib.pyplot as plt

 

import os
original_data = pd.read_csv("data/Pokemon.csv")
print('슝=3')

슝=3

# original_data 변수를 그대로 사용하지 않고 원본 데이터를 담은 변수는 고이 놔둘 것입니다. 
# 다음과 같이 원본 데이터를 복사해서 pokemon이라는 변수를 새로 만들어 사용
pokemon = original_data.copy()
print(pokemon.shape)  #데이터셋의 크기를 출력
pokemon.head()

(800, 13)

 

# 데이터셋은 총 800행, 13열 포켓몬이 총 800마리이고, 각 포켓몬을 설명하는 특성(feature)은 13개라고 해석

# 타겟으로 두고 확인할 데이터는 Legendary (전설의 포켓몬인지 아닌지의 여부)이므로, 
# Legendary == True 값을 가지는 레전드 포켓몬 데이터셋은 legendary 변수에, 
# Legendary == False 값을 가지는 일반 포켓몬 데이터셋은 ordinary 변수에 저장
# 전설의 포켓몬 데이터셋
legendary = pokemon[pokemon["Legendary"] == True].reset_index(drop=True)
print(legendary.shape)
legendary.head()

(65, 13)

# Q. 일반 포켓몬의 데이터셋도 만들어봅시다. 일반은 ["Legendary"] == False로적용
ordinary = pokemon[pokemon["Legendary"] == False].reset_index(drop=True)
print(ordinary.shape)
ordinary.head()

(735, 13)

# 많은 포켓몬 데이터 속에서 전설의 포켓몬을 구별해 내려면, 무엇보다 데이터 자체를 확실히 이해하는 것이 가장 중요

# 데이터를 다루기 전 가장 기본적으로 먼저 해야 할 것! 바로 빈 데이터(결측치) 먼저 확인
pokemon.isnull().sum()

#               0
Name            0
Type 1          0
Type 2        386
Total           0
HP              0
Attack          0
Defense         0
Sp. Atk         0
Sp. Def         0
Speed           0
Generation      0
Legendary       0
dtype: int64

# Type 2 컬럼에만 총 386개의 결측치가 있군요.
# Type 1이 있고 Type2도 있으므로, 뭔가 두 번째 속성이 없는 포켓몬이 있는 것 같습니다.
# 데이터셋을 다룰 때 빈 데이터를 다루는 것은 매우 조심스러운 일입니다. 
# 데이터셋의 성격에 따라 빈 데이터를 어떻게 다루어야 할지에 대한 방법이 달라지기 때문
#전체 컬럼 이해하기

print(len(pokemon.columns)) # 전체 컬럼 출력
pokemon.columns 

13
Index(['#', 'Name', 'Type 1', 'Type 2', 'Total', 'HP', 'Attack', 'Defense',
       'Sp. Atk', 'Sp. Def', 'Speed', 'Generation', 'Legendary'],
      dtype='object')

# : 포켓몬 Id number. 성별이 다르지만 같은 포켓몬인 경우 등은 같은 #값을 가진다. int
# Name : 포켓몬 이름. 포켓몬 각각의 이름으로 저장되고, 800개의 포켓몬의 이름 데이터는 모두 다르다. (unique) str
# Type 1 : 첫 번째 속성. 속성을 하나만 가지는 경우 Type 1에 입력된다. str
# Type 2 : 두 번째 속성. 속성을 하나만 가지는 포켓몬의 경우 Type 2는 NaN(결측값)을 가진다. str
# Total : 전체 6가지 스탯의 총합. int
# HP : 포켓몬의 체력. int
# Attack : 물리 공격력. (scratch, punch 등) int
# Defense : 물리 공격에 대한 방어력. int
# Sp. Atk : 특수 공격력. (fire blast, bubble beam 등) int
# Sp. Def : 특수 공격에 대한 방어력. int
# Speed : 포켓몬 매치에 대해 어떤 포켓몬이 먼저 공격할지를 결정. (더 높은 포켓몬이 먼저 공격한다) int
# Generation : 포켓몬의 세대. 현재 데이터에는 6세대까지 있다. int
# Legendary : 전설의 포켓몬 여부. !! Target feature !! bool

 

# 총 몇 종류의 # 값이 있는지 확인
len(set(pokemon["#"]))

721

# 전체 데이터는 총 800개인데 #컬럼을 집합으로 만든 자료형은 그보다 작은 721개의 데이터를 가집니다.
# 파이썬의 집합(set) 자료형은 중복 데이터를 가질 수 없다 따라서 집합의 크기가 800이 아니라 721이므로
# # 컬럼의 값은 unique하지 않으며(index로 쓸 수 없으며), 같은 번호를 가지는 컬럼들이 있음을 알 수 있다.

 

# 같은 # 값을 가지는 포켓몬을 확인
pokemon[pokemon["#"] == 6]

#  #6의 포켓몬은 Charizard, CharizardMega Charizard X, CharizardMega Charizard Y 세 개로 나뉨
# 기본 포켓몬인 Charizard(리자몽)로부터 시작해서 진화한 Mega Charizard가 있고, 
# X, Y는 버전을 나타내는 것으로 보입니다.

# 이름은 문자열로 나타나는 데이터.
# 모든 포켓몬은 이름을 갖고 있죠. 특별하게 확인할 것은 없는 것 같고, 
# 혹시 모든 이름이 유일한 이름인지만 확인해 보자
# len(...)집합의 길이를 출력하여 유일한 포켓몬 이름의 개수를 확인
print(len(set(pokemon["Name"]))) #포켓몬 이름 컬럼을 집합(set)으로 변환하여 중복을 제거

800

# 각 포켓몬의 속성을 무작위로 두 마리의 포켓몬을 한번 살펴보자
pokemon.loc[[6, 10]]

# 6번 포켓몬인 Charizard는 Fire와 Flying 속성 두 가지를, 
# 8번 포켓몬인 Wartortle은 Water 속성 단 한 가지만 가진다.
# 각 속성의 종류는 총 몇 가지인지 알아보자

len(list(set(pokemon["Type 1"]))), len(list(set(pokemon["Type 2"])))

(18, 19)

# Type 1에는 총 18가지, Type 2에는 총 19가지의 속성
# 여기서 Type 2가 한 가지 더 많은 것은 뭘까?
# 각자를 집합으로 만들어 차집합을 확인

set(pokemon["Type 2"]) - set(pokemon["Type 1"])

{nan}

# 둘의 차집합이 NaN 데이터이니 나머지 18가지 속성은 Type 1, Type 2 모두 같은 세트의 데이터를 알 수 있다

# 포켓몬들의 모든 Type을 types 변수에 저장
types = list(set(pokemon["Type 1"]))
print(len(types))
print(types)

18
['Ice', 'Steel', 'Normal', 'Fighting', 'Dark', 'Ghost', 'Dragon', 'Psychic', 'Fairy', 'Flying', 'Rock', 'Grass', 'Fire', 'Water', 'Electric', 'Bug', 'Ground', 'Poison']

# 그렇다면 Type을 하나만 가지고 있는 포켓몬은 몇 마리일까?
# Type이 단 하나뿐이라면 Type 2는 NaN 값일 것이니 Type 2가 NaN인 포켓몬의 수를 구해보자
# 데이터가 비어있는 NaN값의 개수를 확인하고 싶을 때는 isna() 함수를 활용
pokemon["Type 2"].isna().sum()

386

# 총 386개의 포켓몬은 속성을 하나만 가지고, 나머지는 두 개의 속성을 가진다.

# Type 1 데이터 분포 plot

# 일반 포켓몬과 전설의 포켓몬 속성 분포가 각각 어떤지?
# 이 데이터는 일반 포켓몬보다 전설의 포켓몬 수가 매우 적은 불균형 데이터이기 때문에, 
# 전설의 포켓몬은 따로 시각화해 주는 것이 좋을 것 같다.
# plt의 subplot을 활용해서 두 개의 그래프를 한 번에 그리면서, 
# 그래프는 sns(seaborn)의 countplot을 활용. countplot은 말 그대로 데이터의 개수를 표시하는 플롯
# palette="pastel" 또는 palette="Set2" 등의 컬러 팔레트를 추가하면 여러 색상으로 표현
# palette에는 sns.color_palette("husl"), "coolwarm", "viridis" 등 다양한 컬러 스타일을 사용할 수 있다.
import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(12, 7))  # 그래프 크기 설정 12는 가로길이

plt.subplot(211)
sns.countplot(data=ordinary, x="Type 1", order=types, palette="Set2").set_xlabel('')  
plt.title("[Ordinary Pokemons]")

plt.subplot(212)
sns.countplot(data=legendary, x="Type 1", order=types, palette="Set3").set_xlabel('')
plt.title("[Legendary Pokemons]")

plt.show()

1. 일반 포켓몬(Ordinary Pokemons)의 속성 분포

  • Normal 타입이 가장 많은 비율을 차지하며, Water 타입도 매우 많음.
  • Grass, Fire, Electric, Bug 속성도 비교적 많은 개수를 차지함.
  • 다양한 속성들이 비교적 고르게 분포됨.
  • Flying 타입의 개수는 거의 없음.

2. 전설의 포켓몬(Legendary Pokemons)의 속성 분포

  • Dragon과 Psychic 속성이 가장 높은 비율을 차지.
  • Grass, Water, Electric, Fire 타입도 어느 정도 분포됨.
  • Normal 타입의 전설의 포켓몬은 거의 없음.
  • Fairy, Flying, Dark, Ground 등 일부 속성은 일반 포켓몬보다 적은 개수

3. 일반 포켓몬과 전설의 포켓몬의 주요 차이점

  1. Normal 타입의 차이
    • 일반 포켓몬에서는 Normal 타입이 가장 많지만, 전설의 포켓몬에서는 거의 없음.
    • 이는 Normal 타입이 주로 평범한 포켓몬들에게 부여되는 속성이라는 점을 반영.
  2. Dragon과 Psychic 타입의 차이
    • 전설의 포켓몬에서는 Dragon과 Psychic 속성이 압도적으로 많음.
    • 이는 강력한 포켓몬들이 주로 드래곤이나 초능력 계열로 설정되는 경향을 반영.
  3. Flying 타입의 차이
    • 일반 포켓몬에서는 Flying 속성이 거의 없지만, 전설의 포켓몬에서도 많지 않음.
    • Flying 타입이 단독 속성으로 존재하는 경우가 적고, 다른 타입과 혼합된 경우가 많기 때문.
  4. Water, Grass, Fire 타입
    • 일반 포켓몬과 전설의 포켓몬 모두 Water, Grass, Fire 타입이 적절히 분포.
    • 그러나 전설의 포켓몬에서는 상대적으로 Dragon과 Psychic 비중이 더 높음.

결론

  • 일반 포켓몬: Normal, Water, Grass, Fire 속성이 많고 비교적 균형 잡힌 분포.
  • 전설의 포켓몬: Dragon, Psychic 타입이 두드러지며, Normal 속성은 거의 없음.
  • 전설의 포켓몬은 강한 인상을 주기 위해 특정 속성(특히 Dragon과 Psychic)이 집중적으로 분포하는 반면, 일반 포켓몬은 다양한 속성이 균형 있게 포함됨

이 차이를 기반으로, 전설의 포켓몬은 강력한 속성 위주로 구성되고, 일반 포켓몬은 다양한 환경에서 등장하는 여러 속성을 가진다는 점을 알 수 있다.

# 일반 포켓몬: Normal, Water, Grass, Fire 속성이 많고 비교적 균형 잡힌 분포.
# 전설의 포켓몬: Dragon, Psychic 타입이 두드러지며, Normal 속성은 거의 없음.
# 전설의 포켓몬은 강한 인상을 주기 위해 특정 속성 특히, Dragon과 Psychic이 집중적으로 분포하는 반면, 일반 포켓몬은 다양한 속성이 균형 있게 포함.

#  피벗 테이블(pivot table)로 각 속성에 Legendary 포켓몬들이 몇 퍼센트씩 있는지 확인하자
# Type1별로 Legendary의 비율을 보여주는 피벗 테이블
pd.pivot_table(pokemon, index="Type 1", values="Legendary").sort_values(by=["Legendary"], ascending=False)

# Legendary 비율이 가장 높은 속성은 Flying 으로, 50%의 비율

# Type 2 데이터 분포 plot
# Type 2에는 NaN(결측값)이 존재했었다. Countplot을 그릴 때는 결측값은 자동으로 제외된다.
plt.figure(figsize=(12, 10))  # 화면 해상도에 따라 그래프 크기를 조정

# Ordinary Pokemons의 Type 2 분포
plt.subplot(211)
sns.countplot(data=ordinary, x="Type 2", order=ordinary["Type 2"].dropna().unique(), palette="Set2")
plt.title("[Ordinary Pokemons]")

# Legendary Pokemons의 Type 2 분포
plt.subplot(212)
sns.countplot(data=legendary, x="Type 2", order=legendary["Type 2"].dropna().unique(), palette="Set3")
plt.title("[Legendary Pokemons]")

plt.show()

# Type 2 또한 일반 포켓몬과 전설의 포켓몬 분포 차이가 보입니다.
# Flying 속성의 경우 두 경우 다 가장 많지만, 일반 포켓몬에는 Grass, Rock, Poison같은 속성이 많은 반면 
# 전설의 포켓몬은 하나도 없다.
# 대신 여전히 Dragon, Psychic과 더불어 Fighting과 같은 속성이 많다. 
# Q. Type 2에 대해서도 피벗 테이블을 만들어봅시다.

# Type 2에 대한 피벗 테이블 생성
type2_pivot = pokemon.pivot_table(index="Type 2", values="Name", aggfunc="count").fillna(0)
# 결과 출력
from IPython.display import display
display(type2_pivot) 

 

# 피벗테이블 실수로 다시 작성
pd.pivot_table(pokemon, index="Type 2", values="Legendary").sort_values(by=["Legendary"], ascending=False)

# Type 2에서는 Fire 속성 포켓몬의 Legendary 비율이 25%로 가장 높다.

# 데이터셋에서 포켓몬은 총 6가지의 스탯 값을 가집니다. 
# 포켓몬 데이터의 Total 컬럼은 이 6가지 속성값의 총합입니다.
# 모든 스탯의 종류를 stats라는 변수에 저장해 보자.
stats = ["HP", "Attack", "Defense", "Sp. Atk", "Sp. Def", "Speed"]
stats

['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']

# 실제로 6개 스탯의 총합과 데이터에 제공된 Total값이 맞는지 확인

# 첫 번째 포켓몬에 대해 검증하는 코드
print("#0 pokemon: ", pokemon.loc[0, "Name"])
print("total: ", int(pokemon.loc[0, "Total"]))
print("stats: ", list(pokemon.loc[0, stats]))
print("sum of all stats: ", sum(list(pokemon.loc[0, stats])))

#0 pokemon:  Bulbasaur
total:  318
stats:  [45, 49, 49, 65, 65, 45]
sum of all stats:  318

# 첫 번째 포켓몬에 대해서는 Total 값이 318로 일치

# 전체 포켓몬에 대해 Total 값이 stats의 총합과 같은지 확인
# stats 컬럼 리스트 (예: HP, Attack, Defense, Sp. Atk, Sp. Def, Speed)
stats = ["HP", "Attack", "Defense", "Sp. Atk", "Sp. Def", "Speed"]

# 'Total' 값과 'stats'의 총합이 같은 포켓몬 수 계산
matching_count = sum(pokemon['Total'].values == pokemon[stats].sum(axis=1).values)

# 결과 출력
print("Total 값과 stats의 총합이 같은 포켓몬의 수:", matching_count)

Total 값과 stats의 총합이 같은 포켓몬의 수: 800

# Total값에 따른 분포 plot

#  Total 값과 Legendary 는 어떤 관계가 있을지 확인
fig, ax = plt.subplots()
fig.set_size_inches(12, 6)  # 화면 해상도에 따라 그래프 크기를 조정해 주세요.

sns.scatterplot(data=pokemon, x="Type 1", y="Total", hue="Legendary")
plt.show()

# Legendary 여부에 따라 색깔(hue)을 달리하도록 했습니다. 점의 색깔을 보면 Type 1 별로 Total 값을 확인했을 때, 
# 전설의 포켓몬은 주로 Total 스탯 값이 높다는 것이 확인

 

# 세부스탯: HP, Attack, Defense, Sp. Atk, Sp. Def, Speed
# 각각의 스탯 값은 어떻게 분포되어 있을까요? subplot으로 여러 그래프를 한 번에 확인
# 그래프 설정 (3x2 subplot)
figure, ((ax1, ax2), (ax3, ax4), (ax5, ax6)) = plt.subplots(nrows=3, ncols=2)
figure.set_size_inches(12, 18)  # 그래프 크기 조정

# "HP" 스탯의 scatter plot
sns.scatterplot(data=pokemon, y="Total", x="HP", hue="Legendary", ax=ax1)
ax1.set_title("HP vs Total")

# "Attack" 스탯의 scatter plot
sns.scatterplot(data=pokemon, y="Total", x="Attack", hue="Legendary", ax=ax2)
ax2.set_title("Attack vs Total")

# "Defense" 스탯의 scatter plot
sns.scatterplot(data=pokemon, y="Total", x="Defense", hue="Legendary", ax=ax3)
ax3.set_title("Defense vs Total")

# "Sp. Atk" 스탯의 scatter plot
sns.scatterplot(data=pokemon, y="Total", x="Sp. Atk", hue="Legendary", ax=ax4)
ax4.set_title("Sp. Atk vs Total")

# "Sp. Def" 스탯의 scatter plot
sns.scatterplot(data=pokemon, y="Total", x="Sp. Def", hue="Legendary", ax=ax5)
ax5.set_title("Sp. Def vs Total")

# "Speed" 스탯의 scatter plot
sns.scatterplot(data=pokemon, y="Total", x="Speed", hue="Legendary", ax=ax6)
ax6.set_title("Speed vs Total")

# 그래프 표시
plt.show()

# 각 스탯에 대한 분석
# HP, Defense, Sp. Def
# 전설의 포켓몬은 주로 높은 스탯을 갖지만, 이 세 가지에서는 일반 포켓몬이 전설의 포켓몬보다 
# 특히 높은 몇몇 포켓몬이 있습니다.

# 그러나 그 포켓몬들도 Total 값은 특별히 높지 않은 것으로 보아 특정 스탯만 특별히 높은, 즉 특정 속성에 
# 특화된 포켓몬들로 보입니다. (ex. 방어형, 공격형 등)

# Attack, Sp. Atk, Speed 이 세 가지 스탯은 Total과 거의 비례합니다.
# 전설의 포켓몬이 각 스탯의 최대치를 차지하고 있습니다.

 

# Generation : 포켓몬의 세대

# Generation은 각 포켓몬의 "세대"로, 현재 데이터셋에는 1~6세대의 포켓몬이 존재합니다.
# 각 세대에 대한 포켓몬의 수를 확인
plt.figure(figsize=(12, 10))  # 그래프 크기 조정

# Ordinary Pokemons 세대별 개수 분포
plt.subplot(211)
sns.countplot(data=ordinary, x="Generation", palette="Set2").set_xlabel('')
plt.title("[Ordinary Pokemons]")

# Legendary Pokemons 세대별 개수 분포
plt.subplot(212)
sns.countplot(data=legendary, x="Generation", palette="Set1").set_xlabel('')
plt.title("[Legendary Pokemons]")

plt.show()

# 전설의 포켓몬은 1, 2세대에는 많지 않았나 보네요. 3세대부터 많아졌다가, 6세대에 다시 줄어든 것을 확인할 수 있습니다.
# 여기까지 전체 데이터셋의 모든 컬럼에 대해 일차적으로 살펴보았습니다

 

계속이어서 작성 중