Py) 기초 - Pandas(병합)

Py) 기초 - Pandas(병합)

파이썬 기반 데이터분석을 위하여 Pandas 데이터프레임을 병합(concatenating, joining)하는 연산에 대해 알아보자.


개요

두 개 이상의 데이터프레임 병합은 이어붙이기(concatenating)와 엮어내기(joining)가 있다. 해당 연산을 수행하고자 하는 경우 Pandas 라이브러리의 .concat() 함수, .join() 메서드 또는 .merge() 메서드를 사용할 수 있다. 이와 관련된 개념을 알아보자.

이어붙이기

예를 들어서 특정 연도의 1월 부터 12월 까지의 월별 데이터파일이 있을 때 1년치 데이터를 취합해서 통계량을 산출하고자 한다면 각 파일을 읽어온 후에 합치는 작업이 필요하다. 이렇게 완전히 같은 서식의 파일 또는 객체가 여러개 있을 경우 별다른 특수하고 복잡한 작업 없이 하나로 합치는 것을 Binding 또는 Concatenating 이라고 한다. Pandas 에서는 concat() 함수가 그 역할을 한다.

다음은 데이터프레임 객체 3개를 하나의 객체로 이어붙이는 예시이다. 해당 연산을 수행하기 위해서 Pandas 라이브러리의 concat() 함수가 필요하다.
이어붙이기 예시

엮어내기

조인(Join)연산은 기본적으로 두 개의 테이블에서 관련된 열(컬럼, 변수)을 기준으로 데이터를 결합하는 작업을 의미한다. 이를 통해 각 테이블을 연결하여 단일 결과 집합(테이블)을 생성할 수 있다. 조인연산은 SQL에서 가장 중요하고 강력한 연산 중 하나로, 데이터베이스에서 데이터를 효율적이고 유연하게 추출하는 데 사용된다.

비록 여기서는 데이터베이스의 테이블이 아니라 파이썬 Pandas 데이터프레임을 다루는 상황이지만 여러 데이터프레임 객체를 특정 열을 기준으로 엮고자 하는 경우 조인 연산을 사용할 수 있다. 그리고 조인 연산은 그 종류가 많다. 그 중에서 데이터분석에 가장 많이 사용되는 조인 연산 두 개를 꼽자면 Inner Join와 Left Join이 되겠다.

조인 연산을 위해서는 먼저 두 데이터프레임을 엮기 위한 기준 변수가 필요한데 이를 key변수(DB분야에서는 Primary Key)라고 한다. 예시 그림은 다음과 같다.
조인 연산의 key

즉, 학번이나 사번 같이 각 행을 고유하게 구분할 수 있는 변수가 주로 key변수가 되며 엮어내고자 하는 두 데이터프레임에 공통 key변수가 존재해야 조인 연산을 실시할 수 있다.

Inner Join은 두 key변수를 기준으로 모든 열과 공통 행을 남기며 예시는 다음과 같다.
Inner Join 예시

Left Join은 두 key변수를 기준으로 모든 열을 유지한채 오른쪽 데이터프레임의 공통 행을 남기며 예시는 다음과 같다. 그리고 왼쪽 데이터프레임은 온전히 유지된다.
Left Join 예시

Left Join의 경우 key변수가 왼쪽 데이터프레임에 매칭이 되지 않는 행이 있는 경우 해당 행에 해당하는 오른쪽 데이터프레임 객체의 변수 영역에는 결측치가 채워지게 된다. 그 것이 상기 그림의 회색 영역이다.

실습

실습을 위해 두 데이터프레임 객체를 준비하자.

1
2
3
df1 = pd.DataFrame({"ID": [1, 2, 3, 4, 5],
"name": ["박정수", "최유정", "이율희", "김연지", "고지훈"],
"age": [30, 25, 36, 40, 28]})
ID name age
0 1 박정수 30
1 2 최유정 25
2 3 이율희 36
3 4 김연지 40
4 5 고지훈 28
1
2
3
df2 = pd.DataFrame({"ID": [1, 2, 2, 3, 5],
"prod_nm": ["사과", "바나나", "딸기", "수박", "포도"],
"price": [1000, 2000, 1500, 3000, 2500]})
ID prod_nm price
0 1 사과 1000
1 2 바나나 2000
2 2 딸기 1500
3 3 수박 3000
4 5 포도 2500

concat()

concat() 함수를 사용해 데이터를 이어붙여보자. concat() 함수를 사용하여 데이터프레임을 이어붙이고자 할 경우 이어붙이려하는 데이터프레임을 리스트 객체의 원소로 넣어야 한다. 그리고 이 부분에서 실수가 많이 나니 주의하도록 하자.

1
pd.concat([df1, df2])
ID name age prod_nm price
0 1 박정수 30.0 NaN NaN
1 2 최유정 25.0 NaN NaN
2 3 이율희 36.0 NaN NaN
3 4 김연지 40.0 NaN NaN
4 5 고지훈 28.0 NaN NaN
0 1 NaN NaN 사과 1000.0
1 2 NaN NaN 바나나 2000.0
2 2 NaN NaN 딸기 1500.0
3 3 NaN NaN 수박 3000.0
4 5 NaN NaN 포도 2500.0

상기 코드는 “df1” 객체 아래에 “df2” 객체를 이어붙이는 코드인데 두 변수명이 같은 “ID”는 그대로 이어져있는 반면, “ID” 변수를 제외한 나머지 변수는 두 데이터프레임의 공통변수가 아니라 어긋나있는 것을 알 수 있다. (제대로 이어붙이는 방법은 조금 더 아래에서 소개함)

이번엔 오른쪽으로 이어붙여보자. “axis” 인자에 1을 할당하면 된다.

1
pd.concat([df1, df2], axis = 1)
ID name age ID prod_nm price
0 1 박정수 30 1 사과 1000
1 2 최유정 25 2 바나나 2000
2 3 이율희 36 2 딸기 1500
3 4 김연지 40 3 수박 3000
4 5 고지훈 28 5 포도 2500

제대로 두 객체가 붙긴 했지만 “ID” 변수가 2개 있다. 당장은 에러가 발생하지 않더라도 향후 코드를 작성할 때 문제가 될 수 있기에 concat() 함수를 사용하기 전에 변수명을 바꾼 다음 붙이거나 두 “ID”변수 중 하나를 사전에 제거하여 이어붙이는 것을 권장한다.

앞에서 발생한 문제를 해결하기 위하여 아래로 데이터프레임을 이어붙이는 경우 해당 작업을 시행하기 전에 변수명을 통일하는 작업을 .rename() 메서드를 사용해서 실시하고 concat() 함수를 사용하도록 한다.

1
pd.concat([df1, df2.rename(columns = {"prod_nm": "name", "price": "age"})])
ID name age
0 1 박정수 30
1 2 최유정 25
2 3 이율희 36
3 4 김연지 40
4 5 고지훈 28
0 1 사과 1000
1 2 바나나 2000
2 2 딸기 1500
3 3 수박 3000
4 5 포도 2500

일단 성공적인 데이터 병합을 위해 변수명을 억지로 변경했지만 기존 변수들이 의미하는 바는 다르니 참고만 하길 바란다.

옆으로 이어붙이는 상황에서도 데이터가 어긋나는 경우가 있다. 다음과 같이 두 데이터프레임 객체의 인덱스가 서로 다른 경우를 .reindex() 메서드를 사용하여 억지로 만들고 그 결과를 보면 다음과 같다.

1
pd.concat([df1, df2.reindex([8, 1, 2, 9, 4])], axis = 1)
ID name age ID prod_nm price
0 1.0 박정수 30.0 NaN NaN NaN
1 2.0 최유정 25.0 2.0 바나나 2000.0
2 3.0 이율희 36.0 2.0 딸기 1500.0
3 4.0 김연지 40.0 NaN NaN NaN
4 5.0 고지훈 28.0 5.0 포도 2500.0
8 NaN NaN NaN NaN NaN NaN
9 NaN NaN NaN NaN NaN NaN

병합 결과를 보면 인덱스 번호가 공통인 행은 제대로 붙었지만 그렇지 않은 경우는 결측값이 생기거나 다른 곳에 데이터가 붙어있는 것을 볼 수 있다. 그렇기에 데이터를 이어붙이기 전에 혹시 인덱스로 인한 문제가 발생하지는 않을지, 이어붙이고 나서 혹시 없던 결측값이 생기진 않았는지 확인이 필요하다.
※ 관련 게시물 - Pandas(결측값 처리)

.merge()

두 객체의 변수를 기준으로 합치는 .merge() 메서드의 사용 예시는 다음과 같다. .merge()는 기본적으로 Inner Join 으로 동작한다. 그리고 “df1” 객체와 “df2” 객체의 공통key가 되는 변수는 “ID”로 그 값이 같기 때문에 “on” 인자에 “ID”를 할당하는 것이 간결하고 좋다.

1
df1.merge(df2, on = "ID")
ID name age prod_nm price
0 1 박정수 30 사과 1000
1 2 최유정 25 바나나 2000
2 2 최유정 25 딸기 1500
3 3 이율희 36 수박 3000
4 5 고지훈 28 포도 2500

ID가 2인 “최유정”의 경우 “바나나”와 “딸기”를 구매한 이력이 있기 때문에 2개의 행이 반환된 것을 알 수 있다. 이렇게 1:1 매칭이 아니라 1:n 매칭이 되는 경우 관련 행이 복제가 되는 것을 참고하도록 하자.

기본값이 Inner Join이지만 직접 명시하고자 한다면 “how” 인자에 “inner”를 할당하면 된다.

1
df1.merge(df2, on = "ID", how = "inner")
ID name age prod_nm price
0 1 박정수 30 사과 1000
1 2 최유정 25 바나나 2000
2 2 최유정 25 딸기 1500
3 3 이율희 36 수박 3000
4 5 고지훈 28 포도 2500

Left Join을 하기 위해서는 “how”에 “left”를 할당해야하며 다음과 같다.

1
df1.merge(df2, on = "ID", how = "left")
ID name age prod_nm price
0 1 박정수 30 사과 1000.0
1 2 최유정 25 바나나 2000.0
2 2 최유정 25 딸기 1500.0
3 3 이율희 36 수박 3000.0
4 4 김연지 40 NaN NaN
5 5 고지훈 28 포도 2500.0

Left Join의 경우 key변수의 값이 매칭되지 않는 경우 해당 행에 결측치가 생성되며 이를 기반으로 어떤 데이터가 비어있는지 쉽게 알 수 있다.

앞에서 “on” 인자를 사용했지만, 데이터프레임 객체의 key변수를 명시하거나 병합하고자 하는 각 객체의 key변수명이 서로 다른경우 “left_on”과 “right_on” 인자를 사용할 수 있다. 이 때 “left_on” 인자는 “df1” 데이터프레임 객체의 key변수명을 할당하고, “right_on” 인자는 “df2” 데이터프레임 객체의 key변수명을 할당하면 된다.

1
df1.merge(df2, left_on = "ID", right_on = "ID")
ID name age prod_nm price
0 1 박정수 30 사과 1000
1 2 최유정 25 바나나 2000
2 2 최유정 25 딸기 1500
3 3 이율희 36 수박 3000
4 5 고지훈 28 포도 2500

.join()

데이터프레임의 인덱스를 기준으로 하는 Join연산을 지원하는 .join() 메서드를 알아보자. 그런데 다음과 같이 코드를 작성하면 에러가 발생한다.

1
2
3
4
5
6
7
df1.join(df2)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[28], line 1
----> 1 df1.join(df2)
......
ValueError: columns overlap but no suffix specified: Index(['ID'], dtype='object')

상기 에러는 병합하고자 하는 두 데이터프레임에 “ID”라는 변수명이 같이 들어있어 병합결과가 되는 데이터프레임 객체에서 “ID”가 2개가 생겨 변수명이 겹치기 때문에 발생되는 에러이다. 해당 메서드는 변수명이 겹치는 상황을 방지하기 위해 에러메세지에서 접미사(suffix)관련 내용을 언급하고 있으며 이와 관련된 인자는 “lsuffix”와 “rsuffix”가 있다. 해당 인자를 사용한 코드는 다음과 같다.

1
df1.join(df2, rsuffix = "_2")
ID name age ID_2 prod_nm price
0 1 박정수 30 1 사과 1000
1 2 최유정 25 2 바나나 2000
2 3 이율희 36 2 딸기 1500
3 4 김연지 40 3 수박 3000
4 5 고지훈 28 5 포도 2500

오른쪽에 위치한 .join() 메서드 안의 “df2” 데이터프레임의 “ID”변수명이 연산 결과 “ID_2”로 바뀐것을 알 수 있으며 이는 오른쪽 데이터프레임 객체의 변수명이 겹칠 경우 접미사를 붙이는 “rsuffix”에 영향을 받은 결과이다. 물론 “lsuffix”나 “rsuffix”를 사용하지 않고 미리 .rename() 인자를 사용하여 사전에 변수명을 변경할 수 있다.

그런데 문제점이 있다. 상기 연산 결과는 인덱스를 기준으로 엮어낸 결과이기 때문에 맥락에 맞지 않는 것을 알 수 있다. 올바른 연산을 위해서는 엮어내고자 하는 key변수가 인덱스에 위치해야하는데 이를 위해서 .set_index() 메서드를 사용할 수 있다.

1
df1.set_index("ID").head(2)
name age
ID
1 박정수 30
2 최유정 25
1
df2.set_index("ID").head(2)
prod_nm price
ID
1 사과 1000
2 바나나 2000

상기와 같이 “ID”변수를 인덱스로 위치시킨 두 데이터프레임을 기반으로 다시 연산해보자.

1
df1.set_index("ID").join(df2.set_index("ID"))
name age prod_nm price
ID
1 박정수 30 사과 1000.0
2 최유정 25 바나나 2000.0
2 최유정 25 딸기 1500.0
3 이율희 36 수박 3000.0
4 김연지 40 NaN NaN
5 고지훈 28 포도 2500.0

상기 결과는 Left Join의 결과이며 인덱스의 원소가 매칭이 안된 경우 결측치를 반환한 것을 볼 수 있다. 그리고 연산 후에도 인덱스가 그대로 “ID”변수의 내용으로 차있는 것을 알 수 있다.

.join() 메서드에서 Join 연산방법을 지정하고자 한다면 “how”인자에 적절한 값을 할당하면 되며 Inner Join을 실시하고자 하는 경우 “inner”를 할당해야하고 그 예시는 다음과 같다.

1
df1.set_index("ID").join(df2.set_index("ID"), how = "inner")
name age prod_nm price
ID
1 박정수 30 사과 1000
2 최유정 25 바나나 2000
2 최유정 25 딸기 1500
3 이율희 36 수박 3000
5 고지훈 28 포도 2500

기존 데이터프레임과 같이 인덱스가 아닌 “ID”변수로 취급하려면 .reset_index()를 사용하면 되며 그 결과는 다음과 같다.

1
df1.set_index("ID").join(df2.set_index("ID"), how = "inner").reset_index()
ID name age prod_nm price
0 1 박정수 30 사과 1000
1 2 최유정 25 바나나 2000
2 2 최유정 25 딸기 1500
3 3 이율희 36 수박 3000
4 5 고지훈 28 포도 2500
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×