{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 실습 전 환경 준비 (가상환경 + 커널 등록)\n",
    "\n",
    "아래 순서대로 1회 설정 후, 이 노트북은 등록한 커널로 실행\n",
    "\n",
    "1. 가상환경 생성/활성화\n",
    "2. 패키지 설치\n",
    "3. ipykernel 등록\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1) 가상환경 생성 및 활성화 (OS별)\n",
    "\n",
    "#### Windows (PowerShell)\n",
    "```powershell\n",
    "cd C:\\path\\to\\smote\n",
    "python -m venv .venv\n",
    ".\\.venv\\Scripts\\Activate.ps1\n",
    "python -m pip install --upgrade pip\n",
    "```\n",
    "\n",
    "#### Linux (bash)\n",
    "```bash\n",
    "cd /home/kafa46/classes/ml2/smote\n",
    "python3 -m venv .venv\n",
    "source .venv/bin/activate\n",
    "python -m pip install --upgrade pip\n",
    "```\n",
    "\n",
    "#### macOS (zsh/bash)\n",
    "```bash\n",
    "cd /path/to/smote\n",
    "python3 -m venv .venv\n",
    "source .venv/bin/activate\n",
    "python -m pip install --upgrade pip\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2) 실습 패키지 설치\n",
    "```bash\n",
    "pip install -U pandas lightgbm scikit-learn seaborn matplotlib imbalanced-learn catboost ipykernel notebook\n",
    "```\n",
    "\n",
    "`imbalanced-learn` 설치 후 `from imblearn...` 형태로 import\n",
    "\n",
    "필요 시 아래 명령으로 노트북 실행\n",
    "```bash\n",
    "jupyter notebook\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3) ipykernel 등록\n",
    "```bash\n",
    "python -m ipykernel install --user --name smote-venv --display-name \"Python (smote-venv)\"\n",
    "```\n",
    "\n",
    "노트북 우측 상단 커널 선택 메뉴에서 `Python (smote-venv)` 선택\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import lightgbm as lgb\n",
    "from sklearn.metrics import confusion_matrix\n",
    "import seaborn as sns\n",
    "from matplotlib import pyplot as plt\n",
    "from sklearn.ensemble import AdaBoostClassifier\n",
    "from sklearn.tree import DecisionTreeClassifier"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 실습 데이터 - Credit Card Fraud Detection Data\n",
    "- 데이터 설명 및 다운로드: https://www.kaggle.com/mlg-ulb/creditcardfraud\n",
    "- 28개의 컬럼으로 구성\n",
    "    - 28개의 변수(feature) 의미: Features V1, V2, … V28 are the principal components obtained with PCA.\n",
    "    - 다양한 데이터를 가지고 \n",
    "- Feature 'Amount': the transaction Amount\n",
    "- Feature 'Class': the response variable and it takes value 1 in case of fraud and 0 otherwise.\n",
    "    $\\to$ 타겟 feature, Class (0, 1 ; 이진분류)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "data=pd.read_csv(\"./creditcard.csv\")\n",
    "data.tail()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 불필요한 데이터 제거"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "data=data.drop(['Time','Amount'],axis=1)\n",
    "data.tail()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 데이터 내 NA값 여부 확인\n",
    "data.isnull() # NA 확인\n",
    "data.isnull().any() # 만약 존재한다면 0으로 대체 혹은, 해당 열을 제외하고 진행"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 데이터 구조 확인"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['V1']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_feature = data.drop(['Class'], axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "col_names = data_feature.columns"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "col_names"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 데이터 분포 형태 확인"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_feature = data_feature[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_feature"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_feature['V1'].min()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_feature"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(20, 6))\n",
    "y = data_feature.iloc[0]\n",
    "x = np.arange(len(y))\n",
    "plt.bar(x, y)\n",
    "labels = data_feature.columns\n",
    "plt.title(\"Value Distribution of the First Row\")\n",
    "plt.xticks(x, labels, rotation=0)\n",
    "plt.xlabel('Feature Name')\n",
    "plt.ylabel('Feature Values')\n",
    "plt.tight_layout()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "plt.rcParams[\"figure.figsize\"] = (15,10)\n",
    "plt.plot()\n",
    "for x in range(data_feature.shape[0]):\n",
    "    y = data_feature.iloc[x]\n",
    "    x = np.arange(len(y))\n",
    "    #plt.scatter(x, y)\n",
    "    plt.plot(x, y)\n",
    "labels = data_feature.columns\n",
    "plt.title(\"Value Distribution of 10 Rows\")\n",
    "plt.xticks(x, labels)\n",
    "plt.xlabel('Feature Name')\n",
    "plt.ylabel('Feature Values')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 데이터 기본 통계 확인 (Summary)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "data.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### collection 패키지의 Counter 모듈을 이용해서 클래스 비율 확인\n",
    "- 컨테이너에 동일한 값의 자료가 몇 개인지를 파악하는데 사용하는 객체\n",
    "- Class Imbalance 정도 확인"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 타겟 변수(feature)의 분포 확인\n",
    "from collections import Counter\n",
    "class_count = Counter(data.Class)\n",
    "print(class_count)\n",
    "IR = class_count.get(0) / class_count.get(1)\n",
    "print(f'IR (Class Imbalance Ratio): {IR:.2f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### EDA (Exploratory Data Analysis)\n",
    "- 탐색적 자료 분석\n",
    "- 가능한 그래프를 그려보면서 독립변수, 종속변수의 관계를 유추해 보는 것\n",
    "- 참고: 상자그림(Box-plot) 해석하는 법\n",
    "    - https://codedragon.tistory.com/7012\n",
    "- sns box plot 컨닝\n",
    "    - https://www.kaggle.com/vincentlugat/pima-indians-diabetes-eda-prediction-0-906"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 각 변수별 특성 시각화 및 분석"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "\n",
    "plt.style.use('ggplot') # Using ggplot2 style visuals\n",
    "f, ax = plt.subplots(figsize = (11, 15)) # 그래프 사이즈\n",
    "\n",
    "ax.set_facecolor('#fafafa') # 그래프 색상값\n",
    "ax.set(xlim = (-5, 5)) # X축 범위\n",
    "#ax.set(xlim = (-150, 150)) # X축 범위\n",
    "plt.ylabel('Variables') # Y축 이름\n",
    "plt.title(\"Overview Data Set\") # 그래프 제목\n",
    "ax = sns.boxplot(\n",
    "    data = data.drop(columns = ['Class']), # V1 ~ V28 확인\n",
    "    orient = 'h',\n",
    "    palette = 'Set2'\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 차트 해석 방법 (Box Plot)\n",
    "- 각 가로 박스는 변수(`V1`~`V28`) 하나의 분포 의미\n",
    "- 박스 안의 가운데 선은 **중앙값(median)**\n",
    "- 박스의 양 끝은 **1사분위수(Q1)**, **3사분위수(Q3)**, 박스 길이(Q3-Q1)가 클수록 분산 큼\n",
    "- 수염(whisker)은 일반적인 값의 범위, 수염 밖 점들은 **이상치(outlier)** 후보\n",
    "- 같은 기준(가로축 값)에서 변수별 박스를 비교해 분포 중심/퍼짐/이상치가 큰 변수 우선 확인\n",
    "- 이 그래프는 `Class`를 나누지 않은 전체 분포, 사기/정상 차이 확인은 `Class`별 분리 시각화로 추가 분석 필요\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.loc[data['Class'] == 1] # class가 1인 Row 추출\n",
    "# 개수 확인: 492 rows -> class imbalenced 상황"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "var = data.columns.values[:-1] # V1 ~ V28\n",
    "i = 0\n",
    "t0 = data.loc[data['Class'] == 0] # Class : 0 인 행만 추출 --> 정상 거리\n",
    "t1 = data.loc[data['Class'] == 1] # Class : 1 인 행만 추출 --> Fraud 거래\n",
    "\n",
    "sns.set_style('whitegrid') # 그래프 스타일 지정\n",
    "plt.figure()\n",
    "fig, ax = plt.subplots(8, 4, figsize = (16, 28)) # 축 지정\n",
    "\n",
    "# KDE Plot\n",
    "# Kernel Desntiy Estimation is a non-parametric estimation of a PDF,\n",
    "# Probability Density Function of a random variable.\n",
    "# It's a smoothing process of a discontinuous PDF\n",
    "\n",
    "for idx, feature in enumerate(var):\n",
    "#     i += 1\n",
    "    plt.subplot(7, 4, idx+1) # 28개의 그래프\n",
    "    sns.kdeplot(t0[feature], bw_method = 0.5, label = \"Class = 0\")\n",
    "    sns.kdeplot(t1[feature], bw_method = 0.5, label = \"Class = 1\")\n",
    "    plt.xlabel(feature, fontsize = 12) # 라벨 속성값\n",
    "    plt.xticks()\n",
    "    plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 각 변수 별 그래프를 타겟변수에 대해서 그려보았을 경우, 차이가 있는 변수들은 다음과 같이 정의할 수 있다.\n",
    "- Plot의 모양을 보면서 직관석으로 해석\n",
    "    - 타겟 변수에 대해 분포 차이가 많이 나는 변수 : V4, V11\n",
    "    - 타겟 변수에 대해 분포 차이가 비교적 많이 존재하는 변수 : V12, V14, V18\n",
    "    - 타겟 변수에 대해 분포 차이가 비교적 적게 존재하는 변수 : V1, V2, V3, V10\n",
    "- Class imbalanced 상황을 생각해 보기\n",
    "    - 비슷한 유형의 변수가 많다.\n",
    "    - 특성을 명확히 구분할 수 있는 설명변수가 그리 많지 않다.\n",
    "    - 이럴 경우 Over Sampling 수행 필요성을 곰곰히 생각해 본다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 데이터 구조 재확인"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = np.array(data.iloc[:, data.columns != 'Class'])\n",
    "y = np.array(data.iloc[:, data.columns == 'Class'])\n",
    "X.shape, y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 학습데이터와 평가 데이터 분리"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)\n",
    "print(\"Number transactions X_train dataset: \", X_train.shape)\n",
    "print(\"Number transactions y_train dataset: \", y_train.shape)\n",
    "print(\"Number transactions X_test dataset: \", X_test.shape)\n",
    "print(\"Number transactions y_test dataset: \", y_test.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 모델 성능평가 함수 작성"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def model_evaluation(label, predict):\n",
    "    cf_matrix = confusion_matrix(label, predict)\n",
    "    Accuracy = (cf_matrix[0][0] + cf_matrix[1][1]) / sum(sum(cf_matrix))\n",
    "    Precision = cf_matrix[1][1] / (cf_matrix[1][1] + cf_matrix[0][1])\n",
    "    Recall = cf_matrix[1][1] / (cf_matrix[1][1] + cf_matrix[1][0])\n",
    "    F1_Score = (2 * Recall * Precision) / (Recall + Precision)\n",
    "    print(\"Model_Evaluation with Label:1\")\n",
    "    print(f\"Accuracy: {Accuracy:1.5f}\")\n",
    "    print(f\"Precision: {Precision: 1.5f}\")\n",
    "    print(f\"Recall: { Recall: 1.5f}\")\n",
    "    print(f\"F1-Score: {F1_Score: 1.5f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Light GBM을 기본 모델로 설정"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### LightGBM 데이터셋 객체 생성"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lgb_dtrain = lgb.Dataset(data = pd.DataFrame(X_train), label = pd.DataFrame(y_train)) # 학습 데이터를 LightGBM 모델에 맞게 변환"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### LightGBM 파라미터 설정"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lgb_param = {\n",
    "    'max_depth': 10, # 트리 깊이\n",
    "    'learning_rate': 0.01, # Step Size\n",
    "    'n_estimators': 50, # Number of trees, 트리 생성 개수\n",
    "    'objective': 'binary',\n",
    "    'seed': 0, # 재현성 고정\n",
    "    'verbosity': -1, # 학습 로그 출력 끄기\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### LightGBM 학습 및 평가"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    " # 파라미터 추가, Label must be in [0, num_class) -> num_class보다 1 커야한다.\n",
    "lgb_model = lgb.train(params=lgb_param, train_set=lgb_dtrain) # 학습 진행\n",
    "pred= np.repeat(0, len(y_test))\n",
    "pred[lgb_model.predict(X_test) > 0.5] = 1\n",
    "model_evaluation(y_test, pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Oversampling을 수행\n",
    "- 추가 성능향상이 가능한지 확인"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### SMOTE 이용한 Over Sampling 수행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "from imblearn.over_sampling import SMOTE\n",
    "num_minor_class_before = sum(y_train == 1)[0]\n",
    "print(f\"SMOTE 수행 이전 label '1' 개수: {num_minor_class_before}\") # y_train 중 레이블 값이 1인 데이터의 개수\n",
    "print(f\"SMOTE 수행 이전 label '0' 개수: {sum(y_train==0)[0]:,} \\n\") # y_train 중 레이블 값이 0 인 데이터의 개수\n",
    "\n",
    "sm = SMOTE(random_state=0, sampling_strategy=0.3) # SMOTE 알고리즘, 비율 증가\n",
    "# numpy raval() 함수: 1차원 배열로 변환, 참고 블로그 -> https://m.blog.naver.com/wideeyed/221533365486\n",
    "X_train_res, y_train_res = sm.fit_resample(X_train, y_train.ravel()) # Over Sampling 진행\n",
    "\n",
    "# 결과 출력\n",
    "num_minor_class_after = sum(y_train_res==1)\n",
    "print(f\"SMOTE 수행 결과 label '1' 개수: {num_minor_class_after:,}\")\n",
    "print(f\"SMOTE 수행 결과 label '0' 개수': {sum(y_train_res==0):,}\")\n",
    "print(f\"Minor Class 증가: {num_minor_class_after - num_minor_class_before:,}\")\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 동일한 코드를 이용하여 LightGBM 다시 수행 후 성능향상 확인\n",
    "- 위에서 사용한 코드 그대로 복.붙 \n",
    "- 입력값만 달라짐\n",
    "- SMOTE 전.후 성능지표를 비교\n",
    "    - 만약 성능이 향상된다면\n",
    "    - Class imbalanced problem이 Over Sampling으로 어느정도 극복된 것으로 판단."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 데이터셋 객체 생성"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 학습 데이터를 LightGBM 모델에 맞게 변환\n",
    "lgb_dtrain2 = lgb.Dataset(data = pd.DataFrame(X_train_res), label = pd.DataFrame(y_train_res))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 학습 및 평가 수행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lgb_param2 = {\n",
    "    'max_depth': 10, # 트리 깊이\n",
    "    'learning_rate': 0.01, # Step Size\n",
    "    'n_estimators': 50, # Number of trees, 트리 생성 개수\n",
    "    'objective': 'multiclass', # 목적 함수\n",
    "    'seed': 0, # 재현성 고정\n",
    "    'num_class': len(set(pd.DataFrame(y_train_res))) + 1 # 파라미터 추가, Label must be in [0, num_class) -> num_class보다 1 커야한다.\n",
    "} \n",
    "lgb_model2 = lgb.train(params = lgb_param2, train_set = lgb_dtrain2) # 학습 진행\n",
    "lgb_model2_predict = np.argmax(lgb_model2.predict(X_test), axis = 1) # 평가 데이터 예측, Softmax의 결과값 중 가장 큰 값의 Label로 예측\n",
    "model_evaluation(y_test, lgb_model2_predict) # 모델 분류 평가 결과\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Over sampling이 통한다면...\n",
    "### 그렇다면, Oversampling 크기를 증가시킨다면? (30% $\\to$ 60%)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"SMOTE 수행 이전 label '1' 개수: {sum(y_train == 1)[0]:,}\") # y_train 중 레이블 값이 1인 데이터의 개수\n",
    "print(f\"SMOTE 수행 이전 label '0' 개수: {sum(y_train == 0)[0]:,} \\n\") # y_train 중 레이블 값이 0 인 데이터의 개수\n",
    "\n",
    "sm2 = SMOTE(random_state = 0, sampling_strategy=0.6) # SMOTE 알고리즘, 비율 60%\n",
    "X_train_res2, y_train_res2 = sm2.fit_resample(X_train, y_train.ravel()) # Over Sampling 진행\n",
    "\n",
    "print(f\"SMOTE 수행 결과 label '1' 개수: {sum(y_train_res2==1):,}\")\n",
    "print(f\"SMOTE 수행 결과 label '0' 개수: {sum(y_train_res2==0):,}\")\n",
    "print(f\"Minor Class 증가: {sum(y_train_res2==1) - sum(y_train == 1)[0]:,}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 60% 증강된 데이터를 적용한 객체 생성"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lgb_dtrain3 = lgb.Dataset(data = pd.DataFrame(X_train_res2), label = pd.DataFrame(y_train_res2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 학습 수행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lgb_param3 = {\n",
    "    'max_depth': 10, # 트리 깊이\n",
    "    'learning_rate': 0.01, # Step Size\n",
    "    'n_estimators': 50, # Number of trees, 트리 생성 개수\n",
    "    'objective': 'multiclass', # 목적 함수\n",
    "    'seed': 0, # 재현성 고정\n",
    "    'num_class': len(set(pd.DataFrame(y_train_res2))) + 1 # 파라미터 추가, Label must be in [0, num_class) -> num_class보다 1 커야한다.\n",
    "} \n",
    "\n",
    "lgb_model3 = lgb.train(params = lgb_param3, train_set = lgb_dtrain3) # 학습 진행\n",
    "lgb_model3_predict = np.argmax(lgb_model3.predict(X_test), axis = 1) # 평가 데이터 예측, Softmax의 결과값 중 가장 큰 값의 Label로 예측\n",
    "model_evaluation(y_test, lgb_model3_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 60% 증강 성능 분석\n",
    "- Precision: 0.71074 → 0.24513 (30% 대비 -65.51%)\n",
    "- Recall: 0.85149 → 0.87129 (30% 대비 +2.33%)\n",
    "- F1-Score: 0.77477 → 0.38261 (30% 대비 -50.62%)\n",
    "- 해석: Recall 소폭 향상, Precision 급감\n",
    "- 해석: 오탐 증가로 F1 크게 하락, 30% 대비 비효율\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 극단적인 경우 테스트 (100%)\n",
    "- 1:1 비율로 Oversampling\n",
    "- 수행 결과를 30%, 60% 경우의 성능지표와 비교"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"SMOTE 수행 이전 label '1' 개수: {sum(y_train == 1)[0]:,}\") # y_train 중 레이블 값이 1인 데이터의 개수\n",
    "print(f\"SMOTE 수행 이전 label '0' 개수: {sum(y_train == 0)[0]:,} \\n\") # y_train 중 레이블 값이 0 인 데이터의 개수\n",
    "\n",
    "sm3 = SMOTE(random_state = 0) # SMOTE 알고리즘, Default: 동등\n",
    "X_train_res3, y_train_res3 = sm3.fit_resample(X_train, y_train.ravel()) # Over Sampling 진행\n",
    "\n",
    "print(f\"SMOTE 수행 결과 label '1' 개수: {sum(y_train_res3==1):,}\")\n",
    "print(f\"SMOTE 수행 결과 label '0' 개수: {sum(y_train_res3==0):,}\")\n",
    "print(f\"Minor Class 증가: {sum(y_train_res3==1) - sum(y_train == 1)[0]:,}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 100% 증강된 데이터를 적용한 객체 생성"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lgb_dtrain4 = lgb.Dataset(data = pd.DataFrame(X_train_res3), label = pd.DataFrame(y_train_res3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 학습 수행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "lgb_param4 = {\n",
    "    'max_depth': 10, # 트리 깊이\n",
    "    'learning_rate': 0.01, # Step Size\n",
    "    'n_estimators': 50, # Number of trees, 트리 생성 개수\n",
    "    'objective': 'multiclass', # 목적 함수\n",
    "    'seed': 0, # 재현성 고정\n",
    "    'num_class': len(set(pd.DataFrame(y_train_res3))) + 1 # 파라미터 추가, Label must be in [0, num_class) -> num_class보다 1 커야한다.\n",
    "} \n",
    "lgb_model4 = lgb.train(params = lgb_param4, train_set = lgb_dtrain4) # 학습 진행\n",
    "lgb_model4_predict = np.argmax(lgb_model4.predict(X_test), axis = 1) # 평가 데이터 예측, Softmax의 결과값 중 가장 큰 값의 Label로 예측\n",
    "model_evaluation(y_test, lgb_model4_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 100% 증강 성능 분석\n",
    "- Precision: 0.71074 → 0.08566 (30% 대비 -87.95%)\n",
    "- Recall: 0.85149 → 0.88119 (30% 대비 +3.49%)\n",
    "- F1-Score: 0.77477 → 0.15614 (30% 대비 -79.85%)\n",
    "- 해석: Recall 추가 상승, Precision 급락\n",
    "- 해석: 소수 클래스 과대예측 경향, 실무 관점에서 30%가 균형적\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 30%, 60%, 100% 증강에 따른 성능 비교"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Compare Precision/Recall/F1-Score for No SMOTE, 30%, 60%, 100% SMOTE\n",
    "def calc_metrics(label, predict):\n",
    "    cf = confusion_matrix(label, predict)\n",
    "    precision = cf[1][1] / (cf[1][1] + cf[0][1]) if (cf[1][1] + cf[0][1]) != 0 else 0\n",
    "    recall = cf[1][1] / (cf[1][1] + cf[1][0]) if (cf[1][1] + cf[1][0]) != 0 else 0\n",
    "    f1 = (2 * recall * precision) / (recall + precision) if (recall + precision) != 0 else 0\n",
    "    return [precision, recall, f1]\n",
    "\n",
    "metric_names = [\"Precision\", \"Recall\", \"F1-Score\"]\n",
    "ratio_names = [\"No SMOTE\", \"30%\", \"60%\", \"100%\"]\n",
    "\n",
    "# No SMOTE prediction variable: pred\n",
    "# SMOTE 30/60/100 prediction variables: lgb_model2_predict, lgb_model3_predict, lgb_model4_predict\n",
    "scores_no = calc_metrics(y_test, pred)\n",
    "scores_30 = calc_metrics(y_test, lgb_model2_predict)\n",
    "scores_60 = calc_metrics(y_test, lgb_model3_predict)\n",
    "scores_100 = calc_metrics(y_test, lgb_model4_predict)\n",
    "\n",
    "# rows: metric, cols: ratio\n",
    "scores = np.array([scores_no, scores_30, scores_60, scores_100]).T\n",
    "\n",
    "x = np.arange(len(metric_names))\n",
    "width = 0.2\n",
    "colors = [\"#7F7F7F\", \"#4C72B0\", \"#55A868\", \"#C44E52\"]\n",
    "\n",
    "plt.figure(figsize=(11, 6))\n",
    "for i, ratio in enumerate(ratio_names):\n",
    "    bars = plt.bar(x + (i - 1.5) * width, scores[:, i], width=width, label=ratio, color=colors[i])\n",
    "    for bar in bars:\n",
    "        h = bar.get_height()\n",
    "        plt.text(\n",
    "            bar.get_x() + bar.get_width() / 2,\n",
    "            h + 0.01,\n",
    "            f\"{h:.2f}\",\n",
    "            ha=\"center\",\n",
    "            va=\"bottom\",\n",
    "            fontsize=9\n",
    "        )\n",
    "\n",
    "plt.xticks(x, metric_names)\n",
    "plt.ylim(0, 1.05)\n",
    "plt.xlabel(\"Metric\")\n",
    "plt.ylabel(\"Score\")\n",
    "plt.title(\"Performance Comparison by SMOTE Ratio (No SMOTE vs 30% vs 60% vs 100%)\")\n",
    "plt.legend(title=\"SMOTE Ratio\")\n",
    "plt.tight_layout()\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 다양한 실험 결과 $\\to$ 결론: 30%가 가장 적합"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# BLSM (Borderline SMOTE)\n",
    "from imblearn.over_sampling import BorderlineSMOTE\n",
    "sm4 = BorderlineSMOTE(random_state = 0, sampling_strategy = 0.3) # BLSM 알고리즘 적용\n",
    "X_train_res4, y_train_res4 = sm4.fit_resample(X_train, y_train.ravel()) # Over Sampling 적용"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lgb_dtrain5 = lgb.Dataset(data = pd.DataFrame(X_train_res4), label = pd.DataFrame(y_train_res4)) # 학습 데이터를 LightGBM 모델에 맞게 변환\n",
    "lgb_param5 = {'max_depth': 10, # 트리 깊이\n",
    "            'learning_rate': 0.01, # Step Size\n",
    "            'num_leaves': 64,\n",
    "            'objective': 'multiclass', # 목적 함수\n",
    "            'seed': 0, # 재현성 고정\n",
    "            'num_class': len(set(pd.DataFrame(y_train_res4))) + 1} # 파라미터 추가, Label must be in [0, num_class) -> num_class보다 1 커야한다.\n",
    "lgb_model5 = lgb.train(params = lgb_param5, train_set = lgb_dtrain5) # 학습 진행\n",
    "lgb_model5_predict = np.argmax(lgb_model5.predict(X_test), axis = 1) # 평가 데이터 예측, Softmax의 결과값 중 가장 큰 값의 Label로 예측\n",
    "model_evaluation(y_test, lgb_model5_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# BLSMOTE 보다 기본 SMOTE가 성능이 좋다. \n",
    "# 이 경험적 지식을 바탕으로 다양한 모델에 적용해본다\n",
    "- 선형회귀(로지스틱)\n",
    "- Random Forest\n",
    "- AdaBoost\n",
    "- CatBoost"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# BLSM을 이용해서 Oversampling 한 학습 데이터 셋 : X_train_res2, y_train_res2\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "lr_model = LogisticRegression(C = 1e+10, random_state = 0)\n",
    "# sklearn 의 Logistic Regression은 기본적으로 Ridge 정규화가 포함되어 있기 때문에,\n",
    "# 정규화 텀을 억제하는 C를 크게 적용한다 (C:Inverse of regularization strength)\n",
    "lr_model.fit(X_train_res, y_train_res) # 로지스틱 회귀 모형 학습\n",
    "lr_predict = lr_model.predict(X_test) # 학습 결과를 바탕으로 검증 데이터를 예측\n",
    "model_evaluation(y_test, lr_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Random Forest\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "random_forest_model = RandomForestClassifier(\n",
    "    n_estimators = 50, # 50번 추정\n",
    "    max_depth = 10, # 트리 최대 깊이 10\n",
    "    random_state = 0  # 시드값 고정\n",
    ")\n",
    "rf_model = random_forest_model.fit(X_train_res, y_train_res) # 학습 진행\n",
    "rf_predict = rf_model.predict(X_test) # 평가 데이터 예측\n",
    "model_evaluation(y_test, rf_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# AdaBoost\n",
    "ada_model = AdaBoostClassifier(\n",
    "    estimator=DecisionTreeClassifier(max_depth=2, random_state=0), # 약한 학습기\n",
    "    learning_rate=0.01, # 학습률\n",
    "    n_estimators=50, # 추정기 개수\n",
    "    random_state=0 # 시드값 고정\n",
    ")\n",
    "ada_model.fit(X_train_res, y_train_res) # 학습 진행\n",
    "ada_predict = ada_model.predict(X_test) # 평가 데이터 예측\n",
    "model_evaluation(y_test, ada_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# CatBoost\n",
    "import catboost as cb\n",
    "cb_dtrain = cb.Pool(data = X_train_res, label = y_train_res) # 학습 데이터를 Catboost 모델에 맞게 변환\n",
    "cb_param = {\n",
    "    'max_depth': 10, # 트리 깊이\n",
    "    'learning_rate': 0.01, # Step Size\n",
    "    'n_estimators': 50, # Number of trees, 트리 생성 개수\n",
    "    'eval_metric': 'Accuracy', # 평가 척도\n",
    "    'loss_function': 'MultiClass', # 손실 함수, 목적 함수\n",
    "    'random_seed': 0, # 재현성 고정\n",
    "    'verbose': False # 로그 출력 끄기\n",
    "}\n",
    "cb_model = cb.train(pool = cb_dtrain, params = cb_param) # 학습 진행\n",
    "cb_model_predict = np.argmax(cb_model.predict(X_test), axis = 1) # 평가 데이터 예측, Softmax의 결과값 중 가장 큰 값의 Label로 예측, 인덱스의 순서를 맞추기 위해 +1\n",
    "model_evaluation(y_test, cb_model_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 다양한 모델의 성능 비교\n",
    "- 선형회귀, RandomForest, AdaBoost, CatBoost, LightGBM 성능비교"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def compute_metrics(label, predict):\n",
    "    cf = confusion_matrix(label, predict)\n",
    "    accuracy = (cf[0][0] + cf[1][1]) / cf.sum()\n",
    "    precision = cf[1][1] / (cf[1][1] + cf[0][1]) if (cf[1][1] + cf[0][1]) != 0 else 0\n",
    "    recall = cf[1][1] / (cf[1][1] + cf[1][0]) if (cf[1][1] + cf[1][0]) != 0 else 0\n",
    "    f1 = (2 * precision * recall) / (precision + recall) if (precision + recall) != 0 else 0\n",
    "    return [accuracy, precision, recall, f1]\n",
    "\n",
    "# ada_predict가 먼저 생성되어 있어야 함\n",
    "model_names = [\"Logistic Regression\", \"RandomForest\", \"AdaBoost\", \"CatBoost\", \"LightGBM\"]\n",
    "model_preds = [lr_predict, rf_predict, ada_predict, cb_model_predict, lgb_model2_predict]\n",
    "metric_names = [\"Accuracy\", \"Precision\", \"Recall\", \"F1-Score\"]\n",
    "\n",
    "scores = np.array([compute_metrics(y_test, pred) for pred in model_preds])\n",
    "\n",
    "x = np.arange(len(model_names))\n",
    "width = 0.18\n",
    "\n",
    "plt.figure(figsize=(13, 6))\n",
    "for i, metric in enumerate(metric_names):\n",
    "    bars = plt.bar(x + (i - 1.5) * width, scores[:, i], width=width, label=metric)\n",
    "    for bar in bars:\n",
    "        h = bar.get_height()\n",
    "        plt.text(bar.get_x() + bar.get_width() / 2, h + 0.01, f\"{h:.2f}\", ha=\"center\", va=\"bottom\", fontsize=8)\n",
    "\n",
    "plt.xticks(x, model_names, rotation=10)\n",
    "plt.ylim(0, 1.08)\n",
    "plt.xlabel(\"Model\")\n",
    "plt.ylabel(\"Score\")\n",
    "plt.title(\"Model Performance Comparison\")\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Bagging 적용\n",
    "- 연산량이 커 실행 시간이 증가할 수 있음\n",
    "- RandomForest는 Bagging 기반 모델이며, 여기서는 추가 Bagging 효과를 실험적으로 확인\n",
    "- Bagging 목적: 분산 감소와 일반화 성능 안정화"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "bagging_predict_result = [] # 빈 리스트 생성\n",
    "number_of_bagging = 5 # Bagging 횟수\n",
    "np.random.seed(0) # 재현성 고정\n",
    "for idx in range(number_of_bagging):\n",
    "    data_index = [data_index for data_index in range(X_train_res.shape[0])]\n",
    "    random_data_index = np.random.choice(data_index, X_train_res.shape[0])\n",
    "    random_forest_model2 = RandomForestClassifier(\n",
    "        n_estimators=50, # 50번 추정\n",
    "        max_depth=10, # 트리 최대 깊이 10\n",
    "        random_state=0, # 시드값 고정\n",
    "        verbose=0  # 0: 출력 없음, 1 이상: 학습 로그 출력\n",
    "    )\n",
    "    # 학습 진행\n",
    "    rf_model2 = random_forest_model2.fit(\n",
    "        X = pd.DataFrame(X_train_res).iloc[random_data_index, :],\n",
    "        y = pd.DataFrame(y_train_res).iloc[random_data_index, 0].to_numpy() # 1D로 변경\n",
    "    ) \n",
    "    rf_predict2 = rf_model2.predict(X_test) # 평가 데이터 예측\n",
    "    bagging_predict_result.append(rf_predict2) # 예측 결과를 bagging_predict_result에 저장\n",
    "    print(f\"\\n{idx + 1} Model Evaluation Result:\") # 전체적인 성능 평가\n",
    "    model_evaluation(y_test, rf_predict2) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Bagging을 바탕으로 예측한 결과값에 대해 다수결로 예측"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bagging_predict = [] # 빈 리스트 생성\n",
    "for lst2_index in range(X_test.shape[0]): # 테스트 데이터 개수만큼 반복\n",
    "    temp_predict = [] # 반복문 내 임시 빈 리스트 생성\n",
    "    for lst_index in range(len(bagging_predict_result)): # Bagging 결과 리스트 개수 만큼 반복\n",
    "        temp_predict.append(bagging_predict_result[lst_index][lst2_index]) # 각 Bagging 결과 예측한 값 중 같은 인덱스를 리스트에 저장\n",
    "    if np.mean(temp_predict) >= 0.5: # 0, 1 이진분류이므로, 예측값의 평균이 0.5보다 크면 1, 아니면 0으로 예측 다수결)\n",
    "        bagging_predict.append(1)\n",
    "    elif np.mean(temp_predict) < 0.5: # 예측값의 평균이 0.5보다 낮으면 0으로 결과 저장\n",
    "        bagging_predict.append(0)\n",
    "model_evaluation(y_test, bagging_predict) # 모델 분류 평가 결과"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# F1-score comparison:\n",
    "def f1_from_pred(label, pred):\n",
    "    cf = confusion_matrix(label, pred)\n",
    "    precision = cf[1][1] / (cf[1][1] + cf[0][1]) if (cf[1][1] + cf[0][1]) != 0 else 0\n",
    "    recall = cf[1][1] / (cf[1][1] + cf[1][0]) if (cf[1][1] + cf[1][0]) != 0 else 0\n",
    "    f1 = (2 * precision * recall) / (precision + recall) if (precision + recall) != 0 else 0\n",
    "    return f1\n",
    "\n",
    "# ada_predict가 먼저 생성되어 있어야 함\n",
    "model_names = [\n",
    "    \"Logistic Regression\",\n",
    "    \"RandomForest\",\n",
    "    \"AdaBoost\",\n",
    "    \"CatBoost\",\n",
    "    \"LightGBM\",\n",
    "    \"Bagging\",\n",
    "]\n",
    "preds = [lr_predict, rf_predict, ada_predict, cb_model_predict, lgb_model2_predict, bagging_predict]\n",
    "\n",
    "f1_scores = [f1_from_pred(y_test, p) for p in preds]\n",
    "\n",
    "plt.figure(figsize=(11, 5))\n",
    "bars = plt.bar(model_names, f1_scores, color=[\"#4C72B0\", \"#55A868\", \"#DD8452\", \"#C44E52\", \"#8172B3\", \"#CCB974\"])\n",
    "plt.ylim(0, 1.0)\n",
    "plt.xlabel(\"Model\")\n",
    "plt.ylabel(\"F1-Score\")\n",
    "plt.title(\"F1-Score Comparison Across Models\")\n",
    "plt.xticks(rotation=15)\n",
    "\n",
    "for bar, score in zip(bars, f1_scores):\n",
    "    plt.text(bar.get_x() + bar.get_width() / 2, score + 0.015, f\"{score:.2f}\", ha=\"center\")\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python (smote-venv)",
   "language": "python",
   "name": "smote-venv"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
