데이터 결합: 연결(Concat) 및 추가(Append)

다양한 데이터 소스를 결합하는 작업은 데이터 연구에서 가장 흥미로운 부분 중 하나입니다. 이런 작업은 두 데이터 세트를 단순히 연결하는 것부터, 중복 데이터를 처리하는 복잡한 데이터베이스 스타일의 조인(Join)과 병합(Merge)까지 그 범위가 매우 넓습니다. Pandas의 SeriesDataFrame은 이런 작업을 고려해 설계되었으며, 데이터 랭글링(Data Wrangling)을 빠르고 간단하게 처리할 수 있는 다양한 기능을 제공합니다.

여기서는 pd.concat 함수를 사용해 SeriesDataFrame을 간단히 연결하는 방법을 알아보겠습니다. 이후에는 Pandas에 구현된 더 정교한 병합과 조인 기능을 살펴보겠습니다.

먼저 필요한 라이브러리를 불러오며 시작하겠습니다.

import pandas as pd
import numpy as np

예제에서 유용하게 쓸 수 있도록 특정 형식의 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 배열 연결

SeriesDataFrame 객체의 연결은 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.concatSeriesDataFrame 객체를 간단히 연결할 때 사용합니다.

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.concatenatepd.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() 메서드

배열을 직접 연결하는 작업이 매우 빈번하기 때문에, SeriesDataFrame 객체는 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 리스트의 appendextend 메서드와 달리, Pandas의 append는 원본 객체를 수정하지 않는다는 것입니다. 대신 결합된 데이터로 새로운 객체를 생성합니다. 이 방식은 새로운 인덱스와 데이터 버퍼를 매번 생성해야 하므로 아주 효율적이지는 않습니다. 따라서 여러 번 ‘추가’ 작업을 반복해야 한다면, DataFrame 객체들을 리스트에 모아 두었다가 concat 함수를 한 번만 호출하는 편이 훨씬 좋습니다.

다음 장에서는 여러 소스의 데이터를 결합하는 훨씬 강력한 도구인 pd.merge 함수를 통해 데이터베이스 스타일의 병합과 조인 방법을 살펴보겠습니다. concat, append 및 관련 기능에 대한 더 자세한 내용은 Pandas 문서의 “Merge, join, concatenate and compare” 섹션을 참고하세요.