Pandas. value_counts(), idxmax(), mode()와 MultiIndex 정리
1. 핵심 개념: pandas는 입력 객체에 따라 동작이 달라진다
pandas에서 가장 헷갈리기 쉬운 부분 중 하나는 동일한 메서드라도 입력 객체가 Series인지 DataFrame인지에 따라 동작이 달라질 수 있다는 것이다.
특히 value_counts(), idxmax(), mode()를 이해하려면 다음 세 가지를 구분해야 한다.
- 입력 객체의 자료형: Series인가, DataFrame인가?
- 반환 객체의 자료형: Series인가, DataFrame인가?
- 반환 객체의 인덱스 구조: 일반 Index인가, MultiIndex인가?
이 세 가지를 이해하면 여러 메서드를 연결해서 사용하더라도 결과를 예측하기 쉬워진다.
2. value_counts(): 데이터의 등장 횟수를 계산하는 메서드
value_counts()는 데이터의 등장 횟수를 계산한다.
다만 Series와 DataFrame에서 집계하는 대상이 다르다.
예를 들어 다음과 같은 데이터프레임이 있다고 하자.
import pandas as pddf = pd.DataFrame({ '이름': ['철수', '영희', '철수', '민수', '철수'], '점수': [90, 80, 90, 70, 80]})
| 인덱스 | 이름 | 점수 |
|---|---|---|
| 0 | 철수 | 90 |
| 1 | 영희 | 80 |
| 2 | 철수 | 90 |
| 3 | 민수 | 70 |
| 4 | 철수 | 80 |
2.1. Series에서 value_counts()
df['이름'].value_counts()
결과:
이름
철수 3
영희 1
민수 1
Name: count, dtype: int64
Series에서는 개별 값이 몇 번 등장했는지 계산한다.
여기서 중요한 점은 value_counts()의 결과가 다시 Series라는 것이다.
이 Series의 구조는 다음과 같다.
| 인덱스 (index) | 값 (value) |
|---|---|
| 철수 | 3 |
| 영희 | 1 |
| 민수 | 1 |
즉, 원래 데이터였던 철수, 영희, 민수는 인덱스가 되고, 등장 횟수인 3, 1, 1은 Series의 값이 된다.
2.2. DataFrame에서 value_counts()
이번에는 특정 열이 아니라 DataFrame 전체에 적용해 보자.
df.value_counts()
결과:
이름 점수
철수 90 2
민수 70 1
영희 80 1
철수 80 1
Name: count, dtype: int64
DataFrame에서는 각 행의 값 조합이 몇 번 등장했는지 계산한다.
예를 들어 ('철수', 90)이라는 조합은 두 번 등장했으므로 등장 횟수가 2가 된다.
여기서 주의할 점은 DataFrame에 value_counts()를 적용했지만 반환된 객체는 DataFrame이 아니라 Series라는 것이다.
type(df.value_counts())# pandas.core.series.Series
그리고 이 Series는 일반적인 단일 인덱스가 아니라 MultiIndex를 가진다.
3. MultiIndex: 여러 레벨로 구성된 인덱스
3.1. MultiIndex란?
MultiIndex는 하나의 인덱스를 여러 레벨로 구성한 것이다.
이름 점수
철수 90 2
민수 70 1
영희 80 1
철수 80 1
Name: count, dtype: int64
이 결과를 표로 나타내면 다음과 같다.
| 인덱스 레벨 0 (이름) | 인덱스 레벨 1 (점수) | 값 (count) |
|---|---|---|
| 철수 | 90 | 2 |
| 민수 | 70 | 1 |
| 영희 | 80 | 1 |
| 철수 | 80 | 1 |
여기서 이름과 점수는 일반적인 데이터 열이 아니다.
하나의 인덱스를 구성하는 두 개의 레벨이다.
따라서 각 행의 인덱스는 튜플 형태로 표현할 수 있다.
('철수', 90)('민수', 70)('영희', 80)('철수', 80)
3.2. MultiIndex 확인하기
s = df.value_counts()print(s.index)
결과:
MultiIndex([('철수', 90),
('민수', 70),
('영희', 80),
('철수', 80)],
names=['이름', '점수'])
names=['이름', '점수']는 각 인덱스 레벨의 이름을 의미한다.
3.3. MultiIndex를 이용한 데이터 접근
MultiIndex에서는 튜플을 이용해 특정 데이터를 가져올 수 있다.
s.loc[('철수', 90)]# 2
이는 ('철수', 90)이라는 인덱스에 대응하는 값이 2라는 의미다.
3.4. MultiIndex를 일반 열로 변환하기
MultiIndex가 불편하다면 reset_index()를 사용할 수 있다.
s.reset_index()
결과:
| 이름 | 점수 | count | |
|---|---|---|---|
| 0 | 철수 | 90 | 2 |
| 1 | 민수 | 70 | 1 |
| 2 | 영희 | 80 | 1 |
| 3 | 철수 | 80 | 1 |
기존의 인덱스 레벨인 이름, 점수가 일반 열로 변환되고, 기본 정수 인덱스가 새로 만들어진다.
반환 자료형도 Series에서 DataFrame으로 바뀐다.
type(s.reset_index())# pandas.core.frame.DataFrame
4. idxmax(): 최대값에 해당하는 인덱스를 반환하는 메서드
idxmax()를 이해하려면 max()와 구분해야 한다.
max()는 최대값 자체를 반환한다.idxmax()는 최대값이 위치한 인덱스의 레이블을 반환한다.
4.1. 일반 Series에서 사용하기
s = pd.Series([10, 30, 20], index=['A', 'B', 'C'])
| 인덱스 | 값 |
|---|---|
| A | 10 |
| B | 30 |
| C | 20 |
s.max()# 30s.idxmax()# 'B'
값이 가장 큰 것은 30이지만 그 값에 대응하는 인덱스는 'B'이다.
따라서 idxmax()는 'B'를 반환한다.
idxmax()는 최대값이 아니라 최대값의 인덱스 레이블을 반환한다는 것이 핵심이다.
4.2. value_counts()와 결합하면 최빈값이 되는 이유
df['이름'].value_counts()
결과:
이름
철수 3
영희 1
민수 1
Name: count, dtype: int64
여기에 idxmax()를 적용하면 다음과 같다.
df['이름'].value_counts().idxmax()# '철수'
이 과정을 분해해 보면 다음과 같다.
counts = df['이름'].value_counts()counts.max()# 3counts.idxmax()# '철수'
value_counts()가 원래 데이터 값을 인덱스로 만들었으므로, 가장 큰 등장 횟수의 인덱스를 가져오면 자연스럽게 최빈값이 된다.
즉,
value_counts().idxmax() = 등장 횟수가 가장 많은 값의 인덱스 = 최빈값
이다.
4.3. MultiIndex에서 idxmax()를 사용하면?
s = df.value_counts()
결과:
이름 점수
철수 90 2
민수 70 1
영희 80 1
철수 80 1
Name: count, dtype: int64
s.max()# 2s.idxmax()# ('철수', 90)
최대값은 2이지만 그 값에 대응하는 인덱스는 ('철수', 90)이다.
따라서 idxmax()는 튜플을 반환한다.
이는 idxmax()의 동작이 달라진 것이 아니다.
인덱스의 구조가 달라졌기 때문에 반환값의 형태도 달라진 것이다.
5. mode(): 최빈값을 직접 구하는 메서드
pandas에는 최빈값을 직접 계산하는 mode()라는 메서드도 존재한다.
5.1. Series에서 mode()
df['이름'].mode()
결과:
0 철수
Name: 이름, dtype: object
mode()는 최빈값을 Series 형태로 반환한다.
따라서 최빈값 하나를 스칼라 값으로 가져오려면 다음과 같이 작성할 수 있다.
df['이름'].mode().iloc[0]# '철수'
5.2. mode()와 value_counts().idxmax()의 차이
두 방식은 최빈값이 하나일 때는 같은 결과를 얻을 수 있지만, 최빈값이 여러 개일 때 차이가 발생한다.
s = pd.Series(['A', 'A', 'B', 'B', 'C'])
각 값의 등장 횟수는 다음과 같다.
| 값 | 등장 횟수 |
|---|---|
| A | 2 |
| B | 2 |
| C | 1 |
이때 mode()는 다음과 같다.
s.mode()
결과:
0 A
1 B
dtype: object
반면 다음 코드는
s.value_counts().idxmax()
동률인 최빈값 중 하나만 반환한다. 이 예제에서는 'A'가 반환된다.
따라서 두 방식은 다음과 같이 구분할 수 있다.
| 메서드 | 최빈값이 여러 개일 때 |
|---|---|
mode() |
모든 최빈값 반환 |
value_counts().idxmax() |
그중 하나만 반환 |
5.3. DataFrame에서 mode()
df.mode()
DataFrame에서 mode()를 사용하면 각 열의 최빈값을 독립적으로 계산한다.
앞선 예제에서는 이름 열의 최빈값은 철수이고, 점수 열의 최빈값은 80과 90이다.
따라서 다음 결과가 나온다.
이름 점수
0 철수 80.0
1 NaN 90.0
여기서 두 번째 행에 NaN이 있는 이유는 이름 열에는 최빈값이 하나뿐인데 점수 열에는 두 개가 있기 때문이다.
중요한 것은 df.mode()가 가장 많이 등장한 행의 조합을 찾는 것이 아니라 열별 최빈값을 구한다는 점이다.
행의 조합에 대한 최빈값은 df.value_counts().idxmax()로 구할 수 있다.
6. 메서드를 연결할 때 주의해야 하는 자료형 변화
pandas에서는 메서드들을 연속으로 호출하는 경우가 많다.
df['이름'].value_counts().idxmax()
이 코드를 제대로 이해하려면 각 단계에서 어떤 자료형이 나오는지 알아야 한다.
| 표현식 | 반환 자료형 | 의미 |
|---|---|---|
df |
DataFrame | 전체 데이터 |
df['이름'] |
Series | 이름 열 |
df[['이름']] |
DataFrame | 이름 열만 포함한 DataFrame |
df['이름'].value_counts() |
Series | 이름별 등장 횟수 |
df.value_counts() |
Series | 행 조합별 등장 횟수 |
df.value_counts().idxmax() |
tuple | 가장 많이 등장한 행의 값 조합 |
df.value_counts().reset_index() |
DataFrame | 집계 결과를 일반 열로 변환 |
df.mode() |
DataFrame | 각 열의 최빈값 |
df['이름'].mode() |
Series | 이름 열의 최빈값들 |
여기서 특히 주의할 부분은 다음 두 표현식이다.
df['이름'] # Series 반환df[['이름']] # DataFrame 반환
겉보기에는 거의 같지만 자료형이 달라진다.
따라서 이후에 호출하는 메서드의 동작도 달라질 수 있다.
7. 추가로 알아두면 좋은 주의사항
7.1. value_counts()는 기본적으로 등장 횟수 내림차순으로 정렬한다
df['이름'].value_counts()
별도의 옵션을 지정하지 않으면 등장 횟수가 많은 값부터 출력된다.
df['이름'].value_counts(sort=False)
sort=False를 설정하면 등장 횟수 기준 정렬을 하지 않는다.
7.2. 결측값은 기본적으로 제외한다
df['이름'].value_counts()
기본적으로 NaN 값은 계산에서 제외한다.
결측값까지 포함하려면 다음과 같이 작성한다.
df['이름'].value_counts(dropna=False)
DataFrame에서도 dropna=False를 지정하면 결측값이 포함된 행 조합까지 계산할 수 있다.
7.3. idxmax()는 동률인 최대값을 모두 반환하지 않는다
idxmax()는 최대값이 여러 개 존재하더라도 그중 처음 나타나는 인덱스 레이블 하나만 반환한다.
따라서 최빈값이 여러 개일 수 있고 이를 모두 찾아야 한다면 mode()가 더 적절하다.
8. 요약
| 목적 | 사용 코드 |
|---|---|
| 특정 열의 값별 등장 횟수 | df['이름'].value_counts() |
| 동일한 행 조합의 등장 횟수 | df.value_counts() |
| 특정 열의 최빈값 하나 | df['이름'].value_counts().idxmax() |
| 특정 열의 최빈값 모두 | df['이름'].mode() |
| 가장 자주 등장한 행의 조합 하나 | df.value_counts().idxmax() |
| 가장 많이 등장한 행 조합의 횟수 | df.value_counts().max() |
| 각 열의 최빈값 | df.mode() |
| MultiIndex를 일반 열로 변환 | df.value_counts().reset_index() |
가장 중요한 개념
첫째, value_counts()는 입력에 따라 집계 기준이 달라진다.
Series에서는 개별 값의 등장 횟수를 계산하고, DataFrame에서는 행의 값 조합별 등장 횟수를 계산한다.
둘째, idxmax()는 최대값 자체가 아니라 최대값에 대응하는 인덱스 레이블을 반환한다.
value_counts()가 원래 데이터 값을 인덱스로 사용하기 때문에, idxmax()를 연결하면 최빈값을 구할 수 있다.
셋째, MultiIndex는 여러 개의 인덱스가 따로 존재하는 것이 아니라 하나의 인덱스가 여러 레벨로 구성된 구조다.
따라서 MultiIndex에 idxmax()를 적용하면 여러 레벨의 레이블이 튜플 형태로 반환될 수 있다.
넷째, pandas에서는 메서드의 이름뿐 아니라 입력과 반환 객체의 자료형을 함께 확인해야 한다.
결국 다음 흐름을 이해하는 것이 핵심이다.
DataFrame
│
├── df['이름']
│ │
│ ▼
│ Series
│ │
│ ▼
│ value_counts()
│ │
│ ▼
│ Series (일반 Index)
│ │
│ ▼
│ idxmax()
│ │
│ ▼
│ '철수'
│
└── df.value_counts()
│
▼
Series (MultiIndex)
│
▼
idxmax()
│
▼
('철수', 90)
즉, pandas의 동작을 예측하려면 각 메서드가 어떤 객체를 입력받고, 어떤 구조의 객체를 반환하는지 추적하는 습관이 중요하다.