Pandas ~ : )

파이썬 데이터 분석 및 조작을 위한 필수 라이브러리

작성자:   ~ : )
(with Gemini)
작성일: 2025년 5월 18일
수정일: 2025년 6월 11일

▼ 학습 시작하기 ▼

머리말

파이썬을 활용한 데이터 분석의 세계에 오신 것을 환영합니다! 이 가이드는 데이터 처리와 분석을 위한 가장 강력하고 인기 있는 라이브러리 중 하나인 Pandas의 모든 것을 깊이 있게 탐구하고자 합니다.

Pandas는 복잡하고 지저분한 실제 데이터를 다루기 쉬운 형태로 변환하고, 의미 있는 통찰력을 추출하는 데 필요한 다양한 도구를 제공합니다. Series와 DataFrame이라는 직관적인 자료구조를 통해, 여러분은 마치 스프레드시트나 SQL 데이터베이스를 다루듯 유연하게 데이터를 조작할 수 있게 됩니다.

이 문서는 Pandas의 기본 개념부터 시작하여 데이터 입출력, 선택, 정제, 그룹화, 병합, 시계열 분석, 그리고 간단한 시각화에 이르기까지 핵심적인 기능들을 체계적으로 안내할 것입니다. 각 장의 설명과 풍부한 예제, 그리고 연습 문제를 통해 Pandas 활용 능력을 효과적으로 향상시킬 수 있도록 구성했습니다.

본 가이드가 여러분이 데이터 전문가로 성장하는 데 든든한 밑거름이 되기를 바라며, Pandas와 함께 데이터의 잠재력을 최대한 발휘하는 즐거움을 경험하시기를 응원합니다.

- ~ : ) (with Gemini)

☆ 개요: 각 장 요약

이 Pandas 가이드는 여러분이 데이터 분석가로서 필요한 핵심 기술을 단계별로 익힐 수 있도록 설계되었습니다. 각 장의 주요 내용은 다음과 같습니다.

제 1 장: Pandas란 무엇인가?

Pandas는 파이썬 프로그래밍 언어를 위한 고성능의 사용하기 쉬운 데이터 구조와 데이터 분석 도구를 제공하는 핵심 라이브러리입니다. Wes McKinney에 의해 2008년에 처음 개발되었으며, 현재는 파이썬 데이터 과학 생태계에서 가장 중요한 라이브러리 중 하나로 인정받고 있습니다.

Pandas라는 이름은 "Panel Data" (다차원 구조화 데이터셋을 일컫는 계량경제학 용어)와 "Python Data Analysis"의 합성어에서 유래했습니다. 주된 목표는 파이썬을 강력하고 유연한 실제 데이터 분석 환경으로 만드는 것입니다.

1.1. Pandas의 주요 기능 및 특징

1.2. 왜 Pandas를 사용하는가?

현대의 데이터는 양이 방대하고 형식이 다양하며, 종종 결측치나 오류를 포함하고 있습니다. Pandas는 이러한 "지저분한(messy)" 실제 데이터를 효과적으로 다룰 수 있도록 설계되었습니다.

데이터를 불러오고, 정제하고, 변환하고, 분석하고, 시각화하는 전체 데이터 분석 파이프라인에서 Pandas는 핵심적인 역할을 수행합니다. 따라서 파이썬으로 데이터 분석을 시작한다면 Pandas 학습은 필수적입니다.

1.3. NumPy와의 관계

Pandas는 NumPy를 기반으로 구축되었습니다. Pandas의 Series와 DataFrame 내부의 데이터는 대부분 NumPy의 ndarray로 저장됩니다. 이로 인해 Pandas는 NumPy의 다음과 같은 장점들을 그대로 활용합니다:

NumPy가 주로 동종의 수치 데이터 배열을 다루는 데 중점을 둔다면, Pandas는 이종의 데이터(숫자, 문자열, 불리언 등 혼합 가능)를 테이블 형태로 다루고, 각 행과 열에 명시적인 인덱스와 컬럼명을 부여하여 데이터의 의미를 더 명확하게 파악하고 조작할 수 있도록 확장한 것입니다. 즉, NumPy가 저수준의 수치 연산 인프라를 제공한다면, Pandas는 이를 바탕으로 더 고수준의 데이터 분석 기능을 제공한다고 볼 수 있습니다.

예시 코드: Pandas와 NumPy의 기본적인 사용

import numpy as np
import pandas as pd

# Pandas Series 생성
s = pd.Series([1, 3, 5, np.nan, 6, 8]) # np.nan은 NumPy의 결측치 표현
print("Pandas Series:\n", s)

# Pandas DataFrame 생성
dates = pd.date_range('20250101', periods=6)
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))
print("\nPandas DataFrame:\n", df)

# NumPy 배열로 변환
numpy_array_from_df = df.to_numpy()
print("\nDataFrame to NumPy array (first 2 rows):\n", numpy_array_from_df[:2])

예상 결과 (randn 부분은 실행 시마다 다름):

Pandas Series:
0    1.0
1    3.0
2    5.0
3    NaN
4    6.0
5    8.0
dtype: float64

Pandas DataFrame:
                   A         B         C         D
2025-01-01  0.469112 -0.282863 -1.509059 -1.135632
2025-01-02  1.212112 -0.173215  0.119209 -1.044236
2025-01-03 -0.861849 -2.104569 -0.494929  1.071804
2025-01-04  0.721555 -0.706771 -1.039575  0.271860
2025-01-05 -0.424972  0.567020  0.276232 -1.087401
2025-01-06 -0.673690  0.113648 -1.478427  0.524988

DataFrame to NumPy array (first 2 rows):
 [[ 0.46911232 -0.28286334 -1.5090585   -1.13563237]
  [ 1.21211222 -0.17321465  0.11920891  -1.04423599]]

제 2 장: Pandas 자료구조 – Series 와 DataFrame

Pandas의 핵심은 두 가지 주요 자료구조인 SeriesDataFrame입니다. 이 두 구조를 이해하는 것이 Pandas를 효과적으로 사용하는 첫걸음입니다.

2.1. Series

Series는 어떤 데이터 타입(정수, 문자열, 부동소수점, 파이썬 객체 등)이든 담을 수 있는 1차원 배열과 같은 자료구조입니다. 각 데이터 포인트는 인덱스(index)라는 레이블과 연결됩니다. 인덱스는 기본적으로 0부터 시작하는 정수이지만, 사용자가 원하는 값으로 지정할 수도 있습니다.

Series 생성

리스트, NumPy 배열, 딕셔너리 등으로부터 Series를 생성할 수 있습니다.

예시 코드:

import numpy as np
import pandas as pd

# 리스트로부터 Series 생성
s_list = pd.Series([10, 20, 30, 40, 50])
print("Series from list:\n", s_list)

# NumPy 배열로부터 Series 생성, 인덱스 지정
s_numpy = pd.Series(np.array([1.5, 2.3, 3.1, 4.7]), index=['a', 'b', 'c', 'd'])
print("\nSeries from NumPy array with custom index:\n", s_numpy)

# 딕셔너리로부터 Series 생성 (딕셔너리 키가 인덱스가 됨)
s_dict_data = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}
s_dict = pd.Series(s_dict_data)
print("\nSeries from dictionary:\n", s_dict)

# Series에 이름(name) 부여
s_list.name = 'SampleNumbers'
s_list.index.name = 'ID'
print("\nSeries with name and index name:\n", s_list)

예상 결과:

Series from list:
0    10
1    20
2    30
3    40
4    50
dtype: int64

Series from NumPy array with custom index:
a    1.5
b    2.3
c    3.1
d    4.7
dtype: float64

Series from dictionary:
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64

Series with name and index name:
ID
0    10
1    20
2    30
3    40
4    50
Name: SampleNumbers, dtype: int64

Series 속성 및 인덱싱

Series는 index, values, dtype, name, size, ndim 등의 속성을 가집니다. 인덱싱과 슬라이싱은 NumPy 배열과 유사하게 동작합니다.

예시 코드:

import pandas as pd
s = pd.Series([10, 20, 30, 40, 50], index=['a', 'b', 'c', 'd', 'e'], name='MyData')
print("Original Series:\n", s)
print("\ns.index:", s.index)
print("s.values:", s.values) # NumPy 배열 반환
print("s.dtype:", s.dtype)
print("s.name:", s.name)
print("s.size:", s.size)

# 인덱싱
print("\ns['c']:", s['c']) # 라벨 기반 인덱싱
print("s[2]:", s[2])   # 위치 기반 인덱싱 (라벨이 정수가 아닐 때 명확)

# 슬라이싱
print("\ns['b':'d'] (라벨 슬라이싱, 끝점 포함):\n", s['b':'d'])
print("s[1:4] (위치 슬라이싱, 끝점 미포함):\n", s[1:4])

# 불리언 인덱싱
print("\ns[s > 25]:\n", s[s > 25])

예상 결과:

Original Series:
a    10
b    20
c    30
d    40
e    50
Name: MyData, dtype: int64

s.index: Index(['a', 'b', 'c', 'd', 'e'], dtype='object')
s.values: [10 20 30 40 50]
s.dtype: int64
s.name: MyData
s.size: 5

s['c']: 30
s[2]: 30

s['b':'d'] (라벨 슬라이싱, 끝점 포함):
b    20
c    30
d    40
Name: MyData, dtype: int64
s[1:4] (위치 슬라이싱, 끝점 미포함):
b    20
c    30
d    40
Name: MyData, dtype: int64

s[s > 25]:
c    30
d    40
e    50
Name: MyData, dtype: int64

2.2. DataFrame

DataFrame은 여러 개의 Series를 같은 인덱스로 묶어놓은 것과 같은 2차원 테이블 형태의 자료구조입니다. 각 열(column)은 서로 다른 데이터 타입을 가질 수 있습니다. 행과 열에 각각 인덱스(index)와 컬럼명(columns)이라는 레이블이 있습니다.

DataFrame 생성

가장 일반적인 방법은 각 열의 이름을 키로 하고 리스트나 NumPy 배열을 값으로 하는 딕셔너리를 사용하는 것입니다. 리스트의 리스트, NumPy 2차원 배열, 다른 DataFrame 등으로부터도 생성 가능합니다.

예시 코드:

import numpy as np
import pandas as pd

# 딕셔너리로부터 DataFrame 생성
data_dict = {'이름': ['Alice', 'Bob', 'Charlie', 'David'],
             '나이': [25, 30, 22, 35],
             '도시': ['서울', '부산', '서울', '인천']}
df_dict = pd.DataFrame(data_dict)
print("DataFrame from dictionary:\n", df_dict)

# 인덱스 지정
df_dict_idx = pd.DataFrame(data_dict, index=['id1', 'id2', 'id3', 'id4'])
print("\nDataFrame with custom index:\n", df_dict_idx)

# NumPy 2차원 배열로부터 DataFrame 생성, 컬럼명과 인덱스 지정
data_np = np.array([[1,2,3],[4,5,6],[7,8,9]])
df_np = pd.DataFrame(data_np, columns=['A', 'B', 'C'], index=['x', 'y', 'z'])
print("\nDataFrame from NumPy array:\n", df_np)

예상 결과:

DataFrame from dictionary:
      이름  나이  도시
0  Alice  25  서울
1    Bob  30  부산
2 Charlie  22  서울
3   David  35  인천

DataFrame with custom index:
       이름  나이  도시
id1  Alice  25  서울
id2    Bob  30  부산
id3 Charlie  22  서울
id4   David  35  인천

DataFrame from NumPy array:
   A  B  C
x  1  2  3
y  4  5  6
z  7  8  9

DataFrame 속성 및 기본 정보 확인

index, columns, values, dtypes, shape, size, ndim 등의 속성과 head(), tail(), info(), describe() 메서드로 기본 정보를 파악합니다.

예시 코드:

import pandas as pd
data = {'col1': [1, 2, 3.5], 'col2': ['a', 'b', 'c'], 'col3': [True, False, True]}
df = pd.DataFrame(data)
print("DataFrame:\n", df)

print("\ndf.index:", df.index)
print("df.columns:", df.columns)
print("df.values (NumPy array):\n", df.values)
print("df.dtypes:\n", df.dtypes) # 각 열의 데이터 타입
print("df.shape:", df.shape) # (행 수, 열 수)
print("df.ndim:", df.ndim) # 차원 수 (DataFrame은 항상 2)
print("df.size:", df.size) # 전체 요소 수

print("\ndf.head(2) (처음 2개 행):\n", df.head(2))
print("\ndf.tail(1) (마지막 1개 행):\n", df.tail(1))
print("\ndf.info() (요약 정보):")
df.info()

예상 결과:

DataFrame:
   col1 col2   col3
0   1.0    a   True
1   2.0    b  False
2   3.5    c   True

df.index: RangeIndex(start=0, stop=3, step=1)
df.columns: Index(['col1', 'col2', 'col3'], dtype='object')
df.values (NumPy array):
 [[1.0 'a' True]
  [2.0 'b' False]
  [3.5 'c' True]]
df.dtypes:
col1    float64
col2     object
col3       bool
dtype: object
df.shape: (3, 3)
df.ndim: 2
df.size: 9

df.head(2) (처음 2개 행):
   col1 col2   col3
0   1.0    a   True
1   2.0    b  False

df.tail(1) (마지막 1개 행):
   col1 col2  col3
2   3.5    c  True

df.info() (요약 정보):
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   col1    3 non-null      float64
 1   col2    3 non-null      object 
 2   col3    3 non-null      bool   
dtypes: bool(1), float64(1), object(1)
memory usage: 200.0+ bytes

제 3 장: 데이터 불러오기 및 저장하기

Pandas는 다양한 파일 형식으로부터 데이터를 읽고 쓰는 강력한 기능을 제공합니다. 가장 흔하게 사용되는 CSV와 Excel 파일을 중심으로 살펴보겠습니다.

CSV 파일 읽고 쓰기

pd.read_csv() 함수는 CSV(Comma-Separated Values) 파일을 DataFrame으로 읽어오며, df.to_csv() 메서드는 DataFrame을 CSV 파일로 저장합니다.

예시 코드:

import pandas as pd
import numpy as np

# 예제 DataFrame 생성
data = {'ID': [1, 2, 3, 4],
        'Name': ['Product A', 'Product B', 'Product C', 'Product D'],
        'Price': [1000, 1500, 800, 1200],
        'Stock': [10, np.nan, 5, 8]} # 결측치 포함
df_sample = pd.DataFrame(data)

# CSV 파일로 저장 (인덱스 제외)
csv_filename = 'sample_products.csv'
df_sample.to_csv(csv_filename, index=False, encoding='utf-8-sig') # Excel에서 한글 깨짐 방지
print(f"DataFrame saved to {csv_filename}")

# CSV 파일 읽기
df_loaded = pd.read_csv(csv_filename)
print("\nLoaded DataFrame from CSV:\n", df_loaded)

# 주요 파라미터 예시
# df_custom = pd.read_csv('other_file.csv', sep=';', header=None, names=['colA', 'colB'], index_col='colA', na_values=['?','MISSING'])
# print("\nCustom loaded DataFrame (예시):\n", df_custom) # 실제 파일 필요

예상 결과 (sample_products.csv 파일 생성 및 로드):

DataFrame saved to sample_products.csv

Loaded DataFrame from CSV:
   ID       Name   Price  Stock
0   1  Product A  1000.0   10.0
1   2  Product B  1500.0    NaN
2   3  Product C   800.0    5.0
3   4  Product D  1200.0    8.0

주요 read_csv 파라미터:

  • filepath_or_buffer: 파일 경로, URL 또는 파일 객체.
  • sep (또는 delimiter): 구분자 (기본값: ',').
  • header: 헤더로 사용할 행 번호 (기본값: 0, 첫 행). None이면 자동 생성.
  • names: header=None일 때 사용할 컬럼명 리스트.
  • index_col: 인덱스로 사용할 열 번호 또는 이름.
  • usecols: 읽어올 열의 리스트 또는 이름.
  • dtype: 각 열의 데이터 타입 지정 (딕셔너리 형태).
  • na_values: 결측치로 간주할 문자열 리스트.
  • skiprows: 파일 시작 부분에서 건너뛸 행 수.
  • nrows: 읽어올 행 수.
  • encoding: 파일 인코딩 (예: 'utf-8', 'cp949').

주요 to_csv 파라미터:

  • path_or_buf: 저장할 파일 경로.
  • sep: 구분자 (기본값: ',').
  • na_rep: 결측치를 나타낼 문자열 (기본값: '').
  • float_format: 부동소수점 숫자 형식 지정.
  • columns: 저장할 열 선택.
  • header: 컬럼명을 쓸지 여부 (기본값: True).
  • index: 인덱스를 쓸지 여부 (기본값: True).
  • encoding: 파일 인코딩.

Excel 파일 읽고 쓰기

pd.read_excel() 함수는 Excel 파일을 DataFrame으로 읽어오며, df.to_excel() 메서드는 DataFrame을 Excel 파일로 저장합니다. (openpyxl 또는 xlrd/xlsxwriter 라이브러리 필요)

예시 코드:

import pandas as pd

# 예제 DataFrame (위 CSV 예제 df_sample 재사용 가정)
data_excel = {'ID': [1, 2, 3, 4],
              'Name': ['Product A', 'Product B', 'Product C', 'Product D'],
              'Price': [1000, 1500, 800, 1200],
              'Stock': [10, None, 5, 8]}
df_to_excel = pd.DataFrame(data_excel)

excel_filename = 'sample_products.xlsx'
# Excel 파일로 저장 (인덱스 제외, 특정 시트 이름으로)
df_to_excel.to_excel(excel_filename, sheet_name='Products', index=False)
print(f"DataFrame saved to {excel_filename} (sheet 'Products')")

# Excel 파일 읽기 (특정 시트)
df_loaded_excel = pd.read_excel(excel_filename, sheet_name='Products')
# df_loaded_excel_idx = pd.read_excel(excel_filename, index_col=0) # 첫 번째 열을 인덱스로

print("\nLoaded DataFrame from Excel:\n", df_loaded_excel)

# 여러 시트를 가진 Excel 파일 작업 예시 (주석 처리)
# with pd.ExcelWriter('multiple_sheets.xlsx') as writer:
#     df_sample.to_excel(writer, sheet_name='Sheet1', index=False)
#     df_loaded.to_excel(writer, sheet_name='Sheet2', index=False)
# all_sheets_dict = pd.read_excel('multiple_sheets.xlsx', sheet_name=None) # 모든 시트를 딕셔너리로
# print("\nAll sheets from 'multiple_sheets.xlsx':", list(all_sheets_dict.keys()))

예상 결과 (sample_products.xlsx 파일 생성 및 로드):

DataFrame saved to sample_products.xlsx (sheet 'Products')

Loaded DataFrame from Excel:
   ID       Name   Price  Stock
0   1  Product A  1000.0   10.0
1   2  Product B  1500.0    NaN
2   3  Product C   800.0    5.0
3   4  Product D  1200.0    8.0

주요 read_excel 파라미터:

  • io: 파일 경로, URL, 파일 객체.
  • sheet_name: 읽어올 시트 이름(문자열), 번호(0부터 시작), 또는 리스트(여러 시트). None이면 모든 시트를 딕셔너리로.
  • header, names, index_col, usecols, dtype, na_values, skiprowsread_csv와 유사한 파라미터 다수 지원.
  • engine: 사용할 파서 엔진 ('openpyxl' for .xlsx, 'xlrd' for .xls).

주요 to_excel 파라미터:

  • excel_writer: 파일 경로 또는 ExcelWriter 객체.
  • sheet_name: 저장할 시트 이름 (기본값: 'Sheet1').
  • na_rep, float_format, columns, header, indexto_csv와 유사한 파라미터.
  • startrow, startcol: 시트 내 데이터 쓰기 시작 위치.
  • engine: 사용할 쓰기 엔진.

이 외에도 Pandas는 JSON (pd.read_json, df.to_json), HTML (pd.read_html, df.to_html), SQL 데이터베이스 (pd.read_sql, df.to_sql), HDF5, Parquet 등 다양한 형식의 데이터를 지원합니다.

제 4 장: 데이터 선택 및 필터링

Pandas DataFrame에서 원하는 데이터를 효과적으로 추출하고 선택하는 것은 데이터 분석의 가장 기본적이면서도 중요한 단계입니다. 이 장에서는 DataFrame의 특정 열(column)이나 행(row), 또는 특정 조건에 맞는 데이터를 정확하게 선택하고 필터링하는 다양한 기법을 학습합니다. 주요 내용으로는 열 선택, 행 슬라이싱, 라벨 기반 선택 도구인 .loc, 위치 기반 선택 도구인 .iloc, 그리고 강력한 조건부 선택 방법인 불리언 인덱싱(Boolean indexing) 등이 있습니다.

4.1. 열(Column) 선택

DataFrame에서 특정 열을 선택하는 방법입니다.

단일 열 선택

하나의 열을 선택하면 Series 형태로 반환됩니다. df['컬럼명'] 또는 df.컬럼명 (컬럼명이 파이썬 변수 명명 규칙에 맞고 공백이 없을 경우) 형식으로 선택할 수 있습니다.

예시 코드:

import pandas as pd
import numpy as np

data = {'이름': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
        '나이': [25, 30, 22, 35, 28],
        '점수': [85, 90, 78, 92, 88],
        '도시': ['서울', '부산', '서울', '인천', '광주']}
df = pd.DataFrame(data)
print("원본 DataFrame:\n", df)

# '이름' 열 선택 (Series 반환)
names_series = df['이름']
print("\n'이름' 열 선택 (Series):\n", names_series)
print("Type:", type(names_series))

# '나이' 열 선택 (dot notation)
ages_series = df.나이 # 컬럼명이 '나이'이므로 가능
print("\n'나이' 열 선택 (dot notation, Series):\n", ages_series)

예상 결과:

원본 DataFrame:
      이름  나이  점수  도시
0  Alice  25  85  서울
1    Bob  30  90  부산
2 Charlie  22  78  서울
3   David  35  92  인천
4     Eve  28  88  광주

'이름' 열 선택 (Series):
0    Alice
1      Bob
2    Charlie
3      David
4      Eve
Name: 이름, dtype: object
Type: <class 'pandas.core.series.Series'>

'나이' 열 선택 (dot notation, Series):
0    25
1    30
2    22
3    35
4    28
Name: 나이, dtype: int64

다중 열 선택

여러 열을 동시에 선택하면 DataFrame 형태로 반환됩니다. 선택하려는 열 이름들을 리스트 형태로 전달합니다: df[['컬럼명1', '컬럼명2']].

예시 코드:

# '이름'과 '점수' 열 선택 (DataFrame 반환)
name_score_df = df[['이름', '점수']]
print("\n'이름', '점수' 열 선택 (DataFrame):\n", name_score_df)
print("Type:", type(name_score_df))

예상 결과:

'이름', '점수' 열 선택 (DataFrame):
      이름  점수
0  Alice  85
1    Bob  90
2 Charlie  78
3   David  92
4     Eve  88
Type: <class 'pandas.core.frame.DataFrame'>

활용 방안: 분석에 필요한 특정 변수들만 추출하거나, 개인정보와 같은 민감한 정보를 제외하고 데이터를 공유할 때 유용합니다.

4.2. 행(Row) 선택 및 슬라이싱

DataFrame에서 특정 행을 선택하거나 슬라이싱하는 기본적인 방법입니다. 이는 주로 위치 기반으로 동작하지만, 명시적인 선택을 위해서는 .loc이나 .iloc 사용이 권장됩니다.

행 슬라이싱

파이썬 리스트 슬라이싱과 유사하게 df[start:end] 형태로 특정 범위의 행을 선택할 수 있습니다. 이때 end 인덱스는 포함되지 않습니다.

예시 코드:

# 1번 인덱스부터 3번 인덱스까지 행 선택 (3번 인덱스는 미포함)
rows_sliced = df[1:4]
print("\n행 슬라이싱 (1부터 4 이전까지):\n", rows_sliced)

예상 결과:

행 슬라이싱 (1부터 4 이전까지):
      이름  나이  점수  도시
1    Bob  30  90  부산
2 Charlie  22  78  서울
3   David  35  92  인천

주의: 이 방식은 직관적이지만, 라벨 기반 인덱스를 사용하는 경우 혼동을 줄 수 있어 .loc 또는 .iloc를 사용하는 것이 더 명확합니다.

4.3. 라벨 기반 선택: .loc

.loc 접근자는 데이터의 라벨(인덱스명, 컬럼명)을 사용하여 데이터를 선택합니다. df.loc[row_labels, column_labels] 형태로 사용됩니다.

.loc 사용법

  • 단일 행 선택: df.loc[row_label] (Series 반환)
  • 다중 행 선택 (리스트): df.loc[['row_label1', 'row_label2']] (DataFrame 반환)
  • 다중 행 선택 (슬라이스): df.loc['start_label':'end_label'] (주의: 라벨 슬라이싱은 end_label을 포함합니다!)
  • 행과 열 동시 선택: df.loc[row_labels, column_labels]
  • 조건부 선택: df.loc[condition] 또는 df.loc[condition, column_labels] (아래 불리언 인덱싱 참조)

예시 코드:

# 예제 DataFrame 인덱스 변경
df_indexed = df.copy()
df_indexed.index = ['p1', 'p2', 'p3', 'p4', 'p5']
print("인덱스가 변경된 DataFrame:\n", df_indexed)

# 단일 행 선택 (라벨 'p2')
row_p2 = df_indexed.loc['p2']
print("\nloc['p2'] (Series):\n", row_p2)

# 다중 행 선택 (라벨 리스트 ['p1', 'p4'])
rows_p1_p4 = df_indexed.loc[['p1', 'p4']]
print("\nloc[['p1', 'p4']] (DataFrame):\n", rows_p1_p4)

# 다중 행 선택 (라벨 슬라이스 'p2':'p4') - p4 포함
rows_slice_p2_p4 = df_indexed.loc['p2':'p4']
print("\nloc['p2':'p4'] (DataFrame, p4 포함):\n", rows_slice_p2_p4)

# 특정 행의 특정 열 선택 ('p3' 행의 '이름', '점수' 열)
p3_name_score = df_indexed.loc['p3', ['이름', '점수']]
print("\nloc['p3', ['이름', '점수']] (Series):\n", p3_name_score)

# 모든 행에 대해 특정 열들 선택
all_rows_name_city = df_indexed.loc[:, ['이름', '도시']] # ':'는 모든 행/열을 의미
print("\nloc[:, ['이름', '도시']] (DataFrame):\n", all_rows_name_city)

예상 결과:

인덱스가 변경된 DataFrame:
      이름  나이  점수  도시
p1  Alice  25  85  서울
p2    Bob  30  90  부산
p3 Charlie  22  78  서울
p4   David  35  92  인천
p5     Eve  28  88  광주

loc['p2'] (Series):
이름     Bob
나이      30
점수      90
도시      부산
Name: p2, dtype: object

loc[['p1', 'p4']] (DataFrame):
      이름  나이  점수  도시
p1  Alice  25  85  서울
p4   David  35  92  인천

loc['p2':'p4'] (DataFrame, p4 포함):
      이름  나이  점수  도시
p2    Bob  30  90  부산
p3 Charlie  22  78  서울
p4   David  35  92  인천

loc['p3', ['이름', '점수']] (Series):
이름    Charlie
점수         78
Name: p3, dtype: object

loc[:, ['이름', '도시']] (DataFrame):
       이름  도시
p1  Alice  서울
p2    Bob  부산
p3 Charlie  서울
p4   David  인천
p5     Eve  광주

활용 방안: 데이터에 명시적인 라벨(고객 ID, 상품 코드, 날짜 등)이 있을 때 특정 데이터를 정확하고 가독성 높게 선택할 수 있습니다.

4.4. 위치 기반 선택: .iloc

.iloc 접근자는 데이터의 정수 위치(integer position)를 사용하여 데이터를 선택합니다. 파이썬 리스트/튜플 인덱싱과 유사하게 0부터 시작합니다. df.iloc[row_positions, column_positions] 형태로 사용됩니다.

.iloc 사용법

  • 단일 행 선택: df.iloc[row_position] (Series 반환)
  • 다중 행 선택 (리스트): df.iloc[[pos1, pos2]] (DataFrame 반환)
  • 다중 행 선택 (슬라이스): df.iloc[start_pos:end_pos] (end_pos는 미포함)
  • 행과 열 동시 선택: df.iloc[row_positions, column_positions]

예시 코드: (원본 df 사용)

print("원본 DataFrame (기본 정수 인덱스):\n", df)

# 단일 행 선택 (0번 위치)
row_0 = df.iloc[0]
print("\niloc[0] (Series):\n", row_0)

# 다중 행 선택 (1번, 3번 위치)
rows_1_3 = df.iloc[[1, 3]]
print("\niloc[[1, 3]] (DataFrame):\n", rows_1_3)

# 다중 행 선택 (0번부터 2번 위치까지 슬라이스 - 2번 미포함)
rows_slice_0_2 = df.iloc[0:2]
print("\niloc[0:2] (DataFrame, 2번 미포함):\n", rows_slice_0_2)

# 특정 위치의 행과 열 선택 (1번 행의 0번, 2번 열)
val_1_02 = df.iloc[1, [0, 2]]
print("\niloc[1, [0, 2]] (Series):\n", val_1_02)

# 마지막 행 선택
last_row = df.iloc[-1]
print("\niloc[-1] (마지막 행, Series):\n", last_row)

# 0번, 1번 행과 0번, 1번 열 선택
subset_01_01 = df.iloc[0:2, 0:2]
print("\niloc[0:2, 0:2] (DataFrame):\n", subset_01_01)

예상 결과:

원본 DataFrame (기본 정수 인덱스):
      이름  나이  점수  도시
0  Alice  25  85  서울
1    Bob  30  90  부산
2 Charlie  22  78  서울
3   David  35  92  인천
4     Eve  28  88  광주

iloc[0] (Series):
이름    Alice
나이       25
점수       85
도시       서울
Name: 0, dtype: object

iloc[[1, 3]] (DataFrame):
     이름  나이  점수  도시
1   Bob  30  90  부산
3  David  35  92  인천

iloc[0:2] (DataFrame, 2번 미포함):
     이름  나이  점수  도시
0  Alice  25  85  서울
1    Bob  30  90  부산

iloc[1, [0, 2]] (Series):
이름    Bob
점수     90
Name: 1, dtype: object

iloc[-1] (마지막 행, Series):
이름     Eve
나이      28
점수      88
도시      광주
Name: 4, dtype: object

iloc[0:2, 0:2] (DataFrame):
      이름  나이
0  Alice  25
1    Bob  30

활용 방안: 인덱스 라벨에 관계없이 데이터의 순서(위치)에 기반하여 선택해야 할 때 유용합니다. NumPy 배열과 유사한 방식으로 데이터를 다룰 수 있게 합니다.

4.5. 불리언 인덱싱 (Boolean Indexing)

DataFrame에서 특정 조건을 만족하는 행들을 선택하는 가장 강력하고 유연한 방법입니다. 조건식을 사용하여 True/False 값을 가지는 불리언 Series를 만들고, 이를 인덱서([], .loc[])에 전달하여 True에 해당하는 행들만 선택합니다.

단일 조건

하나의 조건을 사용하여 데이터를 필터링합니다.

예시 코드:

# '나이'가 30 이상인 행 선택
age_condition = df['나이'] >= 30
print("\n'나이' >= 30 조건 (Boolean Series):\n", age_condition)
df_age_over_30 = df[age_condition] # 또는 df.loc[age_condition]
print("\n'나이'가 30 이상인 학생들:\n", df_age_over_30)

# '도시'가 '서울'인 행 선택
city_seoul = df['도시'] == '서울'
df_seoul = df.loc[city_seoul]
print("\n'도시'가 '서울'인 학생들:\n", df_seoul)

예상 결과:

'나이' >= 30 조건 (Boolean Series):
0    False
1     True
2    False
3     True
4    False
Name: 나이, dtype: bool

'나이'가 30 이상인 학생들:
     이름  나이  점수  도시
1   Bob  30  90  부산
3  David  35  92  인천

'도시'가 '서울'인 학생들:
      이름  나이  점수  도시
0  Alice  25  85  서울
2 Charlie  22  78  서울

다중 조건

여러 조건을 논리 연산자(&: AND, |: OR, ~: NOT)로 결합하여 사용합니다. 각 조건은 괄호()로 묶어주는 것이 안전합니다.

예시 코드:

# '나이'가 25 이상이고 '점수'가 90 이상인 행 선택
multi_condition_and = (df['나이'] >= 25) & (df['점수'] >= 90)
df_multi_and = df[multi_condition_and]
print("\n'나이' >= 25 AND '점수' >= 90 인 학생들:\n", df_multi_and)

# '도시'가 '서울'이거나 '점수'가 85 미만인 행 선택, 특정 열('이름', '도시', '점수')만 보기
multi_condition_or = (df['도시'] == '서울') | (df['점수'] < 85)
df_multi_or_selected_cols = df.loc[multi_condition_or, ['이름', '도시', '점수']]
print("\n'도시' == '서울' OR '점수' < 85 인 학생들의 '이름', '도시', '점수':\n", df_multi_or_selected_cols)

# '도시'가 '부산'이 아닌 학생들
not_busan = ~(df['도시'] == '부산')
df_not_busan = df[not_busan]
print("\n'도시'가 '부산'이 아닌 학생들:\n", df_not_busan)

예상 결과:

'나이' >= 25 AND '점수' >= 90 인 학생들:
     이름  나이  점수  도시
1   Bob  30  90  부산
3  David  35  92  인천

'도시' == '서울' OR '점수' < 85 인 학생들의 '이름', '도시', '점수':
      이름  도시  점수
0  Alice  서울  85
2 Charlie  서울  78

'도시'가 '부산'이 아닌 학생들:
      이름  나이  점수  도시
0  Alice  25  85  서울
2 Charlie  22  78  서울
3   David  35  92  인천
4     Eve  28  88  광주

isin() 메서드 활용

특정 열의 값이 여러 값 중 하나에 해당하는지 확인할 때 유용합니다.

예시 코드:

# '도시'가 '서울' 또는 '인천'인 경우
cities_to_filter = ['서울', '인천']
isin_condition = df['도시'].isin(cities_to_filter)
df_isin = df[isin_condition]
print("\n'도시'가 '서울' 또는 '인천'인 학생들:\n", df_isin)

예상 결과:

'도시'가 '서울' 또는 '인천'인 학생들:
      이름  나이  점수  도시
0  Alice  25  85  서울
2 Charlie  22  78  서울
3   David  35  92  인천

문자열 메서드 활용 (.str 접근자)

문자열 타입의 열에 대해 .str 접근자를 사용하여 다양한 문자열 처리 함수(예: contains(), startswith(), endswith() 등)를 조건으로 활용할 수 있습니다.

예시 코드:

# '이름'에 'li'가 포함된 학생 (대소문자 구분)
name_contains_li = df['이름'].str.contains('li')
df_name_li = df[name_contains_li]
print("\n'이름'에 'li'가 포함된 학생:\n", df_name_li)

# '도시'가 '서'로 시작하는 학생
city_starts_with_seo = df['도시'].str.startswith('서')
df_city_seo = df[city_starts_with_seo]
print("\n'도시'가 '서'로 시작하는 학생:\n", df_city_seo)

예상 결과:

'이름'에 'li'가 포함된 학생:
      이름  나이  점수  도시
0  Alice  25  85  서울
2 Charlie  22  78  서울

'도시'가 '서'로 시작하는 학생:
      이름  나이  점수  도시
0  Alice  25  85  서울
2 Charlie  22  78  서울

활용 방안: 데이터 정제(outlier 제거), 특정 기준을 만족하는 샘플 추출, 고객 세분화 등 데이터 분석 전반에 걸쳐 매우 빈번하게 사용됩니다.

4.6. 선택을 이용한 값 변경

.loc이나 .iloc, 불리언 인덱싱 등을 사용하여 선택된 부분에 새로운 값을 할당할 수 있습니다.

예시 코드:

df_copy = df.copy()
print("값 변경 전 DataFrame 복사본:\n", df_copy)

# 'Bob'의 '점수'를 95점으로 변경 (loc 사용)
df_copy.loc[df_copy['이름'] == 'Bob', '점수'] = 95
print("\n'Bob'의 점수 변경 후:\n", df_copy)

# '나이'가 30 미만인 학생들의 '도시'를 '미정'으로 변경
df_copy.loc[df_copy['나이'] < 30, '도시'] = '미정'
print("\n'나이' < 30 학생들 도시 변경 후:\n", df_copy)

# 0번 행의 모든 값을 특정 값으로 변경 (iloc 사용)
df_copy.iloc[0] = ['NewName', 20, 70, 'NewCity']
print("\n0번 행 전체 변경 후:\n", df_copy)

예상 결과:

값 변경 전 DataFrame 복사본:
      이름  나이  점수  도시
0  Alice  25  85  서울
1    Bob  30  90  부산
2 Charlie  22  78  서울
3   David  35  92  인천
4     Eve  28  88  광주

'Bob'의 점수 변경 후:
      이름  나이  점수  도시
0  Alice  25  85  서울
1    Bob  30  95  부산
2 Charlie  22  78  서울
3   David  35  92  인천
4     Eve  28  88  광주

'나이' < 30 학생들 도시 변경 후:
      이름  나이  점수  도시
0  Alice  25  85  미정
1    Bob  30  95  부산
2 Charlie  22  78  미정
3   David  35  92  인천
4     Eve  28  88  미정

0번 행 전체 변경 후:
        이름  나이  점수     도시
0  NewName  20  70  NewCity
1      Bob  30  95       부산
2  Charlie  22  78       미정
3    David  35  92       인천
4      Eve  28  88       미정

활용 방안: 데이터 오류 수정, 특정 조건에 따른 값 일괄 변경, 파생 변수 생성 시 초기값 설정 등 데이터 전처리 과정에서 필수적입니다.

이처럼 Pandas는 매우 다양하고 강력한 데이터 선택 및 필터링 기능을 제공합니다. 상황에 맞게 [], .loc, .iloc, 불리언 인덱싱 등을 적절히 조합하여 사용하면 원하는 데이터를 효율적으로 추출하고 분석 작업을 원활하게 진행할 수 있습니다.

제 5 장: 데이터 조작 및 정제

데이터 분석의 정확성과 신뢰성을 높이기 위해서는 원본 데이터를 분석 가능한 형태로 가공하고 정제하는 과정이 필수적입니다. 이 장에서는 Pandas를 사용하여 데이터를 효과적으로 조작하고 정제하는 핵심 기술들을 학습합니다. 주요 내용으로는 새로운 열(column)을 추가하거나 기존 열을 삭제하는 방법, 데이터에서 결측치(Missing Values)나 중복된 값을 처리하는 방법, 데이터의 타입을 변환하는 방법, 그리고 사용자 정의 함수나 내장 함수를 데이터에 일괄 적용하는 방법 등이 있습니다.

5.1. 열 추가 및 삭제

DataFrame에 새로운 정보를 담은 열을 추가하거나 불필요한 열을 제거하는 기본적인 데이터 조작 방법입니다.

새로운 열 추가

1. 기존 열들의 연산을 통해 추가: `df['새컬럼명'] = df['기존컬럼1'] + df['기존컬럼2']`
2. 특정 값으로 채워진 열 추가: `df['새컬럼명'] = 값`
3. 리스트나 Series를 사용하여 추가: `df['새컬럼명'] = [값1, 값2, ...]` (길이가 DataFrame의 행 수와 일치해야 함)
4. assign() 메서드 사용: 기존 DataFrame을 변경하지 않고 새 열이 추가된 복사본을 반환합니다. 여러 열을 동시에 추가할 때 유용합니다.

예시 코드:

import pandas as pd
import numpy as np

data = {'이름': ['Alice', 'Bob', 'Charlie', 'David'],
        '수학': [90, 75, 88, 92],
        '영어': [85, 80, 90, 78]}
df = pd.DataFrame(data)
print("원본 DataFrame:\n", df)

# 1. 기존 열 연산으로 '총점' 열 추가
df['총점'] = df['수학'] + df['영어']
print("\n'총점' 열 추가 후:\n", df)

# 2. 특정 값으로 '과목수' 열 추가
df['과목수'] = 2
print("\n'과목수' 열 추가 후:\n", df)

# 3. assign()으로 '평균' 열 추가 (원본 df는 변경되지 않음)
df_assigned = df.assign(평균 = df['총점'] / df['과목수'])
print("\nassign()으로 '평균' 열 추가된 DataFrame:\n", df_assigned)
print("\n원본 DataFrame (assign 후에도 변경 없음):\n", df)

예상 결과:

원본 DataFrame:
        이름  수학  영어
0  Alice  90  85
1    Bob  75  80
2 Charlie  88  90
3   David  92  78

'총점' 열 추가 후:
        이름  수학  영어   총점
0  Alice  90  85  175
1    Bob  75  80  155
2 Charlie  88  90  178
3   David  92  78  170

'과목수' 열 추가 후:
        이름  수학  영어   총점  과목수
0  Alice  90  85  175    2
1    Bob  75  80  155    2
2 Charlie  88  90  178    2
3   David  92  78  170    2

assign()으로 '평균' 열 추가된 DataFrame:
        이름  수학  영어   총점  과목수    평균
0  Alice  90  85  175    2  87.5
1    Bob  75  80  155    2  77.5
2 Charlie  88  90  178    2  89.0
3   David  92  78  170    2  85.0

원본 DataFrame (assign 후에도 변경 없음):
        이름  수학  영어   총점  과목수
0  Alice  90  85  175    2
1    Bob  75  80  155    2
2 Charlie  88  90  178    2
3   David  92  78  170    2

열 삭제

1. del 키워드 사용: `del df['컬럼명']` (원본 DataFrame 직접 변경)
2. drop() 메서드 사용: `df.drop('컬럼명', axis=1)` 또는 `df.drop(columns=['컬럼명1', '컬럼명2'])`. `inplace=True` 옵션으로 원본을 직접 수정할 수 있습니다. (기본값은 `inplace=False`로 복사본 반환)

예시 코드:

df_copy = df_assigned.copy() # df_assigned 사용
print("열 삭제 전 DataFrame:\n", df_copy)

# 1. del 키워드로 '과목수' 열 삭제
del df_copy['과목수']
print("\n'과목수' 열 삭제 후 (del):\n", df_copy)

# 2. drop 메서드로 '총점' 열 삭제 (복사본 반환)
df_dropped_total = df_copy.drop('총점', axis=1) # 또는 columns='총점'
print("\n'총점' 열 삭제 후 (drop, 복사본):\n", df_dropped_total)

# 2. drop 메서드로 여러 열 삭제 및 원본 변경
df_copy.drop(columns=['수학', '영어'], inplace=True)
print("\n'수학', '영어' 열 삭제 후 (drop, 원본변경):\n", df_copy)

예상 결과:

열 삭제 전 DataFrame:
        이름  수학  영어   총점  과목수    평균
0  Alice  90  85  175    2  87.5
1    Bob  75  80  155    2  77.5
2 Charlie  88  90  178    2  89.0
3   David  92  78  170    2  85.0

'과목수' 열 삭제 후 (del):
        이름  수학  영어   총점    평균
0  Alice  90  85  175  87.5
1    Bob  75  80  155  77.5
2 Charlie  88  90  178  89.0
3   David  92  78  170  85.0

'총점' 열 삭제 후 (drop, 복사본):
        이름  수학  영어    평균
0  Alice  90  85  87.5
1    Bob  75  80  77.5
2 Charlie  88  90  89.0
3   David  92  78  85.0

'수학', '영어' 열 삭제 후 (drop, 원본변경):
        이름   총점    평균
0  Alice  175  87.5
1    Bob  155  77.5
2 Charlie  178  89.0
3   David  170  85.0

활용 방안: 파생 변수 생성, 분석에 불필요하거나 중복되는 정보 제거, 개인 식별 정보 제거 등 데이터 전처리 단계에서 광범위하게 사용됩니다.

5.2. 결측치(Missing Data) 처리

실제 데이터에는 값이 누락된 경우가 많으며, 이를 결측치(주로 NaN - Not a Number로 표시)라고 합니다. 결측치는 분석 결과에 왜곡을 줄 수 있으므로 적절히 처리해야 합니다.

결측치 식별

isnull() 또는 isna() 메서드는 각 요소가 결측치인지 여부를 불리언 값으로 반환합니다. notnull()은 반대입니다. sum()과 함께 사용하면 열별 결측치 개수를 파악하기 쉽습니다.

예시 코드:

data_nan = {'A': [1, 2, np.nan, 4, 5],
                'B': [np.nan, 7, 8, np.nan, 10],
                'C': [11, np.nan, np.nan, 14, 15]}
df_nan = pd.DataFrame(data_nan)
print("결측치가 포함된 DataFrame:\n", df_nan)

print("\n결측치 여부 (isnull()):\n", df_nan.isnull())
print("\n열별 결측치 개수:\n", df_nan.isnull().sum())
print("\n총 결측치 개수:", df_nan.isnull().sum().sum())

예상 결과:

결측치가 포함된 DataFrame:
     A     B     C
0  1.0   NaN  11.0
1  2.0   7.0   NaN
2  NaN   8.0   NaN
3  4.0   NaN  14.0
4  5.0  10.0  15.0

결측치 여부 (isnull()):
       A      B      C
0  False   True  False
1  False  False   True
2   True  False   True
3  False   True  False
4  False  False  False

열별 결측치 개수:
A    1
B    2
C    2
dtype: int64

총 결측치 개수: 5

결측치 제거: dropna()

결측치가 포함된 행이나 열을 제거합니다.

  • axis: 0이면 행 기준 (기본값), 1이면 열 기준.
  • how: 'any'이면 하나라도 NaN이면 제거 (기본값), 'all'이면 모든 값이 NaN일 때 제거.
  • thresh: 정수 값으로, 해당 행/열에 NaN이 아닌 값이 이 값보다 적으면 제거.
  • subset: 특정 열들을 기준으로 결측치 검사.

예시 코드:

# NaN이 하나라도 있는 행 제거
df_dropna_row_any = df_nan.dropna() # how='any', axis=0 기본값
print("\nNaN이 하나라도 있는 행 제거 후:\n", df_dropna_row_any)

# 모든 값이 NaN인 행 제거 (이 예제에서는 해당 없음)
df_dropna_row_all = df_nan.dropna(how='all')
print("\n모든 값이 NaN인 행 제거 후:\n", df_dropna_row_all)

# NaN이 하나라도 있는 열 제거
df_dropna_col_any = df_nan.dropna(axis=1) # how='any' 기본값
print("\nNaN이 하나라도 있는 열 제거 후:\n", df_dropna_col_any)

# 'A' 열에 NaN이 있는 행만 제거
df_dropna_subset_A = df_nan.dropna(subset=['A'])
print("\n'A'열에 NaN이 있는 행 제거 후:\n", df_dropna_subset_A)

# NaN 아닌 값이 최소 2개 이상인 행만 유지
df_dropna_thresh2 = df_nan.dropna(thresh=2)
print("\nNaN 아닌 값이 최소 2개 이상인 행 유지:\n", df_dropna_thresh2)

예상 결과:

NaN이 하나라도 있는 행 제거 후:
     A     B     C
4  5.0  10.0  15.0

모든 값이 NaN인 행 제거 후:
     A     B     C
0  1.0   NaN  11.0
1  2.0   7.0   NaN
2  NaN   8.0   NaN
3  4.0   NaN  14.0
4  5.0  10.0  15.0

NaN이 하나라도 있는 열 제거 후:
Empty DataFrame
Columns: []
Index: [0, 1, 2, 3, 4]

'A'열에 NaN이 있는 행 제거 후:
     A     B     C
0  1.0   NaN  11.0
1  2.0   7.0   NaN
3  4.0   NaN  14.0
4  5.0  10.0  15.0

NaN 아닌 값이 최소 2개 이상인 행 유지:
     A     B     C
0  1.0   NaN  11.0
1  2.0   7.0   NaN
3  4.0   NaN  14.0
4  5.0  10.0  15.0

결측치 채우기: fillna()

결측치를 특정 값으로 대체합니다.

  • 특정 값으로 채우기: df.fillna(0)
  • 평균/중앙값/최빈값 등으로 채우기: df['컬럼'].fillna(df['컬럼'].mean())
  • 앞/뒤 값으로 채우기 (시계열 데이터에 유용): method='ffill' (앞 값으로 채움, forward fill), method='bfill' (뒷 값으로 채움, backward fill)
  • 열마다 다른 값으로 채우기: 딕셔너리 형태로 df.fillna({'컬럼1': 값1, '컬럼2': 값2})

예시 코드:

# 모든 NaN을 0으로 채우기
df_fillna_0 = df_nan.fillna(0)
print("\n모든 NaN을 0으로 채운 후:\n", df_fillna_0)

# 'A'열의 NaN은 'A'열의 평균으로, 'B'열의 NaN은 'B'열의 중앙값으로 채우기
df_filled_stats = df_nan.copy()
df_filled_stats['A'].fillna(df_filled_stats['A'].mean(), inplace=True)
df_filled_stats['B'].fillna(df_filled_stats['B'].median(), inplace=True)
print("\n'A'는 평균, 'B'는 중앙값으로 채운 후:\n", df_filled_stats)

# ffill: 앞의 값으로 채우기
df_fillna_ffill = df_nan.fillna(method='ffill')
print("\nffill로 채운 후:\n", df_fillna_ffill)

# bfill: 뒤의 값으로 채우기, limit=1 (한 번만 채움)
df_fillna_bfill_limit = df_nan.fillna(method='bfill', limit=1)
print("\nbfill (limit=1)로 채운 후:\n", df_fillna_bfill_limit)

예상 결과:

모든 NaN을 0으로 채운 후:
     A     B     C
0  1.0   0.0  11.0
1  2.0   7.0   0.0
2  0.0   8.0   0.0
3  4.0   0.0  14.0
4  5.0  10.0  15.0

'A'는 평균, 'B'는 중앙값으로 채운 후:
     A     B     C
0  1.00   8.5  11.0
1  2.00   7.0   NaN
2  3.00   8.0   NaN  # A의 평균 (1+2+4+5)/4 = 3.0
3  4.00   8.5  14.0  # B의 중앙값 (7+8+10)/2 (정렬 후) or 8.5 (7,8,10 -> 8) - 여기선 8.5가 나옴. (7,8,10) => median 8. Pandas median은 (7+8+10) 사이 값 중 8.5로 계산. np.nanmedian은 8.
4  5.00  10.0  15.0   # 정확한 중앙값 계산은 (7,8,10)에서 8. Pandas 1.x버전에서는 8.5가 될 수 있음. (np.nanmedian을 사용하면 8)
                       # 여기서는 fillna가 내부적으로 처리하므로 코드가 실행되는 환경에 따라 다를 수 있음.
                       # 예시에서는 Pandas가 (7, 8, 10) -> 8.0 으로 처리한다고 가정하고 작성 (일반적인 중앙값)

ffill로 채운 후:
     A     B     C
0  1.0   NaN  11.0
1  2.0   7.0  11.0
2  2.0   8.0  11.0
3  4.0   8.0  14.0
4  5.0  10.0  15.0

bfill (limit=1)로 채운 후:
     A     B     C
0  1.0   7.0  11.0
1  2.0   7.0   NaN  # C열 1번 인덱스는 뒤에 채울 값이 없음 (limit에 의해 한칸만 진행)
2  4.0   8.0  14.0
3  4.0  10.0  14.0
4  5.0  10.0  15.0

활용 방안: 결측치 제거는 데이터 손실을 야기할 수 있으므로, 데이터의 특성과 분석 목적에 따라 적절한 대치 방법을 선택하는 것이 중요합니다. 예를 들어, 시계열 데이터에서는 ffill이나 bfill이, 일반적인 수치 데이터에서는 평균이나 중앙값 대치가 자주 사용됩니다.

5.3. 중복 데이터 처리

데이터셋에 동일한 행이 반복적으로 나타나는 경우, 분석 결과에 편향을 줄 수 있습니다. 이러한 중복 데이터를 식별하고 제거하는 방법을 알아봅니다.

중복 데이터 식별: duplicated()

각 행이 중복인지 여부를 불리언 Series로 반환합니다. 기본적으로 첫 번째 나타나는 것은 False, 이후 중복은 True를 반환합니다.

  • subset: 특정 열들을 기준으로 중복 검사.
  • keep: 'first' (기본값, 첫 번째 제외하고 중복 표시), 'last' (마지막 제외하고 중복 표시), False (모든 중복을 True로 표시).

예시 코드:

data_dup = {'col1': ['A', 'B', 'A', 'C', 'B', 'A'],
                'col2': [1, 2, 1, 3, 2, 1],
                'col3': [10, 20, 10, 30, 20, 10]}
df_dup = pd.DataFrame(data_dup)
print("중복이 포함된 DataFrame:\n", df_dup)

# 전체 행 기준 중복 확인 (기본 keep='first')
print("\n중복 여부 (keep='first'):\n", df_dup.duplicated())

# 'col1' 기준 중복 확인
print("\n'col1' 기준 중복 여부:\n", df_dup.duplicated(subset=['col1']))

# 모든 중복 행을 True로 표시
print("\n모든 중복 행을 True로 (keep=False):\n", df_dup.duplicated(keep=False))

예상 결과:

중복이 포함된 DataFrame:
  col1  col2  col3
0    A     1    10
1    B     2    20
2    A     1    10  # (0번 행과 중복)
3    C     3    30
4    B     2    20  # (1번 행과 중복)
5    A     1    10  # (0, 2번 행과 중복)

중복 여부 (keep='first'):
0    False
1    False
2     True
3    False
4     True
5     True
dtype: bool

'col1' 기준 중복 여부:
0    False
1    False
2     True
3    False
4     True
5     True
dtype: bool

모든 중복 행을 True로 (keep=False):
0     True
1     True
2     True
3    False
4     True
5     True
dtype: bool

중복 데이터 제거: drop_duplicates()

중복된 행을 제거합니다. duplicated()와 유사한 파라미터(subset, keep)를 가집니다. inplace=True로 원본을 수정할 수 있습니다.

예시 코드:

# 중복 행 제거 (기본 keep='first')
df_no_dup_first = df_dup.drop_duplicates()
print("\n중복 제거 후 (keep='first'):\n", df_no_dup_first)

# 중복 행 제거 (keep='last', 마지막 값 유지)
df_no_dup_last = df_dup.drop_duplicates(keep='last')
print("\n중복 제거 후 (keep='last'):\n", df_no_dup_last)

# 'col1', 'col2' 기준 중복 제거
df_no_dup_subset = df_dup.drop_duplicates(subset=['col1', 'col2'], keep='first')
print("\n'col1', 'col2' 기준 중복 제거 후:\n", df_no_dup_subset)

예상 결과:

중복 제거 후 (keep='first'):
  col1  col2  col3
0    A     1    10
1    B     2    20
3    C     3    30

중복 제거 후 (keep='last'):
  col1  col2  col3
3    C     3    30
4    B     2    20
5    A     1    10

'col1', 'col2' 기준 중복 제거 후:
  col1  col2  col3
0    A     1    10
1    B     2    20
3    C     3    30

활용 방안: 데이터 수집 오류로 인한 중복 항목 제거, 고유한 사용자 또는 아이템 목록 생성 등에 사용됩니다.

5.4. 데이터 타입 변환

DataFrame의 각 열은 특정 데이터 타입을 가집니다. 분석 목적이나 메모리 효율성을 위해 데이터 타입을 적절히 변환해야 할 때가 있습니다.

데이터 타입 확인: dtypes 속성

DataFrame의 각 열의 데이터 타입을 확인합니다.

예시 코드:

df_types = pd.DataFrame({
    'ID': ['1', '2', '3', '4'],
    'Score': ['85.5', '90.2', '77.0', '92.1'],
    'Age': [25, 30, 22, 35],
    'Registered': ['2023-01-01', '2023-01-15', '2023-02-01', '2023-02-10']
})
print("초기 DataFrame:\n", df_types)
print("\n초기 데이터 타입:\n", df_types.dtypes)

예상 결과:

초기 DataFrame:
  ID Score  Age  Registered
0  1  85.5   25  2023-01-01
1  2  90.2   30  2023-01-15
2  3  77.0   22  2023-02-01
3  4  92.1   35  2023-02-10

초기 데이터 타입:
ID            object
Score         object
Age            int64
Registered    object
dtype: object

데이터 타입 변환: astype(), pd.to_numeric(), pd.to_datetime()

  • astype(): 가장 일반적인 타입 변환 메서드. df['컬럼'].astype(새타입) (예: int, float, str, bool, 'category').
  • pd.to_numeric(): 숫자형으로 변환 시도. 변환 불가능한 값이 있을 경우 오류 발생. errors 파라미터로 처리 가능 ('raise', 'coerce', 'ignore').
  • pd.to_datetime(): 날짜/시간 문자열을 datetime 객체로 변환. format 파라미터로 형식 지정 가능.

예시 코드:

df_converted = df_types.copy()

# 'ID'를 정수형으로 변환
df_converted['ID'] = df_converted['ID'].astype(int)

# 'Score'를 부동소수점형으로 변환 (pd.to_numeric 사용)
df_converted['Score'] = pd.to_numeric(df_converted['Score'])

# 'Registered'를 datetime 객체로 변환
df_converted['Registered'] = pd.to_datetime(df_converted['Registered'])

# 'Age'를 문자열로 변환
df_converted['Age'] = df_converted['Age'].astype(str)

print("\n변환 후 데이터 타입:\n", df_converted.dtypes)
print("\n변환 후 DataFrame:\n", df_converted)

예상 결과:

변환 후 데이터 타입:
ID                     int64
Score                float64
Age                   object
Registered    datetime64[ns]  # Pandas < 2.0: datetime64[ns], Pandas >= 2.0: datetime64[us] or similar
dtype: object

변환 후 DataFrame:
   ID  Score Age Registered
0   1   85.5  25 2023-01-01
1   2   90.2  30 2023-01-15
2   3   77.0  22 2023-02-01
3   4   92.1  35 2023-02-10

활용 방안: 수치 연산을 위해 문자열을 숫자로 변환, 메모리 절약을 위해 더 작은 숫자 타입으로 변경, 시계열 분석을 위해 문자열을 datetime 객체로 변환, 범주형 데이터 처리를 위해 'category' 타입으로 변환 등 다양한 상황에서 필요합니다.

5.5. 함수 적용 (Applying Functions)

데이터의 각 요소, 행, 열에 특정 함수를 일괄적으로 적용하여 데이터를 변환하거나 새로운 값을 계산할 수 있습니다.

요소별 함수 적용: map() (Series), applymap() (DataFrame)

  • Series.map(): Series의 각 요소에 함수를 적용하거나, 딕셔너리나 Series를 전달하여 값을 치환합니다.
  • DataFrame.applymap(): DataFrame의 모든 요소에 함수를 개별적으로 적용합니다. (주의: applymap은 곧 없어질 수 있으므로 DataFrame.map() (Pandas 2.1.0+), 또는 DataFrame.apply와 함께 lambda를 사용하는 것을 고려하세요)

예시 코드:

df_func = pd.DataFrame({'A': [1, 2, 3], 'B': [10, 20, 30]})
print("함수 적용 전 DataFrame:\n", df_func)

# Series.map() 예시: 'A' 열의 각 요소에 100을 더함
df_func['A_mapped'] = df_func['A'].map(lambda x: x + 100)
print("\n'A' 열 map 적용 후:\n", df_func)

# DataFrame.applymap() 예시: 모든 요소에 제곱 함수 적용 (숫자형 열에만 의미 있음)
# applymap is deprecated in newer pandas, use map for DataFrames element-wise operations.
# df_func_squared = df_func[['A', 'B']].applymap(lambda x: x*x) # Older Pandas
df_func_squared = df_func[['A', 'B']].map(lambda x: x*x) # Pandas 2.1.0+
print("\nDataFrame 모든 요소 제곱 (map):\n", df_func_squared)

예상 결과 (Pandas 2.1.0+ 기준):

함수 적용 전 DataFrame:
   A   B
0  1  10
1  2  20
2  3  30

'A' 열 map 적용 후:
   A   B  A_mapped
0  1  10       101
1  2  20       102
2  3  30       103

DataFrame 모든 요소 제곱 (map):
   A    B
0  1  100
1  4  400
2  9  900

행/열 단위 함수 적용: apply()

DataFrame의 행(axis=1) 또는 열(axis=0) 전체에 함수를 적용합니다. 함수는 Series를 인자로 받아 스칼라 값이나 새로운 Series를 반환할 수 있습니다.

예시 코드:

df_apply_data = pd.DataFrame(np.random.randn(3, 4), columns=['W', 'X', 'Y', 'Z'])
print("apply 적용 전 DataFrame:\n", df_apply_data)

# 열(axis=0) 단위로 최대값 - 최소값 계산
col_range = df_apply_data.apply(lambda x: x.max() - x.min(), axis=0)
print("\n열별 (최대값 - 최소값):\n", col_range)

# 행(axis=1) 단위로 합계 계산
row_sum = df_apply_data.apply(np.sum, axis=1)
print("\n행별 합계:\n", row_sum)

# 행 단위로 W와 X 열의 차이를 계산하여 새 열 'W-X' 생성
df_apply_data['W-X'] = df_apply_data.apply(lambda row: row['W'] - row['X'], axis=1)
print("\n행 단위 연산으로 새 열 추가 후:\n", df_apply_data)

예상 결과 (randn 부분은 실행 시마다 다름):

apply 적용 전 DataFrame:
          W         X         Y         Z
0  0.626386 -0.303790  0.905374 -0.022715
1 -0.470543 -0.828799  0.346416  0.016854
2 -0.099666  0.549702 -0.486393 -1.107797

열별 (최대값 - 최소값):
W    1.096929
X    1.378500
Y    1.391767
Z    1.085082
dtype: float64

행별 합계:
0    1.205255
1   -0.936072
2   -1.144153
dtype: float64

행 단위 연산으로 새 열 추가 후:
          W         X         Y         Z       W-X
0  0.626386 -0.303790  0.905374 -0.022715  0.930176
1 -0.470543 -0.828799  0.346416  0.016854  0.358256
2 -0.099666  0.549702 -0.486393 -1.107797 -0.649368

활용 방안: 복잡한 조건에 따른 값 계산, 사용자 정의 정규화/표준화 함수 적용, 여러 열의 값을 조합하여 새로운 특성(feature) 생성 등 데이터 변환 및 가공에 매우 유용합니다.

5.6. 데이터 값 변경/치환: replace()

특정 값을 다른 값으로 변경(치환)할 때 사용합니다. 단일 값, 리스트, 딕셔너리 등을 사용하여 유연하게 값을 바꿀 수 있습니다.

예시 코드:

df_replace = pd.DataFrame({'Group': ['A', 'B', 'C', 'A', 'B', 'D'],
                               'Score': [10, -99, 20, 10, 30, -99]})
print("값 치환 전 DataFrame:\n", df_replace)

# 단일 값 치환: -99를 NaN으로
df_replaced_single = df_replace.replace(-99, np.nan)
print("\n-99를 NaN으로 치환 후:\n", df_replaced_single)

# 리스트를 사용한 다중 값 치환: ['A', 'B']를 'Group_AB'로
df_replaced_list = df_replace.replace(['A', 'B'], 'Group_AB')
print("\n['A', 'B']를 'Group_AB'로 치환 후:\n", df_replaced_list)

# 딕셔너리를 사용한 값 치환: {'A': 'Alpha', 'D': 'Delta', -99: 0}
df_replaced_dict = df_replace.replace({'A': 'Alpha', 'D': 'Delta', -99: 0})
print("\n딕셔너리로 여러 값 치환 후:\n", df_replaced_dict)

# 특정 열에 대해서만 치환
df_replaced_col_specific = df_replace.replace({'Score': {-99: np.nan}})
print("\n'Score' 열의 -99만 NaN으로 치환 후:\n", df_replaced_col_specific)

예상 결과:

값 치환 전 DataFrame:
  Group  Score
0     A     10
1     B    -99
2     C     20
3     A     10
4     B     30
5     D    -99

-99를 NaN으로 치환 후:
  Group  Score
0     A   10.0
1     B    NaN
2     C   20.0
3     A   10.0
4     B   30.0
5     D    NaN

['A', 'B']를 'Group_AB'로 치환 후:
      Group  Score
0  Group_AB     10
1  Group_AB    -99
2         C     20
3  Group_AB     10
4  Group_AB     30
5         D    -99

딕셔너리로 여러 값 치환 후:
    Group  Score
0   Alpha     10
1       B      0
2       C     20
3   Alpha     10
4       B     30
5   Delta      0

'Score' 열의 -99만 NaN으로 치환 후:
  Group  Score
0     A   10.0
1     B    NaN
2     C   20.0
3     A   10.0
4     B   30.0
5     D    NaN

활용 방안: 데이터 입력 오류 수정 (예: 특정 코드를 표준 코드로 변경), 결측치로 표현된 값을 실제 NaN으로 변경, 범주형 데이터의 라벨 변경 등에 유용하게 사용됩니다.

데이터 조작 및 정제는 데이터 분석 프로젝트에서 가장 많은 시간을 차지하는 부분 중 하나이지만, 그만큼 결과의 질을 좌우하는 중요한 단계입니다. Pandas가 제공하는 다양한 기능을 숙지하고 활용하여 깨끗하고 신뢰할 수 있는 데이터를 확보하는 것이 중요합니다.

제 6 장: 데이터 정렬 및 순위

데이터를 분석하기 좋은 형태로 만들기 위해서는 종종 특정 기준에 따라 데이터를 정렬하거나 각 데이터의 순위를 매겨야 합니다. Pandas는 이러한 작업을 효과적으로 수행할 수 있는 sort_index(), sort_values(), rank()와 같은 강력한 메서드들을 제공합니다. 이 장에서는 이러한 함수들을 사용하여 데이터를 원하는 순서대로 재배치하고, 데이터 포인트 간의 상대적인 순위를 파악하는 방법을 학습합니다.

6.1. 인덱스(Index) 기준 정렬: sort_index()

DataFrame이나 Series의 인덱스(행 또는 열 레이블)를 기준으로 데이터를 정렬합니다. 기본적으로 오름차순으로 정렬되며, 다양한 옵션을 통해 정렬 방식을 제어할 수 있습니다.

sort_index() 주요 파라미터

  • axis: 정렬할 축을 지정합니다. 0 (또는 'index')은 행 인덱스를 기준으로 정렬하고(기본값), 1 (또는 'columns')은 열 이름을 기준으로 정렬합니다.
  • ascending: 정렬 순서를 지정합니다. True이면 오름차순(기본값), False이면 내림차순으로 정렬합니다.
  • inplace: 원본 DataFrame을 직접 수정할지 여부를 결정합니다. True이면 원본을 변경하고 None을 반환하며, False이면 정렬된 새 DataFrame을 반환합니다(기본값).
  • na_position: 결측치(NaN)의 위치를 지정합니다. 'first'는 결측치를 맨 앞에, 'last'는 맨 뒤에 위치시킵니다(기본값).
  • level: 다중 인덱스(MultiIndex)의 경우 정렬할 특정 레벨을 지정할 수 있습니다.

예시 코드:

import pandas as pd
import numpy as np

data = {'col_B': [4, 7, 1, 5],
        'col_A': [10, 20, 30, 40],
        'col_C': [100, 50, np.nan, 200]}
df_sort_idx = pd.DataFrame(data, index=['d', 'a', 'c', 'b'])
print("원본 DataFrame:\n", df_sort_idx)

# 행 인덱스 기준 오름차순 정렬
df_sorted_rows_asc = df_sort_idx.sort_index() # axis=0, ascending=True 기본값
print("\n행 인덱스 오름차순 정렬:\n", df_sorted_rows_asc)

# 행 인덱스 기준 내림차순 정렬, 원본 변경
df_sort_idx_copy = df_sort_idx.copy()
df_sort_idx_copy.sort_index(ascending=False, inplace=True)
print("\n행 인덱스 내림차순 정렬 (원본 변경):\n", df_sort_idx_copy)

# 열 이름 기준 오름차순 정렬
df_sorted_cols_asc = df_sort_idx.sort_index(axis=1)
print("\n열 이름 오름차순 정렬:\n", df_sorted_cols_asc)

# 열 이름 기준 내림차순 정렬, NaN 값 맨 앞에
df_sorted_cols_desc_nafirst = df_sort_idx.sort_index(axis=1, ascending=False, na_position='first')
# (na_position은 sort_values에서 주로 사용되나, sort_index는 인덱스 자체의 NaN을 다루진 않음.
#  여기서는 열 이름에 NaN이 없으므로 na_position은 큰 의미 없음. 값 정렬 시 유용)
print("\n열 이름 내림차순 정렬:\n", df_sorted_cols_desc_nafirst)

예상 결과:

원본 DataFrame:
   col_B  col_A  col_C
d      4     10  100.0
a      7     20   50.0
c      1     30    NaN
b      5     40  200.0

행 인덱스 오름차순 정렬:
   col_B  col_A  col_C
a      7     20   50.0
b      5     40  200.0
c      1     30    NaN
d      4     10  100.0

행 인덱스 내림차순 정렬 (원본 변경):
   col_B  col_A  col_C
d      4     10  100.0
c      1     30    NaN
b      5     40  200.0
a      7     20   50.0

열 이름 오름차순 정렬:
   col_A  col_B  col_C
d     10      4  100.0
a     20      7   50.0
c     30      1    NaN
b     40      5  200.0

열 이름 내림차순 정렬:
   col_C  col_B  col_A
d  100.0      4     10
a   50.0      7     20
c    NaN      1     30
b  200.0      5     40

활용 방안: 시계열 데이터에서 시간 순서대로 인덱스를 정렬하거나, 특정 카테고리(인덱스) 순으로 데이터를 보고자 할 때 유용합니다. 데이터 병합 전 인덱스를 정렬하면 성능 향상에 도움이 될 수 있습니다.

6.2. 값(Value) 기준 정렬: sort_values()

DataFrame의 특정 열(들)의 값을 기준으로 데이터를 정렬합니다. Series의 경우 Series 내부의 값을 기준으로 정렬합니다.

sort_values() 주요 파라미터

  • by: (DataFrame 필수) 정렬 기준으로 사용할 열 이름 또는 열 이름의 리스트. Series에서는 이 파라미터가 필요 없습니다.
  • axis: 정렬할 축을 지정합니다. 0 (또는 'index')은 행을 정렬(기본값), 1 (또는 'columns')은 열을 정렬합니다 (주로 `by`에 인덱스 레벨을 지정했을 때 사용).
  • ascending: True이면 오름차순(기본값), False이면 내림차순. by에 여러 열을 지정한 경우, 각 열에 대한 정렬 순서를 리스트 형태로 (예: [True, False]) 지정할 수 있습니다.
  • inplace: 원본을 직접 수정할지 여부 (기본값 False).
  • na_position: 결측치(NaN)의 위치를 지정합니다. 'first'는 맨 앞에, 'last'는 맨 뒤에 위치시킵니다(기본값).
  • kind: 정렬 알고리즘을 선택합니다 ('quicksort', 'mergesort', 'heapsort', 'stable'). 기본값은 'quicksort'. 'stable'은 원래 순서를 최대한 유지하려는 안정 정렬입니다.

예시 코드:

data_val = {'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
                'Age': [25, 30, 22, 30, 28],
                'Score': [85, 90, 78, 92, 85]}
df_sort_val = pd.DataFrame(data_val)
print("원본 DataFrame:\n", df_sort_val)

# 'Score' 열 기준 내림차순 정렬
df_sorted_score_desc = df_sort_val.sort_values(by='Score', ascending=False)
print("\n'Score' 기준 내림차순 정렬:\n", df_sorted_score_desc)

# 'Age' 열 오름차순, 같으면 'Score' 열 내림차순 정렬
# NaN 값을 맨 앞에 오도록 설정
df_sorted_multi = df_sort_val.sort_values(by=['Age', 'Score'], ascending=[True, False], na_position='first')
print("\n'Age'(오름차순), 'Score'(내림차순) 기준 정렬 (NaN 맨 앞):\n", df_sorted_multi)

# Series 정렬
s_sort = pd.Series([3, 1, np.nan, 5, 2], index=['a', 'b', 'c', 'd', 'e'])
print("\n원본 Series:\n", s_sort)
print("\nSeries 값 기준 오름차순 정렬 (NaN 맨 뒤):\n", s_sort.sort_values(na_position='last'))

예상 결과:

원본 DataFrame:
      Name  Age  Score
0    Alice   25     85
1      Bob   30     90
2  Charlie   22     78
3    David   30     92
4      Eve   28     85

'Score' 기준 내림차순 정렬:
      Name  Age  Score
3    David   30     92
1      Bob   30     90
0    Alice   25     85
4      Eve   28     85
2  Charlie   22     78

'Age'(오름차순), 'Score'(내림차순) 기준 정렬 (NaN 맨 앞):
      Name  Age  Score
2  Charlie   22     78
0    Alice   25     85
4      Eve   28     85
3    David   30     92  # Age 30 중에서 Score 92가 먼저
1      Bob   30     90  # Age 30 중에서 Score 90이 다음

원본 Series:
a    3.0
b    1.0
c    NaN
d    5.0
e    2.0
dtype: float64

Series 값 기준 오름차순 정렬 (NaN 맨 뒤):
b    1.0
e    2.0
a    3.0
d    5.0
c    NaN
dtype: float64

활용 방안: 가장 높은 판매량을 기록한 상품 순으로 정렬, 성적 우수자 순으로 학생 명단 정렬, 여러 기준을 복합적으로 적용하여 우선순위가 높은 데이터를 상단에 배치하는 등 데이터 분석에서 가장 빈번하게 사용되는 기능 중 하나입니다.

6.3. 데이터 순위(Rank) 매기기: rank()

Series나 DataFrame의 값에 대해 순위를 매깁니다. 동점일 경우의 처리 방법, NaN 값 처리 방법 등 다양한 옵션을 제공하여 유연하게 순위를 계산할 수 있습니다.

rank() 주요 파라미터

  • axis: 순위를 매길 축을 지정합니다. 0은 각 열 내에서 순위(기본값), 1은 각 행 내에서 순위를 매깁니다.
  • method: 동점 처리 방법을 지정합니다.
    • 'average': 동점인 항목들의 평균 순위를 부여 (예: 1, 2.5, 2.5, 4). (기본값)
    • 'min': 동점인 항목들에 대해 가장 낮은 순위를 부여 (예: 1, 2, 2, 4).
    • 'max': 동점인 항목들에 대해 가장 높은 순위를 부여 (예: 1, 3, 3, 4).
    • 'first': 동점인 항목들을 데이터에 나타난 순서대로 순위를 부여 (예: 1, 2, 3, 4).
    • 'dense': 동점인 그룹 다음 순위를 1씩 증가시켜 공백 없이 순위 부여 (예: 1, 2, 2, 3).
  • numeric_only: True이면 숫자형 열에 대해서만 순위를 계산합니다.
  • na_option: 결측치(NaN) 처리 방법을 지정합니다.
    • 'keep': NaN 값에 NaN 순위를 부여 (기본값).
    • 'top': NaN 값을 가장 작은 값으로 간주하여 순위 부여 (오름차순 시 맨 앞 순위).
    • 'bottom': NaN 값을 가장 큰 값으로 간주하여 순위 부여 (오름차순 시 맨 뒤 순위).
  • ascending: True이면 오름차순으로 순위(작은 값이 높은 순위, 기본값), False이면 내림차순으로 순위(큰 값이 높은 순위)를 매깁니다.
  • pct: True이면 순위를 백분위수 형태로 반환합니다 (0과 1 사이 값).

예시 코드:

s_rank = pd.Series([10, 30, 20, 30, 50, np.nan, 20])
print("원본 Series:\n", s_rank)

# 기본 순위 (method='average', ascending=True)
print("\n기본 순위 (average):\n", s_rank.rank())

# 내림차순, method='min' (높은 점수가 높은 순위)
print("\n내림차순, method='min':\n", s_rank.rank(ascending=False, method='min'))

# 오름차순, method='dense', NaN은 가장 낮은 순위로 (top)
print("\n오름차순, method='dense', na_option='top':\n", s_rank.rank(method='dense', na_option='top'))

# 백분위수 순위
print("\n백분위수 순위 (pct=True):\n", s_rank.rank(pct=True))

# DataFrame에서 'Score' 열에 대한 순위를 'Rank_Score' 열로 추가
df_rank_example = df_sort_val.copy() # 이전 예제 df_sort_val 재사용
df_rank_example['Score_Rank_Dense_Desc'] = df_rank_example['Score'].rank(method='dense', ascending=False)
print("\nDataFrame에 점수 순위(dense, 내림차순) 추가:\n", df_rank_example)

예상 결과:

원본 Series:
0    10.0
1    30.0
2    20.0
3    30.0
4    50.0
5     NaN
6    20.0
dtype: float64

기본 순위 (average):
0    1.0  # 10
1    4.5  # 30 (30, 30 -> 4, 5 -> avg 4.5)
2    2.5  # 20 (20, 20 -> 2, 3 -> avg 2.5)
3    4.5  # 30
4    6.0  # 50
5    NaN
6    2.5  # 20
dtype: float64

내림차순, method='min':
0    5.0  # 10 (가장 낮은 점수)
1    2.0  # 30 (30, 30 중 min 순위)
2    3.0  # 20 (20, 20 중 min 순위)
3    2.0  # 30
4    1.0  # 50 (가장 높은 점수)
5    NaN
6    3.0  # 20
dtype: float64

오름차순, method='dense', na_option='top':
0    2.0  # 10
1    4.0  # 30
2    3.0  # 20
3    4.0  # 30
4    5.0  # 50
5    1.0  # NaN (가장 낮은 순위로 간주)
6    3.0  # 20
dtype: float64

백분위수 순위 (pct=True):
0    0.166667  # 1/6
1    0.750000  # 4.5/6
2    0.416667  # 2.5/6
3    0.750000  # 4.5/6
4    1.000000  # 6/6
5         NaN
6    0.416667  # 2.5/6
dtype: float64

DataFrame에 점수 순위(dense, 내림차순) 추가:
      Name  Age  Score  Score_Rank_Dense_Desc
0    Alice   25     85                    3.0 # 85점은 3등 (92, 90 다음)
1      Bob   30     90                    2.0 # 90점은 2등
2  Charlie   22     78                    4.0 # 78점은 4등
3    David   30     92                    1.0 # 92점은 1등
4      Eve   28     85                    3.0 # 85점은 3등

활용 방안: 학생들의 성적 순위 매기기, 제품 판매량 순위 분석, 특정 지표에 따른 기업 순위 평가 등 상대적인 위치나 중요도를 파악하는 데 널리 사용됩니다. 동점 처리 방식을 어떻게 하느냐에 따라 순위 결과가 달라지므로, 분석 목적에 맞는 `method`를 선택하는 것이 중요합니다.

데이터 정렬과 순위 매기기는 데이터를 보다 체계적으로 탐색하고, 패턴을 발견하며, 의미 있는 비교를 가능하게 하는 기본적인 전처리 과정입니다. Pandas의 이러한 기능들을 잘 활용하면 복잡한 데이터도 쉽게 다룰 수 있습니다.

제 7 장: 기술 통계 및 계산

데이터 분석의 초기 단계에서 데이터의 전반적인 특징을 파악하는 것은 매우 중요합니다. Pandas는 합계, 평균, 표준편차, 최소값, 최대값 등 다양한 기술 통계량을 손쉽게 계산할 수 있는 함수들을 제공합니다. 이 장에서는 이러한 함수들을 사용하여 데이터의 분포, 중심 경향성, 산포도 등을 파악하고, describe() 함수를 통해 주요 통계치를 한 번에 요약하는 방법, 그리고 누적 계산 등 다양한 계산 기법들을 학습합니다.

7.1. 기본적인 기술 통계 함수

Pandas의 Series나 DataFrame은 다양한 내장 통계 함수를 가지고 있어 기본적인 통계량을 쉽게 계산할 수 있습니다. 대부분의 함수는 axis 파라미터를 통해 계산 방향(행 또는 열)을 지정할 수 있으며, skipna=True (기본값)로 설정되어 결측치를 제외하고 계산합니다. numeric_only=True 옵션은 숫자형 데이터에 대해서만 연산을 수행하도록 할 때 유용합니다.

주요 통계 함수 리스트

  • count(): NaN이 아닌 데이터의 개수.
  • sum(): 값들의 합계. axis로 합산 방향 지정.
  • mean(): 산술 평균. axis로 평균 계산 방향 지정.
  • median(): 중앙값. axis로 중앙값 계산 방향 지정.
  • min(): 최소값.
  • max(): 최대값.
  • std(): 표준편차 (기본적으로 표본 표준편차, N-1 분모).
  • var(): 분산 (기본적으로 표본 분산, N-1 분모).
  • quantile(q=0.5): 지정된 분위수 계산. q는 0~1 사이 값 (예: 0.25는 1사분위수).
  • mode(): 최빈값. 여러 개일 경우 모두 반환 (Series 형태로).
  • abs(): 각 요소의 절대값.
  • prod() 또는 product(): 모든 요소의 곱.
  • idxmin(): 최소값을 가지는 인덱스 레이블 반환.
  • idxmax(): 최대값을 가지는 인덱스 레이블 반환.

예시 코드:

import pandas as pd
import numpy as np

data = {'A': [1, 2, 3, 4, 5, np.nan],
        'B': [10, 20, 10, 30, 50, 40],
        'C': [100, 200, 300, np.nan, 500, 600]}
df_stats = pd.DataFrame(data)
print("원본 DataFrame:\n", df_stats)

print("\n--- 기본 통계량 ---")
print("df_stats.count():\n", df_stats.count()) # 열별 NaN 아닌 개수
print("df_stats['A'].sum():", df_stats['A'].sum()) # 'A'열 합계
print("df_stats.mean(numeric_only=True):\n", df_stats.mean(numeric_only=True)) # 전체 열 평균
print("df_stats.median(numeric_only=True, axis=0):\n", df_stats.median(numeric_only=True, axis=0)) # 열별 중앙값
print("df_stats['B'].quantile(0.75):", df_stats['B'].quantile(0.75)) # 'B'열 3사분위수
print("df_stats['B'].mode():\n", df_stats['B'].mode()) # 'B'열 최빈값
print("df_stats.std(numeric_only=True):\n", df_stats.std(numeric_only=True)) # 열별 표준편차
print("df_stats.idxmax(numeric_only=True):\n", df_stats.idxmax(numeric_only=True)) # 열별 최대값 인덱스
print("df_stats.min(axis=1, numeric_only=True):\n", df_stats.min(axis=1, numeric_only=True)) # 행별 최소값

예상 결과:

원본 DataFrame:
     A   B      C
0  1.0  10  100.0
1  2.0  20  200.0
2  3.0  10  300.0
3  4.0  30    NaN
4  5.0  50  500.0
5  NaN  40  600.0

--- 기본 통계량 ---
df_stats.count():
A    5
B    6
C    5
dtype: int64
df_stats['A'].sum(): 15.0
df_stats.mean(numeric_only=True):
A      3.000000
B     26.666667
C    340.000000
dtype: float64
df_stats.median(numeric_only=True, axis=0):
A      3.0
B     25.0
C    300.0
dtype: float64
df_stats['B'].quantile(0.75): 37.5
df_stats['B'].mode():
0    10
dtype: int64
df_stats.std(numeric_only=True):
A      1.581139
B     16.329932
C    207.364414
dtype: float64
df_stats.idxmax(numeric_only=True):
A    4
B    4
C    5
dtype: int64
df_stats.min(axis=1, numeric_only=True):
0      1.0
1      2.0
2      3.0
3      4.0
4      5.0
5     40.0  # NaN은 A열에, B열과 C열 값 중 최소는 40
dtype: float64

활용 방안: 데이터의 중심 경향(평균, 중앙값, 최빈값), 변동성(표준편차, 분산), 범위(최소값, 최대값) 등을 파악하여 데이터 분포에 대한 초기 이해를 돕습니다. 이상치 탐지나 데이터 스케일링 전처리 등에도 기초 자료로 활용됩니다.

7.2. 요약 통계: describe()

describe() 메서드는 DataFrame의 숫자형 데이터에 대해 주요 기술 통계량(개수, 평균, 표준편차, 최소값, 사분위수, 최대값)을 한 번에 계산하여 요약해줍니다. 객체형(object)이나 범주형(categorical) 데이터에 대해서는 다른 통계량(개수, 고유값 개수, 최빈값, 최빈값 빈도)을 보여줍니다.

describe() 주요 파라미터

  • percentiles: 결과에 포함할 분위수 리스트를 지정합니다. 기본값은 [.25, .5, .75] (사분위수).
  • include: 분석에 포함할 데이터 타입 리스트. (예: ['object'], [np.number])
  • exclude: 분석에서 제외할 데이터 타입 리스트.

예시 코드:

df_desc = pd.DataFrame({
    'NumericCol': [10, 20, np.nan, 30, 20, 40, 50],
    'StringCol': ['apple', 'banana', 'apple', 'orange', 'banana', 'apple', 'grape'],
    'BoolCol': [True, False, True, True, False, True, False]
})
print("기술 통계용 DataFrame:\n", df_desc)

# 숫자형 열에 대한 describe() (기본)
print("\n숫자형 열 describe():\n", df_desc['NumericCol'].describe())

# 전체 DataFrame에 대한 describe() (숫자형 열만 기본으로 처리)
print("\n전체 DataFrame describe() (숫자형만):\n", df_desc.describe())

# 문자열(object) 열에 대한 describe()
print("\n문자열 열 describe():\n", df_desc['StringCol'].describe())

# 모든 열에 대한 describe() (include='all')
print("\n모든 열 describe() (include='all'):\n", df_desc.describe(include='all'))

# 특정 분위수 포함
print("\n특정 분위수 포함 describe() (NumericCol):\n", df_desc['NumericCol'].describe(percentiles=[.1, .5, .9]))

예상 결과:

기술 통계용 DataFrame:
   NumericCol StringCol  BoolCol
0        10.0     apple     True
1        20.0    banana    False
2         NaN     apple     True
3        30.0    orange     True
4        20.0    banana    False
5        40.0     apple     True
6        50.0     grape    False

숫자형 열 describe():
count     6.000000
mean     28.333333
std      14.719601
min      10.000000
25%      20.000000
50%      25.000000
75%      37.500000
max      50.000000
Name: NumericCol, dtype: float64

전체 DataFrame describe() (숫자형만):
       NumericCol
count    6.000000
mean    28.333333
std     14.719601
min     10.000000
25%     20.000000
50%     25.000000
75%     37.500000
max     50.000000

문자열 열 describe():
count         7
unique        4
top       apple
freq          3
Name: StringCol, dtype: object

모든 열 describe() (include='all'):
       NumericCol StringCol BoolCol
count    6.000000         7       7
unique        NaN         4       2  # BoolCol은 2개의 unique 값 (True, False)
top           NaN     apple    True
freq          NaN         3       4
mean    28.333333       NaN     NaN
std     14.719601       NaN     NaN
min     10.000000       NaN     NaN
25%     20.000000       NaN     NaN
50%     25.000000       NaN     NaN
75%     37.500000       NaN     NaN
max     50.000000       NaN     NaN

특정 분위수 포함 describe() (NumericCol):
count     6.000000
mean     28.333333
std      14.719601
min      10.000000
10%      14.000000  # (10과 20 사이의 10% 지점)
50%      25.000000
90%      46.000000  # (40과 50 사이의 90% 지점)
max      50.000000
Name: NumericCol, dtype: float64

활용 방안: 데이터의 전반적인 분포와 특성을 빠르게 요약하여 보여주므로, 데이터 탐색 단계에서 매우 유용합니다. 데이터 품질 검토, 이상치 유무 판단, 변수 선택 등에 도움을 줍니다.

7.3. 누적 계산 (Cumulative Calculations)

데이터를 처음부터 현재 위치까지 누적하여 계산하는 함수들입니다. 시계열 데이터 분석이나 순서가 중요한 데이터에서 추세를 파악하는 데 유용합니다.

주요 누적 함수

  • cumsum(): 누적 합계.
  • cumprod(): 누적 곱.
  • cummin(): 누적 최소값 (현재까지의 최소값).
  • cummax(): 누적 최대값 (현재까지의 최대값).
  • 모든 함수는 axis 파라미터를 가집니다 (0: 열 방향, 1: 행 방향).

예시 코드:

df_cum = pd.DataFrame(np.arange(1, 10).reshape(3, 3), columns=['X', 'Y', 'Z'])
df_cum.iloc[1, 1] = np.nan # 예시를 위해 NaN 추가
print("누적 계산용 DataFrame:\n", df_cum)

print("\n열 기준 누적 합계 (cumsum, axis=0):\n", df_cum.cumsum())
print("\n행 기준 누적 곱 (cumprod, axis=1):\n", df_cum.cumprod(axis=1, skipna=False)) # skipna=False로 NaN 전파
print("\n열 기준 누적 최소값 (cummin):\n", df_cum.cummin())
print("\n열 기준 누적 최대값 (cummax, skipna=True 기본):\n", df_cum.cummax(skipna=True))

예상 결과:

누적 계산용 DataFrame:
   X    Y  Z
0  1  2.0  3
1  4  NaN  6
2  7  8.0  9

열 기준 누적 합계 (cumsum, axis=0):
     X     Y     Z
0  1.0   2.0   3.0
1  5.0   NaN   9.0  # NaN 이후로는 NaN 또는 이전 값 (버전에 따라) - 여기선 skipna=True 기본값
2 12.0   8.0  18.0  # Y열은 2.0 + (skip NaN) + 8.0 = 10.0. -> 이전 버전은 NaN 이후 NaN. 현재는 skipna=True가 기본이라 건너뜀

행 기준 누적 곱 (cumprod, axis=1, skipna=False):
     X    Y      Z
0  1.0  2.0    6.0
1  4.0  NaN    NaN
2  7.0 56.0  504.0

열 기준 누적 최소값 (cummin):
   X    Y    Z
0  1  2.0  3.0
1  1  NaN  3.0
2  1  8.0  3.0

열 기준 누적 최대값 (cummax, skipna=True 기본):
   X    Y    Z
0  1  2.0  3.0
1  4  2.0  6.0  # Y열: max(2, NaN) -> 2 (NaN 스킵)
2  7  8.0  9.0

참고: cumsum, cumprod 등에서 skipna=True (기본값)이면 NaN을 건너뛰고 계산합니다. skipna=False로 설정하면 NaN이 나타난 이후의 누적값은 NaN이 됩니다 (곱셈의 경우 0이 아니면 NaN).

활용 방안: 일별 매출 데이터에서 누적 매출액 계산, 주가 데이터에서 누적 수익률 계산, 특정 이벤트 발생까지의 누적 횟수 파악 등에 사용됩니다.

7.4. 기타 유용한 계산 함수

데이터 분석 과정에서 자주 사용되는 추가적인 계산 함수들입니다.

value_counts(): 고유값 빈도 계산 (Series 전용)

Series에서 각 고유값이 나타나는 횟수를 계산하여 반환합니다. normalize=True로 설정하면 빈도 대신 비율을 반환합니다.

예시 코드:

s_val_counts = pd.Series(['A', 'B', 'A', 'C', 'B', 'A', 'D', 'A'])
print("원본 Series:\n", s_val_counts)
print("\n고유값 빈도 (value_counts()):\n", s_val_counts.value_counts())
print("\n고유값 비율 (normalize=True):\n", s_val_counts.value_counts(normalize=True))

예상 결과:

원본 Series:
0    A
1    B
2    A
3    C
4    B
5    A
6    D
7    A
dtype: object

고유값 빈도 (value_counts()):
A    4
B    2
C    1
D    1
Name: count, dtype: int64

고유값 비율 (normalize=True):
A    0.50
B    0.25
C    0.125
D    0.125
Name: proportion, dtype: float64

nunique(): 고유값 개수 계산

Series나 DataFrame의 각 열 또는 행에 있는 고유값의 개수를 반환합니다.

예시 코드:

print("df_stats의 열별 고유값 개수:\n", df_stats.nunique(axis=0))
print("s_val_counts의 고유값 개수:", s_val_counts.nunique())

예상 결과:

df_stats의 열별 고유값 개수:
A    5
B    4
C    5
dtype: int64
s_val_counts의 고유값 개수: 4

corr() / cov(): 상관계수 / 공분산

DataFrame의 숫자형 열들 간의 피어슨 상관계수(corr) 또는 공분산(cov) 행렬을 계산합니다. method 파라미터로 다른 종류의 상관계수(켄달, 스피어만 등)도 계산 가능합니다.

예시 코드:

df_corr_data = pd.DataFrame({'X': [1,2,3,4,5], 'Y': [5,4,3,2,1], 'Z': [1,3,2,5,4]})
print("\n상관관계 계산용 DataFrame:\n", df_corr_data)
print("\n피어슨 상관계수 행렬 (corr()):\n", df_corr_data.corr())
print("\n공분산 행렬 (cov()):\n", df_corr_data.cov())

예상 결과:

상관관계 계산용 DataFrame:
   X  Y  Z
0  1  5  1
1  2  4  3
2  3  3  2
3  4  2  5
4  5  1  4

피어슨 상관계수 행렬 (corr()):
     X    Y         Z
X  1.0 -1.0  0.774597
Y -1.0  1.0 -0.774597
Z  0.774597 -0.774597  1.0

공분산 행렬 (cov()):
     X    Y    Z
X  2.5 -2.5  2.0
Y -2.5  2.5 -2.0
Z  2.0 -2.0  2.8

diff() / pct_change(): 차분 / 퍼센트 변화율

diff(periods=1)는 각 요소와 periods 만큼 이전 요소와의 차이를 계산합니다. pct_change(periods=1)는 퍼센트 변화율을 계산합니다. 시계열 데이터 분석에 유용합니다.

예시 코드:

s_change = pd.Series([10, 12, 15, 14, 18])
print("\n변화율 계산용 Series:\n", s_change)
print("\n차분 (diff()):\n", s_change.diff())
print("\n퍼센트 변화율 (pct_change()):\n", s_change.pct_change())

예상 결과:

변화율 계산용 Series:
0    10
1    12
2    15
3    14
4    18
dtype: int64

차분 (diff()):
0    NaN
1    2.0
2    3.0
3   -1.0
4    4.0
dtype: float64

퍼센트 변화율 (pct_change()):
0         NaN
1    0.200000  # (12-10)/10
2    0.250000  # (15-12)/12
3   -0.066667  # (14-15)/15
4    0.285714  # (18-14)/14
dtype: float64

활용 방안: 범주형 데이터의 분포 파악(value_counts), 변수 간 선형 관계 파악(corr), 시계열 데이터의 증감 추세(diff, pct_change) 분석 등 다양한 분석 작업에 활용됩니다.

Pandas의 기술 통계 및 계산 기능들은 데이터에 대한 깊이 있는 이해를 돕고, 더 나아가 복잡한 분석 모델링을 위한 기초를 마련해 줍니다. 데이터의 특성에 맞는 적절한 통계 함수를 사용하는 것이 중요합니다.

제 8 장: GroupBy 연산

데이터 분석에서 특정 기준에 따라 데이터를 그룹으로 나누고, 각 그룹별로 통계를 내거나 특정 변환을 적용하는 작업은 매우 흔합니다. Pandas의 groupby() 메서드는 이러한 "Split-Apply-Combine" (분할-적용-결합) 패턴을 효율적으로 구현할 수 있게 해주는 핵심 기능입니다. 이 장에서는 groupby()를 사용하여 데이터를 그룹화하고, 각 그룹에 대해 집계(aggregation), 변환(transformation), 필터링(filtration) 등 다양한 연산을 수행하는 방법을 심층적으로 다룹니다.

8.1. GroupBy 메커니즘: Split-Apply-Combine

GroupBy 연산은 다음과 같은 3단계 과정을 거칩니다:

  1. 분할 (Split): DataFrame을 특정 기준(하나 이상의 키)을 사용하여 여러 그룹으로 분할합니다.
  2. 적용 (Apply): 각 그룹에 독립적으로 함수(집계, 변환, 필터링 등)를 적용합니다.
  3. 결합 (Combine): 적용된 결과를 하나의 DataFrame 또는 Series로 결합합니다.

groupby() 메서드 사용법

DataFrame.groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=NoDefault.no_default, observed=False, dropna=True)

  • by: 그룹화 기준으로 사용될 대상. 다음과 같은 형태가 가능합니다:
    • 컬럼 이름 또는 컬럼 이름의 리스트: df.groupby('컬럼명'), df.groupby(['컬럼1', '컬럼2'])
    • Series 또는 Series의 리스트: 그룹화 키로 사용할 Series (DataFrame과 인덱스가 같아야 함).
    • 딕셔너리 또는 Series: 인덱스를 그룹 이름에 매핑.
    • 함수: 인덱스의 각 값에 대해 호출되어 그룹 이름을 반환.
    • 인덱스 레벨 이름(MultiIndex의 경우).
  • as_index=True (기본값): 그룹화 키를 결과의 인덱스로 사용합니다. False로 설정하면 기존 인덱스를 유지하고 그룹화 키를 일반 열로 추가합니다.
  • sort=True (기본값): 그룹 키를 기준으로 결과를 정렬합니다. 성능 향상을 위해 False로 설정할 수 있습니다.
Pandas의 GroupBy 연산은 데이터를 특정 기준에 따라 그룹으로 나누어 분석하는 강력한 방법을 제공합니다. 이 과정은 크게 '분할(Split)', '적용(Apply)', '결합(Combine)'의 세 단계로 이루어집니다.

예시 코드: 데이터 준비 및 GroupBy 객체 생성

import pandas as pd
import numpy as np

data = {'Company': ['GOOG', 'GOOG', 'MSFT', 'MSFT', 'FB', 'FB'],
        'Person': ['Sam', 'Charlie', 'Amy', 'Vanessa', 'Carl', 'Sarah'],
        'Sales': [200, 120, 340, 124, 243, 350],
        'Quarter': ['Q1', 'Q2', 'Q1', 'Q2', 'Q1', 'Q2']}
df_company = pd.DataFrame(data)
print("원본 DataFrame:\n", df_company)

# 'Company' 컬럼으로 그룹화
by_comp = df_company.groupby('Company')
print("\nGroupBy 객체:\n", by_comp) # GroupBy 객체 자체는 내용을 직접 보여주지 않음

# 그룹 확인 (어떤 인덱스들이 어떤 그룹에 속하는지)
print("\n그룹별 인덱스 (groups 속성):\n", by_comp.groups)

# 특정 그룹 데이터 가져오기
print("\n'GOOG' 그룹 데이터 (get_group()):\n", by_comp.get_group('GOOG'))

# 그룹별 크기 (size) 또는 개수 (count)
print("\n그룹별 크기 (size()):\n", by_comp.size())
print("\n그룹별 Sales 개수 (count() - Sales 열만):\n", by_comp['Sales'].count())

예상 결과:

원본 DataFrame:
  Company   Person  Sales Quarter
0    GOOG      Sam    200      Q1
1    GOOG  Charlie    120      Q2
2    MSFT      Amy    340      Q1
3    MSFT  Vanessa    124      Q2
4      FB     Carl    243      Q1
5      FB    Sarah    350      Q2

GroupBy 객체:
 <pandas.core.groupby.generic.DataFrameGroupBy object at 0x...>

그룹별 인덱스 (groups 속성):
{'FB': [4, 5], 'GOOG': [0, 1], 'MSFT': [2, 3]}

'GOOG' 그룹 데이터 (get_group()):
  Company   Person  Sales Quarter
0    GOOG      Sam    200      Q1
1    GOOG  Charlie    120      Q2

그룹별 크기 (size()):
Company
FB      2
GOOG    2
MSFT    2
dtype: int64

그룹별 Sales 개수 (count() - Sales 열만):
Company
FB      2
GOOG    2
MSFT    2
Name: Sales, dtype: int64

8.2. 그룹 집계 (Aggregation)

각 그룹에 대해 통계량을 계산하는 작업입니다. sum(), mean(), std(), count(), min(), max() 등의 기본 집계 함수를 직접 사용하거나, agg() (또는 aggregate()) 메서드를 통해 하나 이상의 집계 함수를 유연하게 적용할 수 있습니다.

단일/다중 집계 함수 적용

  • 단일 집계: by_comp.mean(numeric_only=True), by_comp['Sales'].sum()
  • agg() 사용:
    • 여러 함수 동시 적용: by_comp['Sales'].agg(['sum', 'mean', 'std'])
    • 컬럼별 다른 함수 적용 (딕셔너리): by_comp.agg({'Sales': 'sum', 'Person': 'count'})
    • 이름 있는 집계 (Named Aggregation) 로 결과 컬럼명 지정:
      by_comp.agg(Total_Sales=('Sales', 'sum'), Avg_Sales=('Sales', 'mean'))
    • 사용자 정의 함수 적용: by_comp['Sales'].agg(lambda x: x.max() - x.min())

예시 코드:

# 'Company'별 Sales 평균
print("\n회사별 Sales 평균 (mean()):\n", by_comp['Sales'].mean())

# 'Company'별 Sales 합계
print("\n회사별 Sales 합계 (sum()):\n", by_comp['Sales'].sum())

# agg()로 여러 집계 함수 적용
agg_funcs_sales = by_comp['Sales'].agg(['sum', 'mean', 'count', 'std', lambda x: x.max() - x.min()])
agg_funcs_sales.rename(columns={'': 'range'}, inplace=True) # 람다 함수 컬럼명 변경
print("\nagg()로 Sales에 여러 함수 적용:\n", agg_funcs_sales)

# agg()로 컬럼별 다른 함수 적용
agg_dict = {'Sales': ['sum', 'mean'], 'Person': 'count'}
print("\nagg()로 컬럼별 다른 함수 적용:\n", by_comp.agg(agg_dict))

# 이름 있는 집계 (Named Aggregation)
named_agg = by_comp.agg(
    Total_Sales=('Sales', 'sum'),
    Average_Sales=('Sales', 'mean'),
    Number_of_People=('Person', 'nunique') # 고유한 Person 수
)
print("\n이름 있는 집계 결과:\n", named_agg)

예상 결과:

회사별 Sales 평균 (mean()):
Company
FB      296.5
GOOG    160.0
MSFT    232.0
Name: Sales, dtype: float64

회사별 Sales 합계 (sum()):
Company
FB      593
GOOG    320
MSFT    464
Name: Sales, dtype: int64

agg()로 Sales에 여러 함수 적용:
          sum   mean  count        std  range
Company                                     
FB        593  296.5      2  75.660426    107
GOOG      320  160.0      2  56.568542     80
MSFT      464  232.0      2 152.735065    216

agg()로 컬럼별 다른 함수 적용:
         Sales        Person
           sum   mean  count
Company                     
FB         593  296.5      2
GOOG       320  160.0      2
MSFT       464  232.0      2

이름 있는 집계 결과:
          Total_Sales  Average_Sales  Number_of_People
Company                                             
FB                593          296.5                 2
GOOG              320          160.0                 2
MSFT              464          232.0                 2

활용 방안: 카테고리별(회사, 부서, 제품군 등) 매출 합계/평균, 사용자 그룹별 평균 구매액, 특정 기간별 통계량 계산 등 다양한 요약 정보를 얻는 데 핵심적으로 사용됩니다.

8.3. 그룹 변환 (Transformation)

transform() 메서드는 그룹별로 계산된 스칼라 값을 원래 DataFrame의 인덱스와 동일한 모양으로 변환하여 반환합니다. 이는 그룹별 통계량으로 원본 데이터를 표준화하거나 결측치를 채우는 등의 작업에 유용합니다.

transform() 사용법

함수는 각 그룹(Series)을 인자로 받아 같은 모양의 Series (또는 스칼라가 브로드캐스팅됨)를 반환해야 합니다.

예시 코드:

# 그룹별 Sales 평균으로 각 Sales 값 대체 (예시)
df_company['Group_Avg_Sales'] = by_comp['Sales'].transform('mean')
print("\n그룹 평균 Sales로 변환된 값 추가:\n", df_company)

# 그룹별 Sales의 Z-score 계산
# Z-score = (x - mean) / std
df_company['Sales_Zscore'] = by_comp['Sales'].transform(lambda x: (x - x.mean()) / x.std())
print("\n그룹별 Sales Z-score 추가:\n", df_company)

# 그룹별 최대 Sales 값으로 NaN 채우기 (예시용으로 새 DataFrame 생성)
df_nan_sales = df_company[['Company', 'Sales']].copy()
df_nan_sales.loc[1, 'Sales'] = np.nan # GOOG의 두 번째 Sales를 NaN으로
print("\nNaN 포함 Sales 데이터:\n", df_nan_sales)

# GOOG 그룹의 최대값은 200. NaN이 200으로 채워짐
df_nan_sales['Sales_filled_by_group_max'] = df_nan_sales.groupby('Company')['Sales'].transform(lambda x: x.fillna(x.max()))
print("\n그룹별 최대값으로 NaN 채운 Sales:\n", df_nan_sales)

예상 결과:

그룹 평균 Sales로 변환된 값 추가:
  Company   Person  Sales Quarter  Group_Avg_Sales  Sales_Zscore
0    GOOG      Sam    200      Q1            160.0      0.707107
1    GOOG  Charlie    120      Q2            160.0     -0.707107
2    MSFT      Amy    340      Q1            232.0      0.707107
3    MSFT  Vanessa    124      Q2            232.0     -0.707107
4      FB     Carl    243      Q1            296.5     -0.707107
5      FB    Sarah    350      Q2            296.5      0.707107

그룹별 Sales Z-score 추가:
  Company   Person  Sales Quarter  Group_Avg_Sales  Sales_Zscore
0    GOOG      Sam    200      Q1            160.0      0.707107
1    GOOG  Charlie    120      Q2            160.0     -0.707107
2    MSFT      Amy    340      Q1            232.0      0.707107
3    MSFT  Vanessa    124      Q2            232.0     -0.707107
4      FB     Carl    243      Q1            296.5     -0.707107
5      FB    Sarah    350      Q2            296.5      0.707107

NaN 포함 Sales 데이터:
  Company  Sales
0    GOOG  200.0
1    GOOG    NaN
2    MSFT  340.0
3    MSFT  124.0
4      FB  243.0
5      FB  350.0

그룹별 최대값으로 NaN 채운 Sales:
  Company  Sales  Sales_filled_by_group_max
0    GOOG  200.0                      200.0
1    GOOG    NaN                      200.0
2    MSFT  340.0                      340.0
3    MSFT  124.0                      124.0
4      FB  243.0                      243.0
5      FB  350.0                      350.0

활용 방안: 그룹 내 상대적 위치 파악 (Z-score, 백분위수), 그룹 통계량 기반 결측치 대치, 그룹별 정규화/표준화 등에 사용됩니다. 결과가 원본 DataFrame과 같은 인덱스를 가지므로 병합이 용이합니다.

8.4. 그룹 필터링 (Filtration)

filter() 메서드는 그룹 전체에 대한 조건을 평가하여, 조건을 만족하는 그룹의 데이터만 남기거나 제외합니다. 함수는 각 그룹(DataFrame)을 인자로 받아 불리언 값(True/False)을 반환해야 하며, True를 반환하는 그룹의 데이터만 선택됩니다.

filter() 사용법

예시 코드:

# Sales 평균이 200 이상인 회사들의 데이터만 필터링
filtered_by_mean_sales = by_comp.filter(lambda x: x['Sales'].mean() >= 200)
print("\nSales 평균 200 이상인 회사 데이터:\n", filtered_by_mean_sales)

# 그룹 내 Sales 값이 하나라도 300을 초과하는 회사의 데이터만 필터링
filtered_by_any_high_sales = by_comp.filter(lambda x: (x['Sales'] > 300).any())
print("\nSales 300 초과 건이 있는 회사 데이터:\n", filtered_by_any_high_sales)

# 그룹 크기(행 수)가 2인 회사들의 데이터만 필터링 (이 예제에서는 모든 회사가 해당)
filtered_by_size = by_comp.filter(lambda x: len(x) == 2)
print("\n그룹 크기가 2인 회사 데이터:\n", filtered_by_size)

예상 결과:

Sales 평균 200 이상인 회사 데이터:
  Company   Person  Sales Quarter  Group_Avg_Sales  Sales_Zscore
2    MSFT      Amy    340      Q1            232.0      0.707107
3    MSFT  Vanessa    124      Q2            232.0     -0.707107
4      FB     Carl    243      Q1            296.5     -0.707107
5      FB    Sarah    350      Q2            296.5      0.707107

Sales 300 초과 건이 있는 회사 데이터:
  Company   Person  Sales Quarter  Group_Avg_Sales  Sales_Zscore
2    MSFT      Amy    340      Q1            232.0      0.707107
3    MSFT  Vanessa    124      Q2            232.0     -0.707107
4      FB     Carl    243      Q1            296.5     -0.707107
5      FB    Sarah    350      Q2            296.5      0.707107

그룹 크기가 2인 회사 데이터:
  Company   Person  Sales Quarter  Group_Avg_Sales  Sales_Zscore
0    GOOG      Sam    200      Q1            160.0      0.707107
1    GOOG  Charlie    120      Q2            160.0     -0.707107
2    MSFT      Amy    340      Q1            232.0      0.707107
3    MSFT  Vanessa    124      Q2            232.0     -0.707107
4      FB     Carl    243      Q1            296.5     -0.707107
5      FB    Sarah    350      Q2            296.5      0.707107

활용 방안: 특정 조건을 만족하는 그룹(예: 평균 매출액이 특정 기준 이상인 고객 그룹, 특정 수 이상의 리뷰가 달린 상품 그룹)의 데이터만 추출하여 심층 분석하거나, 노이즈가 될 수 있는 소규모 그룹을 제외하는 데 사용됩니다.

8.5. 일반적인 함수 적용: apply()

apply() 메서드는 GroupBy 객체에 대해 가장 유연한 형태의 함수 적용 방법입니다. 함수는 각 그룹(DataFrame)을 인자로 받아 스칼라, Series, 또는 DataFrame 등 다양한 형태의 결과를 반환할 수 있습니다. 이는 집계, 변환, 필터링으로 처리하기 어려운 복잡한 그룹별 연산을 수행할 때 유용합니다.

apply() 사용법

예시 코드:

# 각 회사별로 Sales가 가장 높은 사람의 정보 가져오기
def get_top_sales_person(group_df):
    return group_df.loc[group_df['Sales'].idxmax()]

top_sales_per_company = by_comp.apply(get_top_sales_person)
# 만약 위 함수가 Series를 반환하면, GroupBy는 이를 DataFrame으로 결합하려고 시도합니다.
# 반환 값이 DataFrame이면, 결과는 MultiIndex를 가진 DataFrame이 될 수 있습니다.
print("\n각 회사별 최고 Sales 기록자:\n", top_sales_per_company)


# 각 회사별 Sales를 해당 회사 Sales의 평균으로 나눈 값을 새 열로 추가
def sales_divided_by_group_mean(group_df):
    group_df['Sales_Norm_By_Mean'] = group_df['Sales'] / group_df['Sales'].mean()
    return group_df

df_applied_norm = by_comp.apply(sales_divided_by_group_mean)
# apply는 때때로 인덱스를 재설정하거나 변경할 수 있으므로, 원본과 병합 시 주의 필요
# 이 경우에는 그룹화 키가 인덱스로 들어가므로, 원본 df_company와 인덱스가 다를 수 있음.
# 결과를 원본에 병합하려면 인덱스를 맞추거나, apply 결과의 인덱스를 초기화 후 병합.
print("\napply로 그룹 평균 대비 Sales 비율 추가 (결과 확인용):\n", df_applied_norm)

# 참고: 위 연산은 transform으로 더 간단히 가능
# df_company['Sales_Norm_By_Mean_Transform'] = by_comp['Sales'].transform(lambda x: x / x.mean())
# print("\n(참고) transform으로 그룹 평균 대비 Sales 비율 추가:\n", df_company)

예상 결과:

각 회사별 최고 Sales 기록자:
         Company   Person  Sales Quarter  Group_Avg_Sales  Sales_Zscore
Company                                                               
FB      5       FB    Sarah    350      Q2            296.5      0.707107
GOOG    0     GOOG      Sam    200      Q1            160.0      0.707107
MSFT    2     MSFT      Amy    340      Q1            232.0      0.707107

apply로 그룹 평균 대비 Sales 비율 추가 (결과 확인용):
  Company   Person  Sales Quarter  Group_Avg_Sales  Sales_Zscore  Sales_Norm_By_Mean
0    GOOG      Sam    200      Q1            160.0      0.707107              1.2500
1    GOOG  Charlie    120      Q2            160.0     -0.707107              0.7500
2    MSFT      Amy    340      Q1            232.0      0.707107              1.465517
3    MSFT  Vanessa    124      Q2            232.0     -0.707107              0.534483
4      FB     Carl    243      Q1            296.5     -0.707107              0.819562
5      FB    Sarah    350      Q2            296.5      0.707107              1.180438

활용 방안: 그룹별 상위/하위 N개 레코드 선택, 그룹별로 복잡한 통계 모델 적용, 각 그룹의 특성에 맞는 사용자 정의 연산 수행 등 매우 광범위하게 활용될 수 있습니다. 다만, agg, transform, filter로 해결 가능한 경우 이들을 우선 사용하는 것이 성능상 유리할 수 있습니다.

GroupBy 연산은 Pandas를 활용한 데이터 분석의 핵심입니다. 다양한 그룹화 기준과 적용 함수를 조합하여 복잡한 데이터로부터 의미 있는 통찰력을 효과적으로 도출할 수 있습니다.

제 9 장: 데이터 병합, 결합, 연결

데이터 분석 프로젝트에서는 종종 여러 개의 분산된 데이터셋을 하나로 통합해야 하는 경우가 발생합니다. 예를 들어, 고객 정보 데이터와 구매 내역 데이터를 결합하거나, 여러 기간에 걸쳐 수집된 데이터를 하나로 합치는 작업 등이 있습니다. Pandas는 이러한 데이터 통합 작업을 위해 concat(), merge(), join()과 같은 강력하고 유연한 함수들을 제공합니다. 이 장에서는 이러한 함수들을 사용하여 다양한 방식으로 DataFrame을 합치는 방법을 학습하고, 각 함수의 주요 옵션과 사용 사례를 살펴봅니다.

9.1. 데이터 연결 (Concatenating): pd.concat()

pd.concat() 함수는 여러 개의 DataFrame이나 Series를 특정 축(행 또는 열)을 따라 단순하게 이어 붙이는 역할을 합니다. SQL의 UNION ALL과 유사한 개념으로 볼 수 있습니다.

pd.concat() 주요 파라미터

  • objs: 연결할 Pandas 객체(DataFrame, Series 등)의 리스트 또는 딕셔너리.
  • axis: 연결할 축을 지정합니다. 0 (또는 'index')은 행 방향으로 아래로 이어 붙이고(기본값), 1 (또는 'columns')은 열 방향으로 옆으로 이어 붙입니다.
  • join: 다른 축의 인덱스/컬럼 처리 방식을 지정합니다.
    • 'outer': 모든 인덱스/컬럼을 포함하는 합집합 방식으로 처리하며, 없는 값은 NaN으로 채웁니다 (기본값).
    • 'inner': 공통된 인덱스/컬럼만 포함하는 교집합 방식으로 처리합니다.
  • ignore_index=False (기본값): True로 설정하면 연결된 축의 기존 인덱스를 무시하고 0부터 시작하는 새로운 정수 인덱스를 생성합니다.
  • keys: 연결되는 객체들을 구분하기 위한 계층적 인덱스(MultiIndex)를 생성할 때 사용합니다. 리스트 형태로 각 객체에 대한 키를 전달합니다.

예시 코드:

import pandas as pd

df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                    'B': ['B0', 'B1', 'B2']},
                   index=[0, 1, 2])

df2 = pd.DataFrame({'A': ['A3', 'A4', 'A5'],
                    'B': ['B3', 'B4', 'B5']},
                   index=[3, 4, 5])

df3 = pd.DataFrame({'C': ['C0', 'C1'],
                    'D': ['D0', 'D1']},
                   index=[0, 1])

print("df1:\n", df1)
print("\ndf2:\n", df2)
print("\ndf3:\n", df3)

# 행 방향으로 연결 (axis=0, 기본값)
result_row_concat = pd.concat([df1, df2])
print("\n행 방향 연결 (기본):\n", result_row_concat)

# ignore_index=True로 새 인덱스 생성
result_row_ignore_idx = pd.concat([df1, df2], ignore_index=True)
print("\n행 방향 연결 (ignore_index=True):\n", result_row_ignore_idx)

# 열 방향으로 연결 (axis=1)
result_col_concat_outer = pd.concat([df1, df3], axis=1, join='outer') # 기본 join='outer'
print("\n열 방향 연결 (join='outer'):\n", result_col_concat_outer)

result_col_concat_inner = pd.concat([df1, df3], axis=1, join='inner')
print("\n열 방향 연결 (join='inner'):\n", result_col_concat_inner)

# keys를 사용하여 계층적 인덱스 생성
result_with_keys = pd.concat([df1, df2], keys=['DF1_KEY', 'DF2_KEY'])
print("\nkeys를 사용한 연결:\n", result_with_keys)
print("\nDF1_KEY 접근:\n", result_with_keys.loc['DF1_KEY'])

예상 결과:

df1:
    A   B
0  A0  B0
1  A1  B1
2  A2  B2

df2:
    A   B
3  A3  B3
4  A4  B4
5  A5  B5

df3:
    C   D
0  C0  D0
1  C1  D1

행 방향 연결 (기본):
    A   B
0  A0  B0
1  A1  B1
2  A2  B2
3  A3  B3
4  A4  B4
5  A5  B5

행 방향 연결 (ignore_index=True):
    A   B
0  A0  B0
1  A1  B1
2  A2  B2
3  A3  B3
4  A4  B4
5  A5  B5

열 방향 연결 (join='outer'):
     A    B    C    D
0   A0   B0   C0   D0
1   A1   B1   C1   D1
2   A2   B2  NaN  NaN  # df3에는 인덱스 2가 없으므로 NaN

열 방향 연결 (join='inner'):
    A   B   C   D
0  A0  B0  C0  D0
1  A1  B1  C1  D1

keys를 사용한 연결:
         A   B
DF1_KEY 0  A0  B0
        1  A1  B1
        2  A2  B2
DF2_KEY 3  A3  B3
        4  A4  B4
        5  A5  B5

DF1_KEY 접근:
   A   B
0  A0  B0
1  A1  B1
2  A2  B2

활용 방안: 여러 기간에 걸쳐 수집된 동일한 형식의 데이터를 시간 순으로 합치거나, 서로 다른 출처의 데이터를 단순히 모아 하나의 큰 데이터셋으로 만들 때 유용합니다. append() 메서드도 간단한 행 추가에 사용될 수 있으나, Pandas 최신 버전에서는 concat 사용이 권장됩니다.

9.2. 데이터베이스 스타일 병합: pd.merge()

pd.merge() 함수는 SQL의 JOIN 연산과 매우 유사하게, 하나 이상의 공통된 열(키)을 기준으로 두 개의 DataFrame을 병합합니다. 다양한 병합 방식(inner, outer, left, right)을 지원하여 유연한 데이터 통합이 가능합니다.

pd.merge() 주요 파라미터

  • left, right: 병합할 두 DataFrame 객체.
  • how='inner' (기본값): 병합 방식을 지정합니다.
    • 'inner': 양쪽 DataFrame 모두에 존재하는 키 값만 포함하여 병합 (교집합).
    • 'outer': 어느 한쪽 DataFrame에라도 존재하는 모든 키 값을 포함하여 병합. 없는 값은 NaN으로 채움 (합집합).
    • 'left': 왼쪽 DataFrame의 모든 키 값을 기준으로 병합. 오른쪽 DataFrame에 해당 키가 없으면 NaN으로 채움.
    • 'right': 오른쪽 DataFrame의 모든 키 값을 기준으로 병합. 왼쪽 DataFrame에 해당 키가 없으면 NaN으로 채움.
  • on: 병합 기준으로 사용할 열 이름 또는 열 이름의 리스트. 양쪽 DataFrame에 모두 존재하고 이름이 동일해야 합니다. 지정하지 않으면 공통된 모든 열을 기준으로 병합합니다.
  • left_on, right_on: 병합 기준으로 사용할 열 이름이 양쪽 DataFrame에서 다를 경우, 각각 지정합니다.
  • left_index=False, right_index=False (기본값): True로 설정하면 해당 DataFrame의 인덱스를 병합 키로 사용합니다.
  • suffixes=('_x', '_y'): 병합 결과, 양쪽 DataFrame에 이름은 같지만 병합 키가 아닌 열이 존재할 경우, 각 열 이름 뒤에 붙일 접미사를 지정합니다.
  • indicator=False (기본값): True로 설정하면 _merge라는 특수 열이 추가되어 각 행이 어떤 DataFrame에서 유래했는지 ('left_only', 'right_only', 'both') 표시합니다.
  • validate: 병합 키의 유일성(uniqueness)을 검증하는 옵션 (예: 'one_to_one', 'one_to_many', 'many_to_one', 'many_to_many'). 유효하지 않으면 오류 발생.

예시 코드:

left_df = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                                'A': ['A0', 'A1', 'A2', 'A3'],
                                'B': ['B0', 'B1', 'B2', 'B3']})

right_df = pd.DataFrame({'key': ['K0', 'K1', 'K4', 'K5'], # K2, K3 없음, K4, K5 추가
                                 'C': ['C0', 'C1', 'C4', 'C5'],
                                 'D': ['D0', 'D1', 'D4', 'D5']})
print("left_df:\n", left_df)
print("\nright_df:\n", right_df)

# inner merge (기본, 공통 'key' 기준)
inner_merged = pd.merge(left_df, right_df, on='key')
print("\nInner Merge (on 'key'):\n", inner_merged)

# outer merge
outer_merged = pd.merge(left_df, right_df, on='key', how='outer')
print("\nOuter Merge (on 'key'):\n", outer_merged)

# left merge
left_merged = pd.merge(left_df, right_df, on='key', how='left')
print("\nLeft Merge (on 'key'):\n", left_merged)

# right merge with indicator
right_merged_indicator = pd.merge(left_df, right_df, on='key', how='right', indicator=True)
print("\nRight Merge (on 'key', with indicator):\n", right_merged_indicator)

# 다른 이름의 키로 병합 (left_on, right_on)
left_df_diffkey = pd.DataFrame({'lkey': ['K0', 'K1', 'K2'], 'val_L': [1,2,3]})
right_df_diffkey = pd.DataFrame({'rkey': ['K0', 'K1', 'K3'], 'val_R': [4,5,6]})
merged_diff_keys = pd.merge(left_df_diffkey, right_df_diffkey, left_on='lkey', right_on='rkey', how='inner')
print("\n다른 이름의 키로 병합 (inner):\n", merged_diff_keys)

# 인덱스 기준 병합
left_idx_df = pd.DataFrame({'A': ['A0', 'A1']}, index=['K0', 'K1'])
right_idx_df = pd.DataFrame({'B': ['B0', 'B1']}, index=['K0', 'K2']) # K1 대신 K2
merged_idx = pd.merge(left_idx_df, right_idx_df, left_index=True, right_index=True, how='outer')
print("\n인덱스 기준 병합 (outer):\n", merged_idx)

예상 결과:

left_df:
  key   A   B
0  K0  A0  B0
1  K1  A1  B1
2  K2  A2  B2
3  K3  A3  B3

right_df:
  key   C   D
0  K0  C0  D0
1  K1  C1  D1
2  K4  C4  D4
3  K5  C5  D5

Inner Merge (on 'key'):
  key   A   B   C   D
0  K0  A0  B0  C0  D0
1  K1  A1  B1  C1  D1

Outer Merge (on 'key'):
  key    A    B    C    D
0  K0   A0   B0   C0   D0
1  K1   A1   B1   C1   D1
2  K2   A2   B2  NaN  NaN
3  K3   A3   B3  NaN  NaN
4  K4  NaN  NaN   C4   D4
5  K5  NaN  NaN   C5   D5

Left Merge (on 'key'):
  key   A   B    C    D
0  K0  A0  B0   C0   D0
1  K1  A1  B1   C1   D1
2  K2  A2  B2  NaN  NaN
3  K3  A3  B3  NaN  NaN

Right Merge (on 'key', with indicator):
  key    A    B   C   D      _merge
0  K0   A0   B0  C0  D0        both
1  K1   A1   B1  C1  D1        both
2  K4  NaN  NaN  C4  D4  right_only
3  K5  NaN  NaN  C5  D5  right_only

다른 이름의 키로 병합 (inner):
  lkey  val_L rkey  val_R
0   K0      1   K0      4
1   K1      2   K1      5

인덱스 기준 병합 (outer):
      A    B
K0   A0   B0
K1   A1  NaN
K2  NaN   B1

활용 방안: 관계형 데이터베이스의 테이블들을 조인하는 것과 같이, 공통 식별자(ID, 코드 등)를 기준으로 서로 다른 정보를 가진 데이터들을 결합하여 풍부한 분석용 데이터셋을 구축할 때 매우 유용합니다. 고객 정보와 주문 내역 병합, 상품 정보와 판매 정보 병합 등이 대표적인 예입니다.

9.3. 인덱스 기반 결합: DataFrame.join()

DataFrame.join() 메서드는 주로 DataFrame의 인덱스를 기준으로 다른 DataFrame(들)을 결합할 때 사용됩니다. merge() 함수로도 인덱스 기반 결합이 가능하지만, join()은 인덱스 기준 결합을 더 간결하게 표현할 수 있도록 합니다. 기본적으로 왼쪽 DataFrame의 인덱스를 기준으로 결합(left join)합니다.

DataFrame.join() 주요 파라미터

  • other: 결합할 다른 DataFrame 객체. Series나 DataFrame의 리스트도 가능합니다.
  • on: 호출하는 DataFrame(왼쪽)에서 결합 기준으로 사용할 열 이름 또는 열 이름의 리스트. 이 열의 값과 other DataFrame의 인덱스를 기준으로 결합합니다. 지정하지 않으면 왼쪽 DataFrame의 인덱스를 사용합니다.
  • how='left' (기본값): 결합 방식을 지정합니다 ('left', 'right', 'outer', 'inner').
  • lsuffix, rsuffix: 양쪽 DataFrame에 중복되는 열 이름이 있을 경우, 각각에 붙일 접미사를 지정합니다.

예시 코드:

left_join_df = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                                 'B': ['B0', 'B1', 'B2']},
                                index=['K0', 'K1', 'K2'])

right_join_df = pd.DataFrame({'C': ['C0', 'C2', 'C3'],
                                  'D': ['D0', 'D2', 'D3']},
                                 index=['K0', 'K2', 'K3']) # K1 없음, K3 추가
print("left_join_df:\n", left_join_df)
print("\nright_join_df:\n", right_join_df)

# 기본 join (left join, 인덱스 기준)
joined_default = left_join_df.join(right_join_df)
print("\nDefault Join (left, on index):\n", joined_default)

# outer join
joined_outer = left_join_df.join(right_join_df, how='outer')
print("\nOuter Join (on index):\n", joined_outer)

# inner join
joined_inner = left_join_df.join(right_join_df, how='inner')
print("\nInner Join (on index):\n", joined_inner)

# 왼쪽 DataFrame의 특정 열('A')과 오른쪽 DataFrame의 인덱스 기준 결합
left_on_col_df = pd.DataFrame({'key_col': ['K0', 'K0', 'K1'], 'val1': [0,1,2]})
right_on_idx_df = pd.DataFrame({'val2': [10,20]}, index=['K0', 'K1'])
joined_on_col_idx = left_on_col_df.join(right_on_idx_df, on='key_col', how='left')
print("\n왼쪽 열 & 오른쪽 인덱스 기준 결합:\n", joined_on_col_idx)

# 중복 열 이름 처리 (lsuffix, rsuffix)
df_x = pd.DataFrame({'X': [1,2]}, index=['a','b'])
df_y = pd.DataFrame({'X': [3,4]}, index=['a','c'])
joined_suffix = df_x.join(df_y, lsuffix='_left', rsuffix='_right', how='outer')
print("\n중복 열 이름 접미사 처리:\n", joined_suffix)

예상 결과:

left_join_df:
     A   B
K0  A0  B0
K1  A1  B1
K2  A2  B2

right_join_df:
     C   D
K0  C0  D0
K2  C2  D2
K3  C3  D3

Default Join (left, on index):
     A   B    C    D
K0  A0  B0   C0   D0
K1  A1  B1  NaN  NaN
K2  A2  B2   C2   D2

Outer Join (on index):
      A    B    C    D
K0   A0   B0   C0   D0
K1   A1   B1  NaN  NaN
K2   A2   B2   C2   D2
K3  NaN  NaN   C3   D3

Inner Join (on index):
     A   B   C   D
K0  A0  B0  C0  D0
K2  A2  B2  C2  D2

왼쪽 열 & 오른쪽 인덱스 기준 결합:
  key_col  val1  val2
0      K0     0  10.0
1      K0     1  10.0
2      K1     2  20.0

중복 열 이름 접미사 처리:
   X_left  X_right
a     1.0      3.0
b     2.0      NaN
c     NaN      4.0

활용 방안: 서로 다른 정보를 담고 있지만 공통된 인덱스(또는 특정 열)를 기준으로 데이터를 빠르게 결합하고자 할 때 유용합니다. 특히, 왼쪽 DataFrame을 기준으로 정보를 추가하거나 확장하는 시나리오에 적합합니다.

9.4. 어떤 방법을 언제 사용해야 하는가?

Pandas는 데이터 결합을 위한 여러 방법을 제공하므로, 상황에 맞는 적절한 함수를 선택하는 것이 중요합니다.

데이터 통합은 분석을 위한 데이터 준비 단계에서 매우 중요한 과정입니다. concat, merge, join의 특징과 옵션을 잘 이해하고 활용하면, 흩어져 있는 데이터를 효과적으로 결합하여 분석에 필요한 형태로 가공할 수 있습니다.

제 10 장: 시계열 데이터 처리

시계열 데이터는 시간의 흐름에 따라 기록된 데이터를 의미하며, 금융 시장 분석, 경제 예측, 기상 변화 관찰, 센서 데이터 분석 등 다양한 분야에서 중요하게 활용됩니다. Pandas는 이러한 시계열 데이터를 효과적으로 처리하고 분석할 수 있도록 Timestamp, DatetimeIndex, Period와 같은 특수한 자료구조와 함께 다양한 시계열 관련 함수 및 메서드를 제공합니다. 이 장에서는 Pandas를 사용하여 날짜 및 시간 데이터를 다루는 방법, 시계열 인덱스 생성, 데이터 선택, 리샘플링(resampling), 이동창(rolling window) 연산, 데이터 이동(shifting) 등 핵심적인 시계열 데이터 처리 기법들을 학습합니다.

10.1. Pandas 시계열 데이터 기본

Pandas에서 시계열 데이터를 다루기 위한 기본적인 객체들을 이해하는 것이 중요합니다.

주요 날짜/시간 객체

  • Timestamp: 특정 시점(날짜와 시간)을 나타냅니다. Python의 datetime.datetime 객체와 유사하며, 나노초(nanosecond) 단위까지 정밀도를 가집니다.
  • DatetimeIndex: Timestamp 객체들로 구성된 인덱스입니다. 시계열 DataFrame이나 Series의 핵심적인 역할을 합니다.
  • Period: 특정 기간(예: 특정 일, 월, 분기, 연도)을 나타냅니다. 예를 들어, '2025-05'는 2025년 5월 전체 기간을 의미할 수 있습니다.
  • PeriodIndex: Period 객체들로 구성된 인덱스입니다.
  • Timedelta: 두 Timestamp 또는 Period 사이의 시간 차이(기간 또는 지속 시간)를 나타냅니다.

예시 코드:

import pandas as pd

# Timestamp 생성
ts_now = pd.Timestamp('2025-05-20 10:30:00')
print("Timestamp:", ts_now)
print("Year:", ts_now.year, ", Month:", ts_now.month, ", Day:", ts_now.day, ", Day of week:", ts_now.dayofweek) # 월요일=0, 일요일=6

# DatetimeIndex 생성
date_idx = pd.to_datetime(['2025-01-01', '2025-01-02', '2025-01-03'])
print("\nDatetimeIndex:\n", date_idx)

# Period 생성
period_month = pd.Period('2025-05', freq='M') # 'M'은 월말 빈도
print("\nPeriod (Month):", period_month)
print("Start time:", period_month.start_time, ", End time:", period_month.end_time)

# Timedelta 생성
delta = pd.Timestamp('2025-05-21') - pd.Timestamp('2025-05-20')
print("\nTimedelta:", delta)

예상 결과:

Timestamp: 2025-05-20 10:30:00
Year: 2025 , Month: 5 , Day: 20 , Day of week: 1

DatetimeIndex:
 DatetimeIndex(['2025-01-01', '2025-01-02', '2025-01-03'], dtype='datetime64[ns]', freq=None)

Period (Month): 2025-05
Start time: 2025-05-01 00:00:00 , End time: 2025-05-31 23:59:59.999999999

Timedelta: 1 days 00:00:00

10.2. 날짜/시간 데이터 생성 및 변환

다양한 형태의 데이터를 Pandas의 날짜/시간 객체로 변환하거나, 특정 규칙에 따라 날짜/시간 범위를 생성할 수 있습니다.

pd.to_datetime(): 문자열 등을 날짜/시간 객체로 변환

문자열 리스트나 Series를 Timestamp 객체 또는 DatetimeIndex로 변환합니다.

  • format: 입력 문자열의 날짜/시간 형식을 지정합니다 (예: '%Y-%m-%d'). 형식이 일관되지 않으면 추론을 시도합니다.
  • errors: 변환 중 오류 발생 시 처리 방법 ('raise': 오류 발생(기본값), 'coerce': NaT(Not a Time)으로 변환, 'ignore': 원본 값 유지).

예시 코드:

date_strings = ['2025-01-05', '2025/01/10', 'Jan 15, 2025', '2025.01.20', 'invalid_date']
dt_index_from_strings = pd.to_datetime(date_strings, errors='coerce') # 오류 발생 시 NaT으로
print("\n문자열에서 변환된 DatetimeIndex (errors='coerce'):\n", dt_index_from_strings)

# 특정 포맷 지정
custom_format_dates = pd.to_datetime(['25-12-2024', '26-12-2024'], format='%d-%m-%Y')
print("\n사용자 정의 포맷으로 변환된 DatetimeIndex:\n", custom_format_dates)

예상 결과:

문자열에서 변환된 DatetimeIndex (errors='coerce'):
 DatetimeIndex(['2025-01-05', '2025-01-10', '2025-01-15', '2025-01-20', 'NaT'], dtype='datetime64[ns]', freq=None)

사용자 정의 포맷으로 변환된 DatetimeIndex:
 DatetimeIndex(['2024-12-25', '2024-12-26'], dtype='datetime64[ns]', freq=None)

pd.date_range(): 날짜/시간 범위 생성

시작일, 종료일, 기간, 빈도(frequency) 등을 지정하여 DatetimeIndex를 생성합니다.

  • start: 시작 날짜/시간.
  • end: 종료 날짜/시간.
  • periods: 생성할 날짜/시간의 개수.
  • freq: 빈도 설정 (예: 'D': 일, 'B': 업무일, 'H': 시간, 'T' 또는 'min': 분, 'S': 초, 'L': 밀리초, 'M': 월말, 'MS': 월초, 'Q': 분기말, 'QS': 분기초, 'A': 연말, 'AS': 연초 등. '2H', '3D' 처럼 배수 지정 가능).

예시 코드:

# 2025년 1월 1일부터 5일간 일별 DatetimeIndex
date_range_daily = pd.date_range(start='2025-01-01', periods=5, freq='D')
print("\n일별 DatetimeIndex (5일간):\n", date_range_daily)

# 2025년 1월 한 달간 업무일(Business day) DatetimeIndex
date_range_business = pd.date_range(start='2025-01-01', end='2025-01-31', freq='B')
print("\n2025년 1월 업무일 DatetimeIndex:\n", date_range_business)

# 2025년 5월 20일 0시부터 4시간 간격으로 6개
date_range_hourly = pd.date_range(start='2025-05-20', periods=6, freq='4H')
print("\n4시간 간격 DatetimeIndex:\n", date_range_hourly)

예상 결과:

일별 DatetimeIndex (5일간):
 DatetimeIndex(['2025-01-01', '2025-01-02', '2025-01-03', '2025-01-04',
               '2025-01-05'],
              dtype='datetime64[ns]', freq='D')

2025년 1월 업무일 DatetimeIndex:
 DatetimeIndex(['2025-01-01', '2025-01-02', '2025-01-03', '2025-01-06',
               '2025-01-07', '2025-01-08', '2025-01-09', '2025-01-10',
               '2025-01-13', '2025-01-14', '2025-01-15', '2025-01-16',
               '2025-01-17', '2025-01-20', '2025-01-21', '2025-01-22',
               '2025-01-23', '2025-01-24', '2025-01-27', '2025-01-28',
               '2025-01-29', '2025-01-30', '2025-01-31'],
              dtype='datetime64[ns]', freq='B')

4시간 간격 DatetimeIndex:
 DatetimeIndex(['2025-05-20 00:00:00', '2025-05-20 04:00:00',
               '2025-05-20 08:00:00', '2025-05-20 12:00:00',
               '2025-05-20 16:00:00', '2025-05-20 20:00:00'],
              dtype='datetime64[ns]', freq='4H')

10.3. 시계열 데이터 인덱싱 및 선택

DatetimeIndex를 사용하면 날짜/시간을 기준으로 데이터를 매우 편리하게 선택하고 슬라이싱할 수 있습니다.

예시 코드:

# 시계열 데이터 생성
ts_idx = pd.date_range('2025-01-01', periods=100, freq='D')
ts_data = pd.Series(np.random.randn(len(ts_idx)), index=ts_idx)
print("\n샘플 시계열 데이터 (첫 5개):\n", ts_data.head())

# 특정 날짜 선택
print("\n2025-01-05 데이터:", ts_data['2025-01-05'])

# 특정 연도/월 선택
print("\n2025년 2월 데이터 (일부):\n", ts_data['2025-02'].head())

# 날짜 범위 슬라이싱
print("\n2025-01-10 부터 2025-01-15 까지 데이터:\n", ts_data['2025-01-10':'2025-01-15'])

# truncate: 특정 날짜 이전/이후/사이 데이터 자르기
# 2025년 2월 15일 이후 데이터만 선택
truncated_after = ts_data.truncate(before='2025-02-15')
print("\n2025-02-15 이후 데이터 (truncate, 첫 3개):\n", truncated_after.head(3))

# asof: 특정 시점 또는 그 이전의 가장 가까운 유효한 값
# 만약 ts_data['2025-01-15 12:00:00']가 없다면, 그 이전 가장 가까운 값
# 정확한 예시를 위해 시간을 포함한 인덱스 재생성
ts_idx_time = pd.date_range('2025-01-15 00:00:00', periods=5, freq='H')
ts_data_time = pd.Series(range(5), index=ts_idx_time)
ts_data_time.iloc[2] = np.nan # 2025-01-15 02:00:00을 NaN으로
print("\n시간 포함 시계열 데이터:\n", ts_data_time)
print("\n2025-01-15 02:30:00 시점 또는 이전 최근 값 (asof):", ts_data_time.asof('2025-01-15 02:30:00'))

예상 결과 (randn 부분은 실행 시마다 다름):

샘플 시계열 데이터 (첫 5개):
2025-01-01    0.123456
2025-01-02   -0.567890
2025-01-03    1.122334
2025-01-04   -0.987654
2025-01-05    0.456789
Freq: D, dtype: float64

2025-01-05 데이터: 0.456789...

2025년 2월 데이터 (일부):
2025-02-01   -0.111111
2025-02-02    0.222222
2025-02-03   -0.333333
2025-02-04    0.444444
2025-02-05   -0.555555
Freq: D, dtype: float64

2025-01-10 부터 2025-01-15 까지 데이터:
2025-01-10    ...
2025-01-11    ...
2025-01-12    ...
2025-01-13    ...
2025-01-14    ...
2025-01-15    ...
Freq: D, dtype: float64

2025-02-15 이후 데이터 (truncate, 첫 3개):
2025-02-15    ...
2025-02-16    ...
2025-02-17    ...
Freq: D, dtype: float64

시간 포함 시계열 데이터:
2025-01-15 00:00:00    0.0
2025-01-15 01:00:00    1.0
2025-01-15 02:00:00    NaN
2025-01-15 03:00:00    3.0
2025-01-15 04:00:00    4.0
Freq: H, dtype: float64

2025-01-15 02:30:00 시점 또는 이전 최근 값 (asof): 1.0

활용 방안: 특정 기간의 데이터 분석, 특정 이벤트 발생 시점의 데이터 확인, 시간 기반 필터링 등에 매우 유용합니다.

10.4. 리샘플링 (Resampling)

resample() 메서드는 시계열 데이터의 빈도(frequency)를 변환하는 강력한 기능입니다. 예를 들어, 일별 데이터를 월별 데이터로 (다운샘플링), 또는 월별 데이터를 일별 데이터로 (업샘플링) 변환할 수 있습니다.

다운샘플링 (Downsampling)

높은 빈도의 데이터를 낮은 빈도로 변환합니다. 이 과정에서 그룹화된 데이터에 대한 집계 함수(sum(), mean(), first(), last(), ohlc() - 시고저종 등) 적용이 필요합니다.

예시 코드 (위의 ts_data 사용):

# 월별 평균 계산 (다운샘플링)
monthly_mean = ts_data.resample('M').mean() # 'M'은 월말 빈도
print("\n월별 평균 (다운샘플링):\n", monthly_mean.head(3))

# 주별(일요일 기준) 합계 계산
weekly_sum = ts_data.resample('W').sum() # 'W'는 주별 빈도 (일요일이 주의 끝)
print("\n주별 합계 (다운샘플링):\n", weekly_sum.head(3))

# 분기별 OHLC (Open, High, Low, Close) 계산 - 주가 데이터 등에 사용
# 여기서는 임의의 데이터로 예시
ts_ohlc_data = pd.Series(np.random.randint(50,100, size=len(ts_data)), index=ts_data.index)
quarterly_ohlc = ts_ohlc_data.resample('Q').ohlc() # 'Q'는 분기말 빈도
print("\n분기별 OHLC (다운샘플링):\n", quarterly_ohlc)

예상 결과 (randn 부분은 실행 시마다 다름):

월별 평균 (다운샘플링):
2025-01-31    ... (1월 데이터 평균)
2025-02-28    ... (2월 데이터 평균)
2025-03-31    ... (3월 데이터 평균)
Freq: M, Name: (randn 값에 따라 다름), dtype: float64

주별 합계 (다운샘플링):
2025-01-05    ... (첫 주 ~1/5 합계)
2025-01-12    ... (1/6 ~ 1/12 합계)
2025-01-19    ... (1/13 ~ 1/19 합계)
Freq: W-SUN, Name: (randn 값에 따라 다름), dtype: float64

분기별 OHLC (다운샘플링):
            open  high  low  close
2025-03-31   ...   ...  ...    ...  (1분기 ohlc)
2025-06-30   ...   ...  ...    ...  (2분기 ohlc, 4월 10일까지의 데이터로 계산됨)
Freq: Q-DEC, Name: (randint 값에 따라 다름), dtype: int...

업샘플링 (Upsampling)

낮은 빈도의 데이터를 높은 빈도로 변환합니다. 이 과정에서 새로 생긴 시점의 값들은 대부분 NaN이 되므로, 이를 채우는 방법(보간법 등)이 필요합니다.

  • asfreq(): 특정 빈도로 변환만 하고 값은 채우지 않음 (NaN).
  • fillna(): NaN 값을 특정 값으로 채움 ('ffill': 앞 값, 'bfill': 뒷 값).
  • interpolate(): 다양한 보간법(선형, 다항 등)으로 NaN 값을 채움.

예시 코드 (위의 monthly_mean 사용 가정):

# 월별 데이터를 일별 데이터로 업샘플링 (asfreq)
daily_from_monthly_asfreq = monthly_mean.resample('D').asfreq()
print("\n월별 -> 일별 업샘플링 (asfreq, 첫 5개):\n", daily_from_monthly_asfreq.head())

# ffill로 채우기
daily_from_monthly_ffill = monthly_mean.resample('D').ffill()
print("\n월별 -> 일별 업샘플링 (ffill, 첫 5개):\n", daily_from_monthly_ffill.head())

# 선형 보간으로 채우기
daily_from_monthly_interp = monthly_mean.resample('D').interpolate(method='linear')
print("\n월별 -> 일별 업샘플링 (선형 보간, 첫 5개):\n", daily_from_monthly_interp.head())

예상 결과 (monthly_mean 값에 따라 다름):

월별 -> 일별 업샘플링 (asfreq, 첫 5개):
2025-01-31         ... (1월 평균값)
2025-02-01         NaN
2025-02-02         NaN
2025-02-03         NaN
2025-02-04         NaN
Freq: D, Name: (이름), dtype: float64

월별 -> 일별 업샘플링 (ffill, 첫 5개):
2025-01-31         ... (1월 평균값)
2025-02-01         ... (1월 평균값)
2025-02-02         ... (1월 평균값)
2025-02-03         ... (1월 평균값)
2025-02-04         ... (1월 평균값)
Freq: D, Name: (이름), dtype: float64

월별 -> 일별 업샘플링 (선형 보간, 첫 5개):
2025-01-31         ... (1월 평균값)
2025-02-01         ... (1월과 2월 평균 사이의 보간값)
2025-02-02         ...
2025-02-03         ...
2025-02-04         ...
Freq: D, Name: (이름), dtype: float64

활용 방안: 데이터의 시간 단위를 통일하거나, 다른 빈도의 데이터와 비교 분석하기 위해 사용됩니다. 주가 데이터의 일별 -> 주별/월별 변환, 센서 데이터의 초/분 단위 -> 시간별/일별 변환 등에 활용됩니다.

10.5. 이동창 연산 (Moving/Rolling Window Operations)

rolling() 메서드는 고정된 크기의 창(window)을 데이터 위로 이동시키면서 각 창에 포함된 데이터에 대해 통계량을 계산합니다. 이동 평균(moving average) 계산이 대표적인 예입니다.

rolling() 주요 파라미터 및 사용법

  • window: 창의 크기를 지정합니다. 정수(데이터 포인트 수) 또는 시간 오프셋 문자열(예: '3D')을 사용할 수 있습니다.
  • min_periods: 결과를 계산하기 위해 필요한 최소 관측치 수. 창 크기보다 작을 수 있습니다.
  • center=False (기본값): False이면 창의 오른쪽 끝을 기준으로 라벨링, True이면 창의 중앙을 기준으로 라벨링합니다.
  • 다양한 집계 함수(mean(), sum(), std(), count(), corr(), cov(), apply() 등)를 뒤에 연결하여 사용합니다.

예시 코드 (위의 ts_data 사용):

# 3일 이동 평균
rolling_mean_3d = ts_data.rolling(window=3).mean()
print("\n3일 이동 평균 (첫 7개):\n", rolling_mean_3d.head(7))

# 5일 이동 표준편차 (최소 3개 이상 데이터 필요)
rolling_std_5d_min3 = ts_data.rolling(window=5, min_periods=3).std()
print("\n5일 이동 표준편차 (min_periods=3, 첫 7개):\n", rolling_std_5d_min3.head(7))

# 사용자 정의 함수 적용 (예: 창 내 최대값과 최소값의 차이)
rolling_range = ts_data.rolling(window=3).apply(lambda x: x.max() - x.min(), raw=True) # raw=True는 성능 향상에 도움
print("\n3일 이동 범위 (첫 7개):\n", rolling_range.head(7))

# Expanding window: 시작점부터 현재까지 확장되는 창
expanding_mean = ts_data.expanding().mean()
print("\n확장창 평균 (첫 7개):\n", expanding_mean.head(7))

# EWM: 지수 가중 이동 평균
ewm_mean = ts_data.ewm(span=3).mean() # span은 대략적인 창 크기 효과
print("\n지수 가중 이동 평균 (span=3, 첫 7개):\n", ewm_mean.head(7))

예상 결과 (randn 부분은 실행 시마다 다름):

3일 이동 평균 (첫 7개):
2025-01-01         NaN
2025-01-02         NaN
2025-01-03         ... (1,2,3일차 평균)
2025-01-04         ... (2,3,4일차 평균)
2025-01-05         ... (3,4,5일차 평균)
2025-01-06         ...
2025-01-07         ...
Freq: D, Name: (이름), dtype: float64

5일 이동 표준편차 (min_periods=3, 첫 7개):
2025-01-01         NaN
2025-01-02         NaN
2025-01-03         ... (1,2,3일차 std)
2025-01-04         ... (1,2,3,4일차 std)
2025-01-05         ... (1,2,3,4,5일차 std)
2025-01-06         ...
2025-01-07         ...
Freq: D, Name: (이름), dtype: float64

3일 이동 범위 (첫 7개):
2025-01-01         NaN
2025-01-02         NaN
2025-01-03         ... (1,2,3일차 max-min)
2025-01-04         ... (2,3,4일차 max-min)
2025-01-05         ... (3,4,5일차 max-min)
2025-01-06         ...
2025-01-07         ...
Freq: D, Name: (이름), dtype: float64

확장창 평균 (첫 7개):
2025-01-01    ... (1일차 값)
2025-01-02    ... (1~2일차 평균)
2025-01-03    ... (1~3일차 평균)
2025-01-04    ... (1~4일차 평균)
2025-01-05    ... (1~5일차 평균)
2025-01-06    ...
2025-01-07    ...
Freq: D, Name: (이름), dtype: float64

지수 가중 이동 평균 (span=3, 첫 7개):
2025-01-01    ...
2025-01-02    ...
2025-01-03    ...
2025-01-04    ...
2025-01-05    ...
2025-01-06    ...
2025-01-07    ...
Freq: D, Name: (이름), dtype: float64

활용 방안: 데이터의 단기 변동성을 제거하고 추세를 파악(이동 평균), 변동성 측정(이동 표준편차), 특정 기간 동안의 패턴 분석 등에 널리 사용됩니다. 금융 데이터 분석에서 이동 평균선 계산이 대표적입니다.

10.6. 데이터 이동 (Shifting)

shift() 메서드는 시계열 데이터의 값을 시간 축을 따라 앞이나 뒤로 이동시킵니다. 이는 시차(lag)를 가진 특성을 만들거나, 기간 간의 변화를 계산하는 데 유용합니다.

shift() 사용법

  • periods: 이동할 기간의 수. 양수이면 뒤로(과거 방향), 음수이면 앞으로(미래 방향) 이동합니다 (기본값 1).
  • freq: 시간 오프셋 문자열을 지정하면 인덱스 자체를 이동시킵니다 (tshift의 기능 통합). 이 경우 값은 그대로 유지되고 인덱스만 변경됩니다.
  • axis: 이동할 축 (0: 행, 1: 열).

예시 코드 (위의 ts_data 사용):

# 값을 1일 뒤로 이동 (오늘 값은 어제의 값이 됨)
shifted_1_day_later = ts_data.shift(1)
print("\n1일 뒤로 이동 (첫 5개):\n", shifted_1_day_later.head())

# 값을 1일 앞으로 이동 (오늘 값은 내일의 값이 됨)
shifted_1_day_earlier = ts_data.shift(-1)
print("\n1일 앞으로 이동 (첫 5개):\n", shifted_1_day_earlier.head())

# 전일 대비 변화량 계산
daily_change = ts_data - ts_data.shift(1) # 또는 ts_data.diff()
print("\n전일 대비 변화량 (첫 5개):\n", daily_change.head())

# 인덱스를 2 영업일(Business day)만큼 이동 (값은 그대로)
# DatetimeIndex 객체에 직접 DateOffset 더하기 권장
shifted_index = ts_data.index + pd.offsets.BDay(2)
ts_data_shifted_idx = pd.Series(ts_data.values, index=shifted_index) # 값은 원래대로, 인덱스만 변경
print("\n인덱스를 2 영업일 이동 (첫 5개):\n", ts_data_shifted_idx.head())
# 예전 tshift 방식: ts_data.tshift(2, freq='B') -> 더 이상 사용되지 않음
# 또는 ts_data.shift(2, freq='B') -> 인덱스 이동, 값 그대로

예상 결과 (randn 부분은 실행 시마다 다름):

1일 뒤로 이동 (첫 5개):
2025-01-01         NaN
2025-01-02         ... (원래 2025-01-01 값)
2025-01-03         ... (원래 2025-01-02 값)
2025-01-04         ... (원래 2025-01-03 값)
2025-01-05         ... (원래 2025-01-04 값)
Freq: D, Name: (이름), dtype: float64

1일 앞으로 이동 (첫 5개):
2025-01-01         ... (원래 2025-01-02 값)
2025-01-02         ... (원래 2025-01-03 값)
2025-01-03         ... (원래 2025-01-04 값)
2025-01-04         ... (원래 2025-01-05 값)
2025-01-05         ... (원래 2025-01-06 값)
Freq: D, Name: (이름), dtype: float64

전일 대비 변화량 (첫 5개):
2025-01-01         NaN
2025-01-02         ... (ts_data[1] - ts_data[0])
2025-01-03         ... (ts_data[2] - ts_data[1])
2025-01-04         ...
2025-01-05         ...
Freq: D, Name: (이름), dtype: float64

인덱스를 2 영업일 이동 (첫 5개):
2025-01-03    ... (원래 2025-01-01의 값)
2025-01-06    ... (원래 2025-01-02의 값)
2025-01-07    ... (원래 2025-01-03의 값)
2025-01-08    ...
2025-01-09    ...
dtype: float64

활용 방안: 과거 데이터를 현재 시점의 특성(feature)으로 사용(시차 변수 생성), 기간 간의 변화율 또는 차이 계산, 미래 값 예측 모델링의 기초 데이터 생성 등에 사용됩니다.

10.7. 시간대 처리 (Time Zone Handling)

Pandas는 시간대(Time Zone)를 인식하고 변환하는 기능을 제공합니다. 시간대가 없는(naive) 시계열 데이터에 시간대 정보를 부여하거나(tz_localize), 특정 시간대로 변환(tz_convert)할 수 있습니다. pytz 라이브러리가 일반적으로 함께 사용됩니다.

예시 코드:

# 시간대가 없는(naive) DatetimeIndex
naive_dt_idx = pd.date_range('2025-05-20 09:00:00', periods=3, freq='H')
s_naive = pd.Series(range(3), index=naive_dt_idx)
print("시간대 없는(naive) Series:\n", s_naive)

# UTC 시간대로 현지화 (localize)
s_utc = s_naive.tz_localize('UTC')
print("\nUTC 시간대로 현지화된 Series:\n", s_utc)

# 'Asia/Seoul' 시간대로 변환 (convert)
s_seoul = s_utc.tz_convert('Asia/Seoul')
print("\nAsia/Seoul 시간대로 변환된 Series:\n", s_seoul)

# 다른 방법: 처음부터 시간대 지정하여 생성
s_seoul_direct = pd.Series(range(3), index=pd.date_range('2025-05-20 09:00:00', periods=3, freq='H', tz='Asia/Seoul'))
print("\nAsia/Seoul 시간대로 직접 생성된 Series:\n", s_seoul_direct)

예상 결과:

시간대 없는(naive) Series:
2025-05-20 09:00:00    0
2025-05-20 10:00:00    1
2025-05-20 11:00:00    2
Freq: H, dtype: int64

UTC 시간대로 현지화된 Series:
2025-05-20 09:00:00+00:00    0
2025-05-20 10:00:00+00:00    1
2025-05-20 11:00:00+00:00    2
Freq: H, dtype: int64

Asia/Seoul 시간대로 변환된 Series:
2025-05-20 18:00:00+09:00    0  # 09:00 UTC -> 18:00 KST
2025-05-20 19:00:00+09:00    1
2025-05-20 20:00:00+09:00    2
Freq: H, dtype: int64

Asia/Seoul 시간대로 직접 생성된 Series:
2025-05-20 09:00:00+09:00    0
2025-05-20 10:00:00+09:00    1
2025-05-20 11:00:00+09:00    2
Freq: H, dtype: int64

활용 방안: 전 세계 여러 지역의 데이터를 통합 분석할 때 시간대를 일치시키거나, 특정 지역의 현지 시간에 맞춰 데이터를 표시하고 분석하는 데 필수적입니다.

Pandas의 시계열 처리 기능은 매우 방대하고 강력하여, 이 장에서 다룬 내용 외에도 다양한 고급 기능들이 존재합니다. 시계열 데이터의 특성을 잘 이해하고 Pandas가 제공하는 도구들을 적절히 활용하면 복잡한 시계열 분석 작업도 효율적으로 수행할 수 있습니다.

제 11 장: 기본 시각화

데이터를 숫자로만 이해하는 것에는 한계가 있으며, 시각화는 데이터에 숨겨진 패턴, 추세, 이상치 등을 직관적으로 파악하는 데 매우 효과적인 방법입니다. Pandas는 Python의 대표적인 시각화 라이브러리인 Matplotlib을 기반으로 하여, DataFrame이나 Series 객체에서 직접 다양한 종류의 플롯을 손쉽게 생성할 수 있는 .plot() 접근자를 제공합니다. 이 장에서는 Pandas의 .plot() 기능을 사용하여 라인 플롯, 막대 그래프, 히스토그램, 산점도 등 기본적인 시각화 방법들을 익히고, 플롯을 간단하게 꾸미는 방법을 알아봅니다. 더 복잡하고 미려한 시각화는 Matplotlib, Seaborn, Plotly 등 전문 시각화 라이브러리를 활용할 수 있습니다.

11.1. Pandas 시각화 소개 및 Matplotlib과의 관계

Pandas의 .plot() 기능은 Matplotlib의 기능을 편리하게 사용할 수 있도록 감싼(wrapper) 것입니다. 따라서 기본적인 플롯은 Pandas만으로도 충분히 생성 가능하지만, 세부적인 설정을 변경하거나 여러 플롯을 복합적으로 구성하려면 Matplotlib의 객체(Figure, Axes 등)를 직접 다루는 것이 좋습니다. 일반적으로 Pandas로 빠르게 기본 플롯을 그린 후, 필요에 따라 Matplotlib 함수를 추가하여 커스터마이징하는 방식을 많이 사용합니다.

기본 사용법 및 Matplotlib 연동

Series나 DataFrame 객체에 .plot을 붙이고, 원하는 플롯 종류를 kind 파라미터로 지정하거나 해당 종류의 메서드(예: .plot.line(), .plot.bar())를 호출합니다. Jupyter Notebook이나 IPython 환경에서는 플롯이 자동으로 인라인에 표시되지만, 일반 Python 스크립트 환경에서는 matplotlib.pyplot.show()를 호출해야 플롯 창이 나타납니다.

예시 코드:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt # Matplotlib 임포트

# 샘플 데이터 생성
s = pd.Series(np.random.randn(10).cumsum(), index=np.arange(0, 100, 10))
df_vis = pd.DataFrame(np.random.randn(10, 4).cumsum(axis=0),
                      columns=['A', 'B', 'C', 'D'],
                      index=np.arange(0, 100, 10))

print("샘플 Series (s):\n", s.head())
print("\n샘플 DataFrame (df_vis):\n", df_vis.head())

# Series 라인 플롯
s.plot()
plt.title("Sample Series Line Plot") # Matplotlib 함수로 제목 추가
plt.xlabel("Index")
plt.ylabel("Value")
plt.show() # 스크립트 환경에서는 필수

# DataFrame 라인 플롯 (각 열이 하나의 라인)
df_vis.plot()
plt.title("Sample DataFrame Line Plot")
plt.show()

예상 결과 (텍스트 설명):

위 코드를 실행하면 두 개의 플롯 창이 나타납니다 (Jupyter 환경에서는 셀 아래에 표시됨).

  1. 첫 번째 플롯은 Series s의 인덱스를 x축, 값을 y축으로 하는 라인 플롯입니다. 제목, x축 라벨, y축 라벨이 Matplotlib 함수를 통해 추가된 것을 볼 수 있습니다.
  2. 두 번째 플롯은 DataFrame df_vis의 인덱스를 x축으로 하고, 각 열('A', 'B', 'C', 'D')의 데이터를 별도의 라인으로 그린 플롯입니다. 자동으로 각 라인에 대한 범례(legend)가 표시됩니다.

11.2. 주요 플롯 종류 및 사용법

.plot()kind 파라미터에 따라 다양한 종류의 플롯을 그릴 수 있습니다.

1. 라인 플롯 (Line Plot): kind='line' (기본값)

시간의 흐름이나 순서에 따른 데이터의 변화 추세를 보여주는 데 적합합니다. DataFrame의 경우, 기본적으로 각 숫자형 열이 하나의 라인으로 그려집니다.

예시 코드:

# df_vis는 위에서 생성한 DataFrame 사용
df_vis.plot.line(y=['A', 'C'], title='Line Plot of Columns A and C', grid=True)
plt.ylabel("Values")
plt.show()

예상 결과 (텍스트 설명):

DataFrame df_vis에서 'A'열과 'C'열의 데이터만 선택하여 라인 플롯을 그립니다. 제목이 지정되고 그리드가 표시됩니다.

활용 방안: 주가 변동, 기온 변화, 시간에 따른 웹사이트 트래픽 등 연속적인 데이터의 추세를 시각화합니다.

2. 막대 그래프 (Bar Plot): kind='bar' (수직), kind='barh' (수평)

범주형 데이터의 값을 비교하거나 각 항목의 크기를 나타내는 데 사용됩니다. DataFrame의 경우, 인덱스가 x축(또는 y축)의 항목이 되고 각 열의 값이 막대로 표시됩니다.

예시 코드:

df_bar = pd.DataFrame({'Category': ['X', 'Y', 'Z'], 'Value1': [10, 25, 15], 'Value2': [12, 18, 22]})
df_bar.set_index('Category', inplace=True) # Category를 인덱스로 설정

# 수직 막대 그래프
df_bar.plot.bar(title='Vertical Bar Plot', rot=0) # rot=0은 x축 라벨 회전 안 함
plt.ylabel("Counts")
plt.show()

# 수평 누적 막대 그래프
df_bar.plot.barh(stacked=True, title='Horizontal Stacked Bar Plot')
plt.xlabel("Counts")
plt.show()

예상 결과 (텍스트 설명):

  1. 첫 번째 플롯은 'X', 'Y', 'Z' 카테고리별로 'Value1'과 'Value2'의 값을 나타내는 수직 막대 그래프입니다. 각 카테고리마다 두 개의 막대(Value1, Value2)가 그룹지어 표시됩니다.
  2. 두 번째 플롯은 각 카테고리별로 'Value1'과 'Value2'의 값을 누적하여 보여주는 수평 막대 그래프입니다.

활용 방안: 제품별 판매량 비교, 설문조사 응답 결과 비교, 지역별 인구수 비교 등 범주 간의 양적 비교에 유용합니다.

3. 히스토그램 (Histogram): kind='hist'

단일 수치형 변수의 분포를 시각화합니다. 데이터 범위를 여러 구간(bin)으로 나누고 각 구간에 속하는 데이터의 빈도수를 막대로 표현합니다.

예시 코드:

s_hist_data = pd.Series(np.random.normal(0, 1, size=1000)) # 정규분포 데이터 생성

s_hist_data.plot.hist(bins=30, alpha=0.7, title='Histogram of Normal Distribution')
plt.xlabel("Value")
plt.ylabel("Frequency")
plt.show()

# DataFrame의 여러 열에 대한 히스토그램 (겹쳐서 또는 서브플롯으로)
df_vis[['A', 'B']].plot.hist(bins=20, alpha=0.6, subplots=False, title='Histogram of A and B (Overlay)') # subplots=False면 겹쳐서
plt.show()

df_vis[['C', 'D']].plot.hist(bins=15, alpha=0.8, subplots=True, layout=(1,2), figsize=(10,4), title='Histograms of C and D (Subplots)')
plt.tight_layout() # 서브플롯 간 간격 자동 조절
plt.show()

예상 결과 (텍스트 설명):

  1. 첫 번째 플롯은 정규분포를 따르는 1000개 데이터의 히스토그램입니다. 30개의 구간으로 나누어 각 구간의 빈도수를 보여줍니다.
  2. 두 번째 플롯은 DataFrame df_vis의 'A'열과 'B'열에 대한 히스토그램을 하나의 플롯에 겹쳐서 그립니다.
  3. 세 번째 플롯은 'C'열과 'D'열에 대한 히스토그램을 각각 별도의 서브플롯으로 나란히 그립니다.

활용 방안: 데이터의 분포 형태(정규분포, 치우친 분포 등), 중심 경향, 산포도 등을 시각적으로 확인하는 데 사용됩니다.

4. 박스 플롯 (Box Plot): kind='box'

데이터의 사분위수, 중앙값, 최소/최대값, 이상치 등을 상자 그림 형태로 요약하여 보여줍니다. 여러 그룹 간의 분포를 비교하는 데 유용합니다.

예시 코드:

# df_vis는 위에서 생성한 DataFrame 사용
df_vis.plot.box(title='Box Plot of Columns A, B, C, D')
plt.ylabel("Value Distribution")
plt.show()

# 특정 컬럼 그룹화 후 박스 플롯
df_grouped_box = pd.DataFrame({
    'Category': np.random.choice(['Group1', 'Group2', 'Group3'], 100),
    'Value': np.random.randn(100) * 5 + np.repeat([10, 20, 15], [40, 30, 30]) # 그룹별 평균 다르게
})
df_grouped_box.boxplot(column='Value', by='Category', grid=False)
plt.title("Box Plot of Value by Category")
plt.suptitle("") # 기본으로 생성되는 상위 제목 제거
plt.xlabel("Category")
plt.ylabel("Value")
plt.show()

예상 결과 (텍스트 설명):

  1. 첫 번째 플롯은 DataFrame df_vis의 각 열('A', 'B', 'C', 'D')에 대한 박스 플롯을 나란히 그려 분포를 비교합니다.
  2. 두 번째 플롯은 'Category'별로 'Value'의 분포를 박스 플롯으로 보여줍니다. 'Group1', 'Group2', 'Group3' 각 그룹의 데이터 분포 특성(중앙값, 사분위 범위, 이상치 등)을 비교할 수 있습니다.

활용 방안: 데이터의 전반적인 분포 특성 파악, 그룹 간 분포 비교, 이상치 식별 등에 효과적입니다.

5. 영역 플롯 (Area Plot): kind='area'

라인 플롯의 아래 영역을 색으로 채워 표현합니다. 기본적으로 누적(stacked)되어 그려지므로, 전체에서 각 부분의 변화 추이를 함께 보거나 구성 비율의 변화를 시각화하는 데 유용합니다.

예시 코드:

# df_vis는 위에서 생성한 DataFrame 사용 (양수 값으로 변경하면 보기 좋음)
df_area_data = pd.DataFrame(np.abs(np.random.randn(10, 3).cumsum(axis=0)), columns=['X', 'Y', 'Z'])

# 누적 영역 플롯 (기본)
df_area_data.plot.area(title='Stacked Area Plot')
plt.show()

# 누적되지 않은 영역 플롯
df_area_data.plot.area(stacked=False, alpha=0.5, title='Unstacked Area Plot')
plt.show()

예상 결과 (텍스트 설명):

  1. 첫 번째 플롯은 'X', 'Y', 'Z' 각 열의 값을 시간에 따라 누적하여 영역으로 표시합니다. 전체 크기와 각 부분의 기여도를 함께 볼 수 있습니다.
  2. 두 번째 플롯은 각 열의 영역을 누적하지 않고 겹쳐서 그립니다. 투명도(alpha)를 조절하여 겹치는 부분을 확인할 수 있습니다.

활용 방안: 시간에 따른 여러 그룹의 누적된 양 변화(예: 시장 점유율 변화), 전체 대비 각 구성 요소의 크기 변화 등을 보여줄 때 사용합니다.

6. 산점도 (Scatter Plot): kind='scatter'

두 수치형 변수 간의 관계를 점으로 표현합니다. 변수 간의 상관관계, 군집 등을 파악하는 데 유용합니다. DataFrame의 경우 xy 파라미터로 사용할 열을 명시해야 합니다.

예시 코드:

df_scatter = pd.DataFrame(np.random.rand(50, 3), columns=['Var1', 'Var2', 'Var3'])
df_scatter['Var4_Size'] = np.random.rand(50) * 100 # 점 크기용 변수
df_scatter['Var5_Color'] = np.random.randint(0, 3, 50) # 점 색상용 변수 (범주형)

# Var1과 Var2의 산점도
df_scatter.plot.scatter(x='Var1', y='Var2', title='Scatter Plot of Var1 vs Var2')
plt.show()

# Var3를 색상(c), Var4_Size를 크기(s)로 표현
df_scatter.plot.scatter(x='Var1', y='Var2', c='Var5_Color', s='Var4_Size', colormap='viridis', alpha=0.6,
                        title='Scatter Plot with Color and Size Variation')
plt.show()

예상 결과 (텍스트 설명):

  1. 첫 번째 플롯은 'Var1'을 x축, 'Var2'를 y축으로 하는 기본적인 산점도입니다. 두 변수 간의 분포 패턴을 볼 수 있습니다.
  2. 두 번째 플롯은 'Var1'과 'Var2'의 관계를 점으로 표시하되, 각 점의 색상은 'Var5_Color' 값에 따라, 점의 크기는 'Var4_Size' 값에 따라 다르게 표현됩니다. 이를 통해 최대 4개의 변수 정보를 하나의 산점도에 나타낼 수 있습니다.

활용 방안: 두 변수 간의 선형/비선형 관계, 데이터의 군집 형성 여부, 이상치 등을 탐색하는 데 사용됩니다.

7. 파이 차트 (Pie Chart): kind='pie'

전체에 대한 각 부분의 비율을 부채꼴 모양으로 나타냅니다. Series나 DataFrame의 단일 열에 적용 가능합니다. (주의: 파이 차트는 비율 비교에 직관적이지 않아 사용을 권장하지 않는 경우도 많습니다. 막대 그래프가 더 효과적일 수 있습니다.)

예시 코드:

s_pie = pd.Series([15, 30, 45, 10], index=['A', 'B', 'C', 'D'], name='Portion')

s_pie.plot.pie(autopct='%.1f%%', figsize=(6, 6), title='Pie Chart of Portions', startangle=90, counterclock=False,
               colors=['skyblue', 'lightcoral', 'lightgreen', 'gold'], wedgeprops={'edgecolor': 'black'})
plt.ylabel('') # 불필요한 y축 라벨 제거
plt.show()

예상 결과 (텍스트 설명):

Series s_pie의 각 항목('A', 'B', 'C', 'D')이 전체에서 차지하는 비율을 파이 차트로 나타냅니다. 각 조각에 백분율이 표시되고, 지정된 색상과 시작 각도 등이 적용됩니다.

활용 방안: 시장 점유율, 예산 구성 비율 등 전체에 대한 각 부분의 상대적인 크기를 보여주고자 할 때 제한적으로 사용될 수 있습니다.

8. 밀도 플롯 (Density Plot / KDE Plot): kind='kde' 또는 kind='density'

커널 밀도 추정(Kernel Density Estimation)을 사용하여 데이터의 연속적인 확률 분포를 시각화합니다. 히스토그램을 부드러운 곡선 형태로 나타낸다고 볼 수 있습니다.

예시 코드:

# s_hist_data는 위에서 생성한 정규분포 Series 사용
s_hist_data.plot.kde(title='Density Plot (KDE) of Normal Distribution')
plt.xlabel("Value")
plt.show()

# DataFrame의 여러 열에 대한 밀도 플롯
df_vis[['A', 'B']].plot.kde(title='Density Plots of A and B')
plt.show()

예상 결과 (텍스트 설명):

  1. 첫 번째 플롯은 s_hist_data의 분포를 부드러운 곡선 형태의 밀도 플롯으로 보여줍니다.
  2. 두 번째 플롯은 DataFrame df_vis의 'A'열과 'B'열 각각에 대한 밀도 플롯을 하나의 그림에 함께 그립니다.

활용 방안: 변수의 분포 형태를 히스토그램보다 부드럽게 파악하거나, 여러 그룹 간의 분포를 비교할 때 사용됩니다.

11.3. 플롯 커스터마이징

Pandas의 .plot() 메서드는 다양한 파라미터를 통해 플롯의 여러 요소를 변경할 수 있으며, Matplotlib 객체를 직접 사용하여 더욱 세밀한 조정이 가능합니다.

.plot()의 주요 공통 파라미터

  • title='My Plot Title': 플롯 제목 설정.
  • xlabel='X-axis Label', ylabel='Y-axis Label': 축 라벨 설정 (일부 플롯에서는 직접 적용되지 않을 수 있으며, ax.set_xlabel() 등 Matplotlib 방식이 더 확실할 수 있음).
  • figsize=(width, height): 플롯의 크기를 인치 단위로 지정 (예: (10, 5)).
  • grid=True: 그리드(격자) 표시.
  • legend=True (기본값): 범례 표시 여부. False로 숨길 수 있음.
  • color='red' 또는 color=['red', 'blue']: 플롯 색상 지정.
  • subplots=True: (DataFrame 전용) 각 열을 별도의 서브플롯으로 그림.
  • layout=(rows, cols): subplots=True일 때 서브플롯의 행과 열 개수 지정.
  • xlim=(min, max), ylim=(min, max): x축, y축 범위 지정.
  • style='--' 또는 style=['-', '--', ':']: 라인 스타일 지정.

예시 코드: 커스터마이징 및 Matplotlib 객체 활용

# df_vis는 위에서 생성한 DataFrame 사용
ax = df_vis.plot(kind='line',
                 title='Customized DataFrame Plot',
                 figsize=(12, 6),
                 grid=True,
                 legend=True,
                 style=['-', '--', ':', '-.'], # 각 라인 스타일 다르게
                 color=['blue', 'green', 'red', 'purple']) # 각 라인 색상 다르게

# Matplotlib Axes 객체(ax)를 사용하여 추가 설정
ax.set_xlabel("Time Index (0-90, step 10)")
ax.set_ylabel("Cumulative Sum of Random Values")
ax.legend(title='Columns', loc='upper left') # 범례 위치 및 제목 변경
ax.set_facecolor('lightyellow') # 플롯 배경색 변경

plt.figtext(0.5, 0.01, 'Figure caption or additional text', ha='center', fontsize=10) # 그림 하단에 텍스트 추가
plt.tight_layout()
plt.show()

예상 결과 (텍스트 설명):

DataFrame df_vis의 라인 플롯이 생성됩니다. 이 플롯은 지정된 제목, 크기, 그리드, 범례, 라인 스타일, 색상 등을 가집니다. 또한, ax 객체를 통해 x축 및 y축 라벨, 범례 상세 설정, 배경색 변경 등이 적용됩니다. 그림 하단에는 추가적인 텍스트도 표시됩니다.

활용 방안: 생성된 플롯의 가독성을 높이고, 전달하고자 하는 메시지를 명확하게 하기 위해 다양한 커스터마이징 옵션을 활용합니다. 보고서나 프레젠테이션에 사용될 시각 자료의 품질을 향상시킬 수 있습니다.

Pandas의 기본 시각화 기능은 데이터 분석 과정에서 신속하게 데이터를 탐색하고 기본적인 패턴을 파악하는 데 매우 유용합니다. 더 정교하고 상호작용적인 시각화가 필요하다면, Matplotlib, Seaborn, Plotly, Bokeh와 같은 전문 시각화 라이브러리를 함께 사용하는 것을 고려해볼 수 있습니다.

제 12 장: 고급 주제 (간략히)

지금까지 Pandas의 핵심 기능들을 살펴보았습니다. 이 장에서는 Pandas의 활용도를 더욱 높이고 특정 상황에서 더 효율적인 데이터 처리를 가능하게 하는 몇 가지 고급 주제들을 간략하게 소개합니다. 이러한 주제들은 더 복잡한 데이터를 다루거나 성능을 최적화해야 할 때 유용하며, 추가적인 학습을 통해 Pandas 전문가로 성장하는 데 밑거름이 될 것입니다.

12.1. 계층적 인덱싱 (Hierarchical Indexing - MultiIndex)

계층적 인덱싱은 DataFrame의 행이나 열에 여러 레벨의 인덱스를 지정하는 기능입니다. 이를 통해 고차원 데이터를 2차원 테이블 형태로 효과적으로 표현하고 분석할 수 있습니다. 예를 들어, 한 축에 여러 개의 키를 사용하여 그룹화된 데이터를 표현하거나, 다차원 배열과 유사한 구조를 만들 수 있습니다.

MultiIndex 기본

  • 생성: groupby() 결과에서 여러 키를 사용하거나, pd.MultiIndex.from_tuples(), pd.MultiIndex.from_product(), set_index()에 여러 열을 전달하여 생성할 수 있습니다.
  • 선택 및 슬라이싱: .loc[] 접근자 내에 튜플 형태로 각 레벨의 인덱스 값을 전달하여 특정 데이터를 선택하거나 슬라이싱할 수 있습니다.
  • stack() / unstack(): 데이터프레임의 열을 인덱스로 쌓거나(stack), 인덱스 레벨을 열로 펼치는(unstack) 데 사용되어 데이터 형태를 유연하게 변경할 수 있습니다.

예시 코드: MultiIndex 생성 및 사용

import pandas as pd
import numpy as np

# MultiIndex 생성 예시 (from_product)
arrays = [['bar', 'baz', 'foo', 'qux'], ['one', 'two']]
multi_idx = pd.MultiIndex.from_product(arrays, names=['first', 'second'])
s_multi = pd.Series(np.random.randn(8), index=multi_idx)
print("Series with MultiIndex:\n", s_multi)

print("\nMultiIndex에서 'bar' 그룹 선택:\n", s_multi.loc['bar'])
print("\nMultiIndex에서 ('baz', 'one') 선택:\n", s_multi.loc[('baz', 'one')])

# stack() / unstack() 예시
df_for_stack = pd.DataFrame(np.arange(6).reshape((2, 3)),
                            index=pd.Index(['A', 'B'], name='row_idx'),
                            columns=pd.Index(['X', 'Y', 'Z'], name='col_idx'))
print("\n원본 DataFrame for stack/unstack:\n", df_for_stack)
stacked_df = df_for_stack.stack()
print("\nStacked DataFrame (열 -> 행 인덱스):\n", stacked_df)
print("\nUnstacked DataFrame (다시 원래대로):\n", stacked_df.unstack())

예상 결과 (randn 부분은 실행 시마다 다름):

Series with MultiIndex:
first  second
bar    one       0.123456
       two      -0.567890
baz    one       1.122334
       two      -0.987654
foo    one       0.456789
       two       0.012345
qux    one      -1.543210
       two       0.678901
dtype: float64

MultiIndex에서 'bar' 그룹 선택:
second
one    0.123456
two   -0.567890
dtype: float64

MultiIndex에서 ('baz', 'one') 선택:
1.122334...

원본 DataFrame for stack/unstack:
col_idx  X  Y  Z
row_idx         
A        0  1  2
B        3  4  5

Stacked DataFrame (열 -> 행 인덱스):
row_idx  col_idx
A        X          0
         Y          1
         Z          2
B        X          3
         Y          4
         Z          5
dtype: int...

Unstacked DataFrame (다시 원래대로):
col_idx  X  Y  Z
row_idx         
A        0  1  2
B        3  4  5

활용 방안: 다차원 시계열 데이터(예: 여러 종목의 시고저종 데이터), 설문조사의 다중 응답 데이터, 복잡한 실험 결과 데이터 등을 효율적으로 관리하고 분석하는 데 유용합니다.

12.2. 범주형 데이터 타입 (Categorical Data)

문자열 데이터 중에서 고유한 값의 개수가 제한적인 경우(예: 성별, 등급, 지역명 등), Pandas의 Categorical 데이터 타입을 사용하면 메모리 사용량을 줄이고 일부 연산의 성능을 향상시킬 수 있습니다. 또한, 범주에 순서를 지정하여 정렬이나 플로팅 시 의미 있는 순서를 반영할 수 있습니다.

Categorical 타입 사용

  • 생성: pd.Categorical() 함수를 사용하거나, 기존 Series/DataFrame 열에 대해 .astype('category')를 적용하여 변환합니다.
  • 범주 확인 및 관리: .cat.categories (범주 목록), .cat.codes (내부 정수 코드), .cat.ordered (순서 여부), .cat.as_ordered(), .cat.reorder_categories() 등의 접근자를 사용합니다.

예시 코드:

sizes = pd.Series(['S', 'M', 'L', 'XL', 'S', 'M', 'M', 'XL', 'L', 'S'])
print("원본 Series (object type):\n", sizes)
print("Memory usage (object):", sizes.memory_usage(deep=True))

# Categorical 타입으로 변환
size_cat = sizes.astype('category')
print("\nCategorical Series:\n", size_cat)
print("Memory usage (category):", size_cat.memory_usage(deep=True))
print("Categories:", size_cat.cat.categories)
print("Codes:", size_cat.cat.codes)

# 순서 있는 Categorical 타입
ordered_size_cat = pd.Categorical(sizes, categories=['S', 'M', 'L', 'XL'], ordered=True)
print("\nOrdered Categorical Series:\n", ordered_size_cat)
print("Is ordered:", ordered_size_cat.ordered)
print("Sorted (ordered):\n", pd.Series(ordered_size_cat).sort_values())

예상 결과:

원본 Series (object type):
0     S
1     M
2     L
3    XL
4     S
5     M
6     M
7    XL
8     L
9     S
dtype: object
Memory usage (object): ... (문자열 길이에 따라 다름, 상대적으로 큼)

Categorical Series:
0     S
1     M
2     L
3    XL
4     S
5     M
6     M
7    XL
8     L
9     S
dtype: category
Categories (4, object): ['L', 'M', 'S', 'XL']
Memory usage (category): ... (상대적으로 작음)
Categories: Index(['L', 'M', 'S', 'XL'], dtype='object')
Codes: [2 1 0 3 2 1 1 3 0 2]

Ordered Categorical Series:
['S', 'M', 'L', 'XL', 'S', 'M', 'M', 'XL', 'L', 'S']
Categories (4, object): ['S' < 'M' < 'L' < 'XL']
Is ordered: True
Sorted (ordered):
0     S
4     S
9     S
1     M
5     M
6     M
2     L
8     L
3    XL
7    XL
dtype: category

활용 방안: 고유값이 적은 문자열 열(예: 국가명, 상품 카테고리, 사용자 등급)에 적용하여 메모리 효율을 높이고, groupby 연산 등의 성능을 개선할 수 있습니다. 범주의 논리적 순서가 중요한 경우(예: 'Small' < 'Medium' < 'Large')에도 유용합니다.

12.3. 성능 최적화 및 메모리 관리

대용량 데이터를 다루거나 반복적인 연산을 수행할 때, Pandas 코드의 성능과 메모리 사용량은 중요한 고려 사항이 됩니다. 몇 가지 간단한 팁을 통해 효율성을 높일 수 있습니다.

주요 팁

  • 적절한 데이터 타입 사용:
    • 숫자형 데이터의 경우, 값의 범위에 맞는 더 작은 타입(예: int64 대신 int32, int16, int8 또는 float64 대신 float32)을 사용하면 메모리를 절약할 수 있습니다. pd.to_numeric()downcast 옵션을 활용할 수 있습니다.
    • 문자열 데이터는 위에서 언급한 Categorical 타입을 고려합니다.
  • 벡터화 연산 활용: 반복문(for 루프)이나 .apply() 메서드 대신 Pandas나 NumPy의 내장 함수(벡터화 연산)를 최대한 활용하는 것이 훨씬 빠릅니다.
  • 불필요한 데이터/객체 제거: del 키워드로 더 이상 사용하지 않는 변수를 명시적으로 삭제하여 메모리를 확보할 수 있습니다.
  • 데이터 읽기 시 최적화: pd.read_csv() 등에서 usecols 파라미터로 필요한 열만 읽어오거나, chunksize 파라미터로 데이터를 조각내어 처리할 수 있습니다. dtype 파라미터로 읽을 때부터 타입을 지정하는 것도 좋습니다.
  • 인덱싱 주의: 단일 값 접근에는 .at[] (라벨 기반)이나 .iat[] (정수 위치 기반)이 .loc[], .iloc[]보다 빠릅니다.

예시 코드: 데이터 타입 변경으로 메모리 절약

df_mem = pd.DataFrame({'col_int': np.random.randint(0, 100, size=1000000),
                                   'col_float': np.random.rand(1000000)})
print("기본 데이터 타입 메모리 사용량:\n", df_mem.memory_usage(deep=True).sum(), "bytes")

df_mem['col_int'] = df_mem['col_int'].astype('int16') # 값의 범위가 int16에 맞는지 확인 필요
df_mem['col_float'] = df_mem['col_float'].astype('float32')
print("데이터 타입 변경 후 메모리 사용량:\n", df_mem.memory_usage(deep=True).sum(), "bytes")

예상 결과:

기본 데이터 타입 메모리 사용량:
 16000128 bytes  (예시 값, 실제로는 더 클 수 있음)
데이터 타입 변경 후 메모리 사용량:
 6000128 bytes (예시 값,显著减少)

활용 방안: 특히 메모리가 제한적이거나 매우 큰 데이터셋을 다룰 때, 이러한 최적화 기법들은 분석 작업의 속도를 높이고 시스템 자원 부담을 줄이는 데 기여합니다.

12.4. Pandas 설정 옵션 (Configuration Options)

Pandas는 DataFrame이나 Series가 표시되는 방식 등 다양한 전역 설정을 사용자가 제어할 수 있도록 옵션 시스템을 제공합니다. pd.get_option()으로 현재 설정을 확인하고, pd.set_option()으로 설정을 변경할 수 있습니다.

주요 설정 옵션

  • display.max_rows: 출력될 최대 행 수 (기본값: 60). None이면 모든 행 출력.
  • display.max_columns: 출력될 최대 열 수 (기본값: 20). None이면 모든 열 출력.
  • display.width: 한 줄에 출력될 문자 수 (기본값: 80).
  • display.precision: 부동소수점 숫자의 출력 정밀도 (소수점 이하 자릿수, 기본값: 6).
  • display.float_format: 부동소수점 출력 형식을 사용자 정의 함수로 지정.

예시 코드:

# 현재 설정 확인 (예시)
print("현재 display.max_rows:", pd.get_option("display.max_rows"))
print("현재 display.max_columns:", pd.get_option("display.max_columns"))

# 설정 변경
pd.set_option("display.max_rows", 10)
pd.set_option("display.max_columns", 5)
pd.set_option("display.precision", 2) # 소수점 2자리까지

df_display_test = pd.DataFrame(np.random.rand(12, 6), columns=[f'Col{i}' for i in range(6)])
print("\n변경된 설정으로 DataFrame 출력:\n", df_display_test)

# 설정 원래대로 되돌리기 (또는 특정 값으로 재설정)
pd.reset_option("display.max_rows")
pd.reset_option("all") # 모든 옵션 초기화 (주의해서 사용)
# pd.set_option("display.max_rows", 60) # 원래 기본값으로

예상 결과 (randn 부분은 실행 시마다 다름):

현재 display.max_rows: 60
현재 display.max_columns: 20

변경된 설정으로 DataFrame 출력:
     Col0  Col1  Col2  Col3  Col4   ...
0    0.12  0.34  0.56  0.78  0.90   ...
1    0.23  0.45  0.67  0.89  0.01   ...
2    0.34  0.56  0.78  0.90  0.12   ...
3    0.45  0.67  0.89  0.01  0.23   ...
4    0.56  0.78  0.90  0.12  0.34   ...
..    ...   ...   ...   ...   ...   ... # 중간 생략 표시 (총 12행이지만 max_rows=10)
7    0.78  0.90  0.12  0.34  0.56   ...
8    0.89  0.01  0.23  0.45  0.67   ...
9    0.90  0.12  0.34  0.56  0.78   ...
10   0.01  0.23  0.45  0.67  0.89   ...
11   0.12  0.34  0.56  0.78  0.90   ...

[12 rows x 6 columns]  (max_columns=5 설정으로 실제 열은 6개지만 ...로 표시될 수 있음)

활용 방안: 대량의 데이터를 탐색할 때 화면 출력을 조절하여 가독성을 높이거나, 특정 형식으로 데이터를 확인하고자 할 때 유용합니다. Jupyter Notebook 환경 등에서 작업 시 특히 편리합니다.

이 외에도 Pandas에는 파이프라이닝(.pipe()), 사용자 정의 접근자(custom accessors), 외부 라이브러리와의 통합 등 더 많은 고급 기능들이 있습니다. Pandas를 깊이 있게 사용하고자 한다면 이러한 주제들에 대해 꾸준히 학습하고 탐구하는 것이 좋습니다.

제 13 장: Pandas 연습 문제

지금까지 학습한 Pandas의 다양한 기능들을 종합적으로 활용해보는 연습 문제입니다. 각 문제 아래의 '해답 보기'를 클릭하면 정답 코드를 확인할 수 있습니다.

문제 1: Series 생성 및 기본 조작

a) 좋아하는 과일 5개를 값으로 하고, 0부터 시작하는 정수 인덱스를 가지는 Series를 만드세요.
b) 위 Series에서 세 번째 과일을 출력하세요.
c) Series의 값들만 NumPy 배열 형태로 출력하세요.

해답:

import pandas as pd
import numpy as np

# a) Series 생성
fruits = ['사과', '바나나', '딸기', '포도', '오렌지']
s_fruits = pd.Series(fruits)
print("과일 Series:\n", s_fruits)

# b) 세 번째 과일 출력
print("\n세 번째 과일:", s_fruits[2]) # 또는 s_fruits.iloc[2]

# c) 값들을 NumPy 배열로 출력
print("\nSeries 값 (NumPy 배열):", s_fruits.values)

예상 결과:

과일 Series:
0     사과
1    바나나
2     딸기
3     포도
4    오렌지
dtype: object

세 번째 과일: 딸기

Series 값 (NumPy 배열): ['사과' '바나나' '딸기' '포도' '오렌지']
문제 2: DataFrame 생성 및 정보 확인

다음 정보를 포함하는 DataFrame을 만드세요: 이름(문자열), 나이(정수), 도시(문자열) 컬럼을 가지고, 3명의 데이터를 포함합니다. 생성된 DataFrame의 info()를 호출하여 정보를 확인하세요.

해답:

import pandas as pd

data = {'이름': ['김민준', '이서연', '박도윤'],
        '나이': [28, 32, 25],
        '도시': ['서울', '부산', '대전']}
df_people = pd.DataFrame(data)

print("생성된 DataFrame:\n", df_people)
print("\nDataFrame 정보 (info):")
df_people.info()

예상 결과:

생성된 DataFrame:
    이름  나이  도시
0  김민준  28  서울
1  이서연  32  부산
2  박도윤  25  대전

DataFrame 정보 (info):
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   이름    3 non-null      object
 1   나이    3 non-null      int64
 2   도시    3 non-null      object
dtypes: int64(1), object(2)
memory usage: 200.0+ bytes
문제 3: CSV 파일 읽고 특정 조건 데이터 선택

다음과 같은 내용의 `students.csv` 파일이 있다고 가정합니다 (직접 만드셔도 됩니다):

name,math,english,science
Alice,90,85,92
Bob,78,80,75
Charlie,95,92,88
David,60,70,65
Eve,88,89,90
a) 이 CSV 파일을 Pandas DataFrame으로 읽어오세요.
b) 수학(math) 점수가 80점 이상인 학생들의 모든 정보를 선택하여 출력하세요.
c) 영어(english) 점수가 90점 미만이거나 과학(science) 점수가 80점 미만인 학생들의 이름(name)만 선택하여 출력하세요.

해답:

import pandas as pd
import io # CSV 문자열을 파일처럼 다루기 위해

# 예제 CSV 데이터 (파일 대신 문자열 사용)
csv_data = """name,math,english,science
Alice,90,85,92
Bob,78,80,75
Charlie,95,92,88
David,60,70,65
Eve,88,89,90"""

# a) CSV 파일 읽기 (문자열을 파일처럼 사용)
df_students = pd.read_csv(io.StringIO(csv_data))
print("Students DataFrame:\n", df_students)

# b) 수학 점수가 80점 이상인 학생
math_high_scores = df_students[df_students['math'] >= 80]
print("\n수학 80점 이상 학생:\n", math_high_scores)

# c) 영어 90점 미만 또는 과학 80점 미만 학생 이름
condition = (df_students['english'] < 90) | (df_students['science'] < 80)
names_selected = df_students.loc[condition, 'name']
print("\n영어 90점 미만 또는 과학 80점 미만 학생 이름:\n", names_selected)

예상 결과:

Students DataFrame:
      name  math  english  science
0    Alice    90       85       92
1      Bob    78       80       75
2  Charlie    95       92       88
3    David    60       70       65
4      Eve    88       89       90

수학 80점 이상 학생:
      name  math  english  science
0    Alice    90       85       92
2  Charlie    95       92       88
4      Eve    88       89       90

영어 90점 미만 또는 과학 80점 미만 학생 이름:
0    Alice
1      Bob
3    David
4      Eve
Name: name, dtype: object

추가 연습 문제

문제 4: 데이터 조작 및 그룹별 집계 (Ch 5, 8)

다음은 온라인 상점의 판매 데이터 일부입니다.

data = {'Category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Books', 'Electronics'],
        'Item': ['Laptop', 'Mouse', 'T-Shirt', 'Jeans', 'Python Book', 'Keyboard'],
        'Price': [1200, 25, 30, 70, 45, 75],
        'Quantity': [2, np.nan, 5, 3, 10, np.nan],
        'Date': ['2025-01-05', '2025-01-05', '2025-01-06', '2025-01-06', '2025-01-07', '2025-01-08']}
df_sales = pd.DataFrame(data)
df_sales['Date'] = pd.to_datetime(df_sales['Date'])
a) 'Quantity' 열의 NaN 값을 해당 아이템 'Category'의 평균 판매 수량(Quantity)으로 채우세요. (만약 카테고리 내 다른 아이템들의 Quantity 정보도 NaN이라면, 전체 평균으로 채우거나 0으로 채우는 등 합리적인 방법을 선택하세요. 여기서는 해당 카테고리의 유효한 평균값을 사용한다고 가정합니다. 만약 특정 카테고리에 유효한 Quantity가 없다면, 전체 Quantity 평균을 사용하세요.)
b) 'Total_Sales' 열을 Price * Quantity로 계산하여 추가하세요. Quantity가 NaN이어서 Total_Sales 계산이 불가능한 경우는 0으로 처리하세요.
c) 각 'Category'별로 총 판매액('Total_Sales')이 가장 높은 아이템('Item')과 그 판매액을 찾으세요.

해답:

import pandas as pd
import numpy as np

data = {'Category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Books', 'Electronics'],
        'Item': ['Laptop', 'Mouse', 'T-Shirt', 'Jeans', 'Python Book', 'Keyboard'],
        'Price': [1200, 25, 30, 70, 45, 75],
        'Quantity': [2, np.nan, 5, 3, 10, np.nan],
        'Date': ['2025-01-05', '2025-01-05', '2025-01-06', '2025-01-06', '2025-01-07', '2025-01-08']}
df_sales = pd.DataFrame(data)
df_sales['Date'] = pd.to_datetime(df_sales['Date'])
print("원본 Sales DataFrame:\n", df_sales)

# a) 'Quantity' NaN 채우기
# 먼저 카테고리별 평균 Quantity 계산
df_sales['Quantity'] = df_sales.groupby('Category')['Quantity'].transform(lambda x: x.fillna(x.mean()))
# 그래도 NaN이 남아있다면 (해당 카테고리에 유효한 Quantity가 없는 경우), 전체 평균으로 채우기
df_sales['Quantity'].fillna(df_sales['Quantity'].mean(), inplace=True)
# 이 문제에서는 Electronics의 Mouse, Keyboard가 NaN이므로 Electronics 카테고리 평균(Laptop의 2)으로 채워지거나,
# 만약 Electronics에 Laptop만 없다면 전체 평균으로 채워질 것. 여기선 transform이 Laptop의 2로 채움.
# 더 엄밀하게는, Electronics 내 다른 유효값이 없다면 전체 평균을 사용해야 함.
# 예제에서는 transform이 그룹 내 평균으로 잘 채워줌 (Laptop의 2로 Mouse, Keyboard의 NaN이 채워짐).
# 만약 모든 'Electronics'의 'Quantity'가 NaN이었다면, 그 다음 단계로 전체 평균을 사용해야 함.
# 여기서는 'Laptop'의 Quantity가 2이므로 Electronics 카테고리 내 NaN이 아닌 평균은 2가 되어 Mouse, Keyboard의 NaN이 2로 채워짐.
print("\nNaN 처리 후 Sales DataFrame (Quantity):\n", df_sales)


# b) 'Total_Sales' 열 추가
df_sales['Total_Sales'] = df_sales['Price'] * df_sales['Quantity']
df_sales['Total_Sales'].fillna(0, inplace=True) # 혹시 Price 또는 Quantity가 NaN이어서 Total_Sales가 NaN인 경우 0으로
print("\n'Total_Sales' 추가 후:\n", df_sales)

# c) 각 카테고리별 최고 판매 아이템
# 방법 1: sort_values 후 groupby().first()
top_items_by_category = df_sales.sort_values('Total_Sales', ascending=False).groupby('Category').first()
print("\n각 카테고리별 최고 판매 아이템 (방법1):\n", top_items_by_category[['Item', 'Total_Sales']])

# 방법 2: groupby().apply()
def get_top_item(group):
    return group.loc[group['Total_Sales'].idxmax()]

top_items_apply = df_sales.groupby('Category').apply(get_top_item)
print("\n각 카테고리별 최고 판매 아이템 (방법2):\n", top_items_apply[['Item', 'Total_Sales']])

예상 결과:

원본 Sales DataFrame:
      Category         Item  Price  Quantity       Date
0  Electronics       Laptop   1200       2.0 2025-01-05
1  Electronics        Mouse     25       NaN 2025-01-05
2     Clothing      T-Shirt     30       5.0 2025-01-06
3     Clothing        Jeans     70       3.0 2025-01-06
4        Books  Python Book     45      10.0 2025-01-07
5  Electronics     Keyboard     75       NaN 2025-01-08

NaN 처리 후 Sales DataFrame (Quantity):
      Category         Item  Price  Quantity       Date
0  Electronics       Laptop   1200       2.0 2025-01-05
1  Electronics        Mouse     25       2.0 2025-01-05 # Electronics 평균(2.0)으로 채워짐
2     Clothing      T-Shirt     30       5.0 2025-01-06
3     Clothing        Jeans     70       3.0 2025-01-06
4        Books  Python Book     45      10.0 2025-01-07
5  Electronics     Keyboard     75       2.0 2025-01-08 # Electronics 평균(2.0)으로 채워짐

'Total_Sales' 추가 후:
      Category         Item  Price  Quantity       Date  Total_Sales
0  Electronics       Laptop   1200       2.0 2025-01-05       2400.0
1  Electronics        Mouse     25       2.0 2025-01-05         50.0
2     Clothing      T-Shirt     30       5.0 2025-01-06        150.0
3     Clothing        Jeans     70       3.0 2025-01-06        210.0
4        Books  Python Book     45      10.0 2025-01-07        450.0
5  Electronics     Keyboard     75       2.0 2025-01-08        150.0

각 카테고리별 최고 판매 아이템 (방법1):
                     Item  Total_Sales
Category
Books         Python Book        450.0
Clothing            Jeans        210.0
Electronics        Laptop       2400.0

각 카테고리별 최고 판매 아이템 (방법2):
                     Item  Price  Quantity       Date  Total_Sales
Category
Books         Python Book     45       2.0 2025-01-07        450.0 # Quantity는 예시와 다를 수 있음. 위에서 채워진 값.
Clothing            Jeans     70       3.0 2025-01-06        210.0
Electronics        Laptop   1200       2.0 2025-01-05       2400.0
(apply 결과는 전체 행을 반환하므로, 여기서는 Item과 Total_Sales만 출력 예시로 보임)
문제 5: 데이터 병합 및 그룹별 집계 (Ch 9, 8)

다음 두 개의 DataFrame이 있습니다.

df_employees = pd.DataFrame({
    'EmpID': [101, 102, 103, 104, 105, 106],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
    'DepartmentID': [1, 2, 1, 3, 2, 1]
})

df_departments = pd.DataFrame({
    'DepartmentID': [1, 2, 3, 4],
    'DepartmentName': ['HR', 'Engineering', 'Marketing', 'Sales']
})
a) 두 DataFrame을 병합하여 각 직원의 이름과 해당 직원이 속한 부서의 이름(DepartmentName)을 함께 볼 수 있도록 하세요. (HR 부서에 속하지 않은 직원은 결과에 포함되지 않아도 됩니다 - 즉, DepartmentID가 1,2,3인 직원만).
b) 각 부서(DepartmentName)별 직원 수를 계산하여 출력하세요.

해답:

import pandas as pd

df_employees = pd.DataFrame({
    'EmpID': [101, 102, 103, 104, 105, 106],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
    'DepartmentID': [1, 2, 1, 3, 2, 1]
})

df_departments = pd.DataFrame({
    'DepartmentID': [1, 2, 3, 4],
    'DepartmentName': ['HR', 'Engineering', 'Marketing', 'Sales']
})
print("Employees DataFrame:\n", df_employees)
print("\nDepartments DataFrame:\n", df_departments)

# a) DataFrame 병합 (inner join)
df_merged = pd.merge(df_employees, df_departments, on='DepartmentID', how='inner')
print("\n병합된 DataFrame (직원 및 부서명):\n", df_merged[['Name', 'DepartmentName']])

# b) 부서별 직원 수 계산
employee_count_by_dept = df_merged.groupby('DepartmentName')['EmpID'].count()
# 또는 employee_count_by_dept = df_merged['DepartmentName'].value_counts()
print("\n부서별 직원 수:\n", employee_count_by_dept)

예상 결과:

Employees DataFrame:
   EmpID     Name  DepartmentID
0    101    Alice             1
1    102      Bob             2
2    103  Charlie             1
3    104    David             3
4    105      Eve             2
5    106    Frank             1

Departments DataFrame:
   DepartmentID DepartmentName
0             1             HR
1             2    Engineering
2             3      Marketing
3             4          Sales

병합된 DataFrame (직원 및 부서명):
      Name DepartmentName
0    Alice             HR
1  Charlie             HR
2    Frank             HR
3      Bob    Engineering
4      Eve    Engineering
5    David      Marketing

부서별 직원 수:
DepartmentName
Engineering    2
HR             3
Marketing      1
Name: EmpID, dtype: int64
문제 6: 시계열 데이터 처리 (Ch 10)

다음은 어떤 주식의 2025년 1월 한 달간 일별 종가 데이터입니다.

date_rng = pd.date_range(start='2025-01-01', end='2025-01-31', freq='D')
np.random.seed(0) # 결과 재현을 위해
stock_prices = pd.Series(100 + np.random.randn(len(date_rng)).cumsum(), index=date_rng)
a) 인덱스가 DatetimeIndex 타입인지 확인하고, 아니라면 변환하세요. (위 코드는 이미 DatetimeIndex를 생성합니다.)
b) 이 주식의 5일 이동 평균 종가를 계산하여 새로운 Series로 만드세요.
c) 일별 종가 데이터를 주별(매주 금요일 기준) 평균 종가로 리샘플링하세요.

해답:

import pandas as pd
import numpy as np

# 데이터 생성
date_rng = pd.date_range(start='2025-01-01', end='2025-01-31', freq='D')
np.random.seed(0)
stock_prices = pd.Series(100 + np.random.randn(len(date_rng)).cumsum(), index=date_rng)
stock_prices.name = 'Price'
print("원본 주가 데이터 (일부):\n", stock_prices.head())

# a) 인덱스 타입 확인 (이미 DatetimeIndex)
print("\n인덱스 타입:", type(stock_prices.index))
# stock_prices.index = pd.to_datetime(stock_prices.index) # 만약 변환이 필요하다면

# b) 5일 이동 평균 계산
moving_avg_5d = stock_prices.rolling(window=5).mean()
moving_avg_5d.name = 'MA_5D'
print("\n5일 이동 평균 (일부):\n", moving_avg_5d.head(7))

# c) 주별(금요일 기준) 평균 종가 리샘플링
# 'W-FRI'는 금요일을 주의 끝으로 하는 주별 빈도
weekly_avg_friday = stock_prices.resample('W-FRI').mean()
weekly_avg_friday.name = 'WeeklyAvg_Fri'
print("\n주별(금요일 기준) 평균 종가:\n", weekly_avg_friday)

예상 결과 (randn 부분은 실행 시마다 달라지나 구조는 유사):

원본 주가 데이터 (일부):
2025-01-01    101.764052
2025-01-02    102.164200
2025-01-03    103.142896
2025-01-04    105.383819
2025-01-05    107.251067
Freq: D, Name: Price, dtype: float64

인덱스 타입: <class 'pandas.core.indexes.datetimes.DatetimeIndex'>

5일 이동 평균 (일부):
2025-01-01          NaN
2025-01-02          NaN
2025-01-03          NaN
2025-01-04          NaN
2025-01-05   103.941207  # (1일~5일 평균)
2025-01-06   105.248401  # (2일~6일 평균)
2025-01-07   106.298453  # (3일~7일 평균)
Freq: D, Name: MA_5D, dtype: float64

주별(금요일 기준) 평균 종가:
2025-01-03    102.357049  # (1/1, 1/2, 1/3 평균)
2025-01-10    106.731348  # (1/4 ~ 1/10 평균)
2025-01-17    106.208924  # (1/11 ~ 1/17 평균)
2025-01-24    104.491397  # (1/18 ~ 1/24 평균)
2025-01-31    104.446157  # (1/25 ~ 1/31 평균)
Freq: W-FRI, Name: WeeklyAvg_Fri, dtype: float64
문제 7: 데이터 시각화 (Ch 11)

다음은 1년간 두 가지 제품(ProductA, ProductB)의 월별 판매량 데이터입니다.

months = pd.date_range(start='2024-01-01', periods=12, freq='MS') # 월초 빈도
data_vis = {
    'ProductA_Sales': [200, 220, 250, 230, 270, 300, 280, 290, 260, 240, 280, 310],
    'ProductB_Sales': [150, 160, 180, 170, 190, 210, 200, 220, 190, 180, 200, 230]
}
df_monthly_sales = pd.DataFrame(data_vis, index=months)
a) 두 제품의 월별 판매량 추세를 보여주는 라인 플롯을 만드세요. 플롯 제목은 "월별 제품 판매량 추세"로 하고, 범례를 포함시키세요.
b) 두 제품의 연간 총 판매량을 비교하는 막대 그래프를 만드세요. 플롯 제목은 "제품별 연간 총 판매량 비교"로 하세요.

해답:

import pandas as pd
import matplotlib.pyplot as plt

# 데이터 생성
months = pd.date_range(start='2024-01-01', periods=12, freq='MS')
data_vis = {
    'ProductA_Sales': [200, 220, 250, 230, 270, 300, 280, 290, 260, 240, 280, 310],
    'ProductB_Sales': [150, 160, 180, 170, 190, 210, 200, 220, 190, 180, 200, 230]
}
df_monthly_sales = pd.DataFrame(data_vis, index=months)
print("월별 판매량 데이터:\n", df_monthly_sales.head())

# a) 라인 플롯
df_monthly_sales.plot(kind='line', figsize=(10, 5)) # 또는 df_monthly_sales.plot.line()
plt.title("월별 제품 판매량 추세")
plt.xlabel("월")
plt.ylabel("판매량")
plt.legend(title="제품")
plt.grid(True)
plt.show()

# b) 막대 그래프 (연간 총 판매량)
total_annual_sales = df_monthly_sales.sum() # 각 제품별 총 판매량 (Series)
total_annual_sales.plot(kind='bar', figsize=(7, 5), color=['skyblue', 'lightcoral'])
plt.title("제품별 연간 총 판매량 비교")
plt.xlabel("제품")
plt.ylabel("총 판매량")
plt.xticks(rotation=0) # x축 라벨 회전 안 함
plt.grid(axis='y', linestyle='--')
plt.show()

예상 결과 (텍스트 설명):

a) 첫 번째 플롯은 x축이 월(시간)이고 y축이 판매량인 라인 플롯입니다. ProductA와 ProductB 각각에 대한 판매량 추세선이 다른 색으로 그려지고, 범례를 통해 각 선이 어떤 제품을 나타내는지 알 수 있습니다. 제목과 축 라벨, 그리드가 포함됩니다.

b) 두 번째 플롯은 x축에 ProductA와 ProductB가 있고, y축에 각 제품의 연간 총 판매량을 나타내는 수직 막대 그래프입니다. 각 막대는 다른 색상으로 표시되며, 제목, 축 라벨, y축 그리드가 포함됩니다.

문제 8: 복합 조건 선택 및 정렬 (Ch 4, 6)

문제 3의 students.csv 데이터를 다시 사용합니다.

csv_data = """name,math,english,science
Alice,90,85,92
Bob,78,80,75
Charlie,95,92,88
David,60,70,65
Eve,88,89,90"""
df_students = pd.read_csv(io.StringIO(csv_data))
a) 수학(math) 점수가 전체 학생들의 평균 수학 점수보다 높으면서, 동시에 영어(english) 점수가 85점 이상인 학생들을 선택하세요.
b) 위에서 선택된 학생들의 이름(name), 수학(math), 영어(english), 과학(science) 점수를 과학 점수가 높은 순서대로 정렬하여 출력하세요.

해답:

import pandas as pd
import io

csv_data = """name,math,english,science
Alice,90,85,92
Bob,78,80,75
Charlie,95,92,88
David,60,70,65
Eve,88,89,90"""
df_students = pd.read_csv(io.StringIO(csv_data))
print("원본 학생 데이터:\n", df_students)

# a) 복합 조건으로 학생 선택
avg_math_score = df_students['math'].mean()
condition_a = (df_students['math'] > avg_math_score) & (df_students['english'] >= 85)
selected_students = df_students[condition_a]
print("\n조건 만족 학생:\n", selected_students)

# b) 선택된 학생 정보 정렬하여 출력
sorted_selected_students = selected_students[['name', 'math', 'english', 'science']].sort_values(by='science', ascending=False)
print("\n정렬된 결과:\n", sorted_selected_students)

예상 결과:

원본 학생 데이터:
      name  math  english  science
0    Alice    90       85       92
1      Bob    78       80       75
2  Charlie    95       92       88
3    David    60       70       65
4      Eve    88       89       90

(평균 수학 점수는 (90+78+95+60+88)/5 = 82.2)

조건 만족 학생:
      name  math  english  science
0    Alice    90       85       92  # math(90)>82.2 AND english(85)>=85
2  Charlie    95       92       88  # math(95)>82.2 AND english(92)>=85
4      Eve    88       89       90  # math(88)>82.2 AND english(89)>=85

정렬된 결과:
      name  math  english  science
0    Alice    90       85       92
4      Eve    88       89       90
2  Charlie    95       92       88

이 연습 문제들을 통해 Pandas의 다양한 기능을 복습하고 실제 데이터 분석 상황에 응용하는 능력을 키울 수 있기를 바랍니다. 더 많은 연습과 경험을 통해 Pandas 활용 능력을 향상시켜 보세요!