import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import platform
import warningsEDA 템플릿
데이터 분석
Load Library
Settings
warnings.filterwarnings('ignore')
if platform.system() == 'Darwin': #맥
plt.rc('font', family='AppleGothic')
elif platform.system() == 'Windows': #윈도우
plt.rc('font', family='Malgun Gothic')
elif platform.system() == 'Linux': #리눅스 (구글 콜랩)
#!wget "https://www.wfonts.com/download/data/2016/06/13/malgun-gothic/malgun.ttf"
#!mv malgun.ttf /usr/share/fonts/truetype/
#import matplotlib.font_manager as fm
#fm._rebuild()
plt.rc('font', family='Malgun Gothic')
plt.rcParams['axes.unicode_minus'] = False #한글 폰트 사용시 마이너스 폰트 깨짐 해결
warnings.filterwarnings('ignore')Load Data
df = pd.read_csv('df.csv')
print(f"df shape: {df.shape}")Missing Values
print(df.isnull().sum())
print(df[df.isnull().any(axis=1)])- 결측치 처리는 전처리 참조
단일 column 분석
df.head()df.info()- 순서형 변수는 연속형으로 처리하던가 범주형으로 처리하던가 알아서 정하면 됨.
- 순서형 그 자체로 보고 분석할 수 있는 방법들도 있긴 있음.
범주형
df.describe(include='object')import matplotlib.pyplot as plt
cat_cols = []
num_cols = []
for col in cat_cols:
target_counts = df[col].value_counts().sort_index()
target_ratio = df[col].value_counts(normalize=True).sort_index()
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
target_counts.plot.bar(ax=ax[0])
ax[0].set_title(f"{col}'s Count")
ax[0].set_xlabel(f"{col}'s Level")
ax[0].set_ylabel('count')
wedges, texts, autotexts = ax[1].pie(target_ratio, autopct='%1.1f%%')
ax[1].set_title(f"{col}'s Distribution", fontsize=14, pad=20)
plt.show()
summary = pd.concat([target_counts, target_ratio], axis=1)
summary.columns = ['counts', 'probs']
summary = summary.sort_values('counts', ascending=False)
display(summary)연속형
detailed_stats = []
for col in num_cols:
stats_dict = {
'Mean': df[col].mean(),
'Median': df[col].median(),
'Min': df[col].min(),
'Max': df[col].max(),
'Std Dev': df[col].std(),
'IQR': df[col].quantile(0.75) - df[col].quantile(0.25),
'Skewness': df[col].skew(),
'Kurtosis': df[col].kurtosis(),
'CV (%)': df[col].std() / df[col].mean() * 100
}
detailed_stats.append(pd.Series(stats_dict, name=col))
display(pd.concat(detailed_stats, axis=1).round(2).T)distribution
from scipy.stats import probplot
for col in num_cols:
fig, ax = plt.subplots(1, 3, figsize=(10, 5))
# 히스토그램과 KDE
df[col].hist(ax=ax[0], color='skyblue', density=True)
sns.kdeplot(df[col], color='red', linewidth=2, label='KDE', ax=ax[0])
ax[0].set_title(f'{col} Distribution')
ax[0].set_xlabel(col)
ax[0].set_ylabel('Density')
mean_val = df[col].mean()
median_val = df[col].median()
ax[0].axvline(mean_val, color='red', linestyle='--', label=f'Mean: {mean_val:.1f}')
ax[0].axvline(median_val, color='green', linestyle='--', label=f'Median: {median_val:.1f}')
ax[0].legend()
# 박스플롯
sns.boxplot(y=df[col], ax=ax[1], color='lightgreen')
# Q-Q 플롯
probplot(df[col], plot=ax[2])
plt.show()normality
from scipy.stats import shapiro, anderson, jarque_bera, normaltest
for col in num_cols:
# 소표본 적합
stat, p = shapiro(df[col].dropna())
print(f"Shapiro-Wilk Test: stat={stat:.4f}, p-value={p:.4f}")
result = anderson(df[col].dropna())
print(f"Anderson-Darling Test: stat={result.statistic:.4f}, critical values={result.critical_values}, significance levels={result.significance_level}")
# 왜도, 첨도 기준 판별. 대표본 적합
stat, p = jarque_bera(df[col].dropna())
print(f"Jarque-Bera Test: stat={stat:.4f}, p-value={p:.4f}")
# 왜도, 첨도 기준 판별. 범용적
stat, p = normaltest(df[col].dropna())
print(f"D'Agostino's K-squared Test: stat={stat:.4f}, p-value={p:.4f}")
print()outliers - IQR
outliers_info = []
outliers_mask = pd.DataFrame(False, index=df.index, columns=df.columns)
for col in num_cols:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
out_idx = (df[col] < lower_bound) | (df[col] > upper_bound)
outliers_mask[col] |= out_idx
outliers = df[out_idx]
outliers_info.append(pd.Series({
'Q1': Q1,
'Q3': Q3,
'IQR': IQR,
'Lower Bound': lower_bound,
'Upper Bound': upper_bound,
'Outlier Count': len(outliers),
'Outlier %': len(outliers) / len(df) * 100
}, name=col))
outliers_info = pd.concat(outliers_info, axis=1)
outliers = df[outliers_mask.any(axis=1)]
outliers['reason'] = outliers_mask[outliers_mask.any(axis=1)].apply(lambda x: ', '.join(x.index[x]), axis=1)
normal = df.loc[(~outliers_mask).all(axis=1)]
display(outliers_info.round(2).T)
display(outliers)outliers - Z-score
outliers_info = []
outliers_mask = pd.DataFrame(False, index=df.index, columns=df.columns)
for col in num_cols:
mean = df[col].mean()
std = df[col].std()
z_scores = (df[col] - mean) / std
out_idx = (z_scores.abs() > 3)
outliers_mask[col] |= out_idx
outliers = df[out_idx]
outliers_info.append(pd.Series({
'Mean': mean,
'Std Dev': std,
'Outlier Count': len(outliers),
'Outlier %': len(outliers) / len(df) * 100
}, name=col))
outliers_info = pd.concat(outliers_info, axis=1)
outliers = df[outliers_mask.any(axis=1)]
outliers['reason'] = outliers_mask[outliers_mask.any(axis=1)].apply(lambda x: ', '.join(x.index[x]), axis=1)
normal = df.loc[(~outliers_mask).all(axis=1)]
display(outliers_info.round(2).T)
display(outliers)- 이상치 처리는 전처리 참조
- HUOE(Heterogeneous Univariate Outlier Ensembles) 방법도 있음 (하지만 이를 위한 library는 없는듯)
변수간 관계
연속, 연속
- 산점도로 시각화
- spearman 상관계수 확인
- spearman - pearson 차이로 비선형 관계 확인 가능
- 0.1 이상 차이가 나는지 확인. (공식 기준 아님)
범주, 연속
for col in num_cols:
fig, ax = plt.subplots(1, 3, figsize=(10, 5))
for target_val in df['target'].unique():
subset = df[df['target'] == target_val][col]
subset.hist(ax=ax[0], label=target_val, density=True)
ax[0].set_title(f'{col} Distribution')
ax[0].set_xlabel(col)
ax[0].set_ylabel('Density')
plt.show()이런 식으로 subset 만들어서 단일 분석에서 했던 것과 같이 하면 됨.
분포 확인 후, ANOVA나 Kruskal-Wallis 등으로 그룹 간 차이 검정 진행
t 검정을 사용했다면, Cohen’s d 등으로 효과 크기 확인 가능
자세한건 분산 분석 파트 참조
범주, 범주
- 시각화는 마찬가지로 subset 만들어서 단일 분석에서 했던 것과 같이 하면 됨.
- chi-square 검정 등으로 독립성 검정 진행
- Cramer’s V 등으로 연관성 정도 확인
- 두 변수가 level이 2개면 phi 계수로 확인 가능 (Cramer’s V와 동일한 값 나옴)
from scipy.stats import chi2_contingency
from scipy.stats.contingency import association
contingency_table = pd.crosstab(df['var1'], df['var2'])
chi2, p, dof, expected = chi2_contingency(contingency_table, correction=False)
cramers_v = association(contingency_table, method='cramer')
print(f"Chi-square Test: chi2={chi2:.4f}, p-value={p:.4f}")
print(f"Cramer's V: {cramers_v:.4f}")독립변수 vs 종속변수
- 간단한 트리 모델링 이후 feature importance 확인 가능
- 0.01이 넘는 변수들만 따로 분석 진행
from sklearn.feature_selection import mutual_info_regression
mi_score = mutual_info_regression(df[num_cols], df['target']) # df['target']이 범주형이면 mutual_info_classif 사용
mi_result = pd.Series(mi_score, index=num_cols).sort_values(ascending=False)
mi_result- mutual information로 선형 + 비선형 관계 확인 가능
- 구체적으로 어떤 관계인지는 scatter plot으로 확인 (연속 + 연속이면)
import pingouin as pg
pcorr = pg.partial_corr(data=df, x='var1', y='var2', covar='var3', method='')
pcorr- 하나의 변수를 통제했을 때, 두 변수 간의 상관관계를 분석
- 내부적으로는 회귀분석을 사용
- 도메인 지식으로 통제변수 선정해서 진행
- 혹은 EDA 과정에서 발견한 관계로 선정 (한 마디로 본인이 알아서 잘 선정하기)
- 인과분석을 하면 이 과정은 필요 없으나, ADP 환경에서 인과분석 라이브러리는 제공 안하는듯
Interation Effect
- 두 개 이상의 독립변수가 결합하여 종속변수에 미치는 영향
- 유효한 상호작용 효과가 있다면 새로운 변수를 만들어서 분석에 포함 (예: new_var = var1 * var2)
- 자세한건 분산분석 파트 참조
Polinorminal Relationships
- 회귀분석 파트 참조
다변량 분석
차원 축소
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis', alpha=0.6)
plt.xlabel(f'PC1 ({explained_var_ratio[0]:.1%} variance)')
plt.ylabel(f'PC2 ({explained_var_ratio[1]:.1%} variance)')
plt.title('PCA Projection by Support Level')
plt.colorbar(scatter, label='Support Level')from sklearn.manifold import TSNE
tsne = TSNE(n_components=2)
tsne_result = tsne.fit_transform(df[num_cols])
plt.scatter(tsne_result[:, 0], tsne_result[:, 1], c=df['target'])- UMAP이 전역적으로는 구조를 더 잘 보존하고, 속도도 빠르다.
- ADP 환경에서 UMAP 라이브러리는 제공 안하는듯
- 애초에 2차원으로 정사영해서 plot한 이 자료들로 의미있는 해석을 더 할 수 있는지 의문
DBSCAN, Isolation Forest 등으로 이상치 탐지 가능
- DBSCAN은 비지도 학습 파트 군집분석 참조
from sklearn.preprocessing import OrdinalEncoder
from sklearn.ensemble import IsolationForest
iso_forest = IsolationForest()
oe = OrdinalEncoder()
df_encoded = df.copy()
df_encoded[cat_cols] = oe.fit_transform(df[cat_cols])
outlier_labels = iso_forest.fit_predict(df_encoded)
outliers = df[outlier_labels == -1]
normal = df[outlier_labels != -1]
display(outliers)- 해당 결과들을 voting 등으로 종합해서 이상치로 판단할 수도 있음.
- 자세한건 modeling 파트의 voting 부분 참조
- 개인적인 생각: 지워야 하는 이상치는 측정 과정에서의 오류 등인데, 이런 종합적인 방법은 그런 이상치를 찾는데 유용하지는 않은듯 하다.
- 그래서 이상치 분석 목적이 아니라면 이 방법은 굳이 안써도 되지 않을까
outlier 연속형 분포
for col in num_cols:
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
# 박스플롯
# (이상치 변수 생성 필요)
sns.boxplot(x=df['이상치'], y=normal[col], ax=ax[0], data=df)
ax[0].set_title(f'{col} Boxplot by Outlier Status')
sns.kdeplot(normal[col], color='blue', linewidth=2, label='Normal', ax=ax[1])
sns.kdeplot(outliers[col], color='orange', linewidth=2, label='Outliers', ax=ax[1])
ax[1].set_title(f'{col} Distribution by Outlier Status')
plt.show()outlier 범주형 분포
for col in cat_cols:
normal_counts = normal[col].value_counts().sort_index()
outlier_counts = outliers[col].value_counts().sort_index()
fig, ax = plt.subplots(1, 3, figsize=(10, 5))
normal_counts.plot.bar(ax=ax[0])
ax[0].set_title(f"{col}'s Count")
ax[0].set_xlabel(f"{col}'s Level")
ax[0].set_ylabel('count')
outlier_counts.plot.bar(ax=ax[1], color='orange')
ax[1].set_title(f"{col}'s Count (Outliers)")
ax[1].set_xlabel(f"{col}'s Level")
ax[1].set_ylabel('count')
combined_df = pd.DataFrame({'Normal': normal_counts, 'Outliers': outlier_counts}).fillna(0)
combined_df.plot.bar(ax=ax[2], stacked=True)
ax[2].set_title(f"{col}'s Count Comparison")
ax[2].set_xlabel(f"{col}'s Level")
ax[2].set_ylabel('count')
plt.show()- 그 외 profiling 분석이나 맨 휘트니, 카이제곱 독립성 검정 등으로 이상치와 정상치의 차이 분석 가능