파이썬 데이터 분석 및 조작을 위한 필수 라이브러리
파이썬을 활용한 데이터 분석의 세계에 오신 것을 환영합니다! 이 가이드는 데이터 처리와 분석을 위한 가장 강력하고 인기 있는 라이브러리 중 하나인 Pandas의 모든 것을 깊이 있게 탐구하고자 합니다.
Pandas는 복잡하고 지저분한 실제 데이터를 다루기 쉬운 형태로 변환하고, 의미 있는 통찰력을 추출하는 데 필요한 다양한 도구를 제공합니다. Series와 DataFrame이라는 직관적인 자료구조를 통해, 여러분은 마치 스프레드시트나 SQL 데이터베이스를 다루듯 유연하게 데이터를 조작할 수 있게 됩니다.
이 문서는 Pandas의 기본 개념부터 시작하여 데이터 입출력, 선택, 정제, 그룹화, 병합, 시계열 분석, 그리고 간단한 시각화에 이르기까지 핵심적인 기능들을 체계적으로 안내할 것입니다. 각 장의 설명과 풍부한 예제, 그리고 연습 문제를 통해 Pandas 활용 능력을 효과적으로 향상시킬 수 있도록 구성했습니다.
본 가이드가 여러분이 데이터 전문가로 성장하는 데 든든한 밑거름이 되기를 바라며, Pandas와 함께 데이터의 잠재력을 최대한 발휘하는 즐거움을 경험하시기를 응원합니다.
- ~ : ) (with Gemini)
이 Pandas 가이드는 여러분이 데이터 분석가로서 필요한 핵심 기술을 단계별로 익힐 수 있도록 설계되었습니다. 각 장의 주요 내용은 다음과 같습니다.
Pandas의 정의, 핵심 기능(Series, DataFrame), 그리고 데이터 분석에서 Pandas가 왜 중요한지에 대해 소개합니다. NumPy와의 관계도 간략히 살펴봅니다.
Pandas의 기본 구성 요소인 Series(1차원 배열형 데이터)와 DataFrame(2차원 테이블형 데이터)의 생성 방법, 주요 속성, 기본 연산 및 조작 방법을 학습합니다.
CSV, Excel 파일 등 다양한 외부 데이터 소스로부터 데이터를 읽어오고, 처리된 데이터를 다시 파일로 저장하는 방법을 익힙니다. 주요 함수의 중요 파라미터들을 다룹니다.
DataFrame에서 특정 열이나 행, 또는 원하는 조건에 맞는 데이터를 정확하게 선택하고 필터링하는 다양한 기법(loc, iloc, 불리언 인덱싱 등)을 마스터합니다.
새로운 데이터 열 추가/삭제, 결측치(NaN) 처리, 중복 데이터 제거, 데이터 타입 변환, 함수 적용(apply, map) 등 실질적인 데이터 전처리 기술을 배웁니다.
데이터를 특정 기준(인덱스 또는 값)으로 오름차순 또는 내림차순 정렬하고, 각 데이터의 순위를 매기는 방법을 학습합니다.
데이터의 특성을 요약하는 주요 기술 통계량(평균, 중앙값, 표준편차, 최빈값 등)을 계산하고, describe() 함수를 통해 전체적인 요약 정보를 얻는 방법을 익힙니다.
데이터를 특정 기준으로 그룹화한 후, 각 그룹별로 집계(aggregation), 변환(transformation), 필터링(filtration) 연산을 수행하는 강력한 GroupBy 메커니즘을 이해하고 활용합니다.
여러 개의 DataFrame을 다양한 방식(연결, SQL 스타일 조인, 인덱스 기반 결합)으로 합치는 concat, merge, join 함수 사용법을 배웁니다.
날짜 및 시간 데이터를 다루는 방법, 시계열 인덱스 생성, 리샘플링, 이동 평균(rolling window), 데이터 이동(shifting) 등 시계열 분석의 기초를 다룹니다.
Pandas에 내장된 기본적인 시각화 기능을 사용하여 DataFrame의 데이터를 라인 플롯, 막대 그래프, 히스토그램 등으로 간편하게 시각화하는 방법을 알아봅니다.
계층적 인덱싱(MultiIndex), 범주형 데이터(Categorical), 메모리 최적화 등 Pandas의 좀 더 심도 있는 주제들을 간략히 소개합니다.
각 장에서 배운 내용을 바탕으로 실제 데이터 분석 시나리오를 가정한 연습 문제들을 풀어보며 Pandas 활용 능력을 다집니다.
Pandas는 파이썬 프로그래밍 언어를 위한 고성능의 사용하기 쉬운 데이터 구조와 데이터 분석 도구를 제공하는 핵심 라이브러리입니다. Wes McKinney에 의해 2008년에 처음 개발되었으며, 현재는 파이썬 데이터 과학 생태계에서 가장 중요한 라이브러리 중 하나로 인정받고 있습니다.
Pandas라는 이름은 "Panel Data" (다차원 구조화 데이터셋을 일컫는 계량경제학 용어)와 "Python Data Analysis"의 합성어에서 유래했습니다. 주된 목표는 파이썬을 강력하고 유연한 실제 데이터 분석 환경으로 만드는 것입니다.
groupby 기능을 통해 데이터를 특정 기준으로 그룹화하고, 각 그룹에 대해 다양한 집계 연산(합계, 평균, 개수 등)을 효율적으로 수행할 수 있습니다.loc) 및 정수 위치 기반(iloc) 인덱싱을 모두 지원하여 데이터를 유연하게 선택하고 조작할 수 있습니다.현대의 데이터는 양이 방대하고 형식이 다양하며, 종종 결측치나 오류를 포함하고 있습니다. Pandas는 이러한 "지저분한(messy)" 실제 데이터를 효과적으로 다룰 수 있도록 설계되었습니다.
데이터를 불러오고, 정제하고, 변환하고, 분석하고, 시각화하는 전체 데이터 분석 파이프라인에서 Pandas는 핵심적인 역할을 수행합니다. 따라서 파이썬으로 데이터 분석을 시작한다면 Pandas 학습은 필수적입니다.
Pandas는 NumPy를 기반으로 구축되었습니다. Pandas의 Series와 DataFrame 내부의 데이터는 대부분 NumPy의 ndarray로 저장됩니다. 이로 인해 Pandas는 NumPy의 다음과 같은 장점들을 그대로 활용합니다:
NumPy가 주로 동종의 수치 데이터 배열을 다루는 데 중점을 둔다면, Pandas는 이종의 데이터(숫자, 문자열, 불리언 등 혼합 가능)를 테이블 형태로 다루고, 각 행과 열에 명시적인 인덱스와 컬럼명을 부여하여 데이터의 의미를 더 명확하게 파악하고 조작할 수 있도록 확장한 것입니다. 즉, NumPy가 저수준의 수치 연산 인프라를 제공한다면, Pandas는 이를 바탕으로 더 고수준의 데이터 분석 기능을 제공한다고 볼 수 있습니다.
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])
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]]
Pandas의 핵심은 두 가지 주요 자료구조인 Series와 DataFrame입니다. 이 두 구조를 이해하는 것이 Pandas를 효과적으로 사용하는 첫걸음입니다.
Series는 어떤 데이터 타입(정수, 문자열, 부동소수점, 파이썬 객체 등)이든 담을 수 있는 1차원 배열과 같은 자료구조입니다. 각 데이터 포인트는 인덱스(index)라는 레이블과 연결됩니다. 인덱스는 기본적으로 0부터 시작하는 정수이지만, 사용자가 원하는 값으로 지정할 수도 있습니다.
리스트, 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는 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
DataFrame은 여러 개의 Series를 같은 인덱스로 묶어놓은 것과 같은 2차원 테이블 형태의 자료구조입니다. 각 열(column)은 서로 다른 데이터 타입을 가질 수 있습니다. 행과 열에 각각 인덱스(index)와 컬럼명(columns)이라는 레이블이 있습니다.
가장 일반적인 방법은 각 열의 이름을 키로 하고 리스트나 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
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
Pandas는 다양한 파일 형식으로부터 데이터를 읽고 쓰는 강력한 기능을 제공합니다. 가장 흔하게 사용되는 CSV와 Excel 파일을 중심으로 살펴보겠습니다.
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) # 실제 파일 필요
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: 파일 인코딩.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()))
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, skiprows 등 read_csv와 유사한 파라미터 다수 지원.engine: 사용할 파서 엔진 ('openpyxl' for .xlsx, 'xlrd' for .xls).주요 to_excel 파라미터:
excel_writer: 파일 경로 또는 ExcelWriter 객체.sheet_name: 저장할 시트 이름 (기본값: 'Sheet1').na_rep, float_format, columns, header, index 등 to_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 등 다양한 형식의 데이터를 지원합니다.
Pandas DataFrame에서 원하는 데이터를 효과적으로 추출하고 선택하는 것은 데이터 분석의 가장 기본적이면서도 중요한 단계입니다. 이 장에서는 DataFrame의 특정 열(column)이나 행(row), 또는 특정 조건에 맞는 데이터를 정확하게 선택하고 필터링하는 다양한 기법을 학습합니다. 주요 내용으로는 열 선택, 행 슬라이싱, 라벨 기반 선택 도구인 .loc, 위치 기반 선택 도구인 .iloc, 그리고 강력한 조건부 선택 방법인 불리언 인덱싱(Boolean indexing) 등이 있습니다.
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'>
활용 방안: 분석에 필요한 특정 변수들만 추출하거나, 개인정보와 같은 민감한 정보를 제외하고 데이터를 공유할 때 유용합니다.
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를 사용하는 것이 더 명확합니다.
.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, 상품 코드, 날짜 등)이 있을 때 특정 데이터를 정확하고 가독성 높게 선택할 수 있습니다.
.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]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 배열과 유사한 방식으로 데이터를 다룰 수 있게 합니다.
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 제거), 특정 기준을 만족하는 샘플 추출, 고객 세분화 등 데이터 분석 전반에 걸쳐 매우 빈번하게 사용됩니다.
.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, 불리언 인덱싱 등을 적절히 조합하여 사용하면 원하는 데이터를 효율적으로 추출하고 분석 작업을 원활하게 진행할 수 있습니다.
데이터 분석의 정확성과 신뢰성을 높이기 위해서는 원본 데이터를 분석 가능한 형태로 가공하고 정제하는 과정이 필수적입니다. 이 장에서는 Pandas를 사용하여 데이터를 효과적으로 조작하고 정제하는 핵심 기술들을 학습합니다. 주요 내용으로는 새로운 열(column)을 추가하거나 기존 열을 삭제하는 방법, 데이터에서 결측치(Missing Values)나 중복된 값을 처리하는 방법, 데이터의 타입을 변환하는 방법, 그리고 사용자 정의 함수나 내장 함수를 데이터에 일괄 적용하는 방법 등이 있습니다.
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
활용 방안: 파생 변수 생성, 분석에 불필요하거나 중복되는 정보 제거, 개인 식별 정보 제거 등 데이터 전처리 단계에서 광범위하게 사용됩니다.
실제 데이터에는 값이 누락된 경우가 많으며, 이를 결측치(주로 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이, 일반적인 수치 데이터에서는 평균이나 중앙값 대치가 자주 사용됩니다.
데이터셋에 동일한 행이 반복적으로 나타나는 경우, 분석 결과에 편향을 줄 수 있습니다. 이러한 중복 데이터를 식별하고 제거하는 방법을 알아봅니다.
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
활용 방안: 데이터 수집 오류로 인한 중복 항목 제거, 고유한 사용자 또는 아이템 목록 생성 등에 사용됩니다.
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' 타입으로 변환 등 다양한 상황에서 필요합니다.
데이터의 각 요소, 행, 열에 특정 함수를 일괄적으로 적용하여 데이터를 변환하거나 새로운 값을 계산할 수 있습니다.
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)
함수 적용 전 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)
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) 생성 등 데이터 변환 및 가공에 매우 유용합니다.
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가 제공하는 다양한 기능을 숙지하고 활용하여 깨끗하고 신뢰할 수 있는 데이터를 확보하는 것이 중요합니다.
데이터를 분석하기 좋은 형태로 만들기 위해서는 종종 특정 기준에 따라 데이터를 정렬하거나 각 데이터의 순위를 매겨야 합니다. Pandas는 이러한 작업을 효과적으로 수행할 수 있는 sort_index(), sort_values(), rank()와 같은 강력한 메서드들을 제공합니다. 이 장에서는 이러한 함수들을 사용하여 데이터를 원하는 순서대로 재배치하고, 데이터 포인트 간의 상대적인 순위를 파악하는 방법을 학습합니다.
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
활용 방안: 시계열 데이터에서 시간 순서대로 인덱스를 정렬하거나, 특정 카테고리(인덱스) 순으로 데이터를 보고자 할 때 유용합니다. 데이터 병합 전 인덱스를 정렬하면 성능 향상에 도움이 될 수 있습니다.
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
활용 방안: 가장 높은 판매량을 기록한 상품 순으로 정렬, 성적 우수자 순으로 학생 명단 정렬, 여러 기준을 복합적으로 적용하여 우선순위가 높은 데이터를 상단에 배치하는 등 데이터 분석에서 가장 빈번하게 사용되는 기능 중 하나입니다.
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의 이러한 기능들을 잘 활용하면 복잡한 데이터도 쉽게 다룰 수 있습니다.
데이터 분석의 초기 단계에서 데이터의 전반적인 특징을 파악하는 것은 매우 중요합니다. Pandas는 합계, 평균, 표준편차, 최소값, 최대값 등 다양한 기술 통계량을 손쉽게 계산할 수 있는 함수들을 제공합니다. 이 장에서는 이러한 함수들을 사용하여 데이터의 분포, 중심 경향성, 산포도 등을 파악하고, describe() 함수를 통해 주요 통계치를 한 번에 요약하는 방법, 그리고 누적 계산 등 다양한 계산 기법들을 학습합니다.
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
활용 방안: 데이터의 중심 경향(평균, 중앙값, 최빈값), 변동성(표준편차, 분산), 범위(최소값, 최대값) 등을 파악하여 데이터 분포에 대한 초기 이해를 돕습니다. 이상치 탐지나 데이터 스케일링 전처리 등에도 기초 자료로 활용됩니다.
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
활용 방안: 데이터의 전반적인 분포와 특성을 빠르게 요약하여 보여주므로, 데이터 탐색 단계에서 매우 유용합니다. 데이터 품질 검토, 이상치 유무 판단, 변수 선택 등에 도움을 줍니다.
데이터를 처음부터 현재 위치까지 누적하여 계산하는 함수들입니다. 시계열 데이터 분석이나 순서가 중요한 데이터에서 추세를 파악하는 데 유용합니다.
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).
활용 방안: 일별 매출 데이터에서 누적 매출액 계산, 주가 데이터에서 누적 수익률 계산, 특정 이벤트 발생까지의 누적 횟수 파악 등에 사용됩니다.
데이터 분석 과정에서 자주 사용되는 추가적인 계산 함수들입니다.
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의 기술 통계 및 계산 기능들은 데이터에 대한 깊이 있는 이해를 돕고, 더 나아가 복잡한 분석 모델링을 위한 기초를 마련해 줍니다. 데이터의 특성에 맞는 적절한 통계 함수를 사용하는 것이 중요합니다.
데이터 분석에서 특정 기준에 따라 데이터를 그룹으로 나누고, 각 그룹별로 통계를 내거나 특정 변환을 적용하는 작업은 매우 흔합니다. Pandas의 groupby() 메서드는 이러한 "Split-Apply-Combine" (분할-적용-결합) 패턴을 효율적으로 구현할 수 있게 해주는 핵심 기능입니다. 이 장에서는 groupby()를 사용하여 데이터를 그룹화하고, 각 그룹에 대해 집계(aggregation), 변환(transformation), 필터링(filtration) 등 다양한 연산을 수행하는 방법을 심층적으로 다룹니다.
GroupBy 연산은 다음과 같은 3단계 과정을 거칩니다:
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'])as_index=True (기본값): 그룹화 키를 결과의 인덱스로 사용합니다. False로 설정하면 기존 인덱스를 유지하고 그룹화 키를 일반 열로 추가합니다.sort=True (기본값): 그룹 키를 기준으로 결과를 정렬합니다. 성능 향상을 위해 False로 설정할 수 있습니다.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
각 그룹에 대해 통계량을 계산하는 작업입니다. 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'})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
활용 방안: 카테고리별(회사, 부서, 제품군 등) 매출 합계/평균, 사용자 그룹별 평균 구매액, 특정 기간별 통계량 계산 등 다양한 요약 정보를 얻는 데 핵심적으로 사용됩니다.
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과 같은 인덱스를 가지므로 병합이 용이합니다.
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
활용 방안: 특정 조건을 만족하는 그룹(예: 평균 매출액이 특정 기준 이상인 고객 그룹, 특정 수 이상의 리뷰가 달린 상품 그룹)의 데이터만 추출하여 심층 분석하거나, 노이즈가 될 수 있는 소규모 그룹을 제외하는 데 사용됩니다.
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를 활용한 데이터 분석의 핵심입니다. 다양한 그룹화 기준과 적용 함수를 조합하여 복잡한 데이터로부터 의미 있는 통찰력을 효과적으로 도출할 수 있습니다.
데이터 분석 프로젝트에서는 종종 여러 개의 분산된 데이터셋을 하나로 통합해야 하는 경우가 발생합니다. 예를 들어, 고객 정보 데이터와 구매 내역 데이터를 결합하거나, 여러 기간에 걸쳐 수집된 데이터를 하나로 합치는 작업 등이 있습니다. Pandas는 이러한 데이터 통합 작업을 위해 concat(), merge(), join()과 같은 강력하고 유연한 함수들을 제공합니다. 이 장에서는 이러한 함수들을 사용하여 다양한 방식으로 DataFrame을 합치는 방법을 학습하고, 각 함수의 주요 옵션과 사용 사례를 살펴봅니다.
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 사용이 권장됩니다.
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, 코드 등)를 기준으로 서로 다른 정보를 가진 데이터들을 결합하여 풍부한 분석용 데이터셋을 구축할 때 매우 유용합니다. 고객 정보와 주문 내역 병합, 상품 정보와 판매 정보 병합 등이 대표적인 예입니다.
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을 기준으로 정보를 추가하거나 확장하는 시나리오에 적합합니다.
Pandas는 데이터 결합을 위한 여러 방법을 제공하므로, 상황에 맞는 적절한 함수를 선택하는 것이 중요합니다.
pd.concat():
pd.merge():
DataFrame.join():
merge()로도 가능하지만, 인덱스 기반 결합을 더 간결하게 표현할 수 있습니다.데이터 통합은 분석을 위한 데이터 준비 단계에서 매우 중요한 과정입니다. concat, merge, join의 특징과 옵션을 잘 이해하고 활용하면, 흩어져 있는 데이터를 효과적으로 결합하여 분석에 필요한 형태로 가공할 수 있습니다.
시계열 데이터는 시간의 흐름에 따라 기록된 데이터를 의미하며, 금융 시장 분석, 경제 예측, 기상 변화 관찰, 센서 데이터 분석 등 다양한 분야에서 중요하게 활용됩니다. Pandas는 이러한 시계열 데이터를 효과적으로 처리하고 분석할 수 있도록 Timestamp, DatetimeIndex, Period와 같은 특수한 자료구조와 함께 다양한 시계열 관련 함수 및 메서드를 제공합니다. 이 장에서는 Pandas를 사용하여 날짜 및 시간 데이터를 다루는 방법, 시계열 인덱스 생성, 데이터 선택, 리샘플링(resampling), 이동창(rolling window) 연산, 데이터 이동(shifting) 등 핵심적인 시계열 데이터 처리 기법들을 학습합니다.
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
다양한 형태의 데이터를 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')
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'))
샘플 시계열 데이터 (첫 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
활용 방안: 특정 기간의 데이터 분석, 특정 이벤트 발생 시점의 데이터 확인, 시간 기반 필터링 등에 매우 유용합니다.
resample() 메서드는 시계열 데이터의 빈도(frequency)를 변환하는 강력한 기능입니다. 예를 들어, 일별 데이터를 월별 데이터로 (다운샘플링), 또는 월별 데이터를 일별 데이터로 (업샘플링) 변환할 수 있습니다.
높은 빈도의 데이터를 낮은 빈도로 변환합니다. 이 과정에서 그룹화된 데이터에 대한 집계 함수(sum(), mean(), first(), last(), ohlc() - 시고저종 등) 적용이 필요합니다.
# 월별 평균 계산 (다운샘플링)
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)
월별 평균 (다운샘플링):
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...
낮은 빈도의 데이터를 높은 빈도로 변환합니다. 이 과정에서 새로 생긴 시점의 값들은 대부분 NaN이 되므로, 이를 채우는 방법(보간법 등)이 필요합니다.
asfreq(): 특정 빈도로 변환만 하고 값은 채우지 않음 (NaN).fillna(): NaN 값을 특정 값으로 채움 ('ffill': 앞 값, 'bfill': 뒷 값).interpolate(): 다양한 보간법(선형, 다항 등)으로 NaN 값을 채움.# 월별 데이터를 일별 데이터로 업샘플링 (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())
월별 -> 일별 업샘플링 (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
활용 방안: 데이터의 시간 단위를 통일하거나, 다른 빈도의 데이터와 비교 분석하기 위해 사용됩니다. 주가 데이터의 일별 -> 주별/월별 변환, 센서 데이터의 초/분 단위 -> 시간별/일별 변환 등에 활용됩니다.
rolling() 메서드는 고정된 크기의 창(window)을 데이터 위로 이동시키면서 각 창에 포함된 데이터에 대해 통계량을 계산합니다. 이동 평균(moving average) 계산이 대표적인 예입니다.
rolling() 주요 파라미터 및 사용법window: 창의 크기를 지정합니다. 정수(데이터 포인트 수) 또는 시간 오프셋 문자열(예: '3D')을 사용할 수 있습니다.min_periods: 결과를 계산하기 위해 필요한 최소 관측치 수. 창 크기보다 작을 수 있습니다.center=False (기본값): False이면 창의 오른쪽 끝을 기준으로 라벨링, True이면 창의 중앙을 기준으로 라벨링합니다.mean(), sum(), std(), count(), corr(), cov(), apply() 등)를 뒤에 연결하여 사용합니다.# 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))
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
활용 방안: 데이터의 단기 변동성을 제거하고 추세를 파악(이동 평균), 변동성 측정(이동 표준편차), 특정 기간 동안의 패턴 분석 등에 널리 사용됩니다. 금융 데이터 분석에서 이동 평균선 계산이 대표적입니다.
shift() 메서드는 시계열 데이터의 값을 시간 축을 따라 앞이나 뒤로 이동시킵니다. 이는 시차(lag)를 가진 특성을 만들거나, 기간 간의 변화를 계산하는 데 유용합니다.
shift() 사용법periods: 이동할 기간의 수. 양수이면 뒤로(과거 방향), 음수이면 앞으로(미래 방향) 이동합니다 (기본값 1).freq: 시간 오프셋 문자열을 지정하면 인덱스 자체를 이동시킵니다 (tshift의 기능 통합). 이 경우 값은 그대로 유지되고 인덱스만 변경됩니다.axis: 이동할 축 (0: 행, 1: 열).# 값을 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') -> 인덱스 이동, 값 그대로
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)으로 사용(시차 변수 생성), 기간 간의 변화율 또는 차이 계산, 미래 값 예측 모델링의 기초 데이터 생성 등에 사용됩니다.
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가 제공하는 도구들을 적절히 활용하면 복잡한 시계열 분석 작업도 효율적으로 수행할 수 있습니다.
데이터를 숫자로만 이해하는 것에는 한계가 있으며, 시각화는 데이터에 숨겨진 패턴, 추세, 이상치 등을 직관적으로 파악하는 데 매우 효과적인 방법입니다. Pandas는 Python의 대표적인 시각화 라이브러리인 Matplotlib을 기반으로 하여, DataFrame이나 Series 객체에서 직접 다양한 종류의 플롯을 손쉽게 생성할 수 있는 .plot() 접근자를 제공합니다. 이 장에서는 Pandas의 .plot() 기능을 사용하여 라인 플롯, 막대 그래프, 히스토그램, 산점도 등 기본적인 시각화 방법들을 익히고, 플롯을 간단하게 꾸미는 방법을 알아봅니다. 더 복잡하고 미려한 시각화는 Matplotlib, Seaborn, Plotly 등 전문 시각화 라이브러리를 활용할 수 있습니다.
Pandas의 .plot() 기능은 Matplotlib의 기능을 편리하게 사용할 수 있도록 감싼(wrapper) 것입니다. 따라서 기본적인 플롯은 Pandas만으로도 충분히 생성 가능하지만, 세부적인 설정을 변경하거나 여러 플롯을 복합적으로 구성하려면 Matplotlib의 객체(Figure, Axes 등)를 직접 다루는 것이 좋습니다. 일반적으로 Pandas로 빠르게 기본 플롯을 그린 후, 필요에 따라 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 환경에서는 셀 아래에 표시됨).
s의 인덱스를 x축, 값을 y축으로 하는 라인 플롯입니다. 제목, x축 라벨, y축 라벨이 Matplotlib 함수를 통해 추가된 것을 볼 수 있습니다.df_vis의 인덱스를 x축으로 하고, 각 열('A', 'B', 'C', 'D')의 데이터를 별도의 라인으로 그린 플롯입니다. 자동으로 각 라인에 대한 범례(legend)가 표시됩니다..plot()의 kind 파라미터에 따라 다양한 종류의 플롯을 그릴 수 있습니다.
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'열의 데이터만 선택하여 라인 플롯을 그립니다. 제목이 지정되고 그리드가 표시됩니다.
활용 방안: 주가 변동, 기온 변화, 시간에 따른 웹사이트 트래픽 등 연속적인 데이터의 추세를 시각화합니다.
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()
활용 방안: 제품별 판매량 비교, 설문조사 응답 결과 비교, 지역별 인구수 비교 등 범주 간의 양적 비교에 유용합니다.
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()
df_vis의 'A'열과 'B'열에 대한 히스토그램을 하나의 플롯에 겹쳐서 그립니다.활용 방안: 데이터의 분포 형태(정규분포, 치우친 분포 등), 중심 경향, 산포도 등을 시각적으로 확인하는 데 사용됩니다.
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()
df_vis의 각 열('A', 'B', 'C', 'D')에 대한 박스 플롯을 나란히 그려 분포를 비교합니다.활용 방안: 데이터의 전반적인 분포 특성 파악, 그룹 간 분포 비교, 이상치 식별 등에 효과적입니다.
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()
활용 방안: 시간에 따른 여러 그룹의 누적된 양 변화(예: 시장 점유율 변화), 전체 대비 각 구성 요소의 크기 변화 등을 보여줄 때 사용합니다.
kind='scatter'두 수치형 변수 간의 관계를 점으로 표현합니다. 변수 간의 상관관계, 군집 등을 파악하는 데 유용합니다. DataFrame의 경우 x와 y 파라미터로 사용할 열을 명시해야 합니다.
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()
활용 방안: 두 변수 간의 선형/비선형 관계, 데이터의 군집 형성 여부, 이상치 등을 탐색하는 데 사용됩니다.
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')이 전체에서 차지하는 비율을 파이 차트로 나타냅니다. 각 조각에 백분율이 표시되고, 지정된 색상과 시작 각도 등이 적용됩니다.
활용 방안: 시장 점유율, 예산 구성 비율 등 전체에 대한 각 부분의 상대적인 크기를 보여주고자 할 때 제한적으로 사용될 수 있습니다.
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()
s_hist_data의 분포를 부드러운 곡선 형태의 밀도 플롯으로 보여줍니다.df_vis의 'A'열과 'B'열 각각에 대한 밀도 플롯을 하나의 그림에 함께 그립니다.활용 방안: 변수의 분포 형태를 히스토그램보다 부드럽게 파악하거나, 여러 그룹 간의 분포를 비교할 때 사용됩니다.
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=['-', '--', ':']: 라인 스타일 지정.# 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와 같은 전문 시각화 라이브러리를 함께 사용하는 것을 고려해볼 수 있습니다.
지금까지 Pandas의 핵심 기능들을 살펴보았습니다. 이 장에서는 Pandas의 활용도를 더욱 높이고 특정 상황에서 더 효율적인 데이터 처리를 가능하게 하는 몇 가지 고급 주제들을 간략하게 소개합니다. 이러한 주제들은 더 복잡한 데이터를 다루거나 성능을 최적화해야 할 때 유용하며, 추가적인 학습을 통해 Pandas 전문가로 성장하는 데 밑거름이 될 것입니다.
계층적 인덱싱은 DataFrame의 행이나 열에 여러 레벨의 인덱스를 지정하는 기능입니다. 이를 통해 고차원 데이터를 2차원 테이블 형태로 효과적으로 표현하고 분석할 수 있습니다. 예를 들어, 한 축에 여러 개의 키를 사용하여 그룹화된 데이터를 표현하거나, 다차원 배열과 유사한 구조를 만들 수 있습니다.
groupby() 결과에서 여러 키를 사용하거나, pd.MultiIndex.from_tuples(), pd.MultiIndex.from_product(), set_index()에 여러 열을 전달하여 생성할 수 있습니다..loc[] 접근자 내에 튜플 형태로 각 레벨의 인덱스 값을 전달하여 특정 데이터를 선택하거나 슬라이싱할 수 있습니다.stack() / unstack(): 데이터프레임의 열을 인덱스로 쌓거나(stack), 인덱스 레벨을 열로 펼치는(unstack) 데 사용되어 데이터 형태를 유연하게 변경할 수 있습니다.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())
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
활용 방안: 다차원 시계열 데이터(예: 여러 종목의 시고저종 데이터), 설문조사의 다중 응답 데이터, 복잡한 실험 결과 데이터 등을 효율적으로 관리하고 분석하는 데 유용합니다.
문자열 데이터 중에서 고유한 값의 개수가 제한적인 경우(예: 성별, 등급, 지역명 등), 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')에도 유용합니다.
대용량 데이터를 다루거나 반복적인 연산을 수행할 때, 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 (예시 값,显著减少)
활용 방안: 특히 메모리가 제한적이거나 매우 큰 데이터셋을 다룰 때, 이러한 최적화 기법들은 분석 작업의 속도를 높이고 시스템 자원 부담을 줄이는 데 기여합니다.
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) # 원래 기본값으로
현재 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를 깊이 있게 사용하고자 한다면 이러한 주제들에 대해 꾸준히 학습하고 탐구하는 것이 좋습니다.
지금까지 학습한 Pandas의 다양한 기능들을 종합적으로 활용해보는 연습 문제입니다. 각 문제 아래의 '해답 보기'를 클릭하면 정답 코드를 확인할 수 있습니다.
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 배열): ['사과' '바나나' '딸기' '포도' '오렌지']
다음 정보를 포함하는 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
다음과 같은 내용의 `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으로 읽어오세요.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
다음은 온라인 상점의 판매 데이터 일부입니다.
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 평균을 사용하세요.)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만 출력 예시로 보임)
다음 두 개의 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인 직원만).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
다음은 어떤 주식의 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를 생성합니다.)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)
원본 주가 데이터 (일부):
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
다음은 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) 두 제품의 월별 판매량 추세를 보여주는 라인 플롯을 만드세요. 플롯 제목은 "월별 제품 판매량 추세"로 하고, 범례를 포함시키세요.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축 그리드가 포함됩니다.
문제 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점 이상인 학생들을 선택하세요.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 활용 능력을 향상시켜 보세요!