26. 판다스 시각화 도구#

import numpy as np
import pandas as pd
PREVIOUS_MAX_ROWS = pd.options.display.max_rows
pd.options.display.max_rows = 20
pd.options.display.max_colwidth = 80
pd.options.display.max_columns = 20
np.random.seed(12345)
np.set_printoptions(precision=4, suppress=True)
import matplotlib.pyplot as plt
import matplotlib
plt.rc("figure", figsize=(10, 6))

datetime.datetime 자료형은 시간(날짜) 데이터를 다룬다.

from datetime import datetime

주요 내용

판다스의 시리즈(Series)와 데이터프레임(Dataframe) 객체 모두 plot() 메서드를 지원한다. 실제로는 matplotlib.pyplot.plot()을 이용하기에 xticks, xlim 등 옵션 사용방식 또한 거의 동일하다. 자세한 옵션과 사용법은 공식문서를 참고한다.

26.1. 시리즈 시각화#

26.1.1. 선 그래프#

시리즈는 하나의 선그래프를 그릴 수 있으며, 항목이 y축 좌표로, 인덱스가 x축 좌표로 사용된다.

s = pd.Series(np.random.standard_normal(10).cumsum(), index=np.arange(0, 100, 10))
s
0    -0.204708
10    0.274236
20   -0.245203
30   -0.800933
40    1.164847
50    2.558253
60    2.651161
70    2.932907
80    3.701930
90    4.948364
dtype: float64
s.plot()
<Axes: >
_images/97126a3b2fcdb17740a362f4e9c83be4d404bf02d529e8395294746889cde050.png

시리즈/데이터프레임의 plot() 메서드의 style 키워드 인자는 plt.plot() 함수의 linestyle 키워드 인자와 동일한 기능을 수행한다.

s.plot(style='o:r')
<Axes: >
_images/87e8a561f5b061f38b2c6ce8487b1e3e6dd4eca7107382f40512754f740ff516.png

26.1.2. 막대그래프#

  • 수직 막대그래프: pandas.Series.plot.bar()

  • 수평 막대그래프: pandas.Series.plot.barh()

아래 코드는 시리즈를 이용한 수직/수평 막대그래프를 그리는 기본적인 방법을 보여준다. 수평 막대그래프의 x축/y축 눈금은 수직 막대그래프의 y축/x축 눈금에 해당한다.

np.random.seed(12345)
data = pd.Series(np.random.rand(16), index=list('abcdefghijklmnop'))
data
a    0.929616
b    0.316376
c    0.183919
d    0.204560
e    0.567725
f    0.595545
g    0.964515
h    0.653177
i    0.748907
j    0.653570
k    0.747715
l    0.961307
m    0.008388
n    0.106444
o    0.298704
p    0.656411
dtype: float64

참고: 서브플롯을 지정하려면 ax=서브플롯 옵션을 지정하면 되며, 이는 시리즈와 데이터프레임의 모든 그래프 함수에 사용된다. 아래 데이터는 (2,1) 모양의 Figure 객체에 포함된 두 개의 서브플롯에 각각 수직/수평 막대그래프를 삽입한다.

fig, axes = plt.subplots(2, 1)

data.plot.bar(ax=axes[0], color='k', alpha=0.7)
data.plot.barh(ax=axes[1], color='k', alpha=0.7)
<Axes: >
_images/0b700faa5ed71ce01eb44d616f2f24289eae514e10735a2242ba0425f802487e.png
  • plot() 메서드의 kind=barkind=barh 옵션: kind=bar/kind=barh 옵션이 각각 수직/수평 막대그래프를 그리는 함수와 동일하다.

fig, axes = plt.subplots(2, 1)

data.plot(ax=axes[0], kind='bar', color='k', alpha=0.7)
data.plot(ax=axes[1], kind='barh', color='k', alpha=0.7)
<Axes: >
_images/0b700faa5ed71ce01eb44d616f2f24289eae514e10735a2242ba0425f802487e.png

26.2. 데이터프레임 시각화#

26.2.1. 데이터프레임과 선 그래프#

데이터프레임은 특성 수만큼의 선그래프를 그릴 수 있다. y축 좌표는 특성별 항목이며, x축 좌표는 기본적으로 인덱스가 사용된다. 또한 특성 이름이 범례로 지정된다.

df = pd.DataFrame(np.random.standard_normal((10, 4)).cumsum(0),
                  columns=["A", "B", "C", "D"],
                  index=np.arange(0, 100, 10))
df
A B C D
0 0.274992 0.228913 1.352917 0.886429
10 -1.726646 -0.142930 3.021942 0.447860
20 -2.266387 0.334055 6.270886 -0.573368
30 -2.843474 0.458177 6.573500 -0.049596
40 -2.842534 1.801986 5.859956 -0.880749
50 -5.212766 -0.058774 4.999198 -0.320604
60 -6.478700 0.061053 3.935686 0.012279
70 -8.838119 -0.138490 2.393690 -0.958457
80 -10.145149 0.147860 2.771674 -1.712344
90 -9.813864 1.497602 2.841551 -1.465670
df.plot()
<Axes: >
_images/893aadb4486bbe3beb440a06e306080619095f39f21c35b07c62fc5db9b95b84.png

subplots=True 키워드 인자를 사용하면 각각의 그래프를 1차원 어레이 모양의 독립된 서브플롯에서 그린다.

np.random.seed(12345)
df = pd.DataFrame(np.random.randn(10, 4).cumsum(0),
                  columns=['A', 'B', 'C', 'D'],
                  index=np.arange(0, 100, 10))
df
A B C D
0 -0.204708 0.478943 -0.519439 -0.555730
10 1.761073 1.872349 -0.426531 -0.273984
20 2.530095 3.118784 0.580659 -1.570205
30 2.805087 3.347697 1.933575 -0.683776
40 0.803450 2.975854 3.602601 -1.122346
50 0.263708 3.452839 6.851545 -2.143573
60 -0.313379 3.576961 7.154158 -1.619801
70 -0.312439 4.920770 6.440614 -2.450955
80 -2.682670 3.060010 5.579857 -1.890809
90 -3.948605 3.179837 4.516344 -1.557927
df.plot(subplots=True)
array([<Axes: >, <Axes: >, <Axes: >, <Axes: >], dtype=object)
_images/2946128f371930dc4a4585b15c058b09cc77128276e75b7afd921568ab5c0a7d.png

26.2.2. 데이터프레임과 막대그래프#

  • 수직 막대그래프: pandas.DataFrame.plot.bar()

  • 수평 막대그래프: pandas.DataFrame.plot.barh()

아래 코드는 데이터프레임을 이용한 수직/수평 막대그래프를 그리는 기본적인 방법을 보여준다. 수평 막대그래프의 x축/y축 눈금은 수직 막대그래프의 y축/x축 눈금에 해당한다.

  • 범례 제목: 컬럼 인덱스의 이름이 주어진 경우 사용된다. 아래 코드에서는 Genus이다.

  • 행별 막대그래프를 하나로 묶어서 보여준다.

np.random.seed(12348)

df = pd.DataFrame(np.random.uniform(size=(6, 4)),
                  index=["one", "two", "three", "four", "five", "six"],
                  columns=pd.Index(["A", "B", "C", "D"], name="Genus"))
df
Genus A B C D
one 0.370670 0.602792 0.229159 0.486744
two 0.420082 0.571653 0.049024 0.880592
three 0.814568 0.277160 0.880316 0.431326
four 0.374020 0.899420 0.460304 0.100843
five 0.433270 0.125107 0.494675 0.961825
six 0.601648 0.478576 0.205690 0.560547
df.plot.bar()
<Axes: >
_images/60f2c7293741b1435474f7b3874200d1dcde80768f2e15bf05b34cfa864466df.png
df.plot.barh()
<Axes: >
_images/cdffe56c3806f0fa4f429b92165274f64edfa99bee88b98f6acd8372143664e3.png

누적막대그래프

stacked=True 옵션을 사용하면 각 행의 값들이 하나의 막대에 누적되어 출력된다.

plt.figure()

df.plot.bar(stacked=True, alpha=0.5)
<Axes: >
<Figure size 1000x600 with 0 Axes>
_images/e8cf60b95ad45abbdd12b64d8f2683a9d4ec0ddc8de4588f31a3687b59eff230.png
plt.figure()

df.plot.barh(stacked=True, alpha=0.5)
<Axes: >
<Figure size 1000x600 with 0 Axes>
_images/9ed25159f43f6132017c5263f793ceedd0180dc6e1ca4d745e24865e0c304786.png
  • plot() 메서드의 kind=barkind=barh 옵션: kind=bar/kind=barh 옵션이 각각 수직/수평 막대그래프를 그리는 함수와 동일하다.

df.plot(kind='bar', alpha=0.5)
<Axes: >
_images/1636759ecbd4b2c022b19221038f145445bbf136748752c12b699987580087b5.png
df.plot(kind='barh', stacked=True, alpha=0.5)
<Axes: >
_images/9ed25159f43f6132017c5263f793ceedd0180dc6e1ca4d745e24865e0c304786.png

그래프 스타일 변환

판다스의 시각화 도구는 matplotlib.pyplot 라이브러리를 이용한다. 따라서 matplotlib.pyplot와 동일한 방식으로 그래프 스타일을 변경할 수 있다. 예를 들어 아래 코드를 실행하면 흑백으로 그래프를 그린다.

plt.style.use('grayscale')
plt.figure()

df.plot.barh(stacked=True, alpha=0.5)
<Axes: >
<Figure size 1000x600 with 0 Axes>
_images/447e0e4b84765c16813ef56b43e766d1b4cc62f3e238596f915a88d0efa90fa0.png

예제: 서빙 팁 데이터

서빙 팁 데이터는 어떤 식당에서 일주일 동안 올린 수입 내역을 담고 있다.

tips_path = 'https://raw.githubusercontent.com/codingalzi/datapy/master/jupyter-book/examples/tips.csv'
tips = pd.read_csv(tips_path)

tips
total_bill tip smoker day time size
0 16.99 1.01 No Sun Dinner 2
1 10.34 1.66 No Sun Dinner 3
2 21.01 3.50 No Sun Dinner 3
3 23.68 3.31 No Sun Dinner 2
4 24.59 3.61 No Sun Dinner 4
... ... ... ... ... ... ...
239 29.03 5.92 No Sat Dinner 3
240 27.18 2.00 Yes Sat Dinner 2
241 22.67 2.00 Yes Sat Dinner 2
242 17.82 1.75 No Sat Dinner 2
243 18.78 3.00 No Thur Dinner 2

244 rows × 6 columns

총 244개의 데이터 샘플을 담고 있으며, 열별 라벨(특성)이 의미하는 바는 다음과 같다.

  • total_bill: 총 수입

  • tip: 서빙 팁 수입

  • smoker: 테이블 손님 흡연 여부

  • day: 요일

  • time: 시간대(점심/저녁)

  • size: 테이블 손님 수

범주형 데이터에 사용된 값들은 다음과 같다.

  • 흡연 여부: 비흡연, 흡연

tips['smoker'].unique()
array(['No', 'Yes'], dtype=object)
  • 요일: 일, 토, 목, 금

tips['day'].unique()
array(['Sun', 'Sat', 'Thur', 'Fri'], dtype=object)
  • 시간대: 저녁, 점식

tips['time'].unique()
array(['Dinner', 'Lunch'], dtype=object)
  • 테이블 인원스: 1명에서 6명 사이

tips['size'].unique()
array([2, 3, 4, 1, 6, 5], dtype=int64)
  • 요일과 테이블 당 손님 수 사이의 관계

요일(day)과 테이블 당 손님 수(size) 사이의 관계를 파악하기 위해 교차표를 이용한다. pandas.crosstab() 함수는 지정된 두 특성 사이의 도수분포도를 표로 생성한다.

party_counts = pd.crosstab(tips['day'], tips['size'])

party_counts
size 1 2 3 4 5 6
day
Fri 1 16 1 1 0 0
Sat 2 53 18 13 1 0
Sun 0 39 15 18 3 1
Thur 1 48 4 5 1 3

빈도가 낮은 1인과 6인 테이블은 제외한다.

party_counts = party_counts.loc[:, 2:5]

party_counts
size 2 3 4 5
day
Fri 16 1 1 0
Sat 53 18 13 1
Sun 39 15 18 3
Thur 48 4 5 1
party_counts = party_counts.reindex(index=["Thur", "Fri", "Sat", "Sun"])
party_counts
size 2 3 4 5
day
Thur 48 4 5 1
Fri 16 1 1 0
Sat 53 18 13 1
Sun 39 15 18 3
  • 행별 정규화(Normalization)

행별로 항목의 합이 1이 되도록 값을 정규화한다. 이를 통해 요일별 테이블 당 손님 수의 비율을 보다 정확히 파악할 수 있다.

  • pd.div() 함수: 항목별 나눗셈. axis=0는 행별 나눗셈.

  • sum() 메서드: 행/열별 합 계산. 기본은 열별 합 계산(axis=0). 행별 합은 axis=1 지정.

# 각 행의 합이 1이 되도록 정규화
row_sum = party_counts.sum(axis=1)
party_pcts = party_counts.div(row_sum, axis=0)

party_pcts
size 2 3 4 5
day
Thur 0.827586 0.068966 0.086207 0.017241
Fri 0.888889 0.055556 0.055556 0.000000
Sat 0.623529 0.211765 0.152941 0.011765
Sun 0.520000 0.200000 0.240000 0.040000

요일별 테이블당 손님 수를 막대그래프로 그리면 다음과 같다. 주말일 수록 테이블 당 손님 수가 많아짐을 확인할 수 있다.

party_pcts.plot.bar()
<Axes: xlabel='day'>
_images/fdc20c4d46a73d6ebfee11b3cf43cee3e127aeaf2bcbab71cbfc7dbcf638f3a6.png
party_pcts.plot.bar(stacked=True)
<Axes: xlabel='day'>
_images/58b217d93ff2268bfd21863aa3e86e84f548cdedbfd21167a704d9557734f75b.png

seaborn 스타일로 변환한다.

plt.style.use('seaborn-v0_8')
party_pcts.plot.barh(stacked=True)
<Axes: ylabel='day'>
_images/6192f33145ba567dc39b79f2ba98e27cf5dba5cd9aba0fd325ec7273e56709ba.png

26.3. 주석 추가#

이미지에 설명, 화살표 등 다양한 주석을 추가할 수 있다.

설명을 위해 S&P 500 (스탠다드 앤 푸어스, Standard and Poor’s 500)의 미국 500대 기업을 포함한 주식시장지수 데이터로 그래프를 생성하고 2007-2008년 사이에 있었던 재정위기와 관련된 중요한 날짜를 주석으로 추가한다.

base_url = 'https://raw.githubusercontent.com/codingalzi/datapy/master/jupyter-book/examples/'
data = pd.read_csv(base_url+"spx.csv")
data
Unnamed: 0 SPX
0 1990-02-01 00:00:00 328.79
1 1990-02-02 00:00:00 330.92
2 1990-02-05 00:00:00 331.85
3 1990-02-06 00:00:00 329.66
4 1990-02-07 00:00:00 333.75
... ... ...
5467 2011-10-10 00:00:00 1194.89
5468 2011-10-11 00:00:00 1195.54
5469 2011-10-12 00:00:00 1207.25
5470 2011-10-13 00:00:00 1203.66
5471 2011-10-14 00:00:00 1224.58

5472 rows × 2 columns

0번 열이 시간 데이터를 담고 있기에 파일에서 불러올 때 아예 0번 열을 인덱스로 지정한다. 또한 시간의 흐름 정보를 최대한 활용하기 위해 문자열이 아닌 시계열 데이터로 불러온다. 이 모든 것을 pd.read_csv() 함수의 키워드 인자 지정으로 가능하다.

  • index_col=0: 0번 열을 인덱스 지정.

  • parse_dates=True: 년월일까지 구분해서 인덱스로 사용하도록 함. 기본값은 False.

data = pd.read_csv(base_url+"spx.csv", index_col=0, parse_dates=True)
data
SPX
1990-02-01 328.79
1990-02-02 330.92
1990-02-05 331.85
1990-02-06 329.66
1990-02-07 333.75
... ...
2011-10-10 1194.89
2011-10-11 1195.54
2011-10-12 1207.25
2011-10-13 1203.66
2011-10-14 1224.58

5472 rows × 1 columns

하나의 열만 존재하는 데이터프레임이기에 시리즈로 변환한다.

참고: 반드시 필요한 과정은 아니다. spx 대신 data를 그대로 사용해도 동일하게 작동한다.

spx = data['SPX']
spx
1990-02-01     328.79
1990-02-02     330.92
1990-02-05     331.85
1990-02-06     329.66
1990-02-07     333.75
               ...   
2011-10-10    1194.89
2011-10-11    1195.54
2011-10-12    1207.25
2011-10-13    1203.66
2011-10-14    1224.58
Name: SPX, Length: 5472, dtype: float64
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

ax.plot(spx)
[<matplotlib.lines.Line2D at 0x1f017826f80>]
_images/ef1cf2e2639671863a54a5bef65abe68cb5d881c8d55f9f893e8387a814b9c24.png

우리나라에서 IMF 사태가 벌어진 이유 중에 하나인 2007-2008년 세계적 금융위기가 시작한 지점을 아래 내용으로 그래프에 주석으로 추가하려 한다.

  • 2007년 10월 11일: 주가 강세장 위치

  • 2008년 3월 12일: 베어스턴스 투자은행 붕괴

  • 2008년 9월 15일: 레만 투자은행 파산

crisis_data = [
    (datetime(2007, 10, 11), 'Peak of bull market'),
    (datetime(2008, 3, 12), 'Bear Stearns Fails'),
    (datetime(2008, 9, 15), 'Lehman Bankruptcy')
]

annotate() 메서드 활용

  • xy 속성: 화살표 머리 위치

  • xytext 속성: 텍스트 위치

  • arrowprops 속성: 화살표 속성

  • horizontalalignment: 텍스트 좌우 줄맞춤

  • verticalalignment: 텍스트 상하 줄맞춤

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

spx.plot(ax=ax, style='-')

for date, label in crisis_data:
    ax.annotate(label, 
                xy=(date, spx.asof(date) + 75),
                xytext=(date, spx.asof(date) + 225),
                arrowprops=dict(facecolor='red', headwidth=4, width=2, headlength=4),
                horizontalalignment='left', verticalalignment='top')

# 2007-2010 사이로 확대
ax.set_xlim([datetime(2007, 1, 1), datetime(2011, 1, 1)])
ax.set_ylim([600, 1800])

ax.set_title('Important dates in the 2008-2009 financial crisis')
Text(0.5, 1.0, 'Important dates in the 2008-2009 financial crisis')
_images/67af87ee09ecd301a4cec9b66eb245e398bada13f2284aa7868274e9c553db96.png
plt.close('all')