본문 바로가기
데이터 분석/시각화

[바 차트(Bar chart)] 5. Matplotlib을 이용하여 스택 바 차트(Stacked bar chart) 그리기

by 부자 꽁냥이 2020. 6. 25.

안녕하세요. 꽁냥이에요~

 

이번 포스팅에서는 바 차트 5편으로 Matplotlib을 이용하여 스택 바 차트(Stacked bar chart)를 그리는 방법에 대해서 알아볼 거예요. 바 차트 1~4편에 대한 내용은 아래 링크를 참고하세요.

 

1. Matplotlib을 이용하여 바 차트, 수평 바 차트 그리기 
2. Matplotlib을 이용하여 바 차트 꾸미기 
3. Matplotlib 바 차트 번외 - 막대에 그라데이션 적용하기

4. Matplotlib을 이용하여 그룹 바 차트(Grouped bar chart) 그리기

 

먼저 다음의 상황을 보겠습니다.


꽁냥이 : 팀장님!! 요청하신 4개 영업점 매출을 분기별로 비교할 수 있는 자료 준비했습니다.

 

나팀장 : 수고했어요. 흠.. 어디 보자... 꽁냥씨?

 

꽁냥이 : 네?

 

나팀장 : 이거 말고 매출 구성비를 볼 수 있게 스택 바 차트로 그려주세요.

 

꽁냥이 : 네 알겠습니다.

 

꽁냥이는 오늘도 막차를 놓쳤습니다.


위에서 꽁냥이가 해야할 일은 스택 바 차트를 그리는 것이에요. 스택 바 차트는 2개의 카테고리에 대하여 각종 통계치를 확인하고 싶을 때 사용하는 그래프입니다. 저번 포스팅에서 다루었던 그룹 바 차트가 제공하는 정보와 매우 유사합니다. 꽁냥이가 생각하는 차이점은 스택 바 차트는 통계치(여기서는 매출)를 구성하는 비율에 좀 더 포커싱 되어 있고 그룹 바 차트는 통계치의 크기를 비교하는데 포커싱이 맞춰져 있다는 거예요.


 1. 목표

꽁냥이의 목표는 4개 영업점(A, B, C, D)의 분기별 매출을 다음과 같이 정리할 거예요.

 


 2. 데이터 준비

스택 바 차트로 그려야 할 데이터는 다음과 같습니다.

Quarter A B C D
1Q 300 400 250 100
2Q 100 200 500 200
3Q 300 400 200 300
4Q 250 100 300 400

아래 코드를 이용해서 데이터를 만들어볼게요.

import pandas as pd
### 데이터 
df = pd.DataFrame()
df['Quarter'] = ['1Q','2Q','3Q','4Q']
df['A'] = [300,100,300,250]
df['B'] = [400,200,400,100]
df['C'] = [250,500,200,300]
df['D'] = [100,200,300,400]

 3. 핵심 전략

꽁냥이의 전략은 다음과 같아요.


1. 메인 카테고리와 서브 카테고리를 정한다.

2. 서브 카테고리에 대응하는 막대기를 그린다.

3. 테두리 형식 지정


1) 메인 카테고리와 서브 카테고리 지정

 

먼저 x축에 표시할 메인 카테고리를 지정합니다. 여기서는 '분기'가 메인 카테고리입니다. 그리고 서브 카테고리를 지정하고 이에 대응하는 막대기를 그릴 거예요. 여기서는 'A, B, C, D' 즉, 영업점이 서브 카테고리입니다.

 

2) 서브 카테고리에 대응하는 막대기 그리기

 

다음으로 서브 카테고리에 대응하는 막대기를 그릴 거예요. 이를 위해선 다음의 4가지를 알아야 해요.


막대기 폭의 중심 x좌표

막대기 바닥 y 좌표

막대기 폭

막대기 높이


아래 그림이 보여주는 것은 위의 4가지 정보를 이용하여 서브 카테고리 'B'에 해당하는 막대기를 그리는 과정입니다.

 

3) 테두리 형식 지정

 

꽁냥이는 추가적으로 테두리에 대해서 생각해봤어요 스택 바 차트에서 테두리 형식은 '바깥쪽 테두리''모든 테두리'로 나눌 수 있어요. 아래 그림은 2가지 테두리 형식을 나타낸 거예요.

 

바깥 쪽 테두리는 말 그대로 바깥쪽에만 테두리를 그린다는 거고요. 모든 테두리는 바깥쪽과 안쪽 테두리 모두 그린다는 거예요.

 

테두리를 그릴 때 모든 테두리는 모든 막대기에 테두리를 적용하면 끝이기 때문에 쉽게 해결이 되지요. 문제가 되는 건 바로 바깥쪽 테두리인데요. 왜냐하면 안쪽 테두리 부분을 처리해야 하기 때문이죠. 

 

바깥쪽 테두리를 그릴 때 여러 가지 방법이 있겠지만 꽁냥이가 선택한 방법은 다음과 같아요.

 


 4. 스택 바 차트 그리기

이제 전략을 세웠으니 코드로 구현해봐야겠죠? 꽁냥이가 스택 바 차트를 그리는 함수를 만들어 봤어요~

def draw_stacked_barchart(df,main_category,sub_category,bar_width = 0.5,fig_width=10,fig_height=10,\
                        bar_type='vertical', is_edge=None, config_bar={}):
    '''
    Description :
    스택 바 차트를 그려주는 함수입니다. 
    
    Arguments :
    df = 메인 카테고리와 서브 카테고리로 이루어진 데이터, pd.DataFrame 객체여야 한다.
    main_category = 메인 카테고리 변수를 나타내는 문자열
    sub_category = 서브 카테고리 변수를 모아 놓은 리스트
    bar_width = 막대기 폭
    fig_width = 캔버스 폭
    fig_height = 캔버스 높이
    is_edge = 테두리 형식을 결정. 'all' 또는 'outer'을 가질 수 있으며
              'all'는 테두리를 바깥쪽과 안쪽 모두 표시
              'outer'는 테두리를 바깥쪽만 표시
              단 is_edge가 None이 아닌경우에는 반드시 config_bar['edgecolor']를 지정해야한다.
              
    bar_type = 'vertical' 또는 'horizontal'값을 가질 수 있으며
               'vertical'은 수직 바 차트를 'horizontal'은 수평 바 차트를 그린다.
    config_bar = 바 차트를 꾸미기 위한 옵션. 딕셔너리 형태로 넣어줘야 한다.
    
    Return : 
    스택 바 차트 출력
    '''
    ## Arguments 체크
    if not isinstance(main_category,str):
        print(f'main_category인자의 타입은 {type(main_category)}가 아니고 문자열 입니다.')
        return
    if not main_category in df.columns:
        print(f'데이터가 {main_category} 칼럼을 포함하고 있지 않습니다.')
        return
    if not set(sub_category).issubset(set(df.columns)):
        print(f'{set(sub_category)-set(df.columns)}가 데이터에 없습니다.')
        return
    if bar_width > 1 or bar_width < 0:
        print(f'bar_width인자는 0~1 값을 가집니다.')
        return
    if isinstance(bar_type,str):
        if not bar_type in ['vertical','horizontal']:
            print(f'bar_type인자에는 "vertical"과 "horizontal"만 허용됩니다.')
            return
    else:
        print(f'bar_type인자의 타입은 {type(bar_type)}가 아니고 문자열 입니다.')
        return
    if is_edge:
        if isinstance(is_edge,str):
            if not is_edge in ['outer','all']:
                print(f'is_edge인자에는 "outer"와 "all"만 허용됩니다.')
                return
            else:
                if not config_bar or 'edgecolor' not in config_bar.keys():
                    print('is_edge인자를 넣어준 경우에는 반드시 config_bar에 edgecolor키와 그에 해당하는 값을 넣어주세요')
                    return
        else:
            print(f'is_edge인자의 타입은 {type(is_edge)}가 아니고 문자열 입니다.')
            return
            
    ## 필요모듈 출력
    import matplotlib.pyplot as plt
    import numpy as np
    import seaborn as sns
    %matplotlib inline
    
    num_sub_category = len(sub_category) ## 서브 카테고리 개수
    fig = plt.figure(figsize=(fig_width,fig_height)) ## 캔버스 생성
    fig.set_facecolor('white') ## 캔버스 배경색 설정
    ax = fig.add_subplot() ## 그림이 그려질 축을 생성

    tick_label = list(df[main_category].unique()) ## 메인 카테고리 라벨 생성
    tick_number = len(tick_label) ## 메인 카테고리 눈금 개수
    tick_coord = np.arange(tick_number) ## 메인 카테고리 x좌표 = 막대기 폭의 중심 x좌표
    colors = sns.color_palette('hls',num_sub_category) ## 막대기 색상 지정

    config_tick = dict()
    config_tick['ticks'] = tick_coord ## 메인 카테고리 라벨 x좌표
    config_tick['labels'] = tick_label ## 메인 카테고리 라벨

    if is_edge == 'outer': ## 바깥쪽 테두리인 경우 config_bar에서 edgecolor를 지운다.
        edge_color = config_bar['edgecolor']
        del config_bar['edgecolor']

    if bar_type == 'vertical': ## 수직 바 차트를 그린다.
        plt.xticks(**config_tick) ## x축 눈금 라벨 생성
        bottom = np.zeros_like(tick_coord) ## 막대기 아래쪽 y좌표 초기값
        for i in range(num_sub_category):
            if is_edge == 'outer': 
                bar = ax.bar(tick_coord, df[sub_category[i]], \
                       bar_width, label=sub_category[i], bottom = bottom, \
                       color=colors[i], **config_bar) ## 바 차트 생성
                for b in bar:
                    x_left = b.get_x() ## 막대기 좌측 하단 x좌표
                    x_right = x_left+bar_width ## 막대기 우측 하단 x좌표
                    y_bottom = b.get_y() ## 막대기 아래쪽 y좌표
                    y_top = y_bottom+b.get_height() ## 막대기 위쪽 y 좌표
                    ## 양쪽 세로 테두리를 그린다.
                    ax.plot([x_left,x_left],[y_bottom,y_top],color=edge_color) 
                    ax.plot([x_right,x_right],[y_bottom,y_top],color=edge_color)
                    if i == num_sub_category-1: ## 마지막에 세로 테두리와 위쪽 테두리를 그린다.
                        ax.plot([x_left,x_right],[y_top,y_top],color=edge_color)
            else:
                bar = ax.bar(tick_coord, df[sub_category[i]], \
                       bar_width, label=sub_category[i], bottom = bottom, \
                       color=colors[i], **config_bar)
            bottom += df[sub_category[i]] ## 막대기 아래쪽 y좌표 업데이트한다.
        plt.legend() ##범례 생성
        plt.show()
    else:
        plt.yticks(**config_tick) ## y축 눈금 라벨 생성
        left = np.zeros_like(tick_coord) ## 좌측 x좌표 초기값
        for i in range(num_sub_category):
            if is_edge == 'outer':
                bar = ax.barh(tick_coord, df[sub_category[i]], \
                       bar_width, label=sub_category[i], left = left, \
                       color=colors[i], **config_bar) ## 바 차트 생성
                for b in bar:
                    y_bottom = b.get_y() ## 막대기 아래쪽 y 좌표
                    y_top = y_bottom + bar_width ## 막대기 위쪽 y좌표
                    x_left = b.get_x() ## 막대기 좌측 하단 x좌표
                    x_right = x_left+b.get_width() ## 막대기 우측 하단 x 좌표
                    ## 위아래 가로 테두리를 그린다.
                    ax.plot([x_left, x_right],[y_bottom, y_bottom],color=edge_color)
                    ax.plot([x_left, x_right],[y_top, y_top],color=edge_color)
                    if i == num_sub_category-1: ## 마지막에 위아래 가로 테두리와 오른쪽 세로 테두리를 그린다.
                        ax.plot([x_right, x_right],[y_top, y_bottom],color=edge_color)
            else:
                ax.barh(tick_coord, df[sub_category[i]], \
                       bar_width, label=sub_category[i], left = left, \
                       color=colors[i], **config_bar)
            left += df[sub_category[i]] ## 좌측 x좌표 업데이트
        plt.legend() ##범례 생성
        plt.show()

앞서 소개한 핵심 전략에 맞춰 포인트만 설명할게요. 여기서 설명하지 않는 부분은 주석을 참고하세요.

 

1) 메인 카테고리와 서브 카테고리 지정

 

line 1

메인 카테고리는 main_category, 서브 카테고리는 sub_category에 지정합니다.

내용

 

2) 서브 카테고리에 대응하는 막대기 그리기

 

line 72

막대기 가로 중심 x좌표를 지정합니다. 

 

line 85

막대기의 아래쪽 y좌표를 모두 0으로 초기화합니다.

 

line 89

바 차트 생성 시 아래쪽 시작 부분은 bottom인자를 통하여 지정합니다.

 

line 105

막대기 아래쪽 y좌표를 업데이트합니다. 여기서 높이는 서브 카테고리에 해당하는 막대기의 높이가 됩니다.

 

3) 테두리 형식 지정

 

line 104

모든 테두리를 그리는 경우는 config_bar에서 edgecolor 키를 생성하고 그에 대응하는 값을 입력하면 됩니다(지금은 이해가 안되더라도 아래 실행 코드를 보면 이해할 수 있을 거예요).

 

line 97~98

마지막 서브 카테고리 전까지는 양쪽 세로 테두리만 그립니다.

 

line 99~100

마지막 서브 카테고리에서는 양쪽 세로 테두리와 더불어 위쪽 테두리도 같이 그립니다.

 

수평 바 차트의 경우, 수직 바 차트를 생성하는 방법과 매우 유사합니다. 차이점은 메인 카테고리의 눈금이 y축에 표시된다는 점, bar 메서드 대신 barh 메서드를 사용한다는 점, 막대기 왼쪽에서 옆으로 붙여나가는 것입니다.

 

자~ 그럼 아래 코드를 실행해서 막대기가 잘 그려지는지 볼까요?

main_category = 'Quarter' ## 메인 카테고리
sub_category = ['A','B','C','D'] ## 서브 카테고리
## 수직 스택 바 차트 -- 기본
draw_stacked_barchart(df,main_category,sub_category, bar_width=0.5, fig_width=10, fig_height=10,\
                        bar_type='vertical')

실행 결과

와우~ 잘 나왔네요. 이번엔 수평으로도 그려볼까요? 아래 코드를 실행해주세요.

main_category = 'Quarter' ## 메인 카테고리
sub_category = ['A','B','C','D'] ## 서브 카테고리
## 수평 스택 바 차트 -- 기본
draw_stacked_barchart(df,main_category,sub_category, bar_width=0.5, fig_width=10, fig_height=10,\
                        bar_type='horizontal')

실행 결과

역시 잘 나온 것을 확인할 수 있습니다. 이번엔 테두리를 넣어볼게요. 먼저 모든 테두리를 확인해볼게요.

모든 테두리는 is_edge인자에 'all'을 넣어주시면 됩니다. 이때 유의할 점은 반드시 config_bar에 edgecolor 키를 생성하고 그에 대응하는 값을 넣어줘야 합니다(line 5). 

main_category = 'Quarter' ## 메인 카테고리
sub_category = ['A','B','C','D'] ## 서브 카테고리
## 수직 스택 바 차트 -- 모든 테두리
draw_stacked_barchart(df,main_category,sub_category, bar_width=0.5, fig_width=10, fig_height=10,\
                        bar_type='vertical',is_edge='all',config_bar={'edgecolor':'black'})

실행 결과

예상한 대로 잘 나왔어요~

마지막으로 테두리를 바깥쪽으로만 그려볼게요. 여기서는 수평으로 그리는 것과 막대기 색상을 조금 투명하게 해 봤어요.

 

 

실행 결과

와우~~ 예쁘게 잘 나왔어요~ ^0^.

 

이번 포스팅에서는 스택 바 차트를 그리는 방법에 대해서 알아보았습니다. 여러분에게 도움이 됐으면 정말 좋겠어요. 

 

궁금한 점, 잘못된 점, 그 밖에 하고 싶은 말은 댓글로 남겨주시면 감사해요~


이번 포스팅을 마지막으로 바 차트에 대한 내용은 마무리 지으려고 해요. 

여기까지 쓰면서 많이 힘든 것도 사실이지만 댓글로 응원해주시는분이 있어서 기분도 좋았고요. 또한 제가 쓴 글을 보면서 혼자 뿌듯해하기도 했네요. 다음 포스팅 내용도 열심히 준비해볼게요. 기대해주세요.

 

지금까지 꽁냥이의 글 읽어주셔서 감사합니다.

 


댓글


맨 위로