이번 프로젝트는 kaggle에 있는 'loan_default' 데이터를 가지고
데이터 전처리와 로지스틱 모형을 적용해 보는 프로젝트였다.
오늘 하루 종일 class 가지고 연습을 많이 해 볼 수 있었다.
(GPT에게 도움을 구해도 자꾸 엉뚱한 걸 알려주더라. 내가 원하는 것을
명확하게 말하지 못한 탓이겠지.)

01. 과제개요

  • 이 데이터 셋의 출처는 하기와 같다.

02. 데이터 전처리 및 시각화

  • 이 데이터는 결측치가 꽤 많아서, 단순히 다 제거하기는 어려웠다.
  • 일단 범주형, 수치형 무관하게 1% 미만이면 제거하기로 정했다.
    • 범주형에서 1% 이상 결측치가 나오면 'Missing-value'라고 채웠다.
    • 수치형의 경우 1% 이상 결측치가 나오면 '중간값'을 넣거나 변수변환을 하였다.(skewness 절대값이 2를 넘어가면 log 변환을 하였다.)
  • 중간값을 채워넣어도 되는 수치형 변수는 시각화해 보면 좌우 대칭이거나
    한쪽에 지나치게 치워지지 않는 변수들이었다.
  • 반면에 한쪽으로 너무 치우쳐서 변수변환이 명백히 필요한 변수도 있었다.
  • 나이가 많을수록 'default'일 확률이 높아졌지만 큰 차이는 아닌 것
    같아서 one-hot-encoding을 그대로 해도 될 것 같았다.

03. 데이터 전처리용 클래스 설계

  • 클래스에 필요한 내용은 아래와 같음
    • y변수가 무엇인지
    • x에 해당하는 컬럼들 중 삭제 가능한 컬럼이 반영되는지
    • train_test_split 할 때 test 데이터의 비율
    • random_state 반영
    • 결측치 비율 반영
    • log변환을 위한 '왜도(skewness)' 기준
    • log_transform이 필요한 컬럼
    • x의 수치형 값 scaling 할 것인지 여부
      • StandardScaler()
      • MinMaxScaler()
      • RobustScaler()
class DataPreprocessor:
  def __init__(self,target_var,delete_var=None, test_rate=0.2, random_state=42,
               missing_value_rate=0.01, skewness_abs_criterion=2, log_transform_cols=None,scaling_method=None):
    # class 정의할 때 test_rate=0.2 처럼 초기값을 입력해줘야함
    # 외부에서 정한 값을 직접 참조하는 구조가 아님
    self.target_var = target_var
    self.delete_var = delete_var if delete_var else []
    self.test_rate = test_rate
    self.random_state = random_state
    self.missing_value_rate = missing_value_rate
    self.skewness_abs_criterion = skewness_abs_criterion
    self.log_transform_cols = log_transform_cols if log_transform_cols is not None else []
    self.scaling_method = scaling_method.lower() if scaling_method else None

  def fit_transform(self, df):
    # fit_transform이라는 매서드 정의
    df = df.copy() # pandas warning방지 차원에서 .copy()

    # 1. Drop delete_vars, 불필요한 컬럼 제거
    df.drop(columns = self.delete_var, inplace = True, errors = 'ignore')

    # 2. Separate target
    y = df[self.target_var]  # 수정됨
    df.drop(columns=[self.target_var], inplace=True)  # 수정됨

    # 3. Column type 나누기(수치형, 범주형)
    num_cols = df.select_dtypes(include = ['float64','int64']).columns
    cat_cols = df.select_dtypes(include = ['object']).columns

    # 4. log1p 대상 자동 판단 (없으면)
    if not self.log_transform_cols:
      skewed = df[num_cols].skew().abs()
      self.log_transform_cols = skewed[skewed > self.skewness_abs_criterion].index.tolist()

    # 5. 결측 비율 기준 행 제거
    missing_ratio = df.isnull().mean()
    cols_to_dropna = missing_ratio[missing_ratio < self.missing_value_rate].index.tolist()
    df.dropna(subset=cols_to_dropna, inplace=True)  # 수정됨
    y = y.loc[df.index]  # 수정됨: row 삭제 후 target 재정렬

    # 6. 남은 결측치 처리
    for col in df.columns:
      if col in num_cols:
        if col in self.log_transform_cols:
          df[col] = df[col].clip(lower=0)  # 수정됨: 음수 방지
          df[col] = np.log1p(df[col])
        df[col] = df[col].fillna(df[col].median())
      elif col in cat_cols:
        df[col] = df[col].fillna("Missing_value")

    # 7. 범주형 변수 One-Hot 인코딩
    df_cat = pd.get_dummies(df[cat_cols], drop_first=True)
    df_num = df[num_cols]

    # 8. Scaling 선택적
    if self.scaling_method:
      if self.scaling_method=='standard':
        scaler = StandardScaler()
      elif self.scaling_method=='min_max':
        scaler = MinMaxScaler()
      elif self.scaling_method=='robust':
        scaler = RobustScaler()
      else:
        raise ValueError("Unsupported scaling_method. Choose from 'standard', 'min_max', 'robust', or None.")

    # 9. 피처 조합
    X = pd.concat([df_num, df_cat], axis=1) # y는 앞에서 정의함

    # 🔧 컬럼 이름 정제(xg-boost feature문제 때)
    X.columns = clean_feature_names(X.columns)

    # 10. train-test split
    return train_test_split(X, y, test_size=self.test_rate,
                            random_state=self.random_state, stratify=y)   
   
  • 클래스 정의 후 적용하는 과정
prep = DataPreprocessor(
    target_var='Status',
    delete_var=['ID', 'year'],
    test_rate=0.2,
    random_state=42,
    missing_value_rate=0.01,
    skewness_abs_criterion=2
)
X_train, X_test, y_train, y_test = prep.fit_transform(Loan_Default_df)
  • 전처리 후 모델링 적용(로지스틱)
from sklearn.linear_model import LogisticRegression # 로지스틱 모형 불러오기
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score # 모델 평가지표(accuracy, precision, recall, f1_score)
import time

st = time.time()
num_iter= 3000 # 1000번 하니 warning나옴

lr_clf = LogisticRegression(max_iter = num_iter, n_jobs=-1)  
lr_clf.fit(X_train, y_train)
lr_pred = lr_clf.predict(X_test)

ed = time.time()

print(f'소모시간 : {round(ed-st,2)}초')
print(f'number_of_iteration : {num_iter}')
print('\n')
print("Accuracy:", round(accuracy_score(y_test, lr_pred),4))
print("Precision:", round(precision_score(y_test, lr_pred),4))
print("Recall:", round(recall_score(y_test, lr_pred),4))
print("F1 Score:", round(f1_score(y_test, lr_pred),4))
  • 로지스틱 모형 성능(변수 5개 제거 후)

  • 변수 제거 사유

    • 로지스틱 모형 때는 발견 못했는데, x변수를 다 넣고 random-forest를 돌리면 Accuracy포함 모든 성능지표 값이 1이 나온다.
    • 이는 x변수 중에 이미 test-data의 정답(default-파산, 여부)지를 알려주는
      컬럼이 존재하기 때문이다. 그래서 의사결정나무(Decision-tree)를 적용해 보거나, train_set 내에서 변수와 파산인지 아닌지 여부와의 관련성을
      따져서 영향력이 가장 높은 변수 5개를 제거하였다.

04. 결과

  • 성능지표를 모두 1로 만드는데 의심이 되는 변수 5개를 제거하고도
    Random-forest, XG-boost, LightGBM 모두 높은 성능을 보였다.
모델소요시간(초)AccuracyPrecisionRecallF1-score
Random Forest17.020.9340.89170.83210.8609
XGBoost8.570.93610.87360.86480.8692
LightGBM2.070.93730.87060.87450.8725
Logistic Regression77.910.87130.91880.52120.6651
  • ROC-curve를 그려보니 아래와 같았다.
  • 로지스틱을 빼고, 나머지 세 모델이 괜찮은 성능을 보임을 알 수 있었다.

05. 회고

  • 2주차에 할 내용까지 한꺼번에 포함해 보렸는데, 다음 주에는 class나 함수를 좀 더 다양하게 써볼 수 있게 연습을 해야겠다.
  • 현업에서도 느끼는 거지만, 항상 데이터를 분석하거나 모델링을 할 때
    자신이 종사하는 '필드'의 지식도 중요하지만, 데이터 자체를 의심하는 습관도
    중요함을 새삼 깨달았다.
profile
2025화이팅!

0개의 댓글