오늘날 데이터는 새로운 석유로 불리며 비즈니스, 연구, 심지어 일상생활의 의사결정에까지 지대한 영향을 미치고 있습니다. 이러한 데이터의 홍수 속에서 의미 있는 정보를 길어 올리는 능력은 현대 사회를 살아가는 우리에게 강력한 무기가 됩니다. 파이썬(Python)은 이러한 데이터 과학 분야에서 가장 사랑받는 언어 중 하나이며, 그 중심에는 바로 Pandas 라이브러리가 있습니다. Pandas는 단순히 데이터를 다루는 도구를 넘어, 데이터를 바라보는 관점과 분석의 철학을 제시합니다.
많은 입문자들이 Pandas를 배울 때 단순히 함수의 목록과 사용법을 암기하려 합니다. read_csv()로 파일을 읽고, head()로 앞부분을 보고, 특정 열을 선택하는 식이죠. 하지만 이런 접근 방식은 곧 한계에 부딪힙니다. 복잡하고 비정형적인 실제 데이터 앞에서 길을 잃기 쉽습니다. 이 글의 목표는 단순히 Pandas의 기능을 나열하는 것을 넘어, 왜 Pandas가 이런 방식으로 설계되었는지, 그 핵심 철학은 무엇인지, 그리고 각 기능이 데이터 분석이라는 큰 그림 안에서 어떤 역할을 하는지를 깊이 있게 탐구하는 것입니다. 사실(Fact)의 나열이 아닌, 진실(Truth)에 가까운 이해를 통해 여러분이 어떤 데이터 문제를 만나더라도 자신감 있게 해결의 실마리를 찾아 나갈 수 있도록 돕겠습니다.
1. 모든 것의 시작: Series와 DataFrame의 본질
Pandas를 이해하기 위한 첫걸음은 가장 핵심적인 두 가지 데이터 구조, 바로 Series와 DataFrame을 이해하는 것입니다. 이 둘은 Pandas의 알파이자 오메가이며, 모든 작업은 이 두 구조를 기반으로 이루어집니다. 많은 사람들이 Series를 1차원 배열, DataFrame을 2차원 배열 혹은 엑셀 시트와 같다고 설명합니다. 틀린 말은 아니지만, 이는 본질의 일부만을 설명할 뿐입니다.
더 깊은 이해를 위해 '인덱스(Index)'라는 개념에 주목해야 합니다. 파이썬의 기본 리스트나 NumPy의 배열이 0부터 시작하는 정수 위치로 데이터에 접근하는 반면, Pandas의 Series와 DataFrame은 각 데이터 포인트에 명시적인 '이름표' 즉, 인덱스를 붙여줍니다. 이 인덱스는 단순한 숫자일 수도 있고, 날짜, 문자열 등 다양한 형태가 될 수 있습니다. 바로 이 '인덱스'가 Pandas를 단순한 데이터 컨테이너가 아닌, 강력한 데이터 분석 도구로 만드는 핵심 열쇠입니다.
# Pandas 라이브러리를 pd라는 별칭으로 불러옵니다.
import pandas as pd
import numpy as np
# Series: 이름표(Index)가 붙은 1차원 데이터
# 인덱스를 명시하지 않으면 0부터 시작하는 정수 인덱스가 자동으로 부여됩니다.
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
print("--- Series ---")
print(s)
print("\n인덱스 'c'의 값:", s['c'])
# DataFrame: Series들이 모여 만들어진 2차원 데이터
# 각 Series(열)는 고유한 이름을 가집니다.
data = {'이름': ['김철수', '이영희', '박지성', '손흥민'],
'나이': [28, 35, 42, 31],
'도시': ['서울', '부산', '서울', '런던']}
df = pd.DataFrame(data)
print("\n--- DataFrame ---")
print(df)
위 코드에서 Series는 값(10, 20, 30, 40)과 인덱스('a', 'b', 'c', 'd')의 쌍으로 이루어져 있습니다. 이 덕분에 우리는 s[2]처럼 위치로 접근할 수도 있지만, s['c']처럼 이름표로 값에 직접 접근할 수 있습니다. 이는 코드의 가독성을 높이고 데이터의 의미를 명확하게 만듭니다.
DataFrame은 이러한 Series들이 같은 인덱스를 공유하며 모여있는 구조입니다. '이름', '나이', '도시'라는 열(Column) 이름이 각 Series의 이름이 되며, 행(Row)들은 0, 1, 2, 3이라는 공유된 인덱스를 가집니다. 중요한 점은 각 열이 서로 다른 데이터 타입(문자열, 정수)을 가질 수 있다는 것입니다. 이는 다양한 형태의 데이터를 하나의 표 안에 담을 수 있게 해주는 유연성을 제공합니다. 이처럼 인덱스를 기반으로 데이터를 정렬하고, 연산하고, 결합하는 능력이야말로 Pandas가 단순한 행렬 연산 라이브러리인 NumPy와 차별화되는 지점입니다.
2. 데이터와의 첫 만남: 탐색적 데이터 분석(EDA)의 시작
데이터를 손에 넣었다면, 가장 먼저 해야 할 일은 데이터를 '탐색'하는 것입니다. 마치 낯선 도시에 도착한 여행자가 지도를 펼쳐보고 주변을 둘러보는 것과 같습니다. 이 과정을 탐색적 데이터 분석(Exploratory Data Analysis, EDA)이라고 부릅니다. EDA는 데이터에 대한 직관을 얻고, 숨겨진 패턴이나 이상치를 발견하며, 본격적인 분석에 앞서 데이터의 상태를 점검하는 필수적인 과정입니다. Pandas는 이 과정을 위한 강력하고 직관적인 도구들을 제공합니다.
가장 기본적이면서도 중요한 함수는 다음과 같습니다.
.head(n): 데이터의 첫 n개 행을 보여줍니다. (기본값 n=5) 데이터가 예상대로 잘 불러와졌는지, 컬럼 이름은 무엇인지 빠르게 확인할 수 있습니다..tail(n): 데이터의 마지막 n개 행을 보여줍니다. 데이터의 끝부분을 확인하며 전체적인 구성을 파악하는 데 도움이 됩니다..shape: 데이터프레임의 (행, 열) 개수를 튜플 형태로 반환합니다. 데이터의 전체적인 크기를 가늠할 수 있는 가장 기본적인 정보입니다..info(): 데이터프레임의 요약 정보를 제공합니다. 각 열의 이름, 데이터 타입, 결측치가 아닌 값의 개수, 메모리 사용량 등을 한눈에 볼 수 있습니다. 이는 데이터 정제 단계에서 무엇을 해야 할지 알려주는 로드맵과 같습니다..describe(): 숫자형 데이터 열에 대한 주요 기술 통계량을 요약해 보여줍니다. (개수, 평균, 표준편차, 최소값, 4분위수, 최대값 등) 데이터의 분포를 빠르게 파악하고, 상식적으로 말이 안 되는 값(예: 나이가 200살)과 같은 이상치(Outlier)를 감지하는 데 매우 유용합니다.
# 샘플 데이터프레임 다시 확인
print("--- 기본 정보 탐색 ---")
print("Shape (행, 열):", df.shape)
print("\n--- .info() ---")
# .info()는 출력을 직접 하므로 print()로 감싸지 않습니다.
df.info()
print("\n--- .describe() ---")
print(df.describe())
.info()의 결과를 보면, '나이' 열은 4개의 non-null int64 값으로 이루어져 있고, '이름'과 '도시' 열은 4개의 non-null object 값(주로 문자열)으로 구성되어 있음을 알 수 있습니다. 만약某个 열의 non-null 개수가 전체 행의 개수보다 적다면, 이는 해당 열에 결측치(Missing Value)가 존재한다는 강력한 신호입니다. .describe() 결과에서는 '나이' 열의 평균이 34.0세이고, 최소 28세에서 최대 42세까지 분포함을 알 수 있습니다. 이처럼 몇 가지 간단한 명령만으로도 우리는 데이터의 구조, 타입, 크기, 분포에 대한 풍부한 정보를 얻을 수 있습니다. 이는 분석의 방향을 설정하는 데 결정적인 역할을 합니다.
데이터 탐색은 단순히 함수 몇 개를 실행하는 행위가 아닙니다. 각 결과가 무엇을 의미하는지 비판적으로 사고하는 과정입니다. 예를 들어, .describe() 결과에서 평균(mean)과 중앙값(50%, median)의 차이가 크다면, 데이터가 한쪽으로 치우쳐져 있거나 극단적인 이상치가 존재할 수 있음을 의심해봐야 합니다. 이러한 호기심과 질문이 깊이 있는 분석으로 이어지는 첫걸음입니다.
3. 데이터에 질문하기: 선택(Selection)과 필터링(Filtering)의 기술
데이터를 탐색하여 전반적인 그림을 그렸다면, 이제는 구체적인 질문에 대한 답을 찾을 차례입니다. "서울에 사는 사람들은 누구인가?", "30대인 사람들의 정보만 보고 싶다"와 같은 질문들은 데이터에서 특정 부분을 선택하거나 조건을 만족하는 데이터만 걸러내는 작업을 통해 답을 얻을 수 있습니다. Pandas에서는 이러한 작업을 매우 효율적이고 유연하게 수행할 수 있습니다.
3.1. 기본 중의 기본: 열(Column) 선택
가장 기본적인 작업은 특정 열의 데이터만 가져오는 것입니다. 두 가지 방법이 주로 사용됩니다.
- 대괄호 표기법
df['컬럼명']: 가장 표준적이고 안전한 방법입니다. 컬럼 이름에 띄어쓰기가 있거나, 파이썬 예약어와 겹치는 경우에도 문제없이 사용할 수 있습니다. 여러 개의 열을 선택할 때는 리스트 형태로 묶어줍니다. (df[['컬럼1', '컬럼2']]) - 점 표기법
df.컬럼명: 코드가 간결해 보이지만, 위에서 언급한 제약사항(띄어쓰기, 예약어 충돌)이 있어 주의가 필요합니다. 또한, 새로운 열을 할당할 때는 사용할 수 없어 일관성이 떨어질 수 있습니다. 가급적 대괄호 표기법을 사용하는 것이 좋습니다.
# '이름' 열만 선택하기 (결과는 Series)
names = df['이름']
print("--- 이름 열 (Series) ---")
print(names)
# '이름'과 '도시' 두 개의 열 선택하기 (결과는 DataFrame)
name_and_city = df[['이름', '도시']]
print("\n--- 이름과 도시 열 (DataFrame) ---")
print(name_and_city)
3.2. 행(Row) 선택의 두 가지 관점: .loc와 .iloc
행을 선택하는 것은 열을 선택하는 것보다 조금 더 복잡하며, 많은 입문자들이 혼란을 겪는 부분입니다. Pandas는 행 선택을 위해 두 가지 명확한 방법을 제공하는데, 바로 .loc와 .iloc입니다. 이 둘의 차이를 명확히 이해하는 것이 매우 중요합니다.
.loc[](Label-based indexing): 이름(레이블) 기반으로 데이터에 접근합니다. 인덱스의 이름이나 컬럼의 이름을 직접 사용합니다. 예를 들어,df.loc[0]은 인덱스 '이름'이 0인 행을 찾습니다. 슬라이싱(slicing)을 할 때 끝점을 포함(inclusive)하는 특징이 있습니다. (df.loc[0:2]는 0, 1, 2번 인덱스를 모두 포함).iloc[](Integer position-based indexing): 위치(정수) 기반으로 데이터에 접근합니다. 파이썬 리스트처럼 0부터 시작하는 순서를 사용합니다. 예를 들어,df.iloc[0]은 0번째 위치에 있는 행을 찾습니다. 슬라이싱 시 파이썬의 규칙과 동일하게 끝점을 포함하지 않습니다(exclusive). (df.iloc[0:2]는 0, 1번 위치의 행만 포함)
이 두 가지를 구분해서 사용하는 습관은 코드의 의도를 명확하게 하고 예기치 않은 오류를 방지합니다. 그냥 df[]와 같이 대괄호만 사용하는 것은 상황에 따라 행과 열을 다르게 해석하려는 경향이 있어 혼란을 야기할 수 있으므로, 행 선택 시에는 .loc나 .iloc를 명시적으로 사용하는 것이 좋습니다.
# 인덱스를 '이름'으로 설정하여 .loc의 의미를 더 명확하게 만들어 보겠습니다.
df_indexed = df.set_index('이름')
print("--- 이름이 인덱스인 DataFrame ---")
print(df_indexed)
# .loc: '박지성'이라는 이름(레이블)을 가진 행 선택
print("\n--- .loc['박지성'] ---")
print(df_indexed.loc['박지성'])
# .iloc: 2번째 위치에 있는 행 선택 (박지성)
print("\n--- .iloc[2] ---")
print(df_indexed.iloc[2])
# .loc: '이영희'부터 '손흥민'까지 슬라이싱 (끝점 포함)
print("\n--- .loc['이영희':'손흥민'] ---")
print(df_indexed.loc['이영희':'손흥민'])
# .iloc: 1번 위치부터 3번 위치 전까지 슬라이싱 (끝점 미포함)
print("\n--- .iloc[1:3] ---")
print(df_indexed.iloc[1:3])
3.3. Pandas의 꽃: 불리언 인덱싱 (Boolean Indexing)
지금까지의 선택 방법보다 훨씬 더 강력하고 데이터 분석의 핵심이라 할 수 있는 기능이 바로 불리언 인덱싱입니다. 이는 데이터에 특정 조건을 걸어 True 또는 False 값을 가진 불리언(boolean) Series를 만들고, 이 Series를 이용해 True에 해당하는 행만 필터링하는 방식입니다. 이 개념을 시각적으로 이해하는 것이 중요합니다.
ASCII Art로 표현한 불리언 인덱싱의 과정:
1. 조건 적용 (예: df['나이'] > 30) DataFrame 'df' Boolean Series (결과) +-------+----+------+ +-------------------+ | 이름 | 나이| 도시 | | 0 False | +-------+----+------+ | 1 True | | 김철수| 28 | 서울 | -> | 2 True | | 이영희| 35 | 부산 | | 3 True | | 박지성| 42 | 서울 | +-------------------+ | 손흥민| 31 | 런던 | +-------+----+------+ 2. 불리언 Series를 이용해 필터링 (df[...]) Boolean Series 필터링된 DataFrame +-------------------+ | 0 False | +-------+----+------+ | 1 True | -> | 이영희| 35 | 부산 | | 2 True | | 박지성| 42 | 서울 | | 3 True | | 손흥민| 31 | 런던 | +-------------------+ +-------+----+------+
이처럼 조건에 맞는 행만 '체'로 거르듯이 걸러낼 수 있습니다. 여러 조건을 조합할 때는 `&`(AND), `|`(OR), `~`(NOT) 연산자를 사용합니다. 여기서 매우 중요한 점은, 각 조건을 반드시 소괄호 ()로 묶어주어야 한다는 것입니다. 이는 파이썬의 연산자 우선순위 때문으로, 괄호로 묶지 않으면 예기치 않은 오류가 발생합니다.
# 나이가 30보다 많은 사람 찾기
over_30 = df[df['나이'] > 30]
print("--- 나이가 30 초과인 사람들 ---")
print(over_30)
# 서울에 살면서 30세 이상인 사람 찾기
# 각 조건은 반드시 ()로 묶어야 합니다.
seoul_and_over_30 = df[(df['도시'] == '서울') & (df['나이'] >= 30)]
print("\n--- 서울 거주 및 30세 이상 ---")
print(seoul_and_over_30)
# 도시가 '부산'이거나 '런던'인 사람 찾기
# .isin() 메소드를 사용하면 더 간결합니다.
busan_or_london = df[df['도시'].isin(['부산', '런던'])]
print("\n--- 부산 또는 런던 거주 ---")
print(busan_or_london)
불리언 인덱싱은 데이터 분석가가 데이터와 '대화'하는 가장 기본적인 언어입니다. 이 기술을 자유자재로 사용할 수 있게 되면, 거의 모든 종류의 데이터 필터링 요구사항을 해결할 수 있게 됩니다. 복잡한 조건을 조합하여 원하는 데이터 부분집합(subset)을 정확히 추출하는 능력은 데이터 분석의 정확성과 효율성을 비약적으로 향상시킵니다.
4. 데이터 정제: 보이지 않는 영웅의 역할
현실 세계의 데이터는 결코 깨끗하지 않습니다. 값이 누락되거나, 중복되거나, 잘못된 형식으로 입력된 경우가 비일비재합니다. 이러한 '더러운' 데이터를 그대로 분석에 사용하면 잘못된 결론을 도출하게 될 위험이 매우 큽니다. "Garbage in, garbage out(쓰레기를 넣으면 쓰레기가 나온다)"이라는 말처럼, 분석의 질은 데이터의 질에 의해 결정됩니다. 데이터 과학자의 시간 중 80%가 데이터 정제와 전처리에 사용된다는 말이 있을 정도로 이 과정은 중요하지만, 종종 간과되곤 합니다.
4.1. 결측치(Missing Values) 다루기
결측치(Pandas에서는 보통 NaN - Not a Number로 표시됨)는 데이터 수집 과정에서 다양한 이유로 발생합니다. 이를 처리하는 방법은 크게 두 가지, 제거하거나 다른 값으로 채우는 방법이 있습니다.
- 제거 (
.dropna()): 가장 간단한 방법입니다. 결측치가 포함된 행이나 열 전체를 삭제합니다. 데이터가 충분히 많고 결측치가 일부에만 존재할 때 유용합니다. 하지만 소중한 데이터를 잃어버릴 수 있다는 단점이 있습니다.axis파라미터로 행(0) 또는 열(1)을 지정하고,how='all'을 통해 모든 값이 결측치인 경우에만 삭제하는 등의 옵션을 사용할 수 있습니다. - 채우기 (
.fillna()): 결측치를 특정 값으로 대체하는 방법입니다.- 특정 값으로 채우기:
df.fillna(0)처럼 0이나 "정보 없음"과 같은 특정 값으로 채울 수 있습니다. - 통계값으로 채우기: 해당 열의 평균(
.mean()), 중앙값(.median()), 최빈값(.mode())으로 채우는 것은 매우 일반적인 전략입니다. 어떤 통계값을 사용할지는 데이터의 분포에 따라 신중하게 결정해야 합니다. - 앞/뒤 값으로 채우기:
method='ffill'(앞의 값으로 채우기)이나method='bfill'(뒤의 값으로 채우기)은 시계열 데이터처럼 데이터 간에 순서가 중요할 때 유용합니다.
- 특정 값으로 채우기:
# 결측치를 포함한 샘플 데이터 생성
data_with_nan = {'과목': ['수학', '영어', '과학', '역사', '국어'],
'점수': [90, 85, np.nan, 75, 95],
'학생수': [25, np.nan, 22, 28, 25]}
df_nan = pd.DataFrame(data_with_nan)
print("--- 결측치가 있는 원본 데이터 ---")
print(df_nan)
# 결측치 확인
print("\n--- .isnull()로 결측치 확인 ---")
print(df_nan.isnull())
# 결측치가 있는 행 제거
print("\n--- .dropna()로 행 제거 ---")
print(df_nan.dropna())
# '점수' 열의 결측치를 평균값으로 채우기
score_mean = df_nan['점수'].mean()
df_filled = df_nan.copy() # 원본 보존을 위해 복사
df_filled['점수'] = df_filled['점수'].fillna(score_mean)
print("\n--- .fillna()로 평균값 채우기 ---")
print(df_filled)
결측치를 어떻게 처리할지에 대한 정답은 없습니다. 이는 데이터의 특성과 분석의 목적에 따라 달라지는 '분석가의 결정' 영역입니다. 무작정 제거하거나 평균값으로 채우는 것은 데이터의 분포를 왜곡시킬 수 있으므로, 왜 결측치가 발생했는지 먼저 고민하고, 어떤 처리 방식이 가장 합리적인지 판단하는 것이 중요합니다.
4.2. 중복된 데이터 처리
데이터 입력 오류나 시스템 문제로 인해 완전히 동일한 데이터가 여러 번 기록될 수 있습니다. 이러한 중복 데이터는 집계 시 값을 부풀리는 등 분석 결과를 왜곡시키므로 반드시 처리해야 합니다.
.duplicated(): 각 행이 중복인지 아닌지를 불리언 Series로 반환합니다..drop_duplicates(): 중복된 행을 제거한 새로운 DataFrame을 반환합니다.keep파라미터를 이용해 처음 나타난 값('first'), 마지막 값('last') 중 어떤 것을 남길지 정하거나, 중복된 모든 값을 제거('False')할 수 있습니다.
# 중복 데이터를 포함한 샘플 생성
data_dup = {'A': [1, 2, 2, 3, 4], 'B': ['x', 'y', 'y', 'z', 'w']}
df_dup = pd.DataFrame(data_dup)
print("--- 중복 데이터가 있는 원본 ---")
print(df_dup)
# 중복 확인
print("\n--- .duplicated()로 중복 확인 ---")
print(df_dup.duplicated())
# 중복 제거
print("\n--- .drop_duplicates()로 중복 제거 ---")
print(df_dup.drop_duplicates())
4.3. 데이터 변환 (Transformation)
데이터 정제는 단순히 값을 지우고 채우는 것에서 끝나지 않습니다. 분석에 적합한 형태로 데이터를 '변환'하는 과정도 포함됩니다. 예를 들어, '1,000원'과 같이 문자열로 저장된 가격 정보를 숫자 1000으로 바꾸거나, 카테고리형 데이터를 분석에 용이한 형태로 변경하는 작업 등이 여기에 해당합니다.
.astype(): 열의 데이터 타입을 변경합니다. 메모리 효율성을 높이거나, 숫자 연산을 위해 문자열을 숫자로 바꾸는 등 필수적인 작업입니다..apply(): DataFrame의 행이나 열에 복잡한 함수를 일괄적으로 적용할 때 사용합니다.lambda함수와 함께 사용하면 매우 강력한 데이터 변환 도구가 됩니다..map(),.replace(): Series의 각 요소에 함수를 적용하거나 특정 값을 다른 값으로 대체할 때 사용합니다.
예를 들어, 학생들의 점수를 바탕으로 'A', 'B', 'C' 등급을 부여하는 새로운 '등급' 열을 만든다고 상상해봅시다. .apply()와 사용자 정의 함수를 이용하면 이 작업을 손쉽게 처리할 수 있습니다.
# 등급을 부여하는 함수 정의
def get_grade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
else:
return 'D'
# df_nan 원본을 다시 사용 (결측치 처리 전)
print("--- 점수 데이터 ---")
print(df_nan['점수'])
# '점수' 열에 get_grade 함수를 적용하여 '등급' 열 생성
# .dropna()를 사용하여 NaN이 아닌 값에만 함수를 적용
df_nan['등급'] = df_nan['점수'].dropna().apply(get_grade)
print("\n--- 등급 열 추가 ---")
print(df_nan)
이처럼 데이터 정제와 변환은 분석의 토대를 다지는 매우 중요한 과정입니다. 이 단계에서 시간과 노력을 투자할수록 이후의 분석 과정이 더 순조롭고, 결과의 신뢰도는 더 높아집니다.
5. 데이터 요약하기: 그룹화(Grouping)와 집계(Aggregation)
개별 데이터 포인트를 하나씩 살펴보는 것을 넘어, 데이터 전체의 특징을 파악하고 그룹별로 비교하려면 데이터를 요약하는 과정이 필요합니다. 예를 들어, "각 도시별 평균 나이는 얼마인가?", "각 과목별 최고 점수는 몇 점인가?"와 같은 질문에 답하려면 데이터를 특정 기준에 따라 그룹으로 묶고, 각 그룹에 대한 통계량을 계산해야 합니다. Pandas에서는 이 과정을 .groupby() 연산을 통해 매우 효율적으로 처리하며, 이는 "Split-Apply-Combine"이라는 강력한 패러다임을 따릅니다.
Split-Apply-Combine 패러다임의 시각적 이해:
[ Original DataFrame ]
+-------+----+------+
| 이름 | 나이| 도시 |
+-------+----+------+
| 김철수| 28 | 서울 |
| 이영희| 35 | 부산 |
| 박지성| 42 | 서울 |
| 손흥민| 31 | 런던 |
+-------+----+------+
|
V (Split by '도시')
-------------------------------------------------
[Group: 서울] [Group: 부산] [Group: 런던]
+-------+----+ +-------+----+ +-------+----+
| 김철수| 28 | | 이영희| 35 | | 손흥민| 31 |
| 박지성| 42 | +-------+----+ +-------+----+
+-------+----+
| | |
V (Apply function: .mean() on '나이')
-------------------------------------------------
[Result: 35.0] [Result: 35.0] [Result: 31.0]
| | |
V (Combine)
-------------------------------------------------
[ Final Result (Series) ]
도시
서울 35.0
부산 35.0
런던 31.0
Name: 나이, dtype: float64
- Split: 특정 기준(예: '도시' 열)에 따라 데이터를 여러 그룹으로 분할합니다.
- Apply: 각 그룹에 대해 독립적으로 함수(예: 평균, 합계, 개수 등)를 적용합니다.
- Combine: Apply 단계에서 나온 결과들을 하나의 새로운 데이터 구조(Series 또는 DataFrame)로 결합합니다.
이 패러다임을 이해하면 Pandas의 그룹 연산을 훨씬 더 깊이 있게 활용할 수 있습니다. .groupby() 자체는 분할된 그룹 객체만을 생성하며, 그 뒤에 집계 함수(.sum(), .mean(), .count() 등)를 적용해야 비로소 의미 있는 결과가 나옵니다.
# '도시'를 기준으로 그룹화한 후, 각 도시의 평균 나이를 계산
city_age_mean = df.groupby('도시')['나이'].mean()
print("--- 도시별 평균 나이 ---")
print(city_age_mean)
# 여러 열을 기준으로 그룹화할 수도 있습니다.
# 더 복잡한 데이터를 만들어 실습해 보겠습니다.
sales_data = {'매장': ['강남', '강남', '홍대', '홍대', '잠실'],
'요일': ['월', '화', '월', '화', '월'],
'매출': [100, 120, 80, 90, 150]}
sales_df = pd.DataFrame(sales_data)
print("\n--- 매출 데이터 원본 ---")
print(sales_df)
# 매장별, 요일별 매출 합계 계산
sales_by_store_day = sales_df.groupby(['매장', '요일'])['매출'].sum()
print("\n--- 매장별, 요일별 매출 합계 (MultiIndex Series) ---")
print(sales_by_store_day)
더 나아가, .agg()(aggregate) 메소드를 사용하면 하나의 그룹 연산에서 여러 개의 집계 함수를 동시에 적용할 수 있어 매우 편리합니다.
# 각 매장별 매출의 합계와 평균을 동시에 계산
store_agg = sales_df.groupby('매장')['매출'].agg(['sum', 'mean', 'count'])
print("\n--- 매장별 매출 합계, 평균, 건수 ---")
print(store_agg)
# 각 열마다 다른 집계 함수를 적용할 수도 있습니다.
# '매출'은 합계를, '요일'은 몇 건인지(count) 계산
store_custom_agg = sales_df.groupby('매장').agg({'매출': 'sum', '요일': 'count'})
# 결과 컬럼명을 더 명확하게 변경
store_custom_agg.rename(columns={'매출': '총매출', '요일': '영업일수'}, inplace=True)
print("\n--- 매장별 맞춤 집계 ---")
print(store_custom_agg)
그룹화와 집계는 데이터를 고차원적으로 요약하고 인사이트를 도출하는 핵심적인 과정입니다. 어떤 기준으로 그룹을 나눌지, 각 그룹에서 어떤 정보를 추출할지를 결정하는 것은 분석가의 역량이자 데이터 분석의 창의성이 발휘되는 부분입니다. 이 기능을 통해 우리는 수백만 건의 로우 데이터 속에서도 비즈니스의 핵심 동향과 패턴을 발견할 수 있습니다.
6. 종합 실전 예제: 작은 프로젝트로 흐름 익히기
지금까지 배운 Pandas의 핵심 기능들을 종합하여 작은 미니 프로젝트를 진행해보겠습니다. 가상의 온라인 상점 매출 데이터를 분석하여 의미 있는 정보를 찾아내는 과정을 처음부터 끝까지 따라가 보겠습니다. 이를 통해 개별 기능들이 실제 분석 흐름 속에서 어떻게 유기적으로 연결되는지 체감할 수 있을 것입니다.
Step 1: 데이터 생성 및 로드
실제 상황에서는 pd.read_csv()나 pd.read_excel() 함수로 외부 파일을 불러오겠지만, 여기서는 직접 DataFrame을 생성하겠습니다.
# 가상의 매출 데이터 생성
data = {
'주문번호': range(1001, 1011),
'주문일자': pd.to_datetime(['2023-03-15', '2023-03-15', '2023-03-16', '2023-03-16', '2023-03-17',
'2023-03-17', '2023-03-18', '2023-03-18', '2023-03-18', '2023-03-19']),
'고객ID': ['C01', 'C02', 'C03', 'C01', 'C04', 'C02', 'C03', 'C01', 'C04', np.nan],
'상품카테고리': ['가전', '의류', '식품', '가구', '가전', '식품', '의류', '가전', np.nan, '가구'],
'결제금액': [1200000, 89000, 15000, 450000, 2500000, 22000, 75000, 300000, 1800000, 320000],
'결제수단': ['카드', '현금', '카드', '카드', '할부', '현금', '카드', '카드', '할부', '카드']
}
order_df = pd.DataFrame(data)
print("--- 원본 주문 데이터 ---")
order_df
Step 2: 데이터 탐색 및 구조 파악 (EDA)
데이터를 받았으니, 상태를 점검해 봅시다.
print("--- 데이터 기본 정보 (.info()) ---")
order_df.info()
print("\n--- 숫자 데이터 요약 (.describe()) ---")
print(order_df.describe())
.info()를 통해 '고객ID'와 '상품카테고리'에 결측치가 하나씩 있음을 확인했습니다. '주문일자'는 datetime64 타입으로 잘 변환되어 시간 관련 분석이 용이합니다. .describe()를 보면 결제금액의 편차가 매우 크다는 것을 알 수 있습니다(std가 89만). 평균(67.7만)과 중앙값(31만)의 차이가 큰 것으로 보아 고가의 상품(아마도 가전)이 평균을 끌어올리고 있음을 짐작할 수 있습니다.
Step 3: 데이터 정제 (Cleaning)
탐색 단계에서 발견한 결측치를 처리하겠습니다. '고객ID'가 없는 주문은 분석에서 제외하는 것이 합리적일 수 있습니다. '상품카테고리'가 없는 경우는 '기타'로 분류하거나, 해당 주문의 다른 정보를 보고 추정할 수도 있습니다. 여기서는 결측치가 있는 행을 모두 제거하는 간단한 방법을 선택하겠습니다.
# 결측치가 있는 행 제거
cleaned_df = order_df.dropna()
print("--- 결측치 제거 후 데이터 정보 ---")
cleaned_df.info()
# 중복된 주문이 있는지 확인 (여기서는 없지만 실제 데이터에서는 필수)
print("\n중복된 행의 수:", cleaned_df.duplicated().sum())
결측치가 있던 2개의 행이 제거되어 총 8개의 데이터가 남았습니다.
Step 4: 데이터 분석 및 질문에 답하기
이제 정제된 데이터를 바탕으로 비즈니스 질문에 답해봅시다.
질문 1: 가장 매출이 높은 상품 카테고리는 무엇인가?
카테고리별로 그룹화하여 결제금액의 합계를 구하면 됩니다.
# 상품 카테고리별 매출 합계 계산
sales_by_category = cleaned_df.groupby('상품카테고리')['결제금액'].sum().sort_values(ascending=False)
print("--- 상품 카테고리별 총 매출 ---")
print(sales_by_category)
분석 결과, '가전' 카테고리의 매출이 400만 원으로 가장 높고, '가구', '의류', '식품' 순으로 뒤를 잇습니다.
질문 2: 충성 고객(가장 많이 구매한 고객)은 누구이며, 총 얼마를 사용했는가?
고객ID별로 그룹화하여 결제금액의 합계와 주문 횟수(count)를 계산합니다.
# 고객별 총 결제금액 및 주문 횟수 집계
customer_agg = cleaned_df.groupby('고객ID')['결제금액'].agg(['sum', 'count']).sort_values(by='sum', ascending=False)
customer_agg.columns = ['총구매액', '구매횟수'] # 컬럼 이름 변경
print("\n--- 고객별 구매 분석 ---")
print(customer_agg)
'C01' 고객이 3번의 구매로 총 195만 원을 사용하여 가장 충성도가 높은 고객임을 알 수 있습니다. 'C04' 고객은 단 한 번의 구매로 250만 원을 사용한 VIP 고객이네요.
질문 3: 일자별 매출 추이는 어떠한가?
주문일자별로 그룹화하여 결제금액의 합계를 계산합니다.
# 일자별 매출 합계
daily_sales = cleaned_df.groupby('주문일자')['결제금액'].sum()
print("\n--- 일자별 매출 추이 ---")
print(daily_sales)
# 여기서 더 나아가 Matplotlib 같은 시각화 라이브러리를 사용하면 추이를 그래프로 명확하게 볼 수 있습니다.
# import matplotlib.pyplot as plt
# daily_sales.plot(kind='line', marker='o', figsize=(10, 5))
# plt.title('일자별 매출 추이')
# plt.ylabel('매출액')
# plt.grid(True)
# plt.show()
데이터를 보면 17일에 가장 높은 매출을 기록했음을 알 수 있습니다. 이런 데이터를 시각화하면 그 패턴을 더욱 직관적으로 파악할 수 있습니다.
이 미니 프로젝트를 통해 우리는 데이터 로드, 탐색, 정제, 그룹화, 집계라는 데이터 분석의 전체적인 흐름을 경험했습니다. Pandas의 각 기능은 독립적으로 존재하지 않으며, 이처럼 하나의 목표를 위해 유기적으로 결합될 때 비로소 그 진정한 가치를 발휘합니다.
마치며: Pandas는 끝이 아닌 시작
이 글을 통해 우리는 Pandas의 기본적인 데이터 구조부터 시작하여 데이터 탐색, 선택, 정제, 그리고 그룹화에 이르는 핵심적인 기능들을 깊이 있게 살펴보았습니다. 단순히 함수 사용법을 아는 것을 넘어, 각 기능이 '왜' 필요한지, 그리고 데이터 분석이라는 큰 그림 속에서 어떤 역할을 하는지에 초점을 맞추고자 노력했습니다.
Pandas는 데이터와 소통하는 언어입니다. 처음에는 문법이 낯설고 복잡하게 느껴질 수 있지만, 꾸준히 사용하며 데이터에 질문을 던지고 답을 찾는 과정을 반복하다 보면 어느새 이 언어에 익숙해진 자신을 발견하게 될 것입니다. 오늘 배운 내용들은 여러분이 앞으로 마주할 수많은 데이터를 탐험하는 데 튼튼한 기초가 되어줄 것입니다.
하지만 기억해야 할 것은, Pandas는 데이터 분석 여정의 끝이 아닌 시작이라는 점입니다. Pandas로 정제하고 요약한 데이터는 Matplotlib, Seaborn과 같은 시각화 라이브러리를 통해 강력한 인사이트를 전달하는 차트와 그래프로 다시 태어납니다. 또한 Scikit-learn과 같은 머신러닝 라이브러리의 입력값으로 사용되어 미래를 예측하는 모델을 만드는 데 쓰이기도 합니다. Pandas를 능숙하게 다루는 능력은 이 모든 고급 분석으로 나아가는 필수적인 첫걸음입니다.
이제 여러분의 컴퓨터에 잠자고 있는 흥미로운 데이터 파일을 열어보세요. 그리고 오늘 배운 Pandas의 도구들을 이용해 데이터에게 첫 질문을 던져보시길 바랍니다. 그 질문이 데이터 분석이라는 흥미진진한 세계의 문을 여는 열쇠가 될 것입니다.