파이썬 데이터 분석 #5 그룹과 결합: groupby, pivot_table, merge

7 분 소요

데이터 분석에서 받는 질문은 결국 비슷한 형태로 수렴합니다. “지점별 매출이 얼마인가”, “월별로 어떻게 변했는가”, “지점별 월별로 교차해서 보면 어떤가” 같은 질문입니다. 행 하나하나가 아니라 묶음 단위의 요약을 묻는 질문이고, pandas에서 여기에 답하는 도구가 groupbypivot_table입니다. 답에 필요한 정보가 표 두 개에 나뉘어 있다면 merge로 먼저 합쳐야 합니다. #4 변형과 결측치에서 데이터를 깨끗하게 만들었으니, 이번 글에서는 그 데이터를 묶고 합쳐서 질문에 답하는 단계를 다루겠습니다.

  • #1 시작
  • #2 불러오기와 탐색
  • #3 선택과 필터
  • #4 변형과 결측치
  • #5 그룹과 결합 ← 이번 글
  • #6 시각화
  • #7 polars 맛보기(마무리)

예제 데이터: 지점별 월별 매출 #

이번 글 전체에서 쓸 작은 매출 표를 만들겠습니다. 단위는 백만 원이고, 대구 지점은 3월 데이터가 아직 없습니다.

예제 데이터
import pandas as pd

sales = pd.DataFrame({
    "지점": ["서울", "서울", "서울", "부산", "부산", "부산", "대구", "대구"],
    "월":   ["1월", "2월", "3월", "1월", "2월", "3월", "1월", "2월"],
    "매출": [120, 135, 150, 90, 95, 110, 70, 80],
})

groupby의 사고 모델: 쪼개고, 적용하고, 합친다 #

groupby는 내부적으로 세 단계로 동작합니다. 키 값이 같은 행끼리 쪼개고(split), 각 묶음에 집계 함수를 적용하고(apply), 결과를 하나의 표로 합칩니다(combine). 코드는 한 줄이지만 머릿속에서는 이 세 단계를 그리는 것이 좋습니다.

단일 키 groupby
sales.groupby("지점")["매출"].sum()
# 지점
# 대구    150
# 부산    295
# 서울    405

"지점" 값이 같은 행끼리 묶고, 각 묶음의 매출 열에 sum을 적용한 결과입니다. 키를 리스트로 주면 다중 키 그룹이 되고, 결과 인덱스는 지점과 월의 2단(MultiIndex)이 됩니다. 이 형태가 불편하면 뒤에 .reset_index()를 붙여 평범한 열로 되돌릴 수 있습니다.

다중 키 groupby
sales.groupby(["지점", "월"])["매출"].sum()
# 지점  월
# 대구  1월     70
#      2월     80
# 부산  1월     90
# ...
# 서울  3월    150

agg: 여러 통계를 한 번에 #

합계만이 아니라 평균과 건수도 같이 보고 싶다면 agg에 함수 이름을 리스트로 넘깁니다.

agg 다중 집계
sales.groupby("지점")["매출"].agg(["sum", "mean", "count"])
#     sum        mean  count
# 지점
# 대구  150   75.000000      2
# 부산  295   98.333333      3
# 서울  405  135.000000      3

결과 열 이름을 직접 정하고 싶다면 sales.groupby("지점").agg(총매출=("매출", "sum"), 평균매출=("매출", "mean"))처럼 이름=("열", "함수") 형태를 씁니다. 보고서에 그대로 들어갈 열 이름이 나오므로 실무에서는 이쪽을 더 자주 쓰게 됩니다.

pivot_table: 엑셀 피벗 테이블과 같은 것 #

엑셀에서 피벗 테이블을 써 본 분이라면 pivot_table이 바로 그 기능입니다. 행 축과 열 축을 지정해 교차표를 만듭니다.

pivot_table
sales.pivot_table(index="지점", columns="월", values="매출", aggfunc="sum")
# 월      1월     2월     3월
# 지점
# 대구   70.0   80.0    NaN
# 부산   90.0   95.0  110.0
# 서울  120.0  135.0  150.0

내용물은 다중 키 groupby 결과와 같고, 차이는 모양입니다. groupby는 세로로 긴 결과를 주고, pivot_table은 두 번째 키를 열 방향으로 펼쳐 사람이 보기 좋은 격자를 만듭니다. 대구 지점의 3월 칸이 NaN으로 비어 있는 것도 한눈에 들어옵니다. 빈칸을 0으로 채우려면 fill_value=0 인자를 추가하면 됩니다. 요약하면 계산은 groupby, 사람에게 보여줄 교차표는 pivot_table이라고 구분하면 됩니다.

merge: 두 표를 옆으로 합치기 #

지점장 이름이 담긴 표가 따로 있고, 매출 표에 지점장 열을 붙이고 싶은 상황을 가정하겠습니다.

merge 기본(inner join)
managers = pd.DataFrame({
    "지점": ["서울", "부산", "광주"],
    "지점장": ["김민준", "이서연", "박지호"],
})

pd.merge(sales, managers, on="지점")
#    지점   월   매출  지점장
# 0  서울  1월  120  김민준
# 1  서울  2월  135  김민준
# ...
# 5  부산  3월  110  이서연

merge는 SQL의 JOIN과 같은 개념이고, 기본 동작은 inner join입니다. 양쪽 표에 모두 있는 키만 남기므로, 매출 표에만 있는 대구와 지점장 표에만 있는 광주가 결과에서 조용히 사라졌습니다. 8행이 6행이 된 것입니다. 왼쪽 표의 행을 전부 보존하려면 how="left"를 씁니다. pd.merge(sales, managers, on="지점", how="left")로 바꾸면 대구의 두 행이 살아남고 지점장 열은 NaN이 됩니다. 매출 데이터가 기준이고 지점장은 부가 정보라면 left가 맞는 선택입니다.

키가 중복되면 행이 늘어납니다. merge의 진짜 함정은 행이 사라지는 쪽보다 늘어나는 쪽입니다. 지점장 표에 서울이 두 번 들어 있다면, 서울 매출 행 하나마다 지점장 행 두 개가 각각 짝지어져 결과가 부풀어 오릅니다. 8행짜리 매출 표가 11행이 되는 식입니다. 이렇게 합친 표로 집계하면 서울 매출이 두 배로 잡힙니다. 이 함정을 코드로 막는 도구가 validate 인자입니다.

validate로 키 관계 검증
pd.merge(sales, managers, on="지점", how="left", validate="many_to_one")
# MergeError: Merge keys are not unique in right dataset; not a many-to-one merge

“매출 여러 행이 지점장 한 행에 대응한다"는 기대를 선언해 두면, 기대가 깨지는 순간 pandas가 에러로 알려줍니다. 잘못된 숫자가 보고서까지 흘러가는 것보다 에러가 훨씬 낫습니다.

concat: 같은 형식을 세로로 쌓기 #

merge가 열을 옆으로 붙이는 도구라면, concat은 같은 형식의 표를 세로로 쌓는 도구입니다. 월별로 파일이 따로 떨어져 있는 흔한 상황에서 씁니다.

월별 파일 합치기
from pathlib import Path

frames = [pd.read_csv(p) for p in sorted(Path("data").glob("sales_*.csv"))]
total = pd.concat(frames, ignore_index=True)

ignore_index=True는 각 파일이 갖고 있던 0, 1, 2 인덱스를 버리고 새로 매기는 옵션입니다. 폴더의 파일을 glob으로 모아 처리하는 패턴은 파이썬 자동화 #2 엑셀 자동화에서 다룬 취합 흐름과 같습니다. 그 글에서는 openpyxl로 셀을 직접 옮겼지만, 집계가 목적이라면 이렇게 pandas로 쌓는 쪽이 코드가 훨씬 짧습니다.

결합 후에는 행 수부터 확인 #

mergeconcat을 쓴 직후에는 행 수를 확인하는 습관을 들이는 것이 좋습니다. 결합은 조용히 틀리는 연산이기 때문입니다. 행이 사라져도, 두 배로 불어나도 에러 없이 결과가 나옵니다. assert 한 줄이면 충분합니다. left join 후 행 수가 원본과 다르면 키 중복을, concat 후 합계가 안 맞으면 빠진 파일을 의심하면 됩니다.

행 수 검증
assert len(merged) == len(sales)  # left join: 왼쪽 행 수가 보존되어야 합니다
assert len(total) == sum(len(f) for f in frames)  # concat: 쌓기 전 합과 일치해야 합니다

정리 #

이번 글에서 다룬 내용입니다.

  • groupby는 쪼개고, 적용하고, 합치는 세 단계로 동작하며, agg로 여러 통계를 한 번에 구합니다
  • pivot_table은 엑셀 피벗 테이블과 같은 교차표를 만들며, 계산 자체는 groupby와 같습니다
  • merge는 SQL JOIN에 대응하며, inner는 행을 줄이고 키 중복은 행을 불립니다. validate로 키 관계를 선언해 두면 안전합니다
  • concat은 같은 형식의 표를 세로로 쌓으며, 결합 직후에는 assert로 행 수를 확인합니다

다음 글(#6 시각화)에서는 이번 글에서 만든 집계 결과를 그림으로 옮기겠습니다. pandas의 plot 메서드와 matplotlib으로 막대그래프와 선 그래프를 그리고, 한글 폰트 문제를 해결하는 방법까지 다루겠습니다.

X