(2-1) Bar Plot 사용하기

210811

Bar Plot

직사각형 막대를 사용하여 데이터의 값을 표현하는 차트/그래프

  • 막대 그래프, bar chart, bar graph의 여러 이름을 가짐

  • 범주에 따른 수치 값을 비교하기에 적합

    • 개별비교, 그룹비교 모두 적합하다

막대의 방향에 따른 분류

  • 수직 : .bar()

    • 기본적으로 사용한다

  • 수평 : .barh()

    • 범주가 많을 때 사용한다

다양한 Bar Plot

Sky = [1, 2, 3, 4, 3]
Pink = [4, 3, 2, 5, 1]

위 두 데이터를 비교하기 위한 여러 방법을 사용할 것임

Multiple Bar Plot

  1. 플롯을 여러 개 그리는 방법

  2. 한 개의 플롯에 동시에 나타내는 방법

    • 쌓아서 표현

    • 겹쳐서 표현

    • 이웃에 배치하여 표현

Stacked Bar Plot

2개 이상의 그룹을 쌓아서 표현하는 bar plot

  • 이 때 각 bar에서 나타나는 그룹의 순서는 유지해야 한다. 그렇지 않으면 혼동을 줄 수 있다.

맨 밑에 bar의 분포는 파악하기 쉽다

  • 그러나 그 외의 분포들은 파악하기 어렵다

    • sky 데이터는 파악하기 쉽지만, pink 데이터는 파악하기 어렵다.

    • 이럴 때는 수치를 annotation 달 것을 추천한다.

  • 2개의 그룹이 positive/negative 라면 축 조정이 가능하다

  • .bar() 에서는 bottom 파라미터를 사용하고 .barh() 에서는 left 파라미터를 사용한다

좀 더 데이터 분포 비교가 원활한 Percentage Stacked Bar Chart 도 있다.

  • 각 bar 마다 퍼센트 수치를 알려주는 annotation이 표기되어 있는 모습

Overlapped Bar Plot

2개 그룹만 비교한다면 겹쳐서 만들 수도 있다.

  • 3개 이상부터는 파악이 어렵기 때문

같은 축을 사용하기 때문에 비교와 구현이 쉽다. 이 때, 투명도를 조정해서 겹치는 부분을 파악해야 한다 또, Bar Plot보다는 Area Plot에서 더 효과적이다.

Grouped Bar Plot

그룹별 범주에 따른 bar를 이웃되게 배치하는 방법이다. Matplotlib 로는 구현이 까다로워서 편하게 사용할 수 있는 Seaborn 을 주로 사용한다.

  • .set_xticks(), .set_xticklabels()

대부분의 Bar Plot은 그룹이 최대 7개 이하일 때 효과적이다. 그 외의 그룹은 ETC로 표현하거나 Pie Chart같은 다른 그래프로 표현해야 한다

정확한 Bar Plot

잉크양 비례 법칙

실제값과 그에 표현되는 그래픽으로 표현되는 잉크 양은 비례해야 한다는 뜻이다. 값이 클수록 그래프를 더 길게 또는 더 넓게 그려야 한다는 뜻

반드시 x축의 시작은 0에서 부터 시작해야 하며, 이 법칙은 대부분의 그래프에서 모두 적용된다.

왼쪽 그래프는 가독성이 좋고 차이를 확연히 느낄 수 있지만 좋은 그래프가 아니다. 보는 이에게 "첫번째 막대가 마지막 막대의 2배 이상인가?" 라는 혼동을 줄 수 있기 때문.

따라서, 오른쪽 그래프처럼 표현해야 하며 만약 비율의 차이를 보여주고 싶다면 y축의 길이를 늘려야 한다.

데이터 정렬하기

Pandas에서는 sort_values()sort_index() 를 사용하여 정렬한다

데이터의 종류에 따라 다음의 기준으로 정렬할 수 있다.

  • 시계열 | 시간순

  • 수치형 | 크기순

  • 순서형 | 범주의 순서대로

  • 명목형 | 범주의 값 따라 정렬

그렇지만, 정해진 기준은 없으며 여러가지 기준으로 정렬할 수 있으며 이를 통해 패턴을 발견할 수도 있다. 대시보드에서는 Interactive로 제공하는 것이 유용하다

적절한 공간 활용

여백과 공간을 조정하면 가독성이 높아진다. Matplotlib의 bar plot은 ax에 꽉 차서 답답한 경향이 있는데 다음과 같은 방법으로 조정할 수 있다.

  • X/Y axis Limit : .set_xlim(), .set_ylim()

    • x축과 y축의 범위를 설정한다

    • 인자에는 최솟값과 최댓값이 들어간다

  • Spines : .spines[spine].set_visible()

  • Gap(width)

    • 기본으로 0.8의 값을 가지고 있다

    • 1의 값을 가지면 히스토그램 모양이 된다

    • 보통 0.6에서 0.7을 주면 가독성이 높아진다

  • Legend : .legend()

    • 범위를 어디다 둘 것인가

  • Margin : .margins()

복잡함과 단순함

필요없는 복잡함은 안된다.

  • 사람의 인지는 2D에 최적화되어있다.

  • 3D로 보여주려면 실제로 3D 회전이 가능하게 해야한다.

  • 직사각형이 아닌 다른 형태의 bar는 지양하라

    • 그림자도 없어야 한다

Grid

  • 격자라는 것은 데이터가 정확하게 어떤 값인지 보조적으로 전달하는 역할

    • 데이터의 분포와 비율 등의 큰틀에서 추세를 파악할 때는 격자를 빼는것이 좋다.

ETC

오차 막대를 추가하여 Uncertainty 정보를 추가 가능하다. (errorbar)

Bar 사이 Gap이 0이라면 히스토그램을 사용한다

  • .hist() 를 사용하면 되고 연속된 느낌을 줄 수 있다.

다양한 텍스트를 사용할 수 있다

  • 제목 : .set_title()

  • 라벨 : .set_xlabel(), .set_ylabel()

실습 1-1. Bar Plot 사용하기

1. 기본 Bar Plot

  • bar() : 기본적인 bar plot

  • barh() : horizontal bar plot

import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt

x와 y를 5개 정도로 세팅해서 예시 플롯을 만들어보겠습니다.

fig, axes = plt.subplots(1, 2, figsize=(12, 7))

x = list('ABCDE')
y = np.array([1, 2, 3, 4, 5])

axes[0].bar(x, y)
axes[1].barh(x, y)

plt.show()
# 강의에서 다루지는 않았지만 막대 그래프의 색은 
# 다음과 같이 변경을 전체로 하거나, 개별로 할 수도 있습니다. 
# 개별로 할 때는 막대 개수와 같이 색을 리스트로 전해야 합니다.
# 색에 대해 구체적인 내용은 3차트의 요소-2색에서 다룹니다.

fig, axes = plt.subplots(1, 2, figsize=(12, 7))

x = list('ABCDE')
y = np.array([1, 2, 3, 4, 5])

clist = ['blue', 'gray', 'gray', 'gray', 'red']
color = 'green'
axes[0].bar(x, y, color=clist)
axes[1].barh(x, y, color=color)

plt.show()

2. 다양한 Bar Plot

2-0. 데이터 준비하기

이제 교육용 데이터셋으로 막대 그래프를 사용해보겠습니다.

데이터는 Student Score Dataset입니다. link

  • 1000명 학생 데이터

  • feature에 대한 정보는 head(), describe(), info() 등으로 확인하고

  • unique(), value_counts() 등으로 종류나 큰 분포 확인

  • feautre들

    • 성별 : female / male

    • 인종민족 : group A, B, C, D, E

    • 부모님 최종 학력 : 고등학교 졸업, 전문대, 학사 학위, 석사 학위, 2년제 졸업

    • 점심 : standard와 free/reduced

    • 시험 예습 : none과 completed

    • 수학, 읽기, 쓰기 성적 (0~100)

student = pd.read_csv('./exams.csv')
student.sample(5)
  • 데이터를 살필 때는 .head() 로도 할 수 있다. 하지만 보통 데이터는 순서가 있기 때문에 편향적인 데이터를 볼 가능성이 있어서 .sample() 을 쓰는 것이 좋다

gender

race/ethnicity

parental level of education

lunch

test preparation course

math score

reading score

writing score

437

male

group B

associate's degree

free/reduced

none

90

81

75

25

female

group B

bachelor's degree

free/reduced

none

61

74

69

317

female

group D

associate's degree

standard

completed

85

92

96

145

female

group B

some high school

free/reduced

completed

53

53

66

676

female

group E

some college

standard

none

71

76

69

student.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 8 columns):
 #   Column                       Non-Null Count  Dtype 
---  ------                       --------------  ----- 
 0   gender                       1000 non-null   object
 1   race/ethnicity               1000 non-null   object
 2   parental level of education  1000 non-null   object
 3   lunch                        1000 non-null   object
 4   test preparation course      1000 non-null   object
 5   math score                   1000 non-null   int64 
 6   reading score                1000 non-null   int64 
 7   writing score                1000 non-null   int64 
dtypes: int64(3), object(5)
memory usage: 62.6+ KB

각 컬럼의 데이터 타입을 보며 범주형인지 수치형인지 파악할 수 있다. 또 Null 값이 있는지도 알 수 있다.

student.describe(include='all')

gender

race/ethnicity

parental level of education

lunch

test preparation course

math score

reading score

writing score

count

1000

1000

1000

1000

1000

1000.000000

1000.000000

1000.000000

unique

2

5

6

2

2

NaN

NaN

NaN

top

male

group C

some college

standard

none

NaN

NaN

NaN

freq

526

353

242

673

655

NaN

NaN

NaN

mean

NaN

NaN

NaN

NaN

NaN

67.595000

69.865000

68.812000

std

NaN

NaN

NaN

NaN

NaN

15.030066

14.303054

14.845641

min

NaN

NaN

NaN

NaN

NaN

23.000000

23.000000

24.000000

25%

NaN

NaN

NaN

NaN

NaN

58.000000

60.000000

58.000000

50%

NaN

NaN

NaN

NaN

NaN

67.000000

70.000000

69.000000

75%

NaN

NaN

NaN

NaN

NaN

79.000000

80.000000

79.000000

max

NaN

NaN

NaN

NaN

NaN

100.000000

100.000000

100.000000

그룹에 따른 정보를 시각화해봅시다.

성별에 따른 race/ethincity 분포

코드로는 다음과 같이 쉽게 구할 수 있습니다.

group = student.groupby('gender')['race/ethnicity'].value_counts().sort_index()
display(group)
print(student['gender'].value_counts())
gender  race/ethnicity
female  group A            36
        group B            86
        group C           167
        group D           123
        group E            62
male    group A            31
        group B            95
        group C           186
        group D           126
        group E            88
Name: race/ethnicity, dtype: int64

male      526
female    474
Name: gender, dtype: int64

sort_index() 를 하지 않으면 value_counts() 순으로 정렬이 되서 깔끔하지 않을 수 있다.

2-1. Multiple Bar Plot

우선 기본적으로 그려보겠습니다.

fig, axes = plt.subplots(1, 2, figsize=(15, 7))
axes[0].bar(group['male'].index, group['male'], color='royalblue')
axes[1].bar(group['female'].index, group['female'], color='tomato')
plt.show()

위 그래프를 보면 분포가 비슷해 보이지만 y축을 잘 보면 수치가 다른 것을 알 수 있다. matplotlib는 이렇게 그래프를 꽉차게 보여주는 것이 기본이다.

각 barplot은 자체적으로 y 범위를 맞추기에 좀 더 y축의 범위를 공유할 수 있습니다.

방법1은 subplot을 만들 때, sharey 파라미터를 사용하는 방법입니다.

fig, axes = plt.subplots(1, 2, figsize=(15, 7), sharey=True)
axes[0].bar(group['male'].index, group['male'], color='royalblue')
axes[1].bar(group['female'].index, group['female'], color='tomato')
plt.show()

방법2는 y축 범위를 개별적으로 조정하는 방법입니다. 이렇게 할 때는 반복문을 사용하여 조정하는 방법을 선호합니다.

fig, axes = plt.subplots(1, 2, figsize=(15, 7))
axes[0].bar(group['male'].index, group['male'], color='royalblue')
axes[1].bar(group['female'].index, group['female'], color='tomato')

for ax in axes:
    ax.set_ylim(0, 200)
    
plt.show()

Group간의 비교가 어렵다는 단점이 있습니다.

2-2. Stacked Bar Plot

쌓아서 보면 그룹 A, B, C, D, E에 대한 전체 비율은 알기 쉽습니다.

bottom 파라미터를 사용해서 아래 공간을 비워둘 수 있습니다.

fig, axes = plt.subplots(1, 2, figsize=(15, 7))

group_cnt = student['race/ethnicity'].value_counts().sort_index()
axes[0].bar(group_cnt.index, group_cnt, color='darkgray')
axes[1].bar(group['male'].index, group['male'], color='royalblue')
axes[1].bar(group['female'].index, group['female'], bottom=group['male'], color='tomato')

for ax in axes:
    ax.set_ylim(0, 350)
    
plt.show()

여자의 비율 분포를 비교하기 어렵다는 단점이 있기 때문에 여자와 남자의 순서를 바꿀 수 있는 Interactive 한 기능을 제공하는 것이 중요하다

2-3. Percentage Stacked Bar Plot

좀 더 advanced한 테크닉을 사용한다면 다음과 같습니다.

fig, ax = plt.subplots(1, 1, figsize=(12, 7))

group = group.sort_index(ascending=False) # 역순 정렬
total=group['male']+group['female'] # 각 그룹별 합


ax.barh(group['male'].index, group['male']/total, 
        color='royalblue')

ax.barh(group['female'].index, group['female']/total, 
        left=group['male']/total, 
        color='tomato')

ax.set_xlim(0, 1)
for s in ['top', 'bottom', 'left', 'right']:
    ax.spines[s].set_visible(False)

plt.show()

set_xlim(0, 1)을 하지 않으면 오른쪽에 마진이 남기 때문에 설정해준다.

또, spines.set_visible(False)를 사용해서 테두리를 제거한다.

2-4. Overlapped Bar Plot

겹치는 투명도는 꼭 정해진 것이 아닌 다양한 실험을 통해 선택하면 됩니다.

group = group.sort_index() # 다시 정렬

fig, axes = plt.subplots(2, 2, figsize=(12, 12))
axes = axes.flatten()

for idx, alpha in enumerate([1, 0.7, 0.5, 0.3]):
    axes[idx].bar(group['male'].index, group['male'], 
                  color='royalblue', 
                  alpha=alpha)
    axes[idx].bar(group['female'].index, group['female'],
                  color='tomato',
                  alpha=alpha)
    axes[idx].set_title(f'Alpha = {alpha}')
    
for ax in axes:
    ax.set_ylim(0, 200)
    
    
plt.show()

좀 진한색은 0.5, 연한색은 0.7을 사용하는 것이 좋다

2-5. Grouped Bar Plot

크게 3가지 테크닉으로 구현 가능합니다.

  • x축 조정

  • width 조정

  • xticks, xticklabels

원래 x축이 0, 1, 2, 3로 시작한다면

- 한 그래프는 0-width/2, 1-width/2, 2-width/2 로 구성하면 되고
- 한 그래프는 0+width/2, 1+width/2, 2+width/2 로 구성하면 됩니다.

이 x좌표는 막대 그래프의 중심점을 의미한다

fig, ax = plt.subplots(1, 1, figsize=(12, 7))

idx = np.arange(len(group['male'].index))
width=0.35

ax.bar(idx-width/2, group['male'], 
       color='royalblue',
       width=width)

ax.bar(idx+width/2, group['female'], 
       color='tomato',
       width=width)

ax.set_xticks(idx)
ax.set_xticklabels(group['male'].index)
    
plt.show()

x축의 각 그래프 라벨을 정하려면 우선 set_xticks로 인덱스 넘버를 정해주고 이를 set_xticklabels를 이용해서 변경하는 절차를 거쳐야 한다.

그리고 추가적으로 label + legend를 달아 색에 대한 설명도 추가하면 좋습니다.

fig, ax = plt.subplots(1, 1, figsize=(12, 7))

idx = np.arange(len(group['male'].index))
width=0.35

ax.bar(idx-width/2, group['male'], 
       color='royalblue',
       width=width, label='Male')

ax.bar(idx+width/2, group['female'], 
       color='tomato',
       width=width, label='Female')

ax.set_xticks(idx)
ax.set_xticklabels(group['male'].index)
ax.legend()    
    
plt.show()

그렇다면 그룹이 N개 일때는 어떻게 하면 될까요?

그룹의 개수에 따라 x좌표는 다음과 같습니다.

  • 2개 : -1/2, +1/2

  • 3개 : -1, 0, +1 (-2/2, 0, +2/2)

  • 4개 : -3/2, -1/2, +1/2, +3/2

규칙이 보이시나요?

N12에서N12−\frac{N−1}{2}에서 \frac{N−1}{2}까지 분자에 2간격으로 커지는 것이 특징입니다.

그렇다면 index i(zero-index)에 대해서는 다음과 같이 x좌표를 계산할 수 있습니다.

x+N+1+2×i2×width x+\frac{−N+1+2×i}{2}×width

이번엔 인종/민족 그룹에 따른 Parental Level of Education Grouped Bar Plot으로 그려보겠습니다.

group = student.groupby('parental level of education')['race/ethnicity'].value_counts().sort_index()
group_list = sorted(student['race/ethnicity'].unique())
edu_lv = student['parental level of education'].unique()
fig, ax = plt.subplots(1, 1, figsize=(13, 7))

x = np.arange(len(group_list))
width=0.12

for idx, g in enumerate(edu_lv):
    ax.bar(x+(-len(edu_lv)+1+2*idx)*width/2, group[g], 
       width=width, label=g)

ax.set_xticks(x)
ax.set_xticklabels(group_list)
ax.legend()    
    
plt.show()

이 때 width는 전체가 1 이므로 개수가 n개이면 1/n 보다 작게해서 잘 나눠지면서 가독성도 좋게 할 수 있다. (1/n 으로 설정하면 꽉차보이기 때문) 그렇지만, matplotlib만을 이용할 때의 이야기이고 후에 다른 라이브러리를 배우게 되면 직접 width를 설정할 일은 없게된다.

3. 정확한 Bar Plot

3-1. Principle of Proportion Ink

성별에 따른 성적을 막대그래프로 비교해보겠습니다.

score = student.groupby('gender').mean().T
score

gender

female

male

math score

64.394515

70.479087

reading score

72.845992

67.178707

writing score

72.995781

65.041825

fig, axes = plt.subplots(1, 2, figsize=(15, 7))

idx = np.arange(len(score.index))
width=0.3

for ax in axes:
    ax.bar(idx-width/2, score['male'], 
           color='royalblue',
           width=width)

    ax.bar(idx+width/2, score['female'], 
           color='tomato',
           width=width)

    ax.set_xticks(idx)
    ax.set_xticklabels(score.index)

axes[0].set_ylim(60, 75)
    
plt.show()

정확한 정보량을 통한 비교를 원한다면 오른쪽이, 정보를 통한 강조(부각)가 중요하다면 왼쪽으로 시각화 할 수 있다.

비교를 위한다면 세로를 늘리는 게 더 좋을 수 있습니다.

fig, ax = plt.subplots(1, 1, figsize=(6, 10))

idx = np.arange(len(score.index))
width=0.3

ax.bar(idx-width/2, score['male'], 
       color='royalblue',
       width=width)

ax.bar(idx+width/2, score['female'], 
       color='tomato',
       width=width)

ax.set_xticks(idx)
ax.set_xticklabels(score.index)


    
plt.show()

3-2 데이터 정렬하기

student.head()

gender

race/ethnicity

parental level of education

lunch

test preparation course

math score

reading score

writing score

0

male

group D

high school

standard

none

95

92

86

1

female

group D

high school

standard

none

86

94

93

2

male

group A

some high school

standard

none

58

57

52

3

female

group E

associate's degree

standard

none

93

100

96

4

female

group C

high school

free/reduced

none

29

48

43

3-3. 적절한 공간 활용

다양한 공간 테크닉을 살펴보겠습니다.

대조군을 위해 2개의 같은 플롯을 그려보겠습니다.

  • X/Y axis Limit (.set_xlim(), .set_ylime())

  • Margins (.margins())

  • Gap (width)

  • Spines (.spines[spine].set_visible())

group_cnt = student['race/ethnicity'].value_counts().sort_index()

fig = plt.figure(figsize=(15, 7))

ax_basic = fig.add_subplot(1, 2, 1)
ax = fig.add_subplot(1, 2, 2)

ax_basic.bar(group_cnt.index, group_cnt)
ax.bar(group_cnt.index, group_cnt,
       width=0.7,
       edgecolor='black',
       linewidth=2,
       color='royalblue'
      )

ax.margins(0.1, 0.1)

for s in ['top', 'right']:
    ax.spines[s].set_visible(False)

plt.show()

3-4. 복잡함과 단순함

그리드나 텍스트를 추가해보며 어떤 게 더 좋을지 고민해보겠습니다.

group_cnt = student['race/ethnicity'].value_counts().sort_index()

fig, axes = plt.subplots(1, 2, figsize=(15, 7))

for ax in axes:
    ax.bar(group_cnt.index, group_cnt,
           width=0.7,
           edgecolor='black',
           linewidth=2,
           color='royalblue',
           zorder=10
          )

    ax.margins(0.1, 0.1)

    for s in ['top', 'right']:
        ax.spines[s].set_visible(False)

axes[1].grid(zorder=0)

for idx, value in zip(group_cnt.index, group_cnt):
    axes[1].text(idx, value+5, s=value,
                 ha='center', 
                 fontweight='bold'
                )
        
plt.show()

3-5. ETC

오차막대(errorbar)를 사용하여 편차 등의 정보를 추가해보겠습니다.

score_var = student.groupby('gender').std().T
score_var

gender

female

male

math score

15.357834

14.134272

reading score

14.532297

13.555754

writing score

15.000634

13.660167

fig, ax = plt.subplots(1, 1, figsize=(10, 10))

idx = np.arange(len(score.index))
width=0.3


ax.bar(idx-width/2, score['male'], 
       color='royalblue',
       width=width,
       label='Male',
       yerr=score_var['male'],
       capsize=10
      )

ax.bar(idx+width/2, score['female'], 
       color='tomato',
       width=width,
       label='Female',
       yerr=score_var['female'],
       capsize=10
      )

ax.set_xticks(idx)
ax.set_xticklabels(score.index)
ax.set_ylim(0, 100)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

ax.legend()
ax.set_title('Gender / Score', fontsize=20)
ax.set_xlabel('Subject', fontweight='bold')
ax.set_ylabel('Score', fontweight='bold')

plt.show()

Last updated

Was this helpful?