학습 목표
머신러닝은 데이터와의 씨름입니다. 데이터를 어떻게 이해하느냐에 따라 모델링 전략이 달라지고 예측 성능에 결정적인 영향을 줍니다. 주로 ‘탐색적 데이터 분석’ 과정에서 수행하는 데이터 시각화는 평면적인 데이터에서 주요한 특성을 드러내는 가장 효과적인 수단입니다. 따라서 시각화 기법들을 잘 이해하고 적절히 활용하는 게 아주 중요합니다. 이번 장에서는 다양한 시각화 기법의 개념, 효과, 구현 방법 등을 알아봅니다.
다루는 내용
4.1 데이터 종류
정형 데이터는 크게 수치형 데이터와 범주형 데이터로 나뉩니다.
4.1.1 수치형 데이터
수치형 데이터numerical data는 사칙 연산이 가능한 데이터입니다. 수치형 데이터는 다시 연속형 데이터와 이산형 데이터로 나뉩니다.
- 연속형 데이터continuous data : 값이 연속된 데이터입니다. 예를 들어 ‘키’는 170cm와 171cm 사이에 170.1cm, 170.2cm, 170.9999cm 등 무한히 많은 값이 있습니다. 이렇듯 값이 끊기지 않고 연속된 데이터를 연속형 데이터라고 합니다.
- 이산형 데이터discrete data : 정수로 딱 떨어져 셀 수 있는 데이터입니다. 사과 개수는 3개, 4개로 딱 떨어집니다. 책의 페이지 수도 마찬가지입니다. 100페이지, 200페이지로 딱 떨어집니다. 200.5페이지라는 것은 없습니다.
쉽게 말해, 연속형 데이터는 실수로 표현할 수 있는 데이터, 이산형 데이터는 정수로 표현할 수 있는 데이터라고 봐도 됩니다.
4.1.2 범주형 데이터
범주형 데이터categorical data는 범주를 나눌 수 있는 데이터로, 사칙 연산이 불가능합니다. 범주형 데이터는 다시 순서형 데이터와 명목형 데이터로 나뉩니다.
- 순서형 데이터ordinal data : 순위ranking를 매길 수 있는 데이터입니다. 예컨대 학점과 메달이 있죠. 학점은 A+, A0, A-, B+ 등 순위가 정해져 있습니다.
- 명목형 데이터nominal data : 순위가 따로 없는 데이터입니다. 성별과 우편번호가 여기 속합니다. 남자와 여자는 순위가 없습니다. 우편번호는 숫자로 표현하지만 명목형 데이터입니다. 순위도 없고, 우리집 우편번호와 옆집 우편번호를 더한다고 어떤 의미 있는 우편번호가 되지 않기 때문입니다.
warning
숫자로 되어 있다고 모두 수치형 데이터가 아닙니다.
숫자로 되어 있다고 모두 수치형 데이터가 아닙니다.
4.2 탐색적 데이터 분석과 그래프
탐색적 데이터 분석 단계에서는 다양한 그래프를 그립니다. 그래프는 데이터를 한눈에 파악하는 데 도움을 주기 때문입니다. 그래프를 활용해 데이터가 어떻게 구성돼 있는지, 어떤 피처가 중요한지, 어떤 피처를 제거할지, 어떻게 새로운 피처를 만들지 등 모델링에 필요한 다양한 정보를 얻을 수 있죠. 이번 절에서는 탐색적 데이터 분석에 필요한 주요 그래프 몇 가지를 알아보겠습니다. 간단한 설명과 함께 코드도 덧붙였습니다.
코드 예제는 matplotlib맷플롯립이 아닌 seaborn시본을 기준으로 했습니다. 둘 모두 파이썬으로 그래프를 그릴 때 흔히 사용하는 대표적인 시각화 라이브러리입니다. seaborn을 사용하면 더 간결하고, 기본 설정에서의 그래프가 좀 더 정결합니다.
4.3 수치형 데이터 시각화
수치형 데이터는 일정한 범위 내에서 어떻게 분포distribution되어 있는지가 중요합니다. 고르게 퍼져 있을 수도, 특정 영역에 몰려 있을 수도 있습니다. 이 분포를 알아야 데이터를 어떻게 변환transformation할지, 어떻게 해석해서 활용할지 판단할 수 있습니다.
다음은 seaborn을 임포트하고 데이터를 불러오는 코드입니다. 이번 절의 모든 예시 코드 앞에 다음 2줄이 생략되었다고 생각하시기 바랍니다.
import seaborn as sns titanic = sns.load_dataset('titanic') # 타이타닉 데이터 불러오기
titanic은 seaborn에서 제공하는 데이터로, 타이타닉 경진대회에서 사용하는 데이터와 비슷합니다. 데이터가 어떻게 구성되어 있는지 살펴보시죠.
titanic.head()
수치형 데이터(age, fare 등)와 범주형 데이터(sex, embarked, class 등)가 공존하네요. 이번 절에서는 수치형 데이터를 시각화하는 예를 살펴보고, 범주형 데이터 시각화는 다음 절에서 알아보겠습니다.
다음은 seaborn이 제공하는 주요 분포도 함수입니다.
- histplot() : 히스토그램
- kdeplot() : 커널밀도추정 함수 그래프
- displot() : 분포도
- rugplot() : 러그플롯
차례대로 살펴보시죠.
4.3.1 히스토그램(histplot)
히스토그램histogram은 수치형 데이터의 구간별 빈도수를 나타내는 그래프입니다. histplot() 함수로 그릴 수 있습니다. titanic 데이터의 age 피처에 대한 히스토그램을 그려보겠습니다.
histplot()의 data 파라미터에 전체 데이터셋을 DataFrame 형식으로 전달하고, x 파라미터에 분포를 파악하려는 피처를 전달하면 됩니다.
sns.histplot(data=titanic, x='age');
잘 그려졌네요. 나이를 구간별로 나누어, 각 구간에 해당하는 사람이 몇 명인지 나타낸 그래프입니다. 히스토그램을 보니 구간이 총 20개입니다. 구간 개수를 지정하는 bins 파라미터의 기본값이 ‘auto’라서 자동으로 설정한 결과죠. 구간을 10개로 고정하고 싶다면 다음과 같이 bins=10을 전달합니다.
sns.histplot(data=titanic, x='age', bins=10);
히스토그램은 기본적으로 수치형 데이터 하나에 대한 빈도를 나타냅니다. 그런데 이 빈도를 특정 범주별로도 구분해서 보고 싶군요. 이럴 때는 hue 파라미터에 해당 범주형 데이터를 전달하면 됩니다. 다음은 생존여부(alive 피처)에 따른 연령 분포를 그려주는 코드입니다.
sns.histplot(data=titanic, x='age', hue='alive');
보다시피 생존자 수 그래프와 사망자 수 그래프를 포개지게 그렸습니다. 회색 구간이 두 그래프가 서로 겹친 부분입니다. 포개지 않고 생존자 수와 사망자 수를 누적해 표현하려면 multiple=‘stack’을 전달하면 됩니다.
sns.histplot(data=titanic, x='age', hue='alive', multiple='stack');
깔끔하죠?
4.3.2 커널밀도추정 함수 그래프(kdeplot)
커널밀도추정kernel density estimation 함수는 쉽게 생각해 히스토그램을 매끄럽게 곡선으로 연결한 그래프 정도로 이해하면 됩니다. 커널밀도추정 함수 그래프는 kdeplot()로 그릴 수 있는데, 실제로 탐색적 데이터 분석 시 많이 쓰지는 않습니다.
커널밀도추정이 무엇인지 이해하려면 밀도추정과 커널 함수에 대해 알아야 합니다. 이에 대한 설명은 본 책의 범위를 넘어가므로 생략하겠습니다.
sns.kdeplot(data=titanic, x='age');
앞서 그린 히스토그램과 비교해보세요. 히스토그램을 매끄럽게 연결한 모양이죠? 히스토그램은 이산적이지만, 커널밀도추정 함수 그래프는 연속적이네요.
파라미터로 hue=‘alive’와 multiple=‘stack’을 전달하면 다음과 같이 바뀝니다.
sns.kdeplot(data=titanic, x='age', hue='alive', multiple='stack');
역시 앞서 본 누적 히스토그램과 비교하면 이산적인지 연속적인지 차이가 있을 뿐이지 전체적인 모양새는 거의 같습니다.
4.3.3 분포도(displot)
분포도는 수치형 데이터 하나의 분포를 나타내는 그래프입니다. 캐글에서 분포도를 그릴 땐 displot()을 많이 사용합니다. 파라미터만 조정하면 histplot()과 kdeplot()이 제공하는 기본 그래프를 모두 그릴 수 있기 때문입니다.
warning
seaborn 0.11.0부터 분포도 함수가 distplot()에서 displot()으로 바뀌었습니다. 가운데 ‘t’가 있고 없고의 차이입니다. 기능은 같아도 세부 파라미터가 다르니, 다른 사람이 공유한 코드를 볼 때 유의해주세요. 이 책에서는 displot()만 이용합니다.
seaborn 0.11.0부터 분포도 함수가 distplot()에서 displot()으로 바뀌었습니다. 가운데 ‘t’가 있고 없고의 차이입니다. 기능은 같아도 세부 파라미터가 다르니, 다른 사람이 공유한 코드를 볼 때 유의해주세요. 이 책에서는 displot()만 이용합니다.
파라미터를 기본값으로 두면 히스토그램을 그립니다.
sns.displot(data=titanic, x='age');
앞서 histplot()이 그린 히스토그램과 비교해보면 크기만 다를 뿐이네요. 커널밀도추정 함수 그래프도 그릴 수 있습니다. kind 파라미터에 ‘kde’를 전달하면 됩니다.
sns.displot(data=titanic, x='age', kind='kde');
이번에도 kdeplot( )으로 그린 결과와 그래프 크기만 다릅니다.
히스토그램과 커널밀도추정 함수 그래프를 동시에 그릴 수도 있습니다. 간단히 kde=True를 전 달하면 되죠.
sns.displot(data=titanic, x='age', kde=True);
4.3.4 러그플롯(rugplot)
러그플롯은 주변 분포marginal distribution를 나타내는 그래프입니다. 단독으로 사용하기보다는 주로 다른 분포도 그래프와 함께 사용합니다. 다음은 커널밀도추정 함수 그래프와 러그플롯을 함께 그린 예시입니다.
sns.kdeplot(data=titanic, x='age'); sns.rugplot(data=titanic, x='age');
이렇듯 러그플롯은 단일 피처(여기서는 age 피처)가 어떻게 분포돼 있는지를 x축 위에 작은 선분(러그)으로 표시합니다. 값이 밀집돼 있을수록 작은 선분들도 밀집돼 있습니다.
4.4 범주형 데이터 시각화
범주형 데이터를 시각화하는 방법을 알아보겠습니다. 이번 절에서도 타이타닉 데이터를 활용할 겁니다. 이번 절의 모든 코드 앞에 다음 코드가 있다고 보시면 됩니다.
import seaborn as sns titanic = sns.load_dataset('titanic') # 타이타닉 데이터 불러오기
막대 그래프, 포인트플롯, 박스플롯, 바이올린플롯, 카운트플롯을 차례대로 살펴보겠습니다.
4.4.1 막대 그래프(barplot)
막대 그래프는 범주형 데이터 값에 따라 수치형 데이터 값이 어떻게 달라지는지 파악할 때 사용합니다. barplot()으로 그릴 수 있습니다.
barplot()은 범주형 데이터에 따른 수치형 데이터의 평균과 신뢰구간을 그려줍니다. 수치형 데이터 평균은 막대 높이로, 신뢰구간은 오차 막대error bar로 표현합니다. 원본 데이터를 복원 샘플링하여 얻은 표본을 활용해 평균과 신뢰구간을 구하는 겁니다. 즉, barplot()은 원본 데이터 평균이 아니라 샘플링한 데이터 평균을 구해줍니다.
기본적으로 x 파라미터에 범주형 데이터를, y 파라미터에 수치형 데이터를 전달합니다. data 파라미터에는 전체 데이터셋을 전달하고요.
타이타닉 탑승자 등급별 운임을 barplot()으로 그려보겠습니다. 범주형 데이터인 class(등급) 피처를 x 파라미터에, 수치형 데이터인 fare(운임) 피처를 y 파라미터에 전달했습니다.
sns.barplot(x='class', y='fare', data=titanic);
막대 높이는 등급별 평균 운임을 뜻합니다. 막대 상단의 검은색 세로줄이 오차 막대(신뢰구간)입니다. 등급이 높을수록 평균 운임이 비싸고 신뢰구간이 넓어지네요.
4.4.2 포인트플롯(pointplot)
포인트플롯은 막대 그래프와 모양만 다를 뿐 동일한 정보를 제공합니다. 막대 그래프와 마찬가지로 범주형 데이터에 따른 수치형 데이터의 평균과 신뢰구간을 나타내죠. 다만 그래프를 점과 선으로 나타냅니다.
타이타닉 탑승자 등급별 운임을 pointplot()으로 그려보겠습니다.
sns.pointplot(x='class', y='fare', data=titanic);
보다시피 포인트플롯과 막대 그래프는 동일한 정보를 제공합니다. 그러면 언제 포인트플롯을 사용하는 게 좋을까요? 한 화면에 여러 그래프를 그릴 때입니다. 포인트플롯은 점과 선으로 표현하기 때문에 여러 그래프를 그려도 서로 잘 보이고, 비교하기도 쉽습니다.
다음은 포인트플롯과 막대 그래프를 비교한 그림입니다. 계절에 따른 시간대별 자전거 대여 수량을 나타내는 그래프입니다. 어느 그래프가 눈에 더 잘 들어오나요?
아래쪽 막대 그래프로는 계절별 차이를 알기가 어렵습니다. 반면에 위쪽 포인트플롯은 계절별 차이가 한눈에 들어옵니다. 이렇게 한 화면에 여러 그래프를 그려 비교할 땐 포인트플롯을 사용하는 게 좋겠죠?
4.4.3 박스플롯(boxplot)
박스플롯은 막대 그래프나 포인트플롯보다 더 많은 정보를, 구체적으로 5가지 요약 수치를 제공합니다. 5가지 요약 수치five-number summary는 최솟값, 제1사분위 수(Q1), 제2사분위 수(Q2), 제3사분위 수(Q3), 최댓값을 뜻합니다.
- 제1사분위 수(Q1) : 전체 데이터 중 하위 25%에 해당하는 값
- 제2사분위 수(Q2) : 50%에 해당하는 값(중앙값)
- 제3사분위 수(Q3) : 상위 25%에 해당하는 값
- 사분위 범위 수(IQR) : Q3-Q1
- 최댓값 : Q3+(1.5*IQR)
- 최솟값 : Q1-(1.5*IQR)
- 이상치 : 최댓값보다 큰 값과 최솟값보다 작은 값
박스플롯은 boxplot()으로 그릴 수 있습니다. 타이타닉 탑승자 등급별 나이를 박스플롯으로 그려보겠습니다. boxplot()의 x, y 파라미터에 각각 범주형 데이터(class)와 수치형 데이터(age)를 전달합니다.
sns.boxplot(x='class', y='age', data=titanic);
4.4.4 바이올린플롯(violinplot)
바이올린플롯은 박스플롯과 커널밀도추정 함수 그래프를 합쳐 놓은 그래프라고 볼 수 있습니다. 박스플롯이 제공하는 정보를 모두 포함하며, 모양은 커널밀도추정 함수 그래프 형태입니다. 다음 그림을 보면 쉽게 이해될 겁니다.
바이올린플롯은 violinplot()으로 그릴 수 있습니다. 앞의 박스플롯과 비교해보기 위해 똑같이 등급(class)별 나이(age)를 그려보겠습니다.
sns.violinplot(x='class', y='age', data=titanic);
박스플롯 그래프와 비교해보세요. 각 범주별로 5가지 요약 수치를 한눈에 보고 싶다면 박스플롯 이 좋겠고, 수치형 데이터의 전체적인 분포 양상을 알고 싶다면 바이올린플롯이 좋겠네요.
이어서 성별에 따른 등급별 나이 분포를 살펴볼까요? hue=‘sex’를 추가로 전달하면 되겠죠. split=True를 전달하면 hue에 전달한 피처를 반으로 나누어 보여줍니다.
sns.violinplot(x='class', y='age', hue='sex', data=titanic, split=True);
4.4.5 카운트플롯(countplot)
카운트플롯은 범주형 데이터의 개수를 확인할 때 사용하는 그래프입니다. 주로 범주형 피처나 범주형 타깃값의 분포가 어떤지 파악하는 용도로 사용합니다.
카운트플롯은 countplot()으로 그릴 수 있습니다. x 파라미터에 범주형 데이터를 전달하면 됩니다. 타이타닉 탑승자의 등급별 인원수를 카운트플롯으로 그려보겠습니다.
sns.countplot(x='class', data=titanic);
이렇듯 카운트플롯을 사용하면 범주형 데이터의 개수를 파악할 수 있습니다.
한 가지 팁을 드리자면, x 파라미터를 y로 바꾸면 다음과 같이 그래프 방향을 바꿀 수 있습니다.
sns.countplot(y='class', data=titanic);
범주형 데이터 개수가 많아 그래프가 옆으로 너무 넓어져 보기 불편할 때 아주 유용합니다.
barplot() vs. countplot()
막대 그래프를 그려주는 barplot()과 카운트플롯을 그려주는 countplot()은 비슷한 듯 보이지만 서로 다릅니다. barplot()은 범주형 데이터별 수치형 데이터의 평균을 구해주기 때문에 피처를 두 개 받습니다. 반면 countplot()은 피처를 범주형 데이터 하나만 받습니다.
막대 그래프를 그려주는 barplot()과 카운트플롯을 그려주는 countplot()은 비슷한 듯 보이지만 서로 다릅니다. barplot()은 범주형 데이터별 수치형 데이터의 평균을 구해주기 때문에 피처를 두 개 받습니다. 반면 countplot()은 피처를 범주형 데이터 하나만 받습니다.
sns.barplot(x='class', y='fare', data=titanic); # 막대 그래프 sns.countplot(y='class', data=titanic); # 카운트플롯
한편 barplot( )으로는 평균이 아닌 중앙값, 최댓값, 최솟값을 구할 수도 있습니다.
sns.barplot(x='class', y='fare', data=titanic, estimator=np.median); # 중앙값 sns.barplot(x='class', y='fare', data=titanic, estimator=np.max); # 최댓값 sns.barplot(x='class', y='fare', data=titanic, estimator=np.min); # 최솟값
4.4.6 파이 그래프(pie)
파이 그래프는 범주형 데이터별 비율을 알아볼 때 사용하기 좋은 그래프입니다. 아쉽게도 seaborn에서 파이 그래프를 지원하지 않아 matplotlib을 사용해야 합니다.
파이 그래프는 pie() 함수로 그릴 수 있습니다. x 파라미터에는 비율을, labels 파라미터에는 범주형 데이터 레이블명을 전달하면 됩니다. 또한, autopct 파라미터를 통해 비율을 숫자로 나타낼 수 있습니다.
autopct 파라미터에 전달한 ‘%.1f%%’를 보시죠. %.1f는 소수점 자리수를 의미하고, %%는 퍼센트까지 표시한다는 의미입니다. 소수점 둘째 자리까지 퍼센트로 나타내려면 ‘%.2f%%’를 전달하면 됩니다.
import matplotlib.pyplot as plt x = [10, 60, 30] # 범주형 데이터별 파이 그래프의 부채꼴 크기(비율) labels = ['A', 'B', 'C'] # 범주형 데이터 레이블 plt.pie(x=x, labels=labels, autopct='%.1f%%');
4.5 데이터 관계 시각화
관계도는 여러 데이터 사이의 관계를 살펴보기 위한 그래프입니다. 이번 절에서는 히트맵, 라인플롯, 산점도, 회귀선을 포함한 산점도를 살펴보겠습니다.
4.5.1 히트맵(heatmap)
히트맵은 데이터 간 관계를 색상으로 표현한 그래프입니다. 비교해야 할 데이터가 많을 때 주로 사용하며, heatmap() 함수를 이용합니다.
이번에는 비행기 탑승자 수 데이터를 활용해보겠습니다. 연도별, 월별 탑승자 수를 나타내는 데이터로, 타이타닉 데이터와 마찬가지로 seaborn에서 기본적으로 제공합니다.
먼저, 데이터를 불러옵니다.
import seaborn as sns flights = sns.load_dataset('flights') # 비행기 탑승자 수 데이터 불러오기
flights 데이터는 다음과 같이 구성돼 있습니다.
flights.head()
범주형 데이터 2개(year, month)와 수치형 데이터 1개(passengers)가 있네요.
히트맵을 그리는 데 활용하려면 데이터 구조를 조금 바꿔야 합니다. 판다스의 pivot() 함수를 활용해서요. pivot() 함수는 index와 columns 파라미터에 전달한 피처를 각각 행과 열로 지정하고, values 파라미터에 전달한 피처를 합한 표를 반환합니다.
우리는 각 연도의 월별 승객 수를 알고 싶으니, month를 행으로, year를 열로, 합산할 데이터를 passengers로 지정하겠습니다.
flights_pivot = flights.pivot(index='month', columns='year', values='passengers') flights_pivot
각 연도의 월별 탑승자 수를 나타내는 표가 만들어졌습니다. 하지만 숫자로만 나열되니 추이까지는 한눈에 파악하기 힘드네요. 이럴 때 사용하는 그래프가 히트맵입니다. flights_pivot 데이터를 히트맵으로 표현해보겠습니다.
sns.heatmap(data=flights_pivot);
이렇게 히트맵으로 그리니 전체적인 양상을 쉽게 볼 수 있죠? 표로 볼 때보다 의미를 파악하기가 훨씬 쉽습니다.
4.5.2 라인플롯(lineplot)
라인플롯은 두 수치형 데이터 사이의 관계를 나타낼 때 사용합니다. 기본적으로는 x 파라미터에 전달한 값에 따라 y 파라미터에 전달한 값의 평균과 95% 신뢰구간을 나타냅니다. 다음 코드를 실행해보시죠.
sns.lineplot(x='year', y='passengers', data=flights);
x축은 연도, y축은 평균 승객수를 나타냅니다. 해가 갈수록 평균 승객수가 많아지네요. 실선 주변의 음영은 95% 신뢰구간입니다.
4.5.3 산점도(scatterplot)
산점도는 두 데이터의 관계를 점으로 표현하는 그래프입니다. 산점도 그래프에는 총 비용과 팁 정보를 모아둔 tips 데이터셋을 활용하겠습니다.
tips = sns.load_dataset('tips') # 팁 데이터 불러오기
데이터가 어떻게 구성돼 있는지 출력해보죠.
tips.head()
이 데이터의 산점도를 그려보겠습니다.
sns.scatterplot(x='total_bill', y='tip', data=tips);
대체로 총액이 늘면 팁도 따라서 늘고 있습니다.
다음과 같이 hue 파라미터를 이용하면 산점도를 특정 범주형 데이터별로 나누어 그릴 수 있습니다. 시간(time)에 따라 나눠볼까요?
sns.scatterplot(x='total_bill', y='tip', hue='time', data=tips);
점심과 저녁으로 구분해 그려봤는데, 전체적인 추이는 비슷해 보입니다.
4.5.4 회귀선을 포함한 산점도 그래프(regplot)
regplot()은 산점도와 선형 회귀선을 동시에 그려주는 함수입니다. 회귀선을 그리면 전반적인 상관관계 파악이 좀 더 쉽습니다. 이번에도 tips 데이터를 활용해 그래프를 그려보겠습니다.
sns.regplot(x='total_bill', y='tip', data=tips);
보다시피 산점도와 함께 선형 회귀선이 나타났습니다. 선형 회귀선 주변 음영은 95% 신뢰구간을 의미합니다. 신뢰구간을 99%로 늘리려면 ci 파라미터에 99를 전달하면 됩니다.
sns.regplot(x='total_bill', y='tip', ci=99, data=tips);
99% 신뢰구간으로 설정하니 음영 부분이 더 넓어진 걸 확인할 수 있습니다.