미니프로젝트가 끝나고 첫 수업. 에서 휴가를 쓴 나
미니프로젝트 동안 느꼈던 많은 부족함도 있고, 특히 시각화 부분은 많이 다듬어야겠다고 생각했다.
사흘 뒤면 AICE 시험도 있고, 이제 집 밖에 나갈 일정은 한동안 없으니 신나게 키보드 두드려야지.
오늘 휴가를 쓰긴 했는데, 생각보다 일찍 돌아온 탓에 미프 자료로 이것저것 끄적거려봤다.
따라서 오늘은 미니프로젝트 데이터 추가탐색을 해 보겠다.
미니프로젝트 데이터 추가 탐색
미프 후기는 따로 쓰겠지만, 미니프로젝트에서는 1일차에 구매자 분석을 통한 이탈률 예측,
2일차에 토익 점수 예측을 주제로 데이터 전처리, 탐색을 진행했다.
그 중에 오늘 좀 더 만져본 건 토익 점수 예측 데이터다.
사실 구매자 분석이 좀 더 하고 싶었는데, 나는 그것도 할 시간이 될 줄 알았지(...)
- 라이브러리 임포트
# 라이버러리 로딩
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 한글 폰트 라이브러리
!pip install koreanize_matplotlib
import koreanize_matplotlib
늘 하던 건데, 저 한글 폰트로 시각화할 수 있게 해 주는 라이브러리는 외워 놓아야겠다.
- 데이터 로딩
# 현재 디렉토리 확인
import os
print(os.getcwd())
C:\Users\User\project
# 디렉토리 위치 변경
os.chdir('C:/Users/User/project/AICE 연습')'
# 다시 데이터 로딩
data = pd.read_excel('data04.xlsx')
맨 처음 평소처럼 데이터를 로딩했는데,
No such file or directory 에러가 뜨더라.
떡하니 같은 폴더 안에 있는데 말이지.
찾아 보니까 내가 작업하는 위치가 이 ipynb 파일이 있는 위치와 달랐다.
덕분에 알게 된 두 os 함수.
os.getcwd() : get current wokr directory 다. 지금 작업중인 디렉토리 위치를 반환한다.
os.chdir() : change directory맞겠지. 인자로 받은 위치로 작업 디렉토리를 옮겨 준다.
소소하게 공부가 된다.
- 컬럼 다듬기
# 컬럼명 변경
data = data.rename(columns={
'학습목표' : 'Goal',
'학습방법' : 'Way',
'강의 학습 교재 유형' : 'Textbook_Type',
'학습빈도' : 'Freq',
'기출문제 공부 횟수' : 'Past_Quest',
'취약분야 인지 여부' : 'Know_Weak',
'토익 모의테스트 횟수' : 'Mock_Test'})
# 확인
data
# 불필요한 컬럼 제거
data = data.drop('Student ID', axis=1)
범주형 데이터를 기반으로 탐색을 해 보고 싶어서 이 파일을 시도한건데,
범주형 컬럼이 거의 한글이라 한영키 누르는게 귀찮은고로 다 영어 이름으로 바꿔 줬다. 이름들이 근본없다.
저 Student ID 컬럼은 ID 컬럼과 중복되므로 삭제해준다.
- 데이터 분할
# 개인정보와 시험 관련 정보 분할
df_personal = data[['ID', 'Gender', 'Birth_Year']]
df_test = data[['ID', 'Seq', 'LC_Score', 'RC_Score',
'Total Score', 'Goal', 'Way', 'Textbook_Type',
'Freq', 'Past_Quest', 'Know_Weak', 'Mock_Test']]
display(df_personal.head())
display(df_test.head())
이건 미프때 요구 사항이기도 했다.
데이터프레임을 보면,
테스트가 1차시 2차시 3차시로 나뉘어 있고 따라서 1인당 3개의 row가 할당되어 있다.
그걸 통합하기 위해 각 row를 차시별로 분할해서 컬럼명을 바꾸고 merge하는데,
이 과정에서 개인정보는 어차피 중복되므로 미리 떼는 조치. 괜찮은 아이디어인듯.
- 개인정보 처리
# df_personal 처리 : 데이터가 3중복인지 확인 > 중복행 제거
df_personal.info() # 1500개 row
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1500 entries, 0 to 1499
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ID 1500 non-null int64
1 Gender 1500 non-null object
2 Birth_Year 1500 non-null int64
dtypes: int64(2), object(1)
memory usage: 35.3+ KB
# 1500개가 500x3중복인지 확인
df_personal['ID'].value_counts().sort_index() # 500개 value, 완전 3중복 확인
ID
1 3
2 3
3 3
4 3
5 3
..
496 3
497 3
498 3
499 3
500 3
Name: count, Length: 500, dtype: int64
# 중복 행 제거
df_personal = df_personal.drop_duplicates(subset='ID')
# 인덱스 초기화
df_personal.reset_index(drop=True, inplace=True)
# 확인
df_personal
df_personal은 같은 사람의 개인정보가 세 개씩 중복되어 있다.
그걸 확인하고 중복된거 바로 드랍.
- 시험정보 처리
# df_test 처리 3개의 row를 하나로 병합
# Seq에 따라 세 개의 데이터프레임으로 분할
s1 = df_test.loc[df_test['Seq']==1, :]
s2 = df_test.loc[df_test['Seq']==2, :]
s3 = df_test.loc[df_test['Seq']==3, :]
# 확인
display(s1.head(), s2.head(), s3.head())
# 1회와 2회는 점수만 남기기
s1 = s1[['ID', 'LC_Score', 'RC_Score', 'Total Score']]
s2 = s2[['ID', 'LC_Score', 'RC_Score', 'Total Score']]
# 확인
display(s1, s2)
# 컬럼명 변경
s1 = s1.rename(columns={'LC_Score' : '1st_LC_Score', 'RC_Score' : '1st_RC_Score',
'Total Score' : '1st_Total_Score'})
s2 = s2.rename(columns={'LC_Score' : '2st_LC_Score', 'RC_Score' : '2st_RC_Score',
'Total Score' : '2st_Total_Score'})
# 병합
df_test2 = pd.merge(s3, s2, on='ID')
df_test_merged = pd.merge(df_test2, s1, on='ID')
# 확인
df_test_merged
# 성적 상승 컬럼 생성
df_test_merged['Score_diff_total']
= df_test_merged['Total Score'] - df_test_merged['2st_Total_Score']
df_test_merged.head()
df_test_merged.drop('Seq', axis=1, inplace=True)
# 개인정보와 다시 합치기
df_base = pd.merge(df_test_merged, df_personal, on='ID')
시험 관련 데이터는 미프 때 배운 방식을 한번 더 복습한다는 생각으로 했다.
1차시, 2차시, 3차시의 데이터를 각각 나눈 후,
본체(3차시에서 이전 점수를 빼서 성적 상승량을 구할 거라서 3차시가 본체다.)를 정하고,
이외의 데이터는 구분되는 컬럼(차시별 점수)만 남겨서 이름을 바꾸고 다시 본체에 합친다.
이 과정을 통해 한 명의 시험 정보가 세 개 row에 들어있던 것을, 하나의 row에 담을 수 있었다.
또한 이렇게 구한 차시별 점수의 차는 그 사람의 성적 상승량이라는 새로운 변수가 된다.
- 탐색적 시각화
# 범주형 컬럼 묶기
df_base.select_dtypes(object).columns # 반환값을 복사하기 위해 쓴다.
categorical_cols = ['Goal', 'Way', 'Textbook_Type', 'Freq', 'Know_Weak', 'Gender']
# 성적상승량과 상관관계 분석
plt.figure(figsize=(16, 16))
for i, cols in enumerate(categorical_cols, 1):
plt.subplot(2, 3, i)
sns.barplot(data=df_base, x=cols, y='Score_diff_total', estimator='mean', palette='deep')
plt.title(f'{cols}별 성적상승량')
plt.xticks(rotation=10)
plt.show()
개인적으로 오늘 익힌 것 중에 가장 마음에 드는 것,
for문을 이용한 subplot 작성이다.
enumerate()함수의 두번째 인자가 시작 인덱스를 지정하는 것이라는 점을 활용한다.
드디어 써먹는구나.
프레젠테이션용은 아니라도 한눈에 데이터들이 어떤 컬럼과 어떤 관계인지를 알 수 있다.
시각화를 너무 seaborn에만 의존하는 습관은 주의하자.
- 범주형 데이터 인코딩
# 라벨인코더 임포트
from sklearn.preprocessing import LabelEncoder
# Freq는 라벨인코딩(순서 있음)
le = LabelEncoder()
df_base['Freq_encoded'] = le.fit_transform(df_base['Freq'])
# 나머지는 원핫인코딩(순서 없음)
ohe_cols = ['Goal', 'Way', 'Textbook_Type', 'Know_Weak', 'Gender']
df_base = pd.get_dummies(df_base, columns=ohe_cols, drop_first=True, dtype = int)
# 라벨인코딩 된 원본데이터 삭제
df_base.drop('Freq', axis=1, inplace=True)
이제 내 눈으로는 다 봤으니 기계용으로 번역(인코딩)해 준다.
범주형 데이터를 인코딩하는 방법은 두 가지가 있다.
하나는 이미 배운 One-hot 인코딩이고, 하나는 Label인코딩이다.
기본적으로 원핫은 이진 분류로, Label은 정수화로 범주형 데이터를 인코딩한다.
원핫이 더 자주 쓰이는 건 Label의 위험성 때문인데,
Label은 범주값 A, B, C에 대해 A = 1, B = 2, C = 3이런 식으로 '산술가능한' 값을 주기 때문이다.
이러면 범주들 간에 위계가 생기니까...
다만 내가 쳤던 AICE에서는 Label인코딩을 시켰었다.
여튼 Freq은 공부 빈도고, 당연히 주 1~2회와 주 5~6회는 산술적 위계가 있기에 적용해 봤다.
- 지도학습
# train test split
from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid
= train_test_split(df_base.drop('Score_diff_total', axis=1),
df_base['Score_diff_total'], train_size=0.8,
test_size=0.2, random_state=52)
# 모델 훈련
# 의사결정나무, 랜덤포레스트 기반 학습
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
# 의사결정나무
dt = DecisionTreeRegressor(random_state=52, max_depth=3)
dt.fit(X_train, y_train)
acc_train_dt = round(dt.score(X_train, y_train), 4)
acc_test_dt = round(dt.score(X_valid, y_valid), 4)
# 랜덤포레스트
rf = RandomForestRegressor(random_state=52, max_depth=3)
rf.fit(X_train, y_train)
acc_train_rf = round(rf.score(X_train, y_train), 4)
acc_test_rf = round(rf.score(X_valid, y_valid), 4)
# 정확도 출력
print((f'acc_train_dt : {acc_train_dt}, acc_test_dt : {acc_test_dt}'))
print(f'acc_train_rf : {acc_train_rf}, acc_test_rf : {acc_test_rf}')
train_test_split부분 들여쓰기가 개판인데, 티스토리 코드 블록이 계속 말썽이다.
무튼 오늘은 지도학습까지만 했다. 사실 시간이 없었다.
당연히 아직 잘 못하고, 책 봐 가면서 했다.
train_test_split은 머신에게 훈련시킬 데이터와 머신을 테스트하는 데이터로 나누어 주는 함수다.
입력이야 X_train, X_valid, y_train, y_valid = train_test_split(X 데이터프레임, y 데이터프레임)뿐이라 간단하다.
하지만 하이퍼파라미터에서 신경 쓸 부분이 있다.
우선 train size. 데이터에서 몇할을 훈련용으로 할당할지에 쓴다. 보통 0.7에서 0.8인듯.
그리고 random states. 데이터가 고루 분포하도록 도와 준다.
마지막으로 stratify. y 데이터(= 타겟 데이터)의 쏠림을 막아주는데, y가 범주형일 때만 쓴다.
학습은 4줄로 해결한다.
from sklearn.모델그룹 import 모델명
약어 = 모델명()
약어.fit(X_train, y_train) # fit_transform은 전처리에 쓴다!
y_pred = 약어.predict(X_test)
1. 사용할 학습 모델을 import 한다.
2. 모델에 약어를 부여한다. 약어는 모델명을 따른다. DecisionTree면 dt, RandomForest면 rf 등등...
3. 모델에게 train 데이터를 기반으로 훈련시킨다.
4. 훈련된 모델에게 test 데이터의 정답(y_test)를 예측하게 한다.
그런데 책에서는 4줄에서 바로 .score() 함수를 적용시키더라. 저래도 되나....?
그걸 지금 발견해서 다시 코드 두드려보기.
일단 fit 다음 줄에 y_pred_dt, y_pred_rf를 선언해 놓은 상태에서,
# 성능 예측 지표 import(범주형)
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
# 성능 예측 지표 import(회귀형)
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score
# 성능 예측하기 (회귀)
# 의사결정나무
mae_dt = mean_absolute_error(y_valid, y_pred_dt)
print(f"Mean Absolute Error: {mae_dt:.2f}")
mse_dt = mean_squared_error(y_valid, y_pred_dt)
print(f"Mean Squared Error: {mse_dt:.2f}")
r2_dt = r2_score(y_valid, y_pred_dt)
print(f"R² Score: {r2_dt:.2f}")
# 랜덤포레스트
mae_rf = mean_absolute_error(y_valid, y_pred_rf)
print(f"Mean Absolute Error: {mae_rf:.2f}")
mse_rf = mean_squared_error(y_valid, y_pred_rf)
print(f"Mean Squared Error: {mse_rf:.2f}")
r2_rf = r2_score(y_valid, y_pred_rf)
print(f"R² Score: {r2_rf:.2f}")
원리는 쉽다. 외울게 많을 뿐
타겟 데이터가 범주형인지, 수치(회귀)형인지에 따라 적절한 성능평가 지표를 import해서,
그 지표의 매개변수로 (실제값, 예측값)을 주면 된다.
일반적으로는 (y_test, y_pred)가 국룰인듯.
난 앞에서 실제값을 y_valid로 선언해서 y_valid라고 썼다.
아래는 그 결과 성능 개박살났다.
위가 의사결정나무, 아래가 랜덤포레스트, GPT의 해석은 이렇다.
의사결정나무:
- MAE: 21.70 (예측 값이 실제 값과 평균적으로 21.7 단위만큼 차이가 난다)
- MSE: 939.37 (큰 오차에 대한 패널티가 포함된 값)
- R² Score: 0.26 (모델이 전체 데이터 변동성의 26%를 설명)
랜덤포레스트:
- MAE: 20.42 (랜덤포레스트는 예측의 절대 오차가 약간 더 작음)
- MSE: 895.16 (큰 오차에 대한 패널티도 랜덤포레스트가 더 작음)
- R² Score: 0.30 (랜덤포레스트가 변동성을 약간 더 잘 설명)
결론적으로, 랜덤포레스트가 MAE와 MSE 모두에서 더 작은 값을 가지고 있으므로,
예측 정확도가 더 높다고 볼 수 있습니다.
결론:
- 성능이 많이 좋지 않습니다.
- 특히, R² 스코어가 0.30에 불과해,모델이 데이터를 잘 설명하지 못하고 있는 상황입니다.
- 또한, MAE와 MSE도 상대적으로 큰 값을 가지고 있어 예측이 실제 값과 많이 차이가 납니다.
아니 뭐.... 그냥 인코딩만 하고 통으로 집어넣었으니 당연한가....!