import pandas as pd
import numpy as np데이터 결합: 연결(Concat) 및 추가(Append)
다양한 데이터 소스를 결합하는 작업은 데이터 연구에서 가장 흥미로운 부분 중 하나입니다. 이런 작업은 두 데이터 세트를 단순히 연결하는 것부터, 중복 데이터를 처리하는 복잡한 데이터베이스 스타일의 조인(Join)과 병합(Merge)까지 그 범위가 매우 넓습니다. Pandas의 Series와 DataFrame은 이런 작업을 고려해 설계되었으며, 데이터 랭글링(Data Wrangling)을 빠르고 간단하게 처리할 수 있는 다양한 기능을 제공합니다.
여기서는 pd.concat 함수를 사용해 Series와 DataFrame을 간단히 연결하는 방법을 알아보겠습니다. 이후에는 Pandas에 구현된 더 정교한 병합과 조인 기능을 살펴보겠습니다.
먼저 필요한 라이브러리를 불러오며 시작하겠습니다.
예제에서 유용하게 쓸 수 있도록 특정 형식의 DataFrame을 만드는 함수를 정의하겠습니다.
def make_df(cols, ind):
"""Quickly make a DataFrame"""
data = {c: [str(c) + str(i) for i in ind] for c in cols}
return pd.DataFrame(data, ind)
# example DataFrame
make_df("ABC", range(3))| A | B | C | |
|---|---|---|---|
| 0 | A0 | B0 | C0 |
| 1 | A1 | B1 | C1 |
| 2 | A2 | B2 | C2 |
또한 여러 DataFrame을 나란히 표시하는 클래스도 만들어 보겠습니다. 이 코드는 IPython/Jupyter에서 객체를 풍부하게 표현할 때 사용하는 _repr_html_ 메서드를 활용합니다.
class display(object):
"""Display HTML representation of multiple objects"""
template = """<div style="float: left; padding: 10px;">
<p style='font-family:"Courier New", Courier, monospace'>{0}</p>{1}
</div>"""
def __init__(self, *args):
self.args = args
def _repr_html_(self):
return "\n".join(
self.template.format(a, eval(a)._repr_html_()) for a in self.args
)
def __repr__(self):
return "\n\n".join(a + "\n" + repr(eval(a)) for a in self.args)이어지는 내용을 보면 이 클래스의 용도를 더 확실히 이해할 수 있을 것입니다.
복습: NumPy 배열 연결
Series와 DataFrame 객체의 연결은 NumPy 배열을 연결하는 방식과 비슷합니다. NumPy 배열의 기본에서 살펴본 것처럼 np.concatenate 함수를 사용하면 두 개 이상의 배열을 하나로 합칠 수 있습니다.
x = [1, 2, 3]
y = [4, 5, 6]
z = [7, 8, 9]
np.concatenate([x, y, z])array([1, 2, 3, 4, 5, 6, 7, 8, 9])
첫 번째 인수는 연결할 배열 리스트나 튜플입니다. 다차원 배열의 경우에는 axis 키워드를 사용해 연결할 축을 지정합니다.
x = [[1, 2], [3, 4]]
np.concatenate([x, x], axis=1)array([[1, 2, 1, 2],
[3, 4, 3, 4]])
pd.concat을 활용한 간단한 연결
pd.concat 함수는 np.concatenate와 구문은 비슷하지만, 뒤에서 살펴볼 다양한 옵션을 제공합니다.
# Pandas v1.3.5의 pd.concat 함수 정의
pd.concat(objs, axis=0, join='outer', ignore_index=False, keys=None,
levels=None, names=None, verify_integrity=False,
sort=False, copy=True)np.concatenate가 일반 배열을 연결하듯, pd.concat은 Series나 DataFrame 객체를 간단히 연결할 때 사용합니다.
ser1 = pd.Series(["A", "B", "C"], index=[1, 2, 3])
ser2 = pd.Series(["D", "E", "F"], index=[4, 5, 6])
pd.concat([ser1, ser2])1 A
2 B
3 C
4 D
5 E
6 F
dtype: object
DataFrame과 같은 고차원 객체도 동일한 방식으로 연결합니다.
df1 = make_df("AB", [1, 2])
df2 = make_df("AB", [3, 4])
display("df1", "df2", "pd.concat([df1, df2])")df1
| A | B | |
|---|---|---|
| 1 | A1 | B1 |
| 2 | A2 | B2 |
df2
| A | B | |
|---|---|---|
| 3 | A3 | B3 |
| 4 | A4 | B4 |
pd.concat([df1, df2])
| A | B | |
|---|---|---|
| 1 | A1 | B1 |
| 2 | A2 | B2 |
| 3 | A3 | B3 |
| 4 | A4 | B4 |
는 DataFrame 내에서 행 단위(axis=0)로 연결됩니다. np.concatenate와 마찬가지로 pd.concat에서도 연결할 축을 지정합니다. 다음 예제를 살펴보겠습니다.
df3 = make_df("AB", [0, 1])
df4 = make_df("CD", [0, 1])
display("df3", "df4", "pd.concat([df3, df4], axis='columns')")df3
| A | B | |
|---|---|---|
| 0 | A0 | B0 |
| 1 | A1 | B1 |
df4
| C | D | |
|---|---|---|
| 0 | C0 | D0 |
| 1 | C1 | D1 |
pd.concat([df3, df4], axis='columns')
| A | B | C | D | |
|---|---|---|---|---|
| 0 | A0 | B0 | C0 | D0 |
| 1 | A1 | B1 | C1 | D1 |
axis=1을 지정해도 결과는 같지만, 여기서는 조금 더 직관적인 axis='columns'를 사용했습니다.
중복 인덱스
np.concatenate와 pd.concat의 중요한 차이점 중 하나는 Pandas의 경우 연결 시 중복된 색인이 생기더라도 기존의 인덱스를 그대로 유지한다는 점입니다. 다음 예제를 확인해 보세요.
x = make_df("AB", [0, 1])
y = make_df("AB", [2, 3])
y.index = x.index # make indices match
display("x", "y", "pd.concat([x, y])")x
| A | B | |
|---|---|---|
| 0 | A0 | B0 |
| 1 | A1 | B1 |
y
| A | B | |
|---|---|---|
| 0 | A2 | B2 |
| 1 | A3 | B3 |
pd.concat([x, y])
| A | B | |
|---|---|---|
| 0 | A0 | B0 |
| 1 | A1 | B1 |
| 0 | A2 | B2 |
| 1 | A3 | B3 |
결과를 보면 인덱스가 중복되어 있습니다. DataFrame에서 중복 인덱스가 허용되긴 하지만, 실제로는 바람직하지 않은 경우가 많습니다. pd.concat은 이를 처리하기 위한 몇 가지 방법을 제공합니다.
중복 인덱스 확인(Error Handling)
pd.concat 결과를 생성할 때 인덱스가 겹치지 않는지 확인하고 싶다면 verify_integrity 플래그를 사용하면 됩니다. 이 값을 True로 설정하면 중복된 인덱스가 있을 때 예외가 발생합니다. 이해를 돕기 위해 오류 메시지를 가로채서 출력하는 예제를 살펴보겠습니다.
try:
pd.concat([x, y], verify_integrity=True)
except ValueError as e:
print("ValueError:", e)ValueError: Indexes have overlapping values: Int64Index([0, 1], dtype='int64')
인덱스 무시
인덱스 자체가 별 의미가 없다면 이를 무시하고 새로 생성하는 것이 더 효율적일 수 있습니다. 이때는 ignore_index 플래그를 사용합니다. 이 값을 True로 설정하면 연결 과정에서 기존 인덱스를 버리고 새로운 정수형 인덱스를 생성합니다.
display("x", "y", "pd.concat([x, y], ignore_index=True)")x
| A | B | |
|---|---|---|
| 0 | A0 | B0 |
| 1 | A1 | B1 |
y
| A | B | |
|---|---|---|
| 0 | A2 | B2 |
| 1 | A3 | B3 |
pd.concat([x, y], ignore_index=True)
| A | B | |
|---|---|---|
| 0 | A0 | B0 |
| 1 | A1 | B1 |
| 2 | A2 | B2 |
| 3 | A3 | B3 |
다중 인덱스(MultiIndex) 키 추가
keys 옵션을 사용해 데이터 소스별로 레이블을 지정할 수도 있습니다. 이렇게 하면 계층적 인덱스를 가진 데이터를 얻을 수 있습니다.
display("x", "y", "pd.concat([x, y], keys=['x', 'y'])")x
| A | B | |
|---|---|---|
| 0 | A0 | B0 |
| 1 | A1 | B1 |
y
| A | B | |
|---|---|---|
| 0 | A2 | B2 |
| 1 | A3 | B3 |
pd.concat([x, y], keys=['x', 'y'])
| A | B | ||
|---|---|---|---|
| x | 0 | A0 | B0 |
| 1 | A1 | B1 | |
| y | 0 | A2 | B2 |
| 1 | A3 | B3 |
계층적 인덱싱에서 살펴본 도구들을 사용하면, 이런 다중 인덱스 DataFrame을 원하는 형태로 손쉽게 변환합니다.
조인(Join)을 활용한 연결
지금까지는 열 이름이 서로 같은 DataFrame들을 합치는 경우를 주로 보았습니다. 하지만 실제로는 소스마다 열 이름이 다른 경우가 많습니다. pd.concat은 이런 상황에서도 여러 옵션을 제공합니다. 일부 열만 공통으로 가진 다음 두 DataFrame을 연결하는 상황을 가정해 보겠습니다.
df5 = make_df("ABC", [1, 2])
df6 = make_df("BCD", [3, 4])
display("df5", "df6", "pd.concat([df5, df6])")df5
| A | B | C | |
|---|---|---|---|
| 1 | A1 | B1 | C1 |
| 2 | A2 | B2 | C2 |
df6
| B | C | D | |
|---|---|---|---|
| 3 | B3 | C3 | D3 |
| 4 | B4 | C4 | D4 |
pd.concat([df5, df6])
| A | B | C | D | |
|---|---|---|---|---|
| 1 | A1 | B1 | C1 | NaN |
| 2 | A2 | B2 | C2 | NaN |
| 3 | NaN | B3 | C3 | D3 |
| 4 | NaN | B4 | C4 | D4 |
는 데이터가 없는 부분은 NA 값으로 채워집니다. 이를 바꾸려면 concat 함수의 join 매개변수를 조정하면 됩니다. 기본값은 열들의 합집합인 join='outer'이지만, join='inner'를 사용하면 열들의 교집합만 남길 수 있습니다.
display("df5", "df6", "pd.concat([df5, df6], join='inner')")df5
| A | B | C | |
|---|---|---|---|
| 1 | A1 | B1 | C1 |
| 2 | A2 | B2 | C2 |
df6
| B | C | D | |
|---|---|---|---|
| 3 | B3 | C3 | D3 |
| 4 | B4 | C4 | D4 |
pd.concat([df5, df6], join='inner')
| B | C | |
|---|---|---|
| 1 | B1 | C1 |
| 2 | B2 | C2 |
| 3 | B3 | C3 |
| 4 | B4 | C4 |
연결하기 전에 reindex 메서드를 사용하면, 어떤 열을 남길지 더 세밀하게 제어할 수 있어 유용합니다.
pd.concat([df5, df6.reindex(df5.columns, axis=1)])| A | B | C | |
|---|---|---|---|
| 1 | A1 | B1 | C1 |
| 2 | A2 | B2 | C2 |
| 3 | NaN | B3 | C3 |
| 4 | NaN | B4 | C4 |
append() 메서드
배열을 직접 연결하는 작업이 매우 빈번하기 때문에, Series와 DataFrame 객체는 append 메서드라는 간결한 수단을 제공합니다. 예를 들어 pd.concat([df1, df2]) 대신 df1.append(df2)를 사용합니다.
display("df1", "df2", "df1.append(df2)")df1
| A | B | |
|---|---|---|
| 1 | A1 | B1 |
| 2 | A2 | B2 |
df2
| A | B | |
|---|---|---|
| 3 | A3 | B3 |
| 4 | A4 | B4 |
df1.append(df2)
| A | B | |
|---|---|---|
| 1 | A1 | B1 |
| 2 | A2 | B2 |
| 3 | A3 | B3 |
| 4 | A4 | B4 |
여기서 주의할 점은 Python 리스트의 append나 extend 메서드와 달리, Pandas의 append는 원본 객체를 수정하지 않는다는 것입니다. 대신 결합된 데이터로 새로운 객체를 생성합니다. 이 방식은 새로운 인덱스와 데이터 버퍼를 매번 생성해야 하므로 아주 효율적이지는 않습니다. 따라서 여러 번 ‘추가’ 작업을 반복해야 한다면, DataFrame 객체들을 리스트에 모아 두었다가 concat 함수를 한 번만 호출하는 편이 훨씬 좋습니다.
다음 장에서는 여러 소스의 데이터를 결합하는 훨씬 강력한 도구인 pd.merge 함수를 통해 데이터베이스 스타일의 병합과 조인 방법을 살펴보겠습니다. concat, append 및 관련 기능에 대한 더 자세한 내용은 Pandas 문서의 “Merge, join, concatenate and compare” 섹션을 참고하세요.