08_ML(Machine_Learning)

16_로지스틱 회귀분석_심화

chuu_travel 2025. 4. 8. 17:57
728x90

로지스틱 회귀분석(Logistic Regression)

 

  • 선형 회귀분석과 유사하지만 종속변수가 양적 척도가 아닌 질적 척도임
    • 특성 수치를 예측하는 것이 아니라 어떤 카테고리에 들어갈지 분류하는 모델
  • 기본 모형은 종속변수가 0과 1이라는 이항(binary)으로 이루어져 구매/미구매, 성공/실패, 합격/불합격 등을 예측
  • 만약 종속변수의 범주가 3개 이상일 경우에는 다항 로지스틱 회귀분석(Multinomial Logistic Regression)을 통해 분류 예측을 할 수 있음
  • 로지스틱 회귀는 기존의 선형회귀식의 사상은 그대로 유지하되 종속변수를 1이 될 확률로 변환하여 그 확률에 따라 0과 1의 여부를 예측
  • 선형 회귀선은 이항으로 이루어진 종속변수를 직선으로 표현하여 확률이 양과 음의 무한대로 뻗어나가게 됨
    • 이러한 방식은 확률을 표현하기에 적합하지 않기 때문에 0과 1사이의 S자 곡선의 형태를 갖도록 변환해줘야 함
  • 선형회귀선을 로짓회귀선으로 변환하기 위해서는 오즈값을 구해야 함
    • 오즈(Odds): 사건이 발생할 가능성이 발생하지 않을 가능성보다 어느 정도 큰지를 나타내는 값
      • 분모는 사건이 발생하지 않을 확률, 분자는 사건이 발생할 확률로 하여 사건이 발생하지 않을 확률 대비 사건이 발생할 확률을 비율로 나타냄
      • 즉, 발생할 확률이 그렇지 않을 확률과 50 : 50으로 같으면 1.0, 두 배면 2.0, 다섯 배면 5.0 이됨
      • 만약 발생확률이 60%이고 발생하지 않을 확률이 40%라면 발생확률이 1.5배 높기 때문에 오즈는 1.5가 됨
    • 하지만 직선 형태의 회귀 값을 사건이 일어날 오즈값으로 변환하게 되면 발생확률이 1에 가까워질수록 기하급수적으로 커지고 최솟값은 0이 되는 균형잡히지 않은 형태가 됨

 

 

 

● 따라서 오즈 값에 로그를 취하면 양의 무한대에서 음의 무한대를 갖는 형태가 됨

 

 

하지만 여전히 0에서 1 사이의 범위를 나타내지 못하는 문제가 있음

  • 따라서 확률을 로짓 변환하여 0에서 1사이로 치환
  • 이 변환식을 시그모이드(Sigmoid) 함수라고 함

 

 

시그모이드 함수 적용 과정

 

  • 이렇게 로지스틱 회귀분석은 사건 발생 확률을 0에서 1사이로 변환해서 표현
    • 이러한 분류 모델은 카테고리를 분류하는 임계치를 어떻게 설정하는지가 중요
      • 기본적으로 분류 기준값은 0.5
      • 주제에 따라 달라질 수 있음
    • 최적의 효율을 따져가며 분류 기준을 잡아야하기 때문에, 로지스틱 회귀 모델과 같은 분류 모델은 모델 성능 평가가 매우 중요

 

import pandas as pd
import numpy as np
from sklearn.preprocessing import RobustScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from imblearn.under_sampling import RandomUnderSampler
import statsmodels.api as sm
import seaborn as sns
import matplotlib.pyplot as plt

 

# 데이터 불러오기
df = pd.read_csv("./data/heart_2020_cleaned.csv")

 

df.head()

 

df.shape
(319795, 18)

 

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 319795 entries, 0 to 319794
Data columns (total 18 columns):
 #   Column            Non-Null Count   Dtype  
---  ------            --------------   -----  
 0   HeartDisease      319795 non-null  object 
 1   BMI               319795 non-null  float64
 2   Smoking           319795 non-null  object 
 3   AlcoholDrinking   319795 non-null  object 
 4   Stroke            319795 non-null  object 
 5   PhysicalHealth    319795 non-null  float64
 6   MentalHealth      319795 non-null  float64
 7   DiffWalking       319795 non-null  object 
 8   Sex               319795 non-null  object 
 9   AgeCategory       319795 non-null  object 
 10  Race              319795 non-null  object 
 11  Diabetic          319795 non-null  object 
 12  PhysicalActivity  319795 non-null  object 
 13  GenHealth         319795 non-null  object 
 14  SleepTime         319795 non-null  float64
 15  Asthma            319795 non-null  object 
 16  KidneyDisease     319795 non-null  object 
 17  SkinCancer        319795 non-null  object 
dtypes: float64(4), object(14)
memory usage: 43.9+ MB

 

df.columns
Index(['HeartDisease', 'BMI', 'Smoking', 'AlcoholDrinking', 'Stroke',
       'PhysicalHealth', 'MentalHealth', 'DiffWalking', 'Sex', 'AgeCategory',
       'Race', 'Diabetic', 'PhysicalActivity', 'GenHealth', 'SleepTime',
       'Asthma', 'KidneyDisease', 'SkinCancer'],
      dtype='object')

 

# 명목형 변수 원핫인코딩(중간 과정 생략 가능)
df2 = pd.get_dummies(df, columns = ['HeartDisease', 'Smoking', 'AlcoholDrinking', 'Stroke', 
                                    'DiffWalking', 'Sex', 'AgeCategory', 'Race', 'Diabetic', 
                                    'PhysicalActivity', 'GenHealth', 'Asthma', 'KidneyDisease', 
                                    'SkinCancer'], drop_first = True) 
# drop_first = True : 다중공선성을 피하기 위해 앞의 한 컬럼을 삭제
df2.head()

 

 

df2.columns
Index(['BMI', 'PhysicalHealth', 'MentalHealth', 'SleepTime',
       'HeartDisease_Yes', 'Smoking_Yes', 'AlcoholDrinking_Yes', 'Stroke_Yes',
       'DiffWalking_Yes', 'Sex_Male', 'AgeCategory_25-29', 'AgeCategory_30-34',
       'AgeCategory_35-39', 'AgeCategory_40-44', 'AgeCategory_45-49',
       'AgeCategory_50-54', 'AgeCategory_55-59', 'AgeCategory_60-64',
       'AgeCategory_65-69', 'AgeCategory_70-74', 'AgeCategory_75-79',
       'AgeCategory_80 or older', 'Race_Asian', 'Race_Black', 'Race_Hispanic',
       'Race_Other', 'Race_White', 'Diabetic_No, borderline diabetes',
       'Diabetic_Yes', 'Diabetic_Yes (during pregnancy)',
       'PhysicalActivity_Yes', 'GenHealth_Fair', 'GenHealth_Good',
       'GenHealth_Poor', 'GenHealth_Very good', 'Asthma_Yes',
       'KidneyDisease_Yes', 'SkinCancer_Yes'],
      dtype='object')

 

 

# 스케일링
# 숫자형 변수 분리
df_num = df2[['BMI', 'PhysicalHealth', 'MentalHealth', 'SleepTime']]
df_nom = df2.drop(['BMI', 'PhysicalHealth', 'MentalHealth', 'SleepTime'], axis = 1)
# nomical: 명목상의

 

# 숫자형 변수 RobustScaler 적용
rs = RobustScaler()
df_robust = rs.fit_transform(df_num)

 

● standard / minmax scaler는 이상치에 민감하게 반응

● robust scaler는 이상치의 영향 적음

 

df_num2 = pd.DataFrame(data = df_robust, columns = df_num.columns)
df_num.head()

 

 

df_num2.head()

 

 

# 숫자형 테이블과 더미화 문자형 테이블 결합
df3 = pd.concat([df_num2, df_nom], axis = 1)
df3.head()

 

 

 

# 독립변수와 종속변수 분리
x = df3.drop(["HeartDisease_Yes"], axis = 1)
y = df3[["HeartDisease_Yes"]]
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size = 0.25, stratify = y, random_state = 26)

 

x_train.shape, x_test.shape
((239846, 37), (79949, 37))

 

df3["HeartDisease_Yes"].value_counts()
HeartDisease_Yes
False    292422
True      27373
Name: count, dtype: int64

 

# HeartDisease_Yes 컬럼 클래스 분포 시각화
sns.countplot(data = y_train, x = "HeartDisease_Yes")
plt.show()

 

  • 클래스 불균형이 심한 상황
    • 언더 샘플링이나 오버 샘플링을 적용하여 클래스 균형을 맞춰 줘야 함

 

# 임시 변수명 적용
x_train_re = x_train.copy()
y_train_re = y_train.copy()

 

x_train.shape
(239846, 37)

 

x_temp_name = [f"X{i}" for i in range(1, 38)]
x_temp_name
['X1',
 'X2',
 'X3',
 ...

 

 

y_temp_name = ["y1"]
x_train_re.columns = x_temp_name
y_train_re.columns = y_temp_name
x_train_re.head()

 

 

  • 기존 변수명은 언더샘플링이나 오버샘플링 적용시 오류가 발생하기 때문에 임시 변수명을 부여
# 언더샘플링 적용
x_train_under, y_train_under = RandomUnderSampler(random_state = 26).fit_resample(x_train_re, y_train_re)

 

print("언더샘플링 적용전")
print(x_train_re.shape, y_train_re.shape)
언더샘플링 적용전
(239846, 37) (239846, 1)
 

 

print("언더샘플링 적용후")
print(x_train_under.shape, y_train_under.shape)
언더샘플링 적용후
(41060, 37) (41060, 1)

 

 

print("언더샘플링 적용전 종속변수 분포")
print(y_train_re["y1"].value_counts())
언더샘플링 적용전 종속변수 분포
y1
False    219316
True      20530
Name: count, dtype: int64

 

print("언더샘플링 적용후 종속변수 분포")
print(y_train_under["y1"].value_counts())
언더샘플링 적용후 종속변수 분포
y1
False    20530
True     20530
Name: count, dtype: int64

 

  • 클래스 불균형 문제를 해결하기 위해 언더샘플링을 적용

 

list(x_train)
['BMI',
 'PhysicalHealth',
 'MentalHealth',
 ...

 

 

# 컬럼명 복구
x_train_under.columns = x_train.columns
y_train_under.columns = y_train.columns

 

x_train_under.head()

 

 

x_train_under.shape
(41060, 37)

 

x_test.shape
(79949, 37)

 

 

# 모델 학습
logi = LogisticRegression()
logi.fit(x_train_under, y_train_under)

 

 

print(logi.score(x_train_under, y_train_under))
print(logi.score(x_test, y_test))
0.7644179249878227
0.7494903000662922
 
# 계수값 확인
print(logi.coef_)
[[ 0.07812681  0.00740989  0.01517354 -0.0569881   0.37296029 -0.18396565
   1.25705053  0.30254747  0.74117717 -0.11364318  0.13757743  0.32507719
   0.74856926  0.92766633  1.3493028   1.69041699  1.94715393  2.1916422
   2.50928999  2.70768367  3.05910533 -0.76368449 -0.50568593 -0.2940684
  -0.26922156 -0.25808589  0.2043201   0.51937681  0.27661669  0.03025086
   1.52578414  1.03761581  1.91997426  0.43381892  0.29534956  0.58444019
   0.14760436]]

 

 

# 오류 해결을 위해 데이터 타입을 float로 변경
x_train_under = x_train_under.astype(float)

 

# statsmodels 라이브러리로 로지스틱 모델 학습
logi2 = sm.Logit(y_train_under, x_train_under)
result = logi2.fit()
Optimization terminated successfully.
         Current function value: 0.501118
         Iterations 7

 

 

result.summary()

 

# coef: 기울기

 

# 로지스틱 회귀의 오즈비 확인
np.exp(logi.coef_)
array([[ 1.08125976,  1.00743741,  1.01528924,  0.94460531,  1.45202669,
         0.83196438,  3.51503869,  1.35330192,  2.09840424,  0.89257639,
         1.14749055,  1.38413748,  2.1139733 ,  2.52860136,  3.85473708,
         5.42174103,  7.00871187,  8.94989854, 12.29619657, 14.99450305,
        21.30848455,  0.46594649,  0.60309175,  0.74522552,  0.76397397,
         0.77252887,  1.22669075,  1.68097975,  1.31866082,  1.03071307,
         4.59874821,  2.82247966,  6.82078292,  1.5431394 ,  1.34359595,
         1.79398641,  1.15905424]])

 

result.params
BMI                                 0.087499
PhysicalHealth                      0.006102
MentalHealth                        0.006020
SleepTime                          -0.068517
Smoking_Yes                         0.349063
AlcoholDrinking_Yes                -0.210271
Stroke_Yes                          1.245506
DiffWalking_Yes                     0.276822
Sex_Male                            0.660377
AgeCategory_25-29                  -1.271849
AgeCategory_30-34                  -1.030911
AgeCategory_35-39                  -0.792743
AgeCategory_40-44                  -0.399342
AgeCategory_45-49                  -0.208285
AgeCategory_50-54                   0.224675
AgeCategory_55-59                   0.571438
AgeCategory_60-64                   0.834615
AgeCategory_65-69                   1.086579
AgeCategory_70-74                   1.400997
AgeCategory_75-79                   1.605469
AgeCategory_80 or older             1.932453
Race_Asian                         -2.818775
Race_Black                         -2.533232
Race_Hispanic                      -2.375967
Race_Other                         -2.309628
Race_White                         -2.284929
Diabetic_No, borderline diabetes    0.208379
Diabetic_Yes                        0.512654
Diabetic_Yes (during pregnancy)     0.118510
PhysicalActivity_Yes               -0.121022
GenHealth_Fair                      1.290777
GenHealth_Good                      0.795862
GenHealth_Poor                      1.703415
GenHealth_Very good                 0.200574
Asthma_Yes                          0.233243
KidneyDisease_Yes                   0.574981
SkinCancer_Yes                      0.167291
dtype: float64

 

 

np.exp(result.params)
BMI                                 1.091441
PhysicalHealth                      1.006121
MentalHealth                        1.006039
SleepTime                           0.933777
Smoking_Yes                         1.417739
AlcoholDrinking_Yes                 0.810364
Stroke_Yes                          3.474693
DiffWalking_Yes                     1.318931
Sex_Male                            1.935521
AgeCategory_25-29                   0.280313
AgeCategory_30-34                   0.356682
AgeCategory_35-39                   0.452601
AgeCategory_40-44                   0.670761
AgeCategory_45-49                   0.811976
AgeCategory_50-54                   1.251915
AgeCategory_55-59                   1.770812
AgeCategory_60-64                   2.303926
AgeCategory_65-69                   2.964116
AgeCategory_70-74                   4.059246
AgeCategory_75-79                   4.980195
AgeCategory_80 or older             6.906429
Race_Asian                          0.059679
Race_Black                          0.079402
Race_Hispanic                       0.092925
Race_Other                          0.099298
Race_White                          0.101781
Diabetic_No, borderline diabetes    1.231679
Diabetic_Yes                        1.669717
Diabetic_Yes (during pregnancy)     1.125818
PhysicalActivity_Yes                0.886014
GenHealth_Fair                      3.635609
GenHealth_Good                      2.216351
GenHealth_Poor                      5.492672
GenHealth_Very good                 1.222104
Asthma_Yes                          1.262688
KidneyDisease_Yes                   1.777097
SkinCancer_Yes                      1.182098
dtype: float64

 

 

  • 독립변수가 종속변수의 심장병 여부 확률에 어떤 영향을 미치는지 확인하기 위해 오즈비를 산출
    • 해당 독립변수가 1일 때 심장병 발생 확률이 몇 배 더 큰지를 나타냄
    • 예) Smoking_Yes 변수의 오즈비는 1.4이기 때문에 흡연자는 비흡연자보다 심장병 발생 확률이 1.4배 높다
728x90