+추가
공부하면서 알게됐는데, 범주형 변수들 인코딩하는거!! train, test 나눈 다음에 해야한다!
저번 프로젝트를 제대로 정리하고 공부해야하는데 또 다른 새로운 프로젝트를 시작해야하는 상황이다. ㅋㅋㅋㅋㅋ이런게 부트캠프인가봐.
제대로 정리하기 전에 일단 코드라도 옮겨놓으면서 잠시 복습해보았다.
1. 데이터 불러오기
아파트실거래가 데이터와 우리팀에서 몇 가지 피쳐를 덧붙인 최종 파일 real_final.csv를 불러온다. (test데이터와 train데이터가 합쳐져 있고, is_test가 0이면 train, 1이면 test로 구분되어 있다.)
그리고 데이터 내 피쳐들을 보면서 몇 가지 조정을 해준다. (해제사유발생일은 0 또는 1로 바꿔준다거나, 구매당시의 연식을 나타내는 피쳐를 파생변수로 만들어준다거나.. 그런 것들. 그리고 각 구에 해당하는 백화점 숫자를 int형으로 바꿔줬다.) 원래 여기서 뭔가 더 해볼까 했는데, 일단 시간이 없어 패스 -
#라이브러리 임포트
import pandas as pd
import numpy as np
from tqdm import tqdm
import pickle
import warnings;warnings.filterwarnings('ignore')
# Model
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.ensemble import RandomForestRegressor
from sklearn import metrics
import eli5
from eli5.sklearn import PermutationImportance
#데이터 불러오기
concat = pd.read_csv('/data/ephemeral/home/data/real_final.csv')
concat['is_test'].value_counts()
'''
0 1118822
1 9272
'''
concat.shape #(1128094, 43)
concat.columns
'''
Index(['아파트명', '전용면적', '계약년월', '계약일', '층', '건축년도', '도로명', '해제사유발생일',
'k-단지분류(아파트,주상복합등등)', 'k-건설사(시공사)', '건축면적', 'target', 'is_test', '주소',
'x', 'y', '주변공원개수', 'Gu_column', 'contractyear', 'Department', 'CPI',
'GDP', '3y_KTB_yield', 'base_rate', 'unemployment_rate',
'youth_unemployment_rate', '단위면적당가격', '자치구별_사설학원_학생_비율',
'자치구별_교과학원_학생_비율', '공시가격', 'stations_walkable_dist',
'nearest_station_distance', 'nearest_station_name',
'nearest_busstop_distance', 'busstops_walkable_dist', '전세가격지수',
'주택전세_소비자심리지수', '부동산시장_소비자심리지수', '지가지수', '주택매매_소비자심리지수', 'apt_매매지수',
'규모', '규모별매매지수'],
dtype='object')
'''
#변수 추가 또는 형식 바꾸기
df = concat.copy()
#결측치 많아서 미리 제거
columns_to_drop = ['건축면적','주택전세_소비자심리지수', '부동산시장_소비자심리지수', '주택매매_소비자심리지수']
df = df.drop(columns=columns_to_drop)
#해제사유 발생일 -> 값 존재 1 없으면 0
df['해제사유발생일'] = np.where(df['해제사유발생일'].isna(), 0, 1)
#구매당시 연식 구하는 코드
df['건축년도'] = pd.to_datetime(df['건축년도'], format='%Y')
df['계약년월'] = pd.to_datetime(df['계약년월'].astype(str), format='%Y%m')
df['구매당시연식'] = df['계약년월'].dt.year - df['건축년도'].dt.year
df['구매당시연식'] = np.where(df['구매당시연식'] < 0, 0, df['구매당시연식'])
#백화점 숫자는 범주형이 아닌데 object로 분류되어서 범주형으로 들어감.
def convert_to_int(value):
if value == '-':
return 0
return int(value)
df['Department'] = df['Department'].apply(convert_to_int)
2. 결측치를 채운다.
처음에는 train과 test를 나눈 뒤에 train 데이터만 해도 되겠지..? 했는데, 클로드한테 물어보니 어쨌든 결측치는 채우긴 해야 하는 것 같다. 클로드 말이 "실제 환경에서는 결측치가 있을 수 있으므로, 테스트 세트에도 결측치를 그대로 두는 것이 더 현실적일 수 있습니다."라고.ㅋㅋㅋㅋ 하지만 어쨌든 테스트에 결측치가 있다면 모델이나 전체 파이프라인에서 결측치를 채울 수 있도록 코드를 작성해야 한다. 이번 경우는 트레인과 테스트를 나눈 채로 트레인의 결측치만 모두 보간하고, 테스트는 하지 않은 채로 그냥 진행했다. (나중에 트레인과 테스트 합친채로 결측치 채우고 모델 학습시키기 직전에 나눴는데, 그 결과가 미세하게 좋게 나왔다. 테스트와 트레인에 일관성있도록 전처리를 할 것!)
그리고 RandomForestRegressor와 같은 트리 기반 모델은 결측치를 잘 처리한다.
공시가격은 선형회귀를 적용시켜서 결측치를 채웠고, x, y좌표로 나타난 위도와 경도는 주소에 같은 동을 찾아서 그 동의 x, y 좌표를 똑같이 쓰게 했다. (물론 똑같은 동 안에서도 x, y좌표가 많았는데 그냥 맨 처음 나오는 값으로 채워줌.) 이렇게 해도 남아있는 x, y 좌표 결측치는 선형보간했다. 아파트명이라는 피쳐에도 결측치가 있었는데 얘는 어차피 문자라서 레이블 인코딩하면 큰 의미가 없을 듯 하여, null이라는 글자를 넣어줬다.
test_data = df.query('is_test==1')
train_data = df.query('is_test==0')
#결측치 채우기
#공시가격 결측치 선형회귀 적용시키기
from sklearn import linear_model
from sklearn.ensemble import HistGradientBoostingRegressor
def reg(data):
# lin_reg = linear_model.LinearRegression()
model = HistGradientBoostingRegressor()
X = data.dropna(axis=0)[['전용면적', '계약일', '층', '해제사유발생일', 'x', 'y', '주변공원개수', 'contractyear', 'CPI', 'GDP', '3y_KTB_yield', 'base_rate', 'unemployment_rate', 'youth_unemployment_rate', '단위면적당가격', '자치구별_사설학원_학생_비율', '자치구별_교과학원_학생_비율', '공시가격', 'stations_walkable_dist', 'nearest_station_distance', 'nearest_busstop_distance', 'busstops_walkable_dist', '전세가격지수', '지가지수', 'apt_매매지수', '규모별매매지수', '구매당시연식']]
y = data.dropna(axis=0)['공시가격']
lin_reg_model = model.fit(X, y)
y_pred = lin_reg_model.predict(data.loc[:,['전용면적', '계약일', '층', '해제사유발생일', 'x', 'y', '주변공원개수', 'contractyear', 'CPI', 'GDP', '3y_KTB_yield', 'base_rate', 'unemployment_rate', 'youth_unemployment_rate', '단위면적당가격', '자치구별_사설학원_학생_비율', '자치구별_교과학원_학생_비율', '공시가격', 'stations_walkable_dist', 'nearest_station_distance', 'nearest_busstop_distance', 'busstops_walkable_dist', '전세가격지수', '지가지수', 'apt_매매지수', '규모별매매지수', '구매당시연식']])
data = data['공시가격'].fillna(pd.Series(y_pred.flatten()), inplace=True)
return data
reg(train_data)
#x, y좌표 결측치 채워주기(같은 동 이름을 기준으로, 좌표값 복사)
def extract_road_name(address):
return ''.join([c for c in address if not c.isdigit()]).strip()
def fill_missing_coordinates_safe(df):
# 진행 상황을 확인하기 위한 전체 행 수 계산
total_rows = len(df)
# 도로명 컬럼 추가
print("도로명 추출 중...")
df['road_name'] = df['도로명'].apply(extract_road_name)
# 결측값이 없는 데이터에서 도로명별 대표 좌표 계산
print("도로명별 대표 좌표 계산 중...")
valid_coords = df.dropna(subset=['x', 'y']).groupby('road_name').first().reset_index()[['road_name', 'x', 'y']]
# 결측값 채우기
print("결측값 채우기 중...")
for i, row in df[df['x'].isna() | df['y'].isna()].iterrows():
match = valid_coords[valid_coords['road_name'] == row['road_name']]
if not match.empty:
df.loc[i, 'x'] = match.iloc[0]['x']
df.loc[i, 'y'] = match.iloc[0]['y']
# 진행 상황 출력 (1000행마다)
if i % 1000 == 0:
print(f"진행 중: {i}/{total_rows} 행 처리됨")
# 임시 컬럼 삭제
df = df.drop('road_name', axis=1)
print("처리 완료!")
return df
# 함수 사용
train_data = fill_missing_coordinates_safe(train_data)
# 결과 확인
print(train_data[['주소', 'x', 'y']].head())
print(f"남은 결측치 개수: x - {train_data['x'].isna().sum()}, y - {train_data['y'].isna().sum()}")
#남은 결측치 선형보간 및 null 입력
train_data[xy] = train_data[xy].interpolate(method='linear', axis=0)
train_data['아파트명'] = train_data['아파트명'].fillna('NULL')
3. 스케일링
연속형 변수 중에서 스케일링이 필요할 것 같은 피쳐들을 스케일링했다. 이날 팀원들끼리 저녁에 모여앉아서 피쳐마다 뭘로 스케일링 하면 좋을지 고민했던 게 생각나네(흐뭇)
#스케일링
from scipy import stats
from sklearn.preprocessing import MinMaxScaler, RobustScaler
continuous_columns = []
categorical_columns = []
for column in train_data.columns:
if pd.api.types.is_numeric_dtype(train_data[column]):
continuous_columns.append(column)
else:
categorical_columns.append(column)
print(continuous_columns)
def scale_columns(df, minmax_cols=None, robust_cols=None, boxcox_cols=None):
"""
주어진 컬럼들에 대해 MinMax Scaler, Robust Scaler, Box-Cox 변환을 적용하는 함수.
Parameters:
df (DataFrame): 원본 데이터 프레임
minmax_cols (list): MinMax Scaling을 적용할 컬럼 리스트
robust_cols (list): Robust Scaling을 적용할 컬럼 리스트
boxcox_cols (list): Box-Cox 변환을 적용할 컬럼 리스트
Returns:
DataFrame: 스케일링 또는 변환이 완료된 데이터 프레임
dict: Box-Cox 변환에 사용된 lambda 값
"""
# 복사본 생성
df_scaled = df.copy()
lambda_dict = {} # Box-Cox 변환에 사용된 lambda 값을 저장할 딕셔너리
# MinMax Scaling 적용
if minmax_cols:
minmax_scaler = MinMaxScaler()
df_scaled[minmax_cols] = minmax_scaler.fit_transform(df_scaled[minmax_cols])
# Robust Scaling 적용
if robust_cols:
robust_scaler = RobustScaler()
df_scaled[robust_cols] = robust_scaler.fit_transform(df_scaled[robust_cols])
# Box-Cox 변환 적용
if boxcox_cols:
for col in boxcox_cols:
# Box-Cox는 데이터에 0 또는 음수 값이 있으면 안 됨.
if (df_scaled[col] <= 0).any():
raise ValueError(f"Box-Cox 변환은 음수 또는 0 값을 가질 수 없습니다. '{col}' 컬럼에 음수나 0이 있습니다.")
# Box-Cox 변환 수행
df_scaled[col], lambda_value = stats.boxcox(df_scaled[col])
lambda_dict[col] = lambda_value # lambda 값을 저장
return df_scaled, lambda_dict
# MinMax Scaling을 적용할 컬럼 리스트
# 사용할 스케일링 방법과 컬럼 리스트
# 비율 제외
minmax_columns = ['Department',
#경제지표
#실업율
'unemployment_rate','youth_unemployment_rate',
'전세가격지수', 'CPI','GDP',
'지가지수', '규모별매매지수', '구매당시연식' ]
robust_columns = ['stations_walkable_dist', 'x', 'y',
'nearest_station_distance', 'nearest_busstop_distance',
'busstops_walkable_dist']
boxcox_columns = ['전용면적', '공시가격',
'3y_KTB_yield','base_rate']
# 스케일링 함수 호출
scaled_df, lambda_dict = scale_columns(df=train_data,
minmax_cols=minmax_columns,
robust_cols=robust_columns,
boxcox_cols=boxcox_columns)
# 결과 출력
print("Box-Cox 변환에 사용된 Lambda 값:", lambda_dict)
4. 레이블 인코딩
# 이제 is_test 칼럼은 drop해줍니다.
train_data.drop(['is_test'], axis = 1, inplace=True)
test_data.drop(['is_test'], axis = 1, inplace=True)
print(train_data.shape, test_data.shape)
test_data['target'] = 0
# 파생변수 제작으로 추가된 변수들이 존재하기에, 다시한번 연속형과 범주형 칼럼을 분리해주겠습니다.
continuous_columns_v2 = []
categorical_columns_v2 = []
for column in train_data.columns:
if pd.api.types.is_numeric_dtype(train_data[column]):
continuous_columns_v2.append(column)
else:
categorical_columns_v2.append(column)
print("연속형 변수:", continuous_columns_v2)
print("범주형 변수:", categorical_columns_v2)
for col in categorical_columns_v2:
train_data[col] = train_data[col].astype("str")
test_data[col] = test_data[col].astype("str")
# 아래에서 범주형 변수들을 대상으로 레이블인코딩을 진행해 주겠습니다.
# 각 변수에 대한 LabelEncoder를 저장할 딕셔너리
label_encoders = {}
# Implement Label Encoding
for col in tqdm( categorical_columns_v2 ):
lbl = LabelEncoder()
# Label-Encoding을 fit
lbl.fit( train_data[col].astype(str) )
train_data[col] = lbl.transform(train_data[col].astype(str))
label_encoders[col] = lbl # 나중에 후처리를 위해 레이블인코더를 저장해주겠습니다.
# Test 데이터에만 존재하는 새로 출현한 데이터를 신규 클래스로 추가해줍니다.
for label in np.unique(test_data[col]):
if label not in lbl.classes_: # unseen label 데이터인 경우
lbl.classes_ = np.append(lbl.classes_, label) # 미처리 시 ValueError발생하니 주의하세요!
test_data[col] = lbl.transform(test_data[col].astype(str))
5. 모델 돌리기
mlflow를 쪼금 배웠으니 넣어봤다. 그런데 아까까지 mlflow ui가 잘 나왔는데 지금은 원만 계속 돌아가고 안뜬다ㅠ
3개 모델을 돌렸는데, 결과는 맨 위 스크린샷으로 첨부.
# Target과 독립변수들을 분리해줍니다.
y_train = train_data['target'].copy()
X_train = train_data.drop(['target'], axis=1)
# Hold out split을 사용해 학습 데이터와 검증 데이터를 8:2 비율로 나누겠습니다.
X_train, X_val, y_train, y_val = train_test_split(
X_train, y_train, test_size=0.2, random_state=2023)
import mlflow
mlflow.set_tracking_uri('http://127.0.0.1:5000')
exp = mlflow.set_experiment(experiment_name='apt-price')
from catboost import CatBoostRegressor, Pool
from sklearn.metrics import mean_squared_error
import xgboost as xgb
# 알고리즘별 성능 테스트
models = {
"CatBoostRegressor":CatBoostRegressor(
iterations=1000, # 학습 반복 횟수
learning_rate=0.05, # 학습률
depth=8, # 트리 깊이
l2_leaf_reg = 1,
loss_function='RMSE', # 손실 함수 # 학습 과정 출력
cat_features=categorical_columns_v2,
task_type='GPU',
early_stopping_rounds=50
),
"RandomForestRegressor":RandomForestRegressor(
n_estimators=5,
criterion='squared_error',
random_state=1,
n_jobs=-1
),
"xgb.XGBRegressor":xgb.XGBRegressor(
n_estimators=500,
max_depth=9,
min_child_weight=5,
gamma=0.1,
n_jobs=-1
)
}
mlflow.autolog()
for model_name, model in models.items():
with mlflow.start_run():
model.fit(X_train, y_train)
pred = model.predict(X_val)
rmse = np.sqrt(mean_squared_error(y_val, pred))
print(f'{model_name} RMSE test: {rmse}')
다음 번에는 Cross Validation 이용하는 부분 연습해봐야겠다.
'공부방 > Upstage AI Lab 4기' 카테고리의 다른 글
판다스 | 조건에 따라 데이터 삭제, 중복 데이터 삭제 (1) | 2024.10.02 |
---|---|
판다스 | 결측치 제거팁 (1) | 2024.10.02 |
TinyBert Model로 문장 속 감정 분석하기 (3) | 2024.09.27 |
[FastAPI] 이미지 분류하는 API 모델 서빙 따라하기 (1) | 2024.09.26 |
[FastAPI] FastAPI 기본 사용법, 프로젝트 세팅하기 (1) | 2024.09.26 |