Skip to content
주제
Pandas
Pandas Concat: Python에서 DataFrame을 연결하는 방법

Pandas Concat: Python에서 DataFrame을 연결하는 방법

Updated on

실제 데이터는 단일 파일에 존재하는 경우가 거의 없습니다. 하나의 CSV에서 1월 매출을, 다른 CSV에서 2월 매출을, 세 번째에서 Q1 목표를 가져옵니다. 여러 웹 페이지를 스크래핑하여 별도의 DataFrame에 저장합니다. 대규모 데이터셋을 병렬 처리를 위해 분할한 후 조각들을 다시 조립해야 합니다. 모든 경우에 행을 잃거나, 열을 뒤섞거나, 인덱스를 손상시키지 않고 DataFrame을 결합하는 신뢰할 수 있는 방법이 필요합니다.

pandas concat 함수(pd.concat())는 이 작업을 위한 표준 도구입니다. DataFrame을 수직으로(행 추가) 또는 수평으로(열 추가) 쌓고, 서로 다른 열을 우아하게 처리하며, 단일 호출로 임의의 수의 DataFrame을 처리할 수 있습니다. 이 가이드는 노트북에 직접 붙여넣을 수 있는 실용적인 코드 예제와 함께 필요한 모든 매개변수를 다룹니다.

📚

pd.concat()의 기능 -- 기본 구문

pd.concat()은 DataFrame의 리스트(또는 딕셔너리)를 받아 지정된 축을 따라 결합합니다. 블록을 쌓는 것처럼 생각하세요 -- 수직으로 쌓으면 더 많은 행이 추가되고, 나란히 놓으면 더 많은 열이 추가됩니다.

import pandas as pd
 
result = pd.concat(
    objs,              # list or dict of DataFrames
    axis=0,            # 0 = vertical (rows), 1 = horizontal (columns)
    join='outer',      # 'outer' or 'inner'
    ignore_index=False,
    keys=None,
    sort=False,
    verify_integrity=False
)

주요 매개변수 한눈에 보기

매개변수설명기본값
objs연결할 DataFrame(또는 Series)의 리스트 또는 딕셔너리필수
axis0은 행 쌓기(수직), 1은 열 쌓기(수평)0
join'outer'는 모든 열 유지; 'inner'는 공통 열만 유지'outer'
ignore_indexTrue이면 결과의 인덱스를 0, 1, 2, ...로 재설정False
keys각 행이 어떤 원본 DataFrame에서 왔는지 식별하는 레이블None
sort비연결 축 정렬(axis=0의 경우 열 이름)False
verify_integrity결과에 중복 인덱스 값이 있으면 오류 발생False

모든 예제의 샘플 데이터

아래의 모든 예제에서 이 DataFrame들을 사용합니다:

import pandas as pd
 
df_jan = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [150, 200, 80],
    'revenue': [1500, 3000, 960]
})
 
df_feb = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [170, 180, 95],
    'revenue': [1700, 2700, 1140]
})
 
print(df_jan)
print(df_feb)

출력:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960

    product  units_sold  revenue
0    Widget         170     1700
1    Gadget         180     2700
2  Sprocket          95     1140

DataFrame 수직 연결 (axis=0)

수직 연결은 가장 일반적인 사용 사례입니다. 하나의 DataFrame을 다른 DataFrame 위에 쌓아 행을 추가합니다. 월별 파일, 배치 결과, 또는 동일한 열을 가진 여러 테이블에 분산된 데이터가 있을 때 사용합니다.

combined = pd.concat([df_jan, df_feb])
print(combined)

출력:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960
0    Widget         170     1700
1    Gadget         180     2700
2  Sprocket          95     1140

인덱스를 주목하세요: 두 DataFrame 모두 원래 인덱스 값(0, 1, 2)을 유지하므로, 결과에는 중복 인덱스 값이 있습니다. 이는 보통 원하는 결과가 아닙니다. 해결책은 다음에 다룰 ignore_index 매개변수입니다.

ignore_index 매개변수 -- 인덱스 재설정

ignore_index=True를 설정하면 원래 인덱스를 버리고 0부터 시작하는 새로운 순차 인덱스를 할당합니다:

combined = pd.concat([df_jan, df_feb], ignore_index=True)
print(combined)

출력:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960
3    Widget         170     1700
4    Gadget         180     2700
5  Sprocket          95     1140

사용 시기: 수직 연결 시 거의 항상 사용합니다. 인덱스가 의미 있는 정보(타임스탬프나 고유 ID 등)를 포함하지 않는 한, 나중의 혼동을 피하기 위해 재설정하세요.

keys 매개변수 -- 계층적 인덱스 생성

keys 매개변수는 각 행이 어떤 소스에서 왔는지 레이블링하는 수준을 인덱스에 추가합니다. 이렇게 하면 MultiIndex(계층적 인덱스)가 생성됩니다:

combined = pd.concat([df_jan, df_feb], keys=['January', 'February'])
print(combined)

출력:

               product  units_sold  revenue
January  0      Widget         150     1500
         1      Gadget         200     3000
         2    Sprocket          80      960
February 0      Widget         170     1700
         1      Gadget         180     2700
         2    Sprocket          95     1140

그런 다음 .loc를 사용하여 특정 소스의 데이터를 선택할 수 있습니다:

# Get only January data
jan_data = combined.loc['January']
print(jan_data)

출력:

    product  units_sold  revenue
0    Widget         150     1500
1    Gadget         200     3000
2  Sprocket          80      960

사용 시기: 각 행이 어떤 원본 DataFrame에 속하는지 추적해야 할 때 keys를 사용합니다 -- 예를 들어, 서로 다른 실험, 기간 또는 데이터 소스의 데이터입니다.

수평 연결 (axis=1) -- 나란히 배치

axis=1을 설정하면 DataFrame을 나란히 배치하여 열을 추가합니다. Pandas는 인덱스 값으로 행을 정렬합니다.

targets = pd.DataFrame({
    'target_units': [160, 190, 90],
    'target_revenue': [1600, 2850, 1080]
})
 
result = pd.concat([df_jan, targets], axis=1)
print(result)

출력:

    product  units_sold  revenue  target_units  target_revenue
0    Widget         150     1500           160            1600
1    Gadget         200     3000           190            2850
2  Sprocket          80      960            90            1080

이것은 두 DataFrame이 동일한 인덱스(0, 1, 2)를 공유하므로 깔끔하게 작동합니다. 인덱스가 맞지 않으면 불일치하는 행에 NaN 값이 채워집니다:

df_a = pd.DataFrame({'value_a': [10, 20, 30]}, index=[0, 1, 2])
df_b = pd.DataFrame({'value_b': [40, 50, 60]}, index=[1, 2, 3])
 
result = pd.concat([df_a, df_b], axis=1)
print(result)

출력:

   value_a  value_b
0     10.0      NaN
1     20.0     40.0
2     30.0     50.0
3      NaN     60.0

행 0에는 value_b가 없습니다(df_b에 일치하는 인덱스가 없으므로), 행 3에는 value_a가 없습니다(df_a에 일치하는 인덱스가 없으므로).

join 매개변수 -- Inner vs Outer

join 매개변수는 DataFrame이 서로 다른 열(axis=0의 경우) 또는 서로 다른 인덱스 값(axis=1의 경우)을 가질 때의 동작을 제어합니다.

outer join (기본값) -- 모든 것 유지

df_with_extra = pd.DataFrame({
    'product': ['Widget', 'Gadget'],
    'units_sold': [200, 250],
    'region': ['East', 'West']
})
 
result = pd.concat([df_jan, df_with_extra], join='outer', ignore_index=True)
print(result)

출력:

    product  units_sold  revenue region
0    Widget         150   1500.0    NaN
1    Gadget         200   3000.0    NaN
2  Sprocket          80    960.0    NaN
3    Widget         200      NaN   East
4    Gadget         250      NaN   West

두 DataFrame의 모든 열이 나타납니다. 결측값은 NaN으로 채워집니다.

inner join -- 공통 열만 유지

result = pd.concat([df_jan, df_with_extra], join='inner', ignore_index=True)
print(result)

출력:

   product  units_sold
0   Widget         150
1   Gadget         200
2 Sprocket          80
3   Widget         200
4   Gadget         250

양쪽 DataFrame에 존재하는 열만 유지됩니다. revenue 열(df_with_extra에 없음)과 region 열(df_jan에 없음) 모두 삭제됩니다.

inner join 사용 시기: NaN 값이 없는 깔끔한 결과를 원하고, 모든 DataFrame에 존재하지 않는 열을 잃어도 괜찮을 때 사용합니다.

DataFrame 리스트 연결

pd.concat()가 다른 결합 방법에 비해 가지는 가장 큰 장점 중 하나는 단일 호출로 임의의 수의 DataFrame을 처리할 수 있다는 것입니다. 이것은 루프에서 로드된 파일을 결합하는 표준 패턴입니다:

import pandas as pd
 
# Simulate loading monthly CSV files
months = {
    'Jan': {'product': ['Widget', 'Gadget'], 'units': [150, 200]},
    'Feb': {'product': ['Widget', 'Gadget'], 'units': [170, 180]},
    'Mar': {'product': ['Widget', 'Gadget'], 'units': [190, 210]},
}
 
dfs = []
for month, data in months.items():
    df = pd.DataFrame(data)
    df['month'] = month
    dfs.append(df)
 
all_data = pd.concat(dfs, ignore_index=True)
print(all_data)

출력:

  product  units month
0  Widget    150   Jan
1  Gadget    200   Jan
2  Widget    170   Feb
3  Gadget    180   Feb
4  Widget    190   Mar
5  Gadget    210   Mar

이 패턴 -- DataFrame 리스트를 구성한 다음 마지막에 한 번 concat 호출 -- 은 루프에서 하나씩 DataFrame을 추가하는 것보다 훨씬 빠릅니다. 각 append는 전체 복사본을 만들지만, 단일 pd.concat() 호출은 메모리를 한 번만 할당합니다.

concat vs merge vs append -- 비교 표

Pandas는 DataFrame을 결합하는 여러 방법을 제공합니다. 올바른 방법을 선택하는 것은 어떻게 결합하고 싶은지에 따라 달라집니다:

기능pd.concat()pd.merge()DataFrame.append()
주요 용도DataFrame 쌓기(행 또는 열)공유 열 값으로 조인(SQL처럼)DataFrame에 행 추가
입력 수한 번에 임의의 수한 번에 2개한 번에 2개
매칭 로직인덱스(또는 열 이름)로 정렬키 열 값으로 매칭열 이름으로 정렬
조인 유형outer, innerinner, left, right, outer, crossouter만
기본 동작outer join, 수직 쌓기inner join, 공유 열 기준outer join, 행 추가
최적 용도월별 파일 결합, 배치 결과, 동일 스키마 테이블관계형 조인(고객 + 주문)pandas 2.0부터 더 이상 사용 안 함
성능많은 DataFrame에 대해 빠름2테이블 조인에 최적화느림(매번 데이터 복사)

언제 어떤 것을 사용할까

  • pd.concat()를 사용 -- DataFrame이 동일한 구조(동일한 열)를 가지고 있고 쌓고 싶을 때. 인덱스로 정렬하는 수평 연결에도 사용합니다.
  • pd.merge()를 사용 -- 열 값을 기반으로 행을 매칭해야 할 때 -- 예를 들어 product_id로 판매 테이블과 제품 테이블을 조인하는 경우. 자세한 내용은 pandas merge 가이드를 참조하세요.
  • DataFrame.append()는 사용하지 마세요 -- pandas 1.4에서 더 이상 사용되지 않으며 pandas 2.0에서 제거되었습니다. 대신 pd.concat([df1, df2])를 사용하세요.

자주 발생하는 오류와 해결 방법

1. 열이 일치하지 않음

서로 다른 열 이름을 가진 DataFrame을 연결할 때, 기본 outer join은 결측값을 NaN으로 채웁니다. 예상하지 못했다면 열 이름을 확인하세요:

# Diagnose: compare column names
print(df1.columns.tolist())
print(df2.columns.tolist())
 
# Fix: rename columns to match before concatenating
df2 = df2.rename(columns={'sales': 'revenue', 'qty': 'units_sold'})
combined = pd.concat([df1, df2], ignore_index=True)

2. 연결 후 데이터 타입 불일치

한 DataFrame이 열을 int64로 저장하고 다른 DataFrame이 float64로 저장하면, pandas는 float로 업캐스트합니다. 더 심각하게, 하나가 문자열로 저장하면 object dtype 열이 됩니다:

# Check dtypes after concat
combined = pd.concat([df1, df2], ignore_index=True)
print(combined.dtypes)
 
# Fix: cast before concatenating
df2['units_sold'] = df2['units_sold'].astype(int)
combined = pd.concat([df1, df2], ignore_index=True)

3. 중복 인덱스 값

ignore_index=True 없이 수직 연결하면 원래 인덱스를 유지하여 중복 값이 발생합니다. 이는 .loc 조회에서 문제를 일으킵니다:

combined = pd.concat([df1, df2])
# combined.loc[0] returns TWO rows, not one
 
# Fix option 1: use ignore_index
combined = pd.concat([df1, df2], ignore_index=True)
 
# Fix option 2: use verify_integrity to catch the issue early
combined = pd.concat([df1, df2], verify_integrity=True)  # raises ValueError

4. 실수로 잘못된 축으로 연결

결과가 행의 2배가 아닌 열의 2배가 되었다면(또는 그 반대), axis 매개변수를 확인하세요:

# Wrong: this adds columns side by side
wrong = pd.concat([df1, df2], axis=1)
 
# Right: this stacks rows vertically
right = pd.concat([df1, df2], axis=0)

PyGWalker로 연결된 DataFrame 시각화하기

여러 소스에서 데이터를 연결한 후, 결과를 검증하고 결합된 데이터셋에서 패턴을 탐색해야 하는 경우가 많습니다. matplotlib이나 seaborn으로 수동 플롯 코드를 작성하는 대신, PyGWalker (opens in a new tab)를 사용할 수 있습니다 -- 모든 pandas DataFrame을 Jupyter Notebook 내에서 직접 인터랙티브한 Tableau 스타일의 시각적 탐색 인터페이스로 변환하는 오픈소스 Python 라이브러리입니다.

import pandas as pd
import pygwalker as pyg
 
# Combine monthly sales data
df_jan = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [150, 200, 80],
    'revenue': [1500, 3000, 960],
    'month': ['Jan', 'Jan', 'Jan']
})
 
df_feb = pd.DataFrame({
    'product': ['Widget', 'Gadget', 'Sprocket'],
    'units_sold': [170, 180, 95],
    'revenue': [1700, 2700, 1140],
    'month': ['Feb', 'Feb', 'Feb']
})
 
combined = pd.concat([df_jan, df_feb], ignore_index=True)
 
# Launch interactive visualization
walker = pyg.walk(combined)

PyGWalker를 사용하면 product를 x축에, revenue를 y축에 드래그한 다음 month로 분할하여 기간별 수익 추세를 즉시 비교할 수 있습니다 -- 차트 코드가 필요 없습니다. 필드를 드래그하는 것만으로 막대 차트, 산점도, 라인 차트 등을 만들 수 있습니다. 연결이 올바르게 작동했는지, 다른 소스의 데이터가 예상대로 정렬되는지 검증하는 데 특히 유용합니다.

pip install pygwalker로 PyGWalker를 설치하거나, Google Colab (opens in a new tab) 또는 Kaggle (opens in a new tab)에서 사용해 보세요.

FAQ

pandas concat과 merge의 차이점은 무엇인가요?

pd.concat()은 인덱스를 기준으로 DataFrame을 수직(행 추가) 또는 수평(열 추가)으로 쌓습니다. pd.merge()는 특정 열의 값을 매칭하여 두 DataFrame을 조인합니다(SQL JOIN처럼). DataFrame이 동일한 열을 가지고 행을 결합하고 싶을 때는 concat을 사용하세요. 공유 키 열을 기반으로 행을 매칭해야 할 때는 merge를 사용하세요.

pd.concat()은 원본 DataFrame을 수정하나요?

아니요. pd.concat()은 항상 새로운 DataFrame을 반환합니다. 원본 DataFrame은 변경되지 않습니다. 이는 작업이 데이터를 제자리에서 수정하는 대신 새로운 객체를 반환한다는 pandas 설계 원칙과 일치합니다.

서로 다른 열을 가진 DataFrame을 어떻게 연결하나요?

기본값인 join='outer'pd.concat()을 사용하세요 -- 모든 DataFrame의 모든 열을 유지하고 결측값을 NaN으로 채웁니다. 모든 DataFrame에 존재하는 열만 원하면 join='inner'를 설정하세요. 연결 전에 열 이름을 변경하여 정렬을 보장할 수도 있습니다.

pd.concat()이 DataFrame.append()보다 빠른가요?

네. DataFrame.append()는 pandas 1.4에서 더 이상 사용되지 않으며 pandas 2.0에서 제거되었습니다. 내부적으로 pd.concat()을 호출했지만 매번 복사본을 만들었습니다. 많은 DataFrame을 결합할 때, 리스트에 모은 다음 pd.concat()을 한 번 호출하는 것이 메모리를 한 번만 할당하므로 훨씬 빠릅니다.

연결 후 인덱스를 어떻게 재설정하나요?

pd.concat()ignore_index=True를 전달하세요: pd.concat([df1, df2], ignore_index=True). 이렇게 하면 원래 인덱스 값이 0부터 시작하는 새로운 순차 인덱스로 대체됩니다. 또는 결과에 .reset_index(drop=True)를 호출할 수도 있습니다.

결론

pandas concat() 함수는 동일한 구조를 공유하는 DataFrame을 결합하기 위한 핵심 도구입니다. 주요 요점은 다음과 같습니다:

  • 수직 연결(axis=0)은 행을 쌓는 가장 일반적인 사용 사례입니다 -- 월별 파일, 배치 결과, 분할된 데이터셋을 결합하는 데 이상적입니다.
  • 수평 연결(axis=1)은 DataFrame을 나란히 배치하고 인덱스로 정렬합니다.
  • **ignore_index=True**를 사용하여 깔끔한 순차 인덱스를 얻으세요(대부분의 경우 권장).
  • **keys**를 사용하여 각 행이 어떤 소스에서 왔는지 추적하는 계층적 인덱스를 만드세요.
  • join 매개변수는 일치하지 않는 열의 처리 방법을 제어합니다: 'outer'는 모든 것을 유지, 'inner'는 공통 열만 유지.
  • 항상 DataFrame을 리스트에 모아 루프에서 append하는 대신 pd.concat()을 한 번 호출하세요.
  • 열 값에 대한 SQL 스타일 조인이 필요할 때는 pd.merge()를 대신 사용하세요.

데이터가 연결되면, PyGWalker (opens in a new tab)와 같은 도구를 사용하여 차트 코드를 작성하지 않고도 결합된 결과를 시각적으로 탐색할 수 있어, 데이터 파이프라인을 검증하고 소스 간의 패턴을 발견하는 것이 더 빨라집니다.

📚