
EDA를 처음 배우면서 간단하게 타이타닉 생존률 분석에 대한 EDA 예시를 학습하였다. (주로 캐글 또는 블로그에 EDA 예시를 서칭하였다.)
이후 직접 실습을 통해 데이터 분석을 시도해보았다.
해당 EDA는 GPT 에게 난이도를 기준으로 입문자가 접근하기 좋은 데이터셋 목록을 추천받아 그 중에서 1개를 취사하여 진행하였다.
(처음에는 캐글에서 찾은 데이터셋 - '2024년 스택오버 플로우 개발자 설문조사'를 통해 실습해보았으나, 난이도가 높아 데이터셋을 변경하였다.)
EDA는 코딩보다 사고 과정과 데이터에서 어떻게 인사이트를 얻는지가 중요하므로, 코딩 자체는 AI(Chat GPT) 의 도움을 받았으나, 가설 설정이나 시각화 부분에서는 최대한 스스로 생각해보고, 직접 실행해보면서 수정을 하는 과정을 거쳤다.
*주피터 노트북을 통해 EDA 실습한 내용을 마크다운 파일로 변환 후 이미지를 추가 첨부하였다.
이 데이터셋은 포르투갈 "Vinho Verde" 레드 와인의 화학적 특성(physicochemical properties) 과 품질 점수(quality score) 를 포함하고 있다.
| 컬럼명 | 설명 |
|---|---|
| fixed acidity | 고정 산도 (타르타르산 등, 와인의 기본 산도) |
| volatile acidity | 휘발성 산도 (아세트산 등, 높은 값은 식초 맛 유발) |
| citric acid | 구연산 함량 (와인의 신선함, 풍미와 관련) |
| residual sugar | 잔여 당분 (발효 후 남은 당분, 단맛과 관련) |
| chlorides | 염화물 (소금 농도, 맛에 영향) |
| free sulfur dioxide | 자유 이산화황 (박테리아 억제, 보존제 역할) |
| total sulfur dioxide | 총 이산화황 (자유 + 결합, 높은 값은 산화 억제) |
| density | 밀도 (알코올과 당분에 의해 결정) |
| pH | 산도 (낮을수록 산성, 보통 2.5 ~ 4.0) |
| sulphates | 황산염 (항산화제, 풍미와 보존성 관련) |
| alcohol | 알코올 도수 |
| quality | 와인 품질 점수 (0~10, 전문가 평가) |
데이터 변수에 대한 정의(간단한 설명)를 GPT를 통해 진행하고, 해당 내용을 바탕으로 와인 품질 점수에 대해 영향을 끼칠 요소들을 예측하였다.
가설 1. 휘발성 산도 수치가 높을수록 품질 점수는 낮을 것이다. (높을 수록 식초 맛을 유발하므로 품질 점수와 연관성이 높을 것으로 예측함)
가설 2. 구연산 함량 수치가 특정 범위에서 높은 품질 점수를 보일 것이다. (풍미와 관련된 수치이므로, 풍미가 적절하게 높은 구연산 함량 범위가 있을 것으로 예측함)
가설 3. 황산염 함량 수치가 높을수록 품질 점수는 높을 것이다. (수치가 클수록 풍미와 보존성이 커져 높은 품질 점수를 받을 것으로 예측함.)
가설 4. 알코올 도수가 높을 수록 높은 품질 점수를 보일 것이다. (통상 도수가 높을 수록 가격이 비싸진 다는 점에서 높은 품질 점수를 받을 것으로 예측함)
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
wine = pd.read_csv('data/winequality-red.csv')
wine.head()
| fixed acidity | volatile acidity | citric acid | residual sugar | chlorides | free sulfur dioxide | total sulfur dioxide | density | pH | sulphates | alcohol | quality | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7.4 | 0.70 | 0.00 | 1.9 | 0.076 | 11.0 | 34.0 | 0.9978 | 3.51 | 0.56 | 9.4 | 5 |
| 1 | 7.8 | 0.88 | 0.00 | 2.6 | 0.098 | 25.0 | 67.0 | 0.9968 | 3.20 | 0.68 | 9.8 | 5 |
| 2 | 7.8 | 0.76 | 0.04 | 2.3 | 0.092 | 15.0 | 54.0 | 0.9970 | 3.26 | 0.65 | 9.8 | 5 |
| 3 | 11.2 | 0.28 | 0.56 | 1.9 | 0.075 | 17.0 | 60.0 | 0.9980 | 3.16 | 0.58 | 9.8 | 6 |
| 4 | 7.4 | 0.70 | 0.00 | 1.9 | 0.076 | 11.0 | 34.0 | 0.9978 | 3.51 | 0.56 | 9.4 | 5 |
print(wine.shape)
wine.info()
(1599, 12)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1599 entries, 0 to 1598
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 fixed acidity 1599 non-null float64
1 volatile acidity 1599 non-null float64
2 citric acid 1599 non-null float64
3 residual sugar 1599 non-null float64
4 chlorides 1599 non-null float64
5 free sulfur dioxide 1599 non-null float64
6 total sulfur dioxide 1599 non-null float64
7 density 1599 non-null float64
8 pH 1599 non-null float64
9 sulphates 1599 non-null float64
10 alcohol 1599 non-null float64
11 quality 1599 non-null int64
dtypes: float64(11), int64(1)
memory usage: 150.0 KB
max = wine['volatile acidity'].max()
min = wine['volatile acidity'].min()
print(f"휘발성 산도 수치의 최댓 값: {max}, 최소 값: {min}, 차이: {max-min}")
휘발성 산도 수치의 최댓 값: 1.58, 최소 값: 0.12, 차이: 1.46
# 구간화
bins = [0.1, 0.4, 0.7, 1.0, 1.3, 1.6]
labels = ["0.1~0.4", "0.4~0.7", "0.7~1.0", "1.0~1.3", "1.3~1.6"]
wine["va_bin"] = pd.cut(wine["volatile acidity"], bins=bins, labels=labels, include_lowest=True)
# 구간별 평균 품질 점수 계산
grouped = wine.groupby("va_bin")["quality"].mean().reset_index()
print(grouped)
va_bin quality
0 0.1~0.4 6.069663
1 0.4~0.7 5.533835
2 0.7~1.0 5.262376
3 1.0~1.3 4.611111
4 1.3~1.6 4.333333
#시각화 (막대그래프)
sns.barplot(x="va_bin", y="quality", data=grouped)
plt.title("Average Wine Quality by Volatile Acidity Range")
plt.xlabel("Volatile Acidity Range")
plt.ylabel("Average Quality Score")
plt.show()

# 구연산 함량 수치 범위 확인
print(wine["citric acid"].min(), wine["citric acid"].max())
# 구연산 구간 나누기 (예: 0.0 ~ 1.0을 5등분)
wine["citric_bin"] = pd.cut(wine["citric acid"], bins=5)
# 피벗테이블 (구간별 평균 품질 점수)
pivot = wine.pivot_table(index="citric_bin", values="quality", aggfunc="mean")
# 순위 추가 (평균 점수가 높은 순 = 1위)
pivot["rank"] = pivot["quality"].rank(method="dense", ascending=False).astype(int)
# 결과 출력 (순위순 정렬)
print(pivot.sort_values("rank"))
0.0 1.0
quality rank
citric_bin
(0.6, 0.8] 6.012987 1
(0.4, 0.6] 5.865753 2
(0.2, 0.4] 5.632381 3
(-0.001, 0.2] 5.462758 4
(0.8, 1.0] 4.000000 5
# 황산염 함량 수치 범위 확인
print(wine["sulphates"].min(), wine["sulphates"].max())
plt.figure(figsize=(8, 5))
sns.boxplot(
data=wine, x='quality', y='sulphates'
)
plt.title('Sulphates by quality (box plot)')
plt.xlabel('quality'); plt.ylabel('sulphates')
plt.tight_layout(); plt.show()
0.33 2.0

시각화된 박스 플롯에서, 두 변수 간 연관성은 찾기 어렵다.
등급이 올라갈수록 중앙값이 약간 오르긴 하지만, 차이가 작고 등급 간 분포가 많이 겹친다.
각 품질 등급의 IQR과 중앙값이 서로 비슷해 변별력이 낮다. 같은 황산염 수준에서도 여러 품질 등급이 뒤섞여 나타난다.
특히 중간 등급(5~7) 구간에 높은 이상치가 다수 존재해 평균과 변동을 키우며, “높을수록 품질이 향상된다”라는 인상을 과장할 수 있다.
sns.scatterplot(x="alcohol", y="quality", data=wine, alpha=0.5)
plt.ylim(1, 9)
plt.show()

sns.regplot(x="alcohol", y="quality", data=wine, scatter_kws={"alpha":0.5})
plt.ylim(1, 9)
plt.title("Alcohol vs Wine Quality")
plt.show()

wine_corr = wine.corr(method='pearson', numeric_only=True).round(2)
wine_corr
| fixed acidity | volatile acidity | citric acid | residual sugar | chlorides | free sulfur dioxide | total sulfur dioxide | density | pH | sulphates | alcohol | quality | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fixed acidity | 1.00 | -0.26 | 0.67 | 0.11 | 0.09 | -0.15 | -0.11 | 0.67 | -0.68 | 0.18 | -0.06 | 0.12 |
| volatile acidity | -0.26 | 1.00 | -0.55 | 0.00 | 0.06 | -0.01 | 0.08 | 0.02 | 0.23 | -0.26 | -0.20 | -0.39 |
| citric acid | 0.67 | -0.55 | 1.00 | 0.14 | 0.20 | -0.06 | 0.04 | 0.36 | -0.54 | 0.31 | 0.11 | 0.23 |
| residual sugar | 0.11 | 0.00 | 0.14 | 1.00 | 0.06 | 0.19 | 0.20 | 0.36 | -0.09 | 0.01 | 0.04 | 0.01 |
| chlorides | 0.09 | 0.06 | 0.20 | 0.06 | 1.00 | 0.01 | 0.05 | 0.20 | -0.27 | 0.37 | -0.22 | -0.13 |
| free sulfur dioxide | -0.15 | -0.01 | -0.06 | 0.19 | 0.01 | 1.00 | 0.67 | -0.02 | 0.07 | 0.05 | -0.07 | -0.05 |
| total sulfur dioxide | -0.11 | 0.08 | 0.04 | 0.20 | 0.05 | 0.67 | 1.00 | 0.07 | -0.07 | 0.04 | -0.21 | -0.19 |
| density | 0.67 | 0.02 | 0.36 | 0.36 | 0.20 | -0.02 | 0.07 | 1.00 | -0.34 | 0.15 | -0.50 | -0.17 |
| pH | -0.68 | 0.23 | -0.54 | -0.09 | -0.27 | 0.07 | -0.07 | -0.34 | 1.00 | -0.20 | 0.21 | -0.06 |
| sulphates | 0.18 | -0.26 | 0.31 | 0.01 | 0.37 | 0.05 | 0.04 | 0.15 | -0.20 | 1.00 | 0.09 | 0.25 |
| alcohol | -0.06 | -0.20 | 0.11 | 0.04 | -0.22 | -0.07 | -0.21 | -0.50 | 0.21 | 0.09 | 1.00 | 0.48 |
| quality | 0.12 | -0.39 | 0.23 | 0.01 | -0.13 | -0.05 | -0.19 | -0.17 | -0.06 | 0.25 | 0.48 | 1.00 |
plt.figure(figsize=(9, 9))
sns.heatmap(wine_corr_2, annot=True, annot_kws={"size": 9}, square=True)
plt.tight_layout()
plt.show()

와인의 화학적 특성에 따른 품질 점수의 피어슨 상관계수를 히트맵으로 표현하였다.
*quality 와 나머지 변수 간 상관계수를 파악할 것이므로, 해당 히트맵에서는 가장 오른쪽 열의 수치만 확인하면 된다.
그 결과, 양의 상관계수 중 알코올(alcohol)이 가장 높은 0.48을 보였고, 나머지 변수들은 대체로 약한 수준을 보였다.
음의 상관계수 중에서는 휘발성 산도(volatile acidity)가 -0.39 로 품질 점수와 보다 높은 상관관계를 나타냈다.
이번 Red Wine Quality 데이터셋 EDA를 통해 다음과 같은 인사이트를 얻을 수 있었다.
(1) 휘발성 산도(Volatile Acidity)
(2) 구연산(Citric Acid)
(3) 황산염(Sulphates)
(4) 알코올(Alcohol)
(5) 종합 평가
이번 실습을 통해, EDA는 단순 코딩보다 데이터의 특성을 이해하고 인사이트를 도출하는 사고 과정이 중요함을 다시 한번 확인할 수 있었다.
특히, 시각화를 통해 변수 간 상관 관계를 한눈에 파악할 수 있었고, 가설 검증 과정에서 의미 있는 결론을 도출할 수 있었다.
다만, 해당 EDA는 전문성(사전 지식) 없이 예측한 데이터 셋이므로 가설을 설정함에 있어서 다소 설득력이 떨어질 우려가 있다. 이처럼 데이터 분석은 코딩 지식을 떠나 데이터셋에 대한 도메인 지식이나 흥미가 있어야 분석에 더 유리할 것으로 판단된다.
또한, EDA 연습용 데이터셋이 아닌 실제 상황에서 이를 어떻게 활용할 것인지 고민해보게 된다.
실제 데이터를 통해 어떠한 인사이트를 얻고자 하는가, 어떠한 사업적/실무적 이득을 얻고자하는가에 대한 방향성에 따라서 무엇을 분석할지가 결정될 것이다.
이러한 관점에서, 실무에서는 '어떤 데이터 셋을 분석할 것인가?'가 중요한 질문이 될 것이라 생각한다.