근로자의 날인데 비가 추적추적 온다.
대놓고 놀기에는 목요일이기때문에, 살살 놀면서 공부하는 중 ㅋ
어차피 내일부터 2박3일로 경주여행을 가기로 했기 때문에 양심상 오늘은 해야한다.
양심?이라기보다는 사실 더는 밀리면 미래의 내가 너무 고통받을게 뻔하기 때문에 낮잠 한숨 자고 집앞 카페로 다시 나왔다.
요즘 집앞 카페에 자주 온다.
원래는 10분거리에 3층짜리 할리스 가는걸 좋아하는데(넓고 독서실처럼 되어있고, 눈치안보임, 흡연실 있음)
늦잠자거나, 공부 조금만 할 계획이거나, 밤늦게까지 해야하거나, 비가 오거나, 카이막이 먹고싶으면 집앞으로 온다
???이정도면 할리스 가는거보다 여기오는 걸 좋아한다고 말해야하는 것이 아닌가
어제 11시까지 공부하느라 아메리카노를 하나 더 시켰는데,
알고보니 매장에서 먹으면 1500원에 리필을 해주신다 개 꿀~
그러고 어제 커피 두잔먹어서 새벽 3시에 잤다는..
어제 오랜만에 새벽 1시 넘어서까지 빡공을 달렸는데, 사실 공모전이 드디어 발등에 불떨어져서 타의로 달린 것.
공모전 진행기도 써야하는데, 이제 슬슬 마음이 쫄리고 무겁고 근심이 가득해지고 있어서 언제 쓸지는 모르겠다.
진행기 쓸 시간있으면 데이터전처리해야함 ㅋ
진짜 데이터는 전처리보다 서칭하는게 더 힘들고 귀찮고 까다로운 것 같다.
아무튼 오늘 포스팅은 어제에 이어 못다한 회귀모델 전반부에 대해 쓰려고 한다.
3. Penalized Regression
0. 라이브러리 준비
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm
import scipy.stats as stats
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
# from statsmodels.stats.outliers_influence import variance_inflation_factor
# from statsmodels.tools.tools import add_constant
# from sklearn.preprocessing import StandardScaler
# from mlxtend.feature_selection import SequentialFeatureSelector as SFS
# from sklearn.inspection import permutation_importance
# from sklearn.metrics import mean_squared_error
# from scipy.stats import probplot
from sklearn.linear_model import Ridge, Lasso
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
Ridge와 Lasso 회귀 모델 성능을 최적화하고 평가하는데 사용하는 from sklearn.linear_model import Ridge, Lasso,
(Ridge는 다중공선성이 있을 때, Lasso는 특성 선택이 필요한 경우 효과적이다.)
*Ridge 회귀: L2 정규화를 통해 모델의 계수를 작게 만들어 과적합(overfitting)을 방지
*Lasso 회귀: L1 정규화를 통해 불필요한 특성의 계수를 0으로 만들어 변수 선택 효과도 제공
# from mlxtend.feature_selection import SequentialFeatureSelector as SFS
: 특성을 하나씩 추가하거나 제거하면서 모델 성능을 기준으로 가장 좋은 조합의 특성을 선택
- Forward Selection: 특성을 하나씩 추가
- Backward Elimination: 모든 특성에서 하나씩 제거
- Floating 방식도 지원 (조합 최적화)
- 너무 많은 특성이 있을 경우 forward=True만 사용하면 시간이 오래 걸릴 수 있으므로, 주요 특성 선별 후 적용하는 게 좋다.
# from sklearn.inspection import permutation_importance
: 이미 학습된 모델에서 각 특성을 무작위로 섞어 성능 변화량을 보고, 특성 중요도를 정량적으로 계산
- 학습 데이터가 아닌 검증 데이터나 테스트 데이터에서 사용하는 게 중요
- 모델 자체가 feature_importances_ 속성을 제공하지 않는 경우 특히 유용
1. 데이터 읽기
2. 벌점 회귀 모형
2-1. 독립변수 및 종속변수 설정
train = rd
X_train = train.drop(columns=['y'])
y_train = train['y']
2-2. 표준화
릿지(Ridge)와 라쏘(Lasso) 회귀 모형을 적용할 시 표준화를 하는 것이 일반적
- 규제 효과: 변수 크기 차이로 인한 비일관적인 페널티 적용을 방지
- 스케일링 문제: 변수들의 다른 척도(크기)로 인해 특정 변수가 과도하게 영향을 미치는 것을 방지
- 해석 용이성: 표준화된 변수를 사용하면 가중치를 비교하고 해석하기 좋음
표준화 방법:
- 각 변수에서 평균을 빼고 표준편차로 나누어 z-score로 변환
- 이때 유의할 점은, 모형에 적합한(학습한) 데이터에 대한 𝜇, 𝜎 값을 저장해놨다가, 새로운 데이터에 동일하게 적용해야 함
- 표준화된 값= (𝑋 − 𝜇)/𝜎
- 𝑋 는 원래 변수 값, 𝜇는 변수의 평균, 𝜎는 변수의 표준편차
2-2-1) 데이터 스케일링
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
모든 특성의 스케일을 평균 0, 표준편차 1로 정규화하여 모델이 특성 변수에 치우치지 않도록 해준다.
2-2-2) 기본모델
model_raw = LinearRegression()
model_raw.fit(X = X_train_scaled, # 독립변수
y = y_train) # 종속 변수
print('R2:', model_raw.score(X_train_scaled, y_train)) ## R2값
# 적합 모형 수정 R^2 값 확인
def adjusted_r2(model, X, y):
r2 = model.score(X, y)
n = X.shape[0]
p = X.shape[1]
adj_r2 = 1 - (1 - r2) * (n - 1) / (n - p - 1)
return adj_r2
adj_r2_raw = adjusted_r2(model_raw, X_train_scaled, y_train) ## adj R2 값
print('adj R2:', adj_r2_raw)
LinearRegression()으로 선형 회귀 모델을 생성하고, 학습용 데이터로 모델을 학습시킨다.
.score()로 R² 점수를 반환시킨다.
결정계수 R²는 모델이 종속 변수의 분산을 얼마나 잘 설명하는지 나타낸다. (1에 가까울수록 GOOD, 0은 설명력 없음)
하지만 R²은 특성이 많아질수록 높아질 수 있으므로 과적합에 유의해야한다.
수정된 결정계수 Adjusted R²는 특성이 많을수록 무조건 올라가는 R²를 방지하고, 모델의 복잡도를 보정해주는 지표이다.
불필요한 변수를 포함할 경우 Adjusted R²는 오히려 줄어든다.
2-3. Ridge
2-3-1) 릿지 회귀 모델의 최적 람다 결정
## 리지 회귀 모델의 최적 람다 결정
ridge_params = {'alpha': np.logspace(-5, 5, 50)}
ridge_params
np.logspace(-5, 5, 50)은 10^-5부터 10^5까지 기하급수적으로 증가하는 숫자들 중 50개를 생성해준다.
즉, 탐색할 alpha 범위는 다음과 같다.
[1.00000e-05, 1.62378e-05, ..., 1.00000e+05]
이렇게 하면 아주 작은 규제부터 매우 강한 규제까지 폭넓게 테스트할 수 있다.
alpha는 규제 강도를 조절하는데,
작을수록 모델이 선형회귀에 가까워져 과적합 위험이 발생하고,
클수록 계수가 거의 0에 수렴해 과소적합 위험이 발생(정보손실)한다.
# 리지 회귀 계수 저장
ridge_coefs = []
for alpha in ridge_params['alpha']:
ridge = Ridge(alpha=alpha)
ridge.fit(X_train_scaled, y_train)
ridge_coefs.append(ridge.coef_)
# 리지 회귀 계수 시각화
ridge_coefs = np.array(ridge_coefs)
plt.figure(figsize=(10, 5))
for i in range(ridge_coefs.shape[1]):
plt.plot(ridge_params['alpha'], ridge_coefs[:, i],
label = X_train.columns[i])
plt.xscale('log')
plt.xlabel('Lambda (alpha)')
plt.ylabel('Coefficients')
plt.title('Ridge Coefficients as a Function of Lambda')
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.grid()
plt.show()
릿지 회귀의 계수가 α(lambda) 값에 따라 어떻게 변하는지를 시각화함으로써, 정규화 강도에 따른 계수의 민감도를 분석하는 코드이다.
alpha 값마다 릿지 회귀 모델을 학습시키고, 각 모델의 계수(ridge.coef_)를 저장한다.
각 특성에 대해 50개의 alpha 값에 따른 계수 변화를 line plot로 시각화한다.
이 때 alpha가 지수적으로 증가하기 때문에, x축은 log scale을 사용한다.
alpha가 작을수록 (왼쪽) : 계수 크기가 크고, 과적합 위험 존재
alpha가 클수록 (오른쪽) : 모든 계수가 0에 가까워짐 (과소적합), 일반화는 좋아질 수 있음
선이 수평에 가까운 특성 : alpha 변화에 덜 민감 → 중요한 변수일 수 있음
이 그래프를 통해 어떤 특성이 정규화에 의해 얼마나 강하게 억제되는지를 시각적으로 확인 가능
ridge = Ridge() # 릿지 함수 객체 생성
ridge_grid = GridSearchCV(ridge,
ridge_params, # 람다 후보
cv = 5, # cv > 1 이면, 여러번 반복해서 더 정확하게 추정 가능 보통 5, 10 사용
scoring = 'r2') # adjusted_r2
ridge_grid.fit(X_train_scaled, y_train)
ridge_grid
GridSearch를 이용해 릿지 회귀의 최적 alpha 값을 찾는 교차 검증 기반 튜닝을 수행하는 단계이다.
**파라미터 의미
cv=5 : 데이터를 5개의 Fold로 나눠 교차 검증 수행
scoring='r2' : R² 점수를 기준으로 모델 성능 평가
ridge_params : alpha 값 후보 (지수 로그스케일로 다양하게 구성됨)
2-3-2) 결과 확인
# R2 값(scoring 값)
print("R2:", ridge_grid.best_score_) ## R2 값
# 최적의 람다(알파) 값
print("Optimal lambda: ", ridge_grid.best_params_)
이 최적의 alpha 값은 ridge 회귀 모델의 일반화 성능을 극대화하는 정규화 강도를 보여준다.
# Optimal Ridge Model
best_ridge = ridge_grid.best_estimator_ ## 최적 모델 저장!
best_ridge
GridSearchCV를 통해 찾은 최적의 Ridge 회귀 모델 객체를 추출한다.
ridge_grid.best_estimator_는 교차 검증 과정에서 가장 성능이 좋았던 하이퍼파라미터를 적용한 Ridge 모델 인스턴스를 반환한다.
best_ridge는 이제 바로 예측하거나 테스트 데이터에 적용할 수 있는 완전한 모델이다.
# 결과
y_pred_ridge = best_ridge.predict(X_train_scaled)
y_pred_ridge
최적의 Ridge 모델 (best_ridge)을 이용해 훈련 데이터에 대한 예측값을 계산하는 과정이다.
2-3-3) 릿지 회귀 모델의 회귀 계수
# Ridge 회귀 모델의 회귀 계수
ridge_coefs = pd.Series(best_ridge.coef_,
index = X_train.columns)
print("Ridge 회귀 모델의 회귀 계수:")
print(ridge_coefs)
최적화된 Ridge 회귀 모델의 각 특성에 대한 회귀 계수를 확인하고 있다.
이 정보는 어떤 특성이 종속 변수에 얼마나 영향을 미치는지를 이해하는 데 핵심적인 역할을 한다.
2-4. Lasso
2-4-1) 라쏘 회귀 모델의 최적 람다 결정
## 라쏘 회귀 모델의 최적 람다 결정
lasso_params = {'alpha': np.logspace(-5, 5, 50)}
lasso_params
# 라쏘 회귀 계수 저장
lasso_coefs = []
for alpha in lasso_params['alpha']:
lasso = Lasso(alpha = alpha)
lasso.fit(X_train_scaled, y_train)
lasso_coefs.append(lasso.coef_)
# 라쏘 회귀 계수 시각화
lasso_coefs = np.array(lasso_coefs)
plt.figure(figsize=(10, 5))
for i in range(lasso_coefs.shape[1]):
plt.plot(lasso_params['alpha'], lasso_coefs[:, i],
label = X_train.columns[i])
plt.xscale('log')
plt.xlabel('Lambda (alpha)')
plt.ylabel('Coefficients')
plt.title('lasso Coefficients as a Function of Lambda')
plt.legend(bbox_to_anchor=(1.05, 1), loc= 'upper left')
plt.grid()
plt.show()
lasso = Lasso() # 라쏘 함수 객체 생성
lasso_grid = GridSearchCV(lasso,
lasso_params, # 람다 후보
cv = 5, # cv > 1 이면, 여러번 반복해서 더 정확하게 추정 가능 보통 5, 10 사용
scoring = 'r2') # adjusted_r2
lasso_grid.fit(X_train_scaled, y_train)
lasso_grid
2-4-2) 결과 확인
# R2 값(scoring 값)
print("R2:", lasso_grid.best_score_) ## R2 값
# 최적의 람다(알파) 값
print("Optimal lambda: ", lasso_grid.best_params_)
## Optimal lasso Model
best_lasso = lasso_grid.best_estimator_ ## 최적 모델을 저장
best_lasso
## 결과
y_pred_lasso = best_lasso.predict(X_train_scaled)
y_pred_lasso
2-4-3) 라쏘 회귀 모델의 회귀 계수
# Lasso 회귀 모델의 회귀 계수
lasso_coefs = pd.Series(best_lasso.coef_,
index = X_train.columns)
print("Lasso 회귀 모델의 회귀 계수:")
print(lasso_coefs)
# Lasso 회귀 모델에서 선택된 변수 (계수가 0이 아닌 변수들)
sel_features_lasso = lasso_coefs[lasso_coefs != 0].index.tolist()
print("Lasso 회귀 모델에서 선택된 변수들:")
print(sel_features_lasso)
Lasso 회귀 모델을 통해 선택된 중요 변수들을 추출하고 있다.
Lasso 회귀는 L1 정규화를 통해 불필요한 변수의 계수를 0으로 만들어 자동으로 변수 선택을 해준다는 장점을 지닌다.
2-5. 비교
2-5-1) 데이터 준비
2-5-2) 표준화 동일하게 적용
test = rd2
X_test = test.drop(columns=['y']) # y_test = test['y']
X_test_scaled = scaler.transform(X_test) # 새로운 데이터가 들어오면 동일하게 적용!!!
새로운 테스트 데이터셋 rd2를 기존 훈련 데이터에서 학습된 스케일러 기준으로 변환하는 전처리 과정을 수행한다.
2-5-3) 새로운 데이터에 모델 적용
# 새로운 데이터에 모델 적용
y_pred_ridge = best_ridge.predict(X_test_scaled)
y_pred_lasso = best_lasso.predict(X_test_scaled)
y_pred_raw = model_raw.predict(X_test_scaled)
학습된 모델들을 새로운 데이터(X_test_scaled)에 적용하여 예측값을 산출하는 단계이다.
# 기본 모델 평가
mse_raw, mae_raw, r2_raw, mape_raw = evaluate_model(y_test, y_pred_raw)
print(f"Raw Regression - MSE: {mse_raw}, MAE: {mae_raw}, R2: {r2_raw}, MAPE: {mape_raw}")
# 릿지 모델 평가
mse_ridge, mae_ridge, r2_ridge, mape_ridge = evaluate_model(y_test, y_pred_ridge)
print(f"Best Ridge Alpha: {best_ridge.alpha}")
print(f"Ridge Regression - MSE: {mse_ridge}, MAE: {mae_ridge}, R2: {r2_ridge}, MAPE: {mape_ridge}")
# 라쏘 모델 평가
mse_lasso, mae_lasso, r2_lasso, mape_lasso = evaluate_model(y_test, y_pred_lasso)
print(f"Best Lasso Alpha: {best_lasso.alpha}")
print(f"Lasso Regression - MSE: {mse_lasso}, MAE: {mae_lasso}, R2: {r2_lasso}, MAPE: {mape_lasso}")
evaluate_model() 함수를 이용해 세 가지 회귀 모델 (기본 선형, Ridge, Lasso) 을 동일한 기준으로 평가하고,
MSE, MAE, R², MAPE 네 가지 주요 지표를 출력한다.
print('raw 변수:', X_train.columns.tolist())
print("raw 변수 값:", model_raw.coef_.tolist())
print('Ridge 변수:', X_train.columns.tolist())
print("Ridge 변수 값:", best_ridge.coef_.tolist())
print('lasso 변수:', sel_features_lasso)
non_zero_coefs = [coef for coef in best_lasso.coef_.tolist() if coef != 0]
print("lasso 변수 값:", non_zero_coefs)
세 가지 회귀 모델(기본 선형, Ridge, Lasso)에 대해 변수 이름과 각 변수에 대응하는 회귀 계수를 비교한다.
특히 Lasso는 자동 변수 선택 기능이 있으므로, 계수가 0이 아닌 변수만 따로 추출한다.
2-5-3) Ridge, Lasso 회귀모형 결과 사용방법
LASSO와 Ridge 회귀의 결과에 대해 일반적으로 추가적인 통계적 유의성 검증을 하지 않음
- Ridge 및 LASSO 회귀는 통계적 유의성을 평가하는 p-value를 직접 제공하지 않음
- pvalue는 주로 OLS 회귀 분석에서 각 독립 변수의 회귀 계수가 통계적으로 유의미한지 평가하기 위해 사용됨
- LASSO와 Ridge는 주로 예측 성능 향상과 과적합 방지, 전통적인 통계적 유의성 검증은 변수의 개별적 영향력을 평가하는 데 중점
- 정규화(Regularization): LASSO와 Ridge는 계수를 축소하거나 0으로 만들어 자체적으로 변수 선택 또는 중요도 평가를 수행
- 교차 검증(cv): 일반적으로 교차 검증을 통해 모델의 성능을 평가하고 최적의 정규화 람다 값을 선택
회귀 분석에서 유의미하지 않은 변수를 포함할지 여부를 결정할때.
- 도메인 지식: 회귀 분석 결과는 유의미하지 않지만 도메인 지식에 따라 중요한 변수라면 포함시켜도 무방
- 모델 목적: 예측 정확도가 목표라면 제거, 변수 해석이 중요하다면 포함
- 다중공선성: 변수들 간 상관관계가 높아 모델 안정성을 해칠 경우 제외
- 모델 복잡성: 불필요한 변수는 모델 복잡성을 높이고 과적합 위험 증가시킴.
- 검증 및 실험: 교차 검증 등을 통해 유의미하지 않은 변수를 포함한 모델과 제외한 모델의 성능 비교 가능
2-6. 참고
# 특정 람다 값으로 리지 회귀 모델 학습
ridge_alpha = 1
ridge = Ridge(alpha = ridge_alpha)
ridge.fit(X_train_scaled, y_train)
y_pred_ridge = ridge.predict(X_test_scaled)
특정한 alpha 값(= 1) 을 사용하여 리지(Ridge) 회귀 모델을 직접 학습시키고, 새로운 데이터에 대한 예측을 수행하는 흐름이다.
GridSearchCV 없이 사용자가 직접 특정 정규화 수준을 실험하고 싶은 경우, alpha 값을 직접 설정한다.
# 특정 람다 값으로 라쏘 회귀 모델 학습
lasso_alpha = 10
lasso2 = Lasso(alpha = lasso_alpha)
lasso2.fit(X_train_scaled, y_train)
y_pred_lasso2 = lasso2.predict(X_test_scaled)
특정한 정규화 강도(alpha=10)로 Lasso 회귀 모델을 직접 학습하고, 스케일링된 테스트 데이터에 대해 예측을 수행한다.
상당히 강한 정규화를 사용함으로써 불필요한 변수의 계수를 더 쉽게 0으로 만들어 변수 선택 효과 증가시킨다.
# Lasso 회귀 모델의 회귀 계수
lasso_coefs2 = pd.Series(lasso2.coef_, index = X_train.columns)
print("Lasso 회귀 모델의 회귀 계수:")
print(lasso_coefs2)
특정 alpha 값(=10)으로 학습된 Lasso 회귀 모델의 회귀 계수를 pandas.Series로 정리한다.
4. Logistic Regression
0. 라이브러리 준비
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm
import scipy.stats as stats
from sklearn.linear_model import LogisticRegression
from mlxtend.feature_selection import SequentialFeatureSelector as SFS ## sklearn 하고 이름은 같으나 다름
from sklearn.metrics import accuracy_score
이진 분류 혹은 다중 클래스 분류를 위한 선형 모델로, 입력값에 선형 결합 후 시그모이드 또는 소프트맥스 함수로 확률화시키는
from sklearn.linear_model import LogisticRegression,
사용자가 지정한 모델과 평가 기준에 따라 특성을 순차적으로 선택하고 sklearn보다 더 다양한 기능을 지원하는
from mlxtend.feature_selection import SequentialFeatureSelector as SFS까지 모두 불러와준다.
1. 데이터 준비
1-1. 데이터 로드
rd = pd.read_csv(ppath + '/1. data/diabete_lgr_tr.csv')
1-2. 종속변수 확인
# 'class' 변수 범주형으로 변경
rd['class'] = rd['class'].astype('category')
# Logistic regression model
X = rd.drop(columns=['class'])
X = sm.add_constant(X) # 상수항을 모델에 넣어줘야 함 Adds a constant term to the predictor!!
y = rd['class']
로지스틱 회귀에서는 종속 변수가 범주형이어야 하므로, 문자열이 아닌 정수형일 경우에도 category로 변환해주는게 좋다.
2. 로지스틱 모델 적합
2-1. 모델 적합
# statsmodels 사용
model_sm = sm.Logit(X, y)
result_sm = model_sm.fit()
# Summary of the model
print(result_sm.summary())
2-2. 결과해석
2-2-1) 해석1
overview
- 로지스틱 회귀 모델의 경우, R2 및 (adjusted R2)는 일반적으로 사용X
- R2 및 adj R2 는 주로 선형 회귀 모델에서 사용
- 로지스틱 회귀 모델에서는 대안적인 적합도 평가 지표를 사용(로그-유사도(log-likelihood) 및 pseudo R2)
- 로지스틱에서는 주로 새로운(테스트) 데이터에 대한 정확도로 모델 평가
모델 적합도
- Pseudo R-squared: 0.2529 - 모델이 종속 변수 변동의 약 25.29%를 설명
- Log-Likelihood: -257.38 - 로그 가능도 값은 모델의 적합도를 나타냄
- LL-Null: -344.51 - 기본 모델의 로그 가능도 값임
- LLR p-value: 1.639e-33 - 모델의 유의성, 매우 작은 값이므로 모델이 유의미함
변수별 회귀 계수 및 유의성
- const (절편): -8.0064 - 절편의 p-value가 0.000으로 매우 유의미
- preg (임신 횟수): 0.1108 - p-value가 0.003으로 유의미하며, 임신 횟수가 증가할수록 당뇨병 발생 확률이 증가
- plas (혈장 포도당 농도): 0.0310 - p-value가 0.000으로 매우 유의미하며, 혈장 포도당 농도가 증가할수록 당뇨병 발생 확률이 증가
- pres (혈압): -0.0149 - p-value가 0.009로 유의미하며, 혈압이 증가할수록 당뇨병 발생 확률이 감소
- skin (피부 두께): -0.0007 - p-value가 0.927로 유의미하지 않음
- insu (인슐린 농도): -0.0017 - p-value가 0.103으로 유의미하지 않음
- mass (체질량 지수): 0.1008 - p-value가 0.000으로 매우 유의미하며, 체질량 지수가 증가할수록 당뇨병 발생 확률이 증가
- pedi (당뇨병 계통 기능): 0.9019 - p-value가 0.011로 유의미하며, 당뇨병 계통 기능 값이 높을수록 당뇨병 발생 확률이 증가
- age (나이): 0.0138 - p-value가 0.208로 유의미하지 않음
요약
- 혈장 포도당 농도, 임신 횟수, 혈압, 체질량 지수, 당뇨병 계통 기능 등이 중요한 요인
- 피부 두께, 인슐린 농도, 나이는 유의미하지 않은 변수로 추정됨
2-3. 회귀식 만들기
# 회귀식 만들기
coefficients = result_sm.params
# 회귀식 생성
logit_equation = f"log(p / (1 - p)) = {coefficients.iloc[0]:.4f}"
for i in range(1, len(coefficients)):
logit_equation += f" + ({coefficients.iloc[i]:.4f} * {X.columns[i]})"
print("\nLogistic Regression Equation:")
print(logit_equation)
로지스틱 회귀 결과로부터 추정된 계수를 기반으로 회귀식(로그 오즈 형태)을 문자열로 표현하는 과정을 수행한다.
# 오즈 비 계산
# Odds Ratios
odds_ratios = np.exp(result_sm.params)
print("Odds Ratios:\n",
odds_ratios,
sep = '')
로지스틱 회귀의 회귀 계수(coef)를 오즈 비(Odds Ratio) 로 변환하는 것으로, np.exp() 함수를 이용해 지수변환을 이용한다.
*오즈비란?
- 계수(coef)는 로그 오즈(Log Odds) 를 의미
- exp(coef)을 계산하면, 오즈비(Odds Ratio)가 된다.
2-3-1) 변수선택법 적용
# sklearn 사용, statsmodels도 사용은 가능
log_reg = LogisticRegression(max_iter=10000, solver='liblinear')
# 전진 선택법 적용
sfs = SFS(log_reg,
k_features = 'best',
forward = True,
floating = False, ## True 이면 각 단계에서 변수를 추가하거나 제거할 때, 이전에 선택/제거된 변수들도 재고려한다는 의미
scoring = 'r2',
cv = 0, # cv > 1 이면, 여러번 반복해서 더 정확하게 추정 가능 보통 5, 10 사용
verbose = 2)
sfs = sfs.fit(X, y)
# 선택된 변수들
selected_features = list(sfs.k_feature_names_)
selected_features
liblinear는 작은 데이터셋에서 안정적인 L1/L2 정규화 지원해주고,
수렴이 느릴 수 있으므로, max_iter=10000로 설정해서 반복 횟수를 충분히 설정해준다.
# 변수 선택법 적용시 회귀계수 확인
X_selected = X[selected_features]
final_model_lr = LogisticRegression(max_iter=10000,
solver='liblinear')
final_model_lr.fit(X_selected, y)
# 회귀 계수와 변수명 정리
coefficients = final_model_lr.coef_[0] # 로지스틱 회귀는 다중 클래스가 아닌 경우 하나의 계수 배열을 반환
intercept = final_model_lr.intercept_[0]
# 데이터프레임으로 정리
coef_df = pd.DataFrame({
'Feature': selected_features,
'Coefficient': coefficients
})
coef_df
변수 선택법으로 선택된 변수들만 사용하여 최종 로지스틱 회귀 모델을 학습하고,
그에 따른 회귀 계수를 정리하여 데이터프레임 형태로 정리해준다.
# statsmodels 사용!!
model_sm_sel = sm.Logit(exog = X_selected,# 독립변수
endog = y) # 종속변수
final_result_sm = model_sm_sel.fit()
# Summary of the model
print(final_result_sm.summary())
2-3-2) 오즈비와 신뢰구간 차팅
# 오즈비와 신뢰구간 계산 based on the statsmodels
# 전체 변수 사용시
conf_int = np.exp(result_sm.conf_int())
odds_ratios = np.exp(result_sm.params)
# 변수 선택시
# conf_int = np.exp(final_result_sm.conf_int())
# odds_ratios = np.exp(final_result_sm.params)
# 데이터프레임 생성
odds_df = pd.DataFrame({
'OR': odds_ratios,
'Lower CI': conf_int[0],
'Upper CI': conf_int[1]
})
# 상수항 제거 (있다면)
if 'const' in odds_df.index:
odds_df = odds_df.drop('const')
# 플롯 생성
fig, ax = plt.subplots(figsize=(10, len(odds_df) * 0.5))
# 오즈비와 신뢰구간 플로팅
odds_df = odds_df.sort_values('OR')
y_pos = np.arange(len(odds_df))
ax.errorbar(odds_df['OR'], y_pos,
xerr = [odds_df['OR'] - odds_df['Lower CI'],
odds_df['Upper CI'] - odds_df['OR']],
fmt = 'o', capsize = 5, capthick = 2, ecolor = 'black')
# y축 레이블 설정
ax.set_yticks(y_pos)
ax.set_yticklabels(odds_df.index)
# x축을 로그 스케일로 설정
ax.set_xscale('log')
# 1인 지점에 수직선 추가 (오즈비가 1인 경우 효과 없음을 의미함!)
ax.axvline(x = 1, color = 'red', linestyle = '--')
# 레이블과 제목 추가
ax.set_xlabel('Odds Ratio (log scale)')
ax.set_title('Odds Ratios with 95% Confidence Intervals')
# 그리드 추가
ax.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
# 플롯 표시
plt.tight_layout()
plt.show()
statsmodels로 학습된 로지스틱 회귀 모델의 오즈비(Odds Ratio)와 95% 신뢰구간을 계산하고, 이를 시각화한다.
오즈비를 로그 스케일로 표현해 해석력을 높이고, OR = 1 기준선도 추가해준다.
오즈비가 1보다 크면
- 해당 변수의 값이 증가할 때 종속 변수(예: 질병 발생)의 오즈가 증가한다는 의미
오즈비가 1보다 작으면
- 해당 변수의 값이 증가할 때 종속 변수의 오즈가 감소한다는 의미
오즈비가 1이면
- 해당 변수의 값이 종속 변수의 오즈에 영향을 미치지 않는다는 의미
신뢰구간 (Confidence Interval)
- 각 점은 해당 변수의 오즈비를 나타내고, 수평선은 95% 신뢰구간
- 신뢰구간이 1을 포함하지 않으면 해당 변수는 통계적으로 유의미
- 신뢰구간이 1을 포함하면 해당 변수는 통계적으로 유의미하지 않을 수 있음
수직선 (1의 지점)
- x축에 있는 수직선(빨간색 점선)은 오즈비가 1인 지점
- 오즈비가 1보다 크면 긍정적인 효과를, 1보다 작으면 부정적인 효과를 의미
2-4. 새로운 데이터에 적용하기
2-4-1) 예시 데이터
# Predicting probabilities for a new patient
patient1 = pd.DataFrame({
'const': 1,
'preg': [4],
'plas': [50],
'pres': [80],
'skin': [30],
'insu': [20],
'mass': [75],
'pedi': [0.8],
'age': [35]
})
pred_prob_patient1 = result_sm.predict(patient1)
print("환자 1의 당뇨병 예측 확률:", pred_prob_patient1[0])
statsmodels는 sklearn과 달리 절편을 자동으로 추가하지 않기 때문에,
sm.add_constant() 혹은 수동으로 const=1 컬럼 추가가 필수적이다.
2-4-2) 변수 1단위 올라갔을 때
patient1_mass76 = patient1.copy()
patient1_mass76['mass'] = 76
# statsmodels
# 로짓 계산
logit_patient1_mass75 = result_sm.predict(patient1,
which = "linear")[0] ## 로짓 출력시 which="linear"
logit_patient1_mass76 = result_sm.predict(patient1_mass76,
which = "linear")[0]
print("logit_patient1_mass75:", logit_patient1_mass75)
print("logit_patient1_mass76:", logit_patient1_mass76)
로지스틱 회귀 모델을 이용하여 mass 변수의 값이 75 → 76으로 증가할 때 logit(로그 오즈)의 변화량을 비교해본다.
2-4-3) 오즈 변화 확인
# 로지스틱 회귀: 𝑳𝒐𝒈𝒊𝒕(로그오즈)은 𝜷만큼 변화 (오즈비는 𝒆^𝜷 만큼 변화)
# 로그오즈(로짓) 변화
logit_diff7675 = logit_patient1_mass76 - logit_patient1_mass75
logit_diff7675
mass 값이 75 → 76으로 1 증가할 때의 로그오즈의 변화량을 구하는 것으로, 회귀 계수의 실제 의미를 수치로 확인한다.
logit 값은 선형 결합이므로, 변수 하나가 1 증가할 때 logit은 그 변수의 회귀 계수(𝛽) 만큼 변화한다.
예를 들어 logit이 +0.038만큼 증가했다면 → 오즈는 약 1.0388배 증가
이는 mass 값이 1 증가할 때 당뇨병일 가능성이 약 3.9% 증가한다는 의미로 해석할 수 있다는 것이다.
# 오즈비 변화 (오즈비는 𝒆^𝜷 만큼 변화)
# np.exp(1) ## 오일러 상수 or 자연로그의 밑, 통칭 e
odds_ratio_7675 = np.exp(logit_diff7675) # e^beta
odds_ratio_7675
logit_diff7675는 mass가 75 → 76으로 1 증가할 때의 logit(로그오즈) 변화량이고,
이 값에 np.exp()를 취하면 오즈비(Odds Ratio) 로 변환된다.
# 개별 오즈 계산
# odds_75
odds_75 = np.exp(logit_patient1_mass75)
print(odds_75)
# odds_76
odds_76 = np.exp(logit_patient1_mass76)
print(odds_76) # = odds_75 * np.exp(coef_of_mass) #
# odds_diff
odds_diff = odds_76/odds_75
print("odds_diff:", odds_diff)
계수(𝛽) → logit 변화량
exp(𝛽) → 오즈비
exp(logit) → 오즈
오즈비 = 오즈 증가 비율 = 새로운 오즈 / 기존 오즈
# mass의 회귀 계수로 계산하기
coef_of_mass = result_sm.params[result_sm.params.index == 'mass'].values[0] #
print("coef_of_mass:", coef_of_mass)
print('e ^ beta:', np.exp(coef_of_mass))
coef_of_mass는 로지스틱 회귀 모델에서 mass 변수의 계수(𝛽_mass)이고,
np.exp(coef_of_mass)는 해당 계수에 대한 오즈비 (Odds Ratio) 계산: 사건이 일어날 오즈 변화량이다.
pred_prob_patient1_76 = result_sm.predict(patient1_mass76)
print("환자 1의 + mass 76 당뇨병 예측 확률:", pred_prob_patient1_76[0])
2-4-3) 여러개 데이터에 대해 적용
# Predicting probabilities for a test set
patient2 = pd.read_csv(ppath + '/1. data/diabete_lgr_te.csv')
X_test = patient2.drop(columns=['class'])
X_test = sm.add_constant(X_test) # Adds a constant term to the predictor
y_test = patient2['class']
# 개별 환자에 대한 확률 값 계산
pred_prob = result_sm.predict(X_test)
pred_prob
로지스틱 회귀 모델(result_sm)을 사용하여 테스트 데이터셋(diabete_lgr_te.csv)에 속한 환자들의 당뇨병 발생 확률을 일괄 예측한다.
# 0,1 분류, 기본 threshold = 0.5
pred_class = (pred_prob > 0.5).astype(int)
accuracy = accuracy_score(y_test, pred_class)
print("Accuracy at 0.5 threshold:", accuracy) ## 정확도 계산
예측 확률(pred_prob)을 기반으로 0 또는 1로 이진 분류하고, 정확도(Accuracy) 를 계산해본다.
2-4-4) 0,1로 만들기
# 값이 0.5보다 크면 1로, 아니면 0으로 변환
def make_binary(value, threshold = 0.5):
output = (value > threshold).astype(int)
return output
pred_class = make_binary(pred_prob > 0.5)
pred_class
예측 확률을 이진 분류 결과로 변환하는 threshold 기반 로직을 함수화한다.
# 확률값과, 예측 값
result_df = pd.DataFrame({'pred_prob': pred_prob, # 예측 확률값
'pred_class': pred_class, # 예측 클래스 값
'class_true': y_test}) # 정답
result_df
# 정확도 계산
accuracy = (result_df['pred_class'] == result_df['class_true']).mean()
print('정확도:', accuracy)
2-4-5) [참고] threshold 결과 확인
# 5. Finding the optimal cut-off threshold
thresholds = np.arange(0.1, 1.0, 0.1)
accuracy_list = []
for threshold in thresholds:
pred_class = (pred_prob > threshold).astype(int)
accuracy = accuracy_score(y_test, pred_class) ## 정확도 계산
accuracy_list.append((threshold, accuracy))
print(f"Threshold: {threshold}, Accuracy: {accuracy}")
# Find the threshold with the highest accuracy
optimal_threshold = max(accuracy_list, key=lambda x: x[1])
print("Optimal threshold:", optimal_threshold[0],
"with accuracy:", optimal_threshold[1])
로지스틱 회귀 모델에서 최적의 임계값(threshold) 을 찾기 위해 여러 threshold 값을 실험하고,
각 임계값에서의 정확도(accuracy)를 비교하여 정확도를 극대화하는 threshold를 선택해본다.
**로지스틱 회귀에서 임계값(threshold)을 0.5보다 크게 하거나 작게 해야 하는 상황
1) 임계값을 0.5보다 작게 하는 경우
: 실제 양성(False Negative)을 놓치면 안 되는 경우:
- 암 검진에서는 암 환자를 놓치는 것이 매우 위험함
- 따라서 임계값을 낮춰 더 많은 사람을 양성(암 환자)으로 분류함
: 재현율(Recall)이 중요한 경우:
- 재현율은 실제 양성 중에서 모델이 맞게 예측한 비율
- 재현율을 높이기 위해 임계값을 낮출 수 있음..
2) 임계값을 0.5보다 크게 하는 경우
: 실제 음성(False Positive)을 잘못 양성으로 분류하면 안 되는 경우:
- 스팸 필터에서는 정상 이메일을 스팸으로 잘못 분류하면 안 됨
- 따라서 임계값을 높여 더 많은 이메일을 음성(정상)으로 분류
: 정밀도(Precision)가 중요한 경우:
- 정밀도는 모델이 양성으로 예측한 것 중 실제로 양성인 비율
- 정밀도를 높이기 위해 임계값을 높일 수 있음
5. 부록
0. 라이브러리 준비
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_percentage_error, mean_squared_error
from sklearn.preprocessing import PowerTransformer
import statsmodels.api as sm
from sklearn.preprocessing import StandardScaler
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
변수의 분포를 정규분포에 가깝게 만들기 위한 변환 도구로,
특히 회귀 모델에서 정규성, 등분산성, 선형성 가정을 만족시키기 위해 유용하게 사용할 수 있는
from sklearn.preprocessing import PowerTransformer도 불러와준다.
1. 데이터 로드
1-1. 데이터 준비
train = pd.read_csv(ppath + "/1. data/kc_house_train_data.csv")
test = pd.read_csv(ppath + "/1. data/kc_house_test_data.csv")
1-2. 데이터 로드
## 종속변수의 분포 확인
plt.hist(train['price'])
plt.show()
# 1) Stepwise Regression
X_train = train.drop(columns=['price'])
y_train = train['price']
X_test = test.drop(columns=['price'])
y_test = test['price']
회귀 분석에서 Stepwise Regression(단계적 변수 선택)을 수행하기 위한 전처리 단계로,
독립 변수(X)와 종속 변수(y)를 훈련/테스트 데이터로 분리하는 작업이다.
price가 예측 대상 변수이므로 이를 기준으로 분리한다.
# 데이터 표준화
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_train_scaled = pd.DataFrame(X_train_scaled, columns=X_train.columns)
X_test_scaled = scaler.transform(X_test) ## X_train의 평균, 표준편차로 표준화!!!
X_test_scaled = pd.DataFrame(X_test_scaled, columns=X_test.columns)
X_train의 평균과 표준편차를 기준으로 학습하고 변환하고,
X_test를 동일한 기준으로 변환 (데이터 누수 방지)하고,
numpy 배열을 다시 DataFrame으로 변환하여 컬럼명 유지한다.
2. 기본 모델
# 모든 변수를 이용한 full model
lm_full = sm.OLS(y_train, sm.add_constant(X_train_scaled)).fit()
# 변수가 없는 null model
lm_null = sm.OLS(y_train, sm.add_constant(pd.DataFrame(np.ones(len(y_train))))).fit()
# 단계적 선택법 적용
sfs = SFS(LinearRegression(),
k_features='best',
forward = False, # 시작방향이 Full model (backward)
floating = True, # True이면 각 단계에서 변수를 추가하거나 제거할 때, 이전에 선택/제거된 변수들도 재고려
scoring = 'neg_mean_squared_error',
cv = 5,
verbose = 2)
sfs = sfs.fit(X_train_scaled, y_train)
selected_features_step = list(sfs.k_feature_names_)
후진 제거법 방식의 단계적 변수 선택을 mlxtend의 SequentialFeatureSelector(SFS)를 활용하여 구현한다.
특히 floating=True 설정을 통해 유연한 변수 선택을 하고, 성능 지표로는 평균 제곱 오차(MSE) 를 기반으로 최적의 변수 조합을 찾는다.
# 최종 회귀 모델
X_train_stepwise = sm.add_constant(pd.DataFrame(X_train_scaled, columns=X_train.columns)[selected_features_step])
X_test_stepwise = sm.add_constant(pd.DataFrame(X_test_scaled, columns=X_test.columns)[selected_features_step])
step_model_f = sm.OLS(y_train, X_train_stepwise).fit()
# 최종 회귀식 확인
step_model_f.summary()
# test data에 적용
pred = step_model_f.predict(X_test_stepwise)
# 평가
lm_mape = mean_absolute_percentage_error(y_test, pred)
lm_rmse = mean_squared_error(y_test, pred, squared=False)
print(f'MAPE: {lm_mape}, RMSE: {lm_rmse}')
단계적 선택으로 선택된 변수만을 사용하여 학습된 모델(step_model_f)을 테스트 데이터에 적용한 후,
성능 평가 지표인 MAPE와 RMSE를 계산한다.
3. BOX-COX 변환
3-1. 최적 람다 구하기
# 2) 종속변수 Box-cox 변환
pt = PowerTransformer(method='box-cox')
# Box-Cox 변환 적용 (음수가 없어야 함!!)
y_train_bc = pt.fit_transform(y_train.values.reshape(-1, 1)).flatten()
y_test_bc = pt.transform(y_test.values.reshape(-1, 1)).flatten()
# pt.lambdas_ = [-1/] # 지정을 원한 경우
print("최적 람다:", pt.lambdas_)
종속 변수 y에 Box-Cox 변환을 적용하여 정규성 확보와 선형 회귀 모델의 가정 만족을 유도한다.
PowerTransformer는 sklearn에서 안정적으로 사용할 수 있으며, lambdas_는 Box-Cox 변환에서 최적의 파라미터(λ)를 나타낸다.
3-2. 시각화
plt.hist(y_train_bc)
3-3. 단계적 선택법 적용
# 단계적 선택법 적용 (Box-Cox 변환된 데이터 사용)
sfs_trf = SFS(LinearRegression(),
k_features = 'best',
forward = False,
floating = True,
scoring = 'neg_mean_squared_error',
cv = 5,
verbose = 2)
sfs_trf = sfs_trf.fit(X_train_scaled, y_train_bc)
selected_features_step_trf = list(sfs_trf.k_feature_names_)
Box-Cox 변환된 종속 변수(y_train_bc)를 기반으로, 전
처리된 독립 변수(X_train_scaled)에 대해 후진 제거법과 floating 방식의 단계적 변수 선택을 적용한다.
3-4. 최종 회귀모델
# 최종 회귀 모델 (Box-Cox 변환된 데이터 사용)
X_train_stepwise_trf = sm.add_constant(pd.DataFrame(X_train_scaled, columns=X_train.columns)[selected_features_step_trf])
X_test_stepwise_trf = sm.add_constant(pd.DataFrame(X_test_scaled, columns=X_test.columns)[selected_features_step_trf])
step_model_trf_f = sm.OLS(y_train_bc, X_train_stepwise_trf).fit()
# 최종 회귀식 확인
step_model_trf_f.summary()
3-5. test data 적용
# test data에 적용
pred_trf = step_model_trf_f.predict(X_test_stepwise_trf)
# Box-Cox 역변환(원래 단위로 역변환)
pred_trf_inv = pt.inverse_transform(np.array(pred_trf).reshape(-1, 1)).flatten()
pred_trf_inv
Box-Cox 변환된 회귀 모델의 예측값을 다시 원래 단위로 되돌리는 역변환 과정으로,
회귀 예측 결과를 실제 해석 가능한 단위(원래 가격 등) 로 복원시킨다.
3-6. 평가
# 평가
lm_mape_trf = mean_absolute_percentage_error(y_test, pred_trf_inv)
lm_rmse_trf = mean_squared_error(y_test, pred_trf_inv, squared=False)
print(f'MAPE: {lm_mape_trf}, RMSE: {lm_rmse_trf}')
Box-Cox 변환을 적용한 회귀 모델의 예측 결과를 평가하는 단계로,
실제 단위로 복원된 예측값(pred_trf_inv)과 정답값(y_test)을 비교하여 MAPE와 RMSE를 계산한다.
4. 비교
# 3) 결과 비교
result = pd.DataFrame({
'MAPE': [lm_mape, lm_mape_trf],
'RMSE': [lm_rmse, lm_rmse_trf]
}, index=['original', 'transformed'])
result
Box-Cox 변환을 적용하지 않은 모델 과 적용한 모델의 예측 성능을 MAPE, RMSE 지표로 비교하는 표를 만들어서 직관적으로 분석한다.
🤯
드디어 4주차 전반부가 끝났다.
경주 여행 끝나고 다음 주 월요일에는 4주차 후반부인 회귀 모델 심화편에 대해 포스팅 할 예정이다.
과제코드도 같이 올려야하는데, 분량은 어떻게 조절할지 고민중이다.
머신러닝 들어오니까 진도가 갑자기 파파팍 나가서 정신이 조금 없다.
거기다 공모전 준비도 같이하느라 거북목을 넘어서 거북이가 되어 바다로 돌아가기 직전이다.
실습코드도 다시 혼자 쳐보고싶은데, 시간이 없을 것 같아서 고민이 된다.
공모전 끝나면 다시 정처기 필기해야하고, 정처기 끝나면 다시 빅분기 실기다.ㅋ
바쁘다바빠현대사회
'데이터 취업기 > 부트캠프 학습기' 카테고리의 다른 글
[5주차] 머신러닝 3. 분류 모델 기초 - (1) (0) | 2025.05.29 |
---|---|
[4주차] 머신러닝 2. 회귀 모델 심화 - (3) (0) | 2025.05.27 |
[4주차] 머신러닝 2. 회귀 모델 기초 - (1) (4) | 2025.04.30 |
[3주차] 머신러닝 1. 모델링 기초 - (2) (0) | 2025.04.28 |
[3주차] 머신러닝 1. 모델링 기초 - (1) (0) | 2025.04.23 |