오늘은 회귀입니다. …회기는 그 경희대 있는 지하철역이예요 선생님들.
주성분분석
머고 오늘은 ML 안함? 아니 할건데… 저게 회귀분석 할거라고 했죠? 저 데이터 칼럼이 12개인데 하나빼고 다 독립변수니까 지금 독립변수가 11개거든요. 그거 그대로 때려박으면 다중공선성 터질수도 있으니까 압축할 수 있는건 압축하고 가자 이겁니다. 11개 언제 넣었다 뺐다 할거임?
wine_x = wine.copy()
X = wine_x.drop('quality', axis=1)
y = wine['quality']
# 1. 스케일링 (PCA 전 필수!)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 2. PCA 객체 생성 (일단 모든 성분을 다 뽑아봅니다)
pca = PCA()
X_pca = pca.fit_transform(X_scaled)
그나마 데이터가 다 수치형이라 FAMD 안 가고 주성분분석 했음.
# 전체 성분(11개)에 대해 PCA 수행
pca_full = PCA().fit(X_scaled)
# 개별 분산 설명력
individual_var = pca_full.explained_variance_ratio_
# 누적 분산 설명력
cum_var = np.cumsum(individual_var)
# 결과 출력
for i, (ind, cum) in enumerate(zip(individual_var, cum_var)):
print(f"PC{i+1}: 개별 {ind:.2f} / 누적 {cum:.2f}")
if cum >= 0.85 and i > 0 and cum_var[i-1] < 0.85:
print(f"--- 여기까지 딱 끊으면 정보의 {cum*100:.1f}%가 보존됩니다! ---")
PC1: 개별 0.28 / 누적 0.28
PC2: 개별 0.18 / 누적 0.46
PC3: 개별 0.14 / 누적 0.60
PC4: 개별 0.11 / 누적 0.71
PC5: 개별 0.09 / 누적 0.80
PC6: 개별 0.06 / 누적 0.86
--- 여기까지 딱 끊으면 정보의 85.5%가 보존됩니다! ---
PC7: 개별 0.05 / 누적 0.91
PC8: 개별 0.04 / 누적 0.95
PC9: 개별 0.03 / 누적 0.98
PC10: 개별 0.02 / 누적 0.99
PC11: 개별 0.01 / 누적 1.00
제 6 주성분까지 채용하면 85% 이상의 설명력을 갖는다. 그래서 그 주성분들이 뭔데?
for i in range(n_comp):
loading_scores = pd.Series(
pca_full.components_[i],
index=X.columns
)
sorted_loadings = loading_scores.abs().sort_values(ascending=False)
print(f"\nPC{i+1} 핵심 변수 TOP3")
print(sorted_loadings.head(3))
PC1 핵심 변수 TOP3
fixed acidity 0.489314
citric acid 0.463632
pH 0.438520
dtype: float64
PC2 핵심 변수 TOP3
total sulfur dioxide 0.569487
free sulfur dioxide 0.513567
alcohol 0.386181
dtype: float64
PC3 핵심 변수 TOP3
alcohol 0.471673
volatile acidity 0.449963
free sulfur dioxide 0.428793
dtype: float64
PC4 핵심 변수 TOP3
chlorides 0.666195
sulphates 0.550872
residual sugar 0.372793
dtype: float64
PC5 핵심 변수 TOP3
residual sugar 0.732144
alcohol 0.350681
pH 0.267530
dtype: float64
PC6 핵심 변수 TOP3
pH 0.522116
volatile acidity 0.411449
density 0.391152
dtype: float64
이 에미나이 변수 압축한다고 했는데 산점도 볼때부터 알아봤어야 했는데…
1. 제 1주성분: fixed acidity, citric acid, pH
2. 제 2주성분: total sulfur dioxide, free sulfur dioxide, alcohol
3. 제 3주성분: alcohol, volatile acidity, free sulfur dioxide
4. 제 4주성분: chlorides, sulphates, residual sugar
5. 제 5주성분: residual sugar, alcohol, pH
6. 제 6주성분: pH, volatile acidity, density
이렇게 나왔으면 중복 쳐내면 됩니다.
{'free sulfur dioxide', 'fixed acidity', 'sulphates', 'pH', 'total sulfur dioxide', 'residual sugar', 'density', 'chlorides', 'alcohol', 'volatile acidity', 'citric acid'}
저거 리스트에 다 때려박고 set으로 바꾸면 중복 다 쳐냄.
회귀분서억
X = X_scaled # 저기 주성분 돌리기 전에 거쳤어요 스케일러
model = LinearRegression()
model.fit(X, y)
pred = model.predict(X)
print("MAE:", mean_absolute_error(y, pred))
print("MSE:", mean_squared_error(y, pred))
rmse = np.sqrt(mean_squared_error(y, pred))
print("RMSE:", rmse)
print("R²:", r2_score(y, pred))
MAE: 0.5004899635644883
MSE: 0.416767167221408
RMSE: 0.6455750670692045
R²: 0.3605517030386882
거 설명력이 너무 약한 거 아니오?

잔차분석 했더니 고질라 왔다간거 실화냐?
VIF(다중공선성)
이거 왜 보냐면 독립변수끼리 상관이 있나를 보는겁니다. 지들끼리 상관이 있으면 모델 시망됨.
wine_x = wine_x.drop('quality', axis=1)
# X는 독립변수 데이터프레임 (스케일링 안 해도 되지만 보통 해도 상관없음)
X_vif = pd.DataFrame()
X_vif["variable"] = wine_x.columns
X_vif["VIF"] = [variance_inflation_factor(wine_x.values, i) for i in range(wine_x.shape[1])]
print(X_vif.sort_values(by="VIF", ascending=False))
variable VIF
7 density 1479.287209
8 pH 1070.967685
10 alcohol 124.394866
0 fixed acidity 74.452265
9 sulphates 21.590621
1 volatile acidity 17.060026
2 citric acid 9.183495
4 chlorides 6.554877
6 total sulfur dioxide 6.519699
5 free sulfur dioxide 6.442682
3 residual sugar 4.662992
망했는데…? 들어내봐야겠지 이거…?
부록-알콜과 퀄리티
wine.corr()['quality'].sort_values(ascending=False)
quality 1.000000
alcohol 0.476166
sulphates 0.251397
citric acid 0.226373
fixed acidity 0.124052
residual sugar 0.013732
free sulfur dioxide -0.050656
pH -0.057731
chlorides -0.128907
density -0.174919
total sulfur dioxide -0.185100
volatile acidity -0.390558
Name: quality, dtype: float64
알콜이 상관계수가 제일 높은데?
plt.figure()
scatter = plt.scatter(
wine['alcohol'],
wine['quality'],
c=wine['quality'],
cmap='viridis',
alpha=0.6
)
plt.xlabel('Alcohol')
plt.ylabel('Quality')
plt.title('Alcohol vs Wine Quality')
plt.colorbar(scatter, label='Quality')
plt.show()

이냥반들아 알콜 많이 들어가봐야 소독용 에탄올이라고... 내가 그래서 고량주 안마심. 실험실에서 맡던 소독용 에탄올 냄새 나서...
부록 2-XGBoost가 여기서 왜 나와요
우리 방금까지 회귀했는데 쟈는 또 뭐임? 어제 그 분류했던 친구입니다. 이 데이터셋으로 회귀도 하고 분류도 한다고 함.
# 일단 쨈
wine_df = wine.copy()
X = wine_df.drop("quality", axis=1)
y = wine_df["quality"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = XGBRegressor(
n_estimators=300,
max_depth=4,
learning_rate=0.05,
random_state=42
)
model.fit(X_train, y_train)
pred = model.predict(X_test)
print("R2:", r2_score(y_test, pred)) # R2: 0.4512701630592346
어째 회귀보다 분류가 설명력이 더 좋은 것 같다.
SHAP
얘는 또 뭥미? 이건 그러니까 XGBoost와 대화의 시간을 가지면서 왜 그렇게 분류한건지 물어보는 친구다. 오은영박사님 어… 아니 생각하는 의자까지는 아니고…
# 1. 모델의 predict 함수를 직접 전달
# 2. masker를 사용하여 데이터의 통계적 분포를 SHAP에게 알려줍니다.
masker = shap.maskers.Independent(data=X_test)
explainer = shap.Explainer(xgb_model.predict, masker)
# 3. SHAP 값 계산 (Permutation 방식은 속도는 좀 걸리지만 매우 정확합니다)
shap_values = explainer(X_test)
# 4. 시각화
shap.summary_plot(shap_values, X_test)

그니까 이제 XGBoost한테 저 샤프가 오은영박사님에 빙의해서 왜 이렇게 분류했냐고 물어봤을 거 아닙니까? 그러니까 모델이 인제 얘를 이렇게 분류한 이유를 얘기해주는겁니다. 얘는 산도가 너무 높아서 이렇게 했고 얘는 밀도가 이래서 이렇게 분류했어요, 이렇게. 그게 저 그래프임다.
'Coding > EDA' 카테고리의 다른 글
| clinVar EDA를 Polars로 해보자 (0) | 2026.04.13 |
|---|---|
| Medical Cost Personal Datasets (0) | 2026.03.18 |
| Palmer Archipelago (Antarctica) penguin data (0) | 2026.03.11 |
| Google Play Store – Most Downloaded Android Apps (0) | 2026.03.02 |
| 얘! clinvar도 EDA가 된단다! (3) (0) | 2026.02.18 |