Pandas 데이터 분석 라이브러리 핵심 가이드

Pandas 라이브러리 기초 및 활용

Python에서 데이터 분석을 수행할 때 가장 널리 사용되는 라이브러리는 Pandas이다. 최근에는 Rust로 구현된 Polars라는 고성능 라이브러리도 등장하여 주목받고 있다.

Excel 파일 읽기 오류 해결

Pandas로 Excel 파일을 읽을 때 xlrd 라이브러리를 사용하는데, xlrd가 2.0.1 버전으로 업데이트된 이후 xlsx 파일을 지원하지 않게 되었다. 이 문제를 해결하려면 engine 매개변수를 지정해야 한다:

import pandas as pd

# openpyxl 엔진 사용
df = pd.read_excel('파일경로/데이터.xlsx', engine='openpyxl')

Series와 DataFrame의 개념적 차이

Series: 1차원 데이터 구조

Series는 한 행 또는 한 열의 데이터이며, 리스트로 변환 가능하다.

import pandas as pd

# 리스트로 Series 생성
series_data = pd.Series([1, 2, 3, "four", "5", 6.0], 
                        index=["첫번째", "두번째", "세번째", "네번째", "다섯번째", "여섯번째"])

# 인덱스 접근
print(series_data.index)
print(series_data.values)
print(series_data.values.tolist())

사전으로 Series를 생성하면 키가 인덱스가 되고 값이 데이터가 된다:

series_dict = pd.Series({"첫째": 1, "둘째": 2, "셋째": 3})

데이터 조회

# 여러 값 조회 (Series 반환)
print(series_data[["첫번째", "두번째"]])

# 단일 값 조회
print(series_data["첫번째"])

DataFrame: 2차원 데이터 구조

DataFrame은 행과 열이 있는 2차원 또는 다차원 데이터 구조이다. 행 인덱스(row index)와 열 인덱스(column index)를 모두 가진다.

DataFrame 생성 방법

사전 형식에서 외부 키가 열 인덱스가 되고 값이 행 데이터가 된다:

import pandas as pd
from openpyxl import Workbook

# 사전으로 DataFrame 생성
data_dict = {
    "위험유형1": {"서울": 2},
    "위험유형2": {"부산": 10, "서울": 20, "광주": 30},
    "위험유형3": {"광주": 100, "서울": 200}
}
df1 = pd.DataFrame(data_dict)

리스트의 각 요소가 행 데이터가 된다:

# 리스트로 DataFrame 생성
data_list = [
    {"위험유형1": 1, "위험유형2": 2},
    {"위험유형1": 10, "위험유형2": 20},
    {"위험유형2": 100, "위험유형3": 200}
]
df2 = pd.DataFrame(data_list, index=["지역A", "지역B", "지역C"])

중첩 리스트로도 생성 가능:

# 중첩 리스트로 DataFrame 생성
nested_data = [
    [1, 2, 3, 4, 5],
    [7, 8, 9, 10, 11],
    [17, 18, 19, 20, 21]
]
df3 = pd.DataFrame(nested_data, 
                   index=["지역X", "지역Y", "지역Z"],
                   columns=["데이터1", "데이터2", "데이터3", "데이터4", "데이터5"])

파일에서 DataFrame 읽기

# CSV 파일 읽기
df = pd.read_csv('파일경로/data.csv', encoding='utf-8')

# Excel 파일 읽기
df = pd.read_excel('파일경로/data.xlsx', engine='openpyxl')

DataFrame 데이터 조회

열(column) 조회

# 단일 열 조회
print(df["열이름"])

# 여러 열 조회
print(df[["열1", "열2"]])

행(row) 조회

# loc: 라벨 기반 인덱싱
print(df.loc[1:])
print(df.loc[:2])
print(df.loc[0:4])  # loc는 양쪽 포함 구간

데이터 타입 변환

df['열이름'] = df['열이름'].astype(int)

loc와 iloc 메서드

loc: 라벨 인덱싱

# 단일 라벨로 조회
print(df.loc["2011/3/1", "날씨"])

# 여러 라벨로 조회
print(df.loc["2011/3/1", ["날씨", "최고온도"]])

# 슬라이스로 범위 조회
print(df.loc["2011/3/1":"2011/3/3", "날씨"])
print(df.loc["2011/3/1", "최저온도":"날씨"])

# 조건식으로 조회
print(df.loc[df["날씨"] == "맑음", :])
print(df.loc[(df["날씨"] == "맑음") | (df["날씨"] == "구름"), :])

# 함수로 조회
print(df.loc[lambda x: x["날씨"] == "맑음", :])

# 사용자 정의 함수
def filter_weather(df):
    return (df.index.str.startswith("2011")) & (df["날씨"] == "맑음")

print(df.loc[filter_weather, :])

iloc: 정수 인덱싱

iloc는 위치 기반 인덱싱으로, loc와 유사한 사용법을 가진다:

# 특정 행/열 조회
print(df.iloc[0, 0])
print(df.iloc[:, [11, 12]])

# 조건부 필터링
result = df[(df.iloc[:, 11].notnull()) & (df.iloc[:, 12].notnull())]

where와 mask 메서드

where는 조건이 False인 위치를替换하고, mask는 조건이 True인 위치를替换한다:

import pandas as pd
import numpy as np

df = pd.DataFrame({'A': [1, 2, 3, 4],
                   'B': [5, 6, 7, 8],
                   'C': [9, 10, 11, 12]})

# 조건이 False인 요소를 0으로 대체
df_where = df.where(df > 5, 0)

# 조건이 True인 요소를 0으로 대체
df_mask = df.mask(df > 5, 0)

# query 메서드로 행 필터링
df_query = df.query('A > 2')

데이터 추가 방법

1. 직접 할당

# 온도에서 단위 제거 후 정수형 변환
df.loc[:, "최고온도"] = df.loc[:, "최고온도"].str.replace("℃", "").astype("int")
df.loc[:, "최저온도"] = df.loc[:, "최저온도"].str.replace("℃", "").astype("int")

#温差 열 추가
df.loc[:, "온도차이"] = df["최고온도"] - df["최저온도"]

2. apply 메서드

apply는 Series 열을 처리하며, axis 매개변수로 행/열 방향을 지정한다:

def temp_classify(row):
    if row["최고온도"] >= 10:
        return "고온"
    else:
        return "저온"

df.loc[:, "온도분류"] = df.apply(temp_classify, axis=1)
print(df["온도분류"].value_counts())

3. assign 메서드

assign은 여러 열을 동시에 추가할 수 있으며, axis를 지정할 필요 없다:

df = df.assign(
    최고온도_증가 = lambda x: x["최고온도"] + 10,
    최저온도_증가 = lambda x: x["최저온도"] + 10
)

4. 조건부 할당

df["온도차이여부"] = ""
df.loc[df["최고온도"] - df["최저온도"] > 10, "온도차이여부"] = "차이10도이상"

통계 함수

요약 통계

# 전체 수치형 열에 대한 요약 통계
print(df.describe())

# 특정 열의 통계
print(df["최저온도"].mean(), df["최저온도"].std())

고유값 및 개수

# 고유값 조회
print(df["날씨"].unique())

# 값별 개수
print(df["날씨"].value_counts())

상관계수 및 공분산

# 공분산
print(df["최저온도"].cov(df["최고온도"]))

# 상관계수
print(df["최저온도"].corr(df["최고온도"]))

결측치 처리

결측치 확인

# 전체 결측치 확인
print(df.isnull())

# 특정 열의 결측치 확인
print(df["점수"].isnull())

# 결측치가 아닌 데이터 조회
print(df.loc[df["점수"].notnull(), :])

결측치 삭제

# 행 기준 결측치 삭제
df.dropna(axis="index", how="any", inplace=True)

# 열 기준 결측치 삭제
df.dropna(axis="columns", how="all", inplace=True)

# 특정 열만 확인하여 행 삭제
df.dropna(subset=["열A", "열C"], inplace=True)

결측치 채우기

# 특정 값으로 채우기
df.fillna({"점수": 0})

# 이전 값으로 채우기 (ffill)
df.loc[:, "이름"] = df["이름"].fillna(method="ffill")

# 이후 값으로 채우기 (bfill)
df.loc[:, "이름"] = df["이름"].fillna(method="bfill")

SettingWithCopyWarning 해결

이 경고는 체인 인덱싱으로 인해 발생한다:

# 잘못된 방식 (경고 발생)
# df[condition]["새열"] = 값

# 해결 방법 1: loc 사용
df.loc[condition, "새열"] = 값

# 해결 방법 2: 복사본 사용
df_copy = df[condition].copy()
df_copy["새열"] = 값

데이터 정렬

# Series 정렬
df["열"].sort_values(ascending=True, inplace=True)

# DataFrame 정렬 (단일 열)
df.sort_values(by="열", ascending=True, inplace=True)

# DataFrame 정렬 (여러 열)
df.sort_values(by=["열1", "열2"], ascending=True, inplace=True)

# 각 열마다 다른 정렬 순서
df.sort_values(by=["열1", "열2"], ascending=[True, False], inplace=True)

문자열 처리

Pandas의 문자열 메서드는 정규 표현식을 지원한다:

# Series의 str 속성 사용
df["열"] = df["열"].str.replace("단위", "", regex=True).astype(int)

# 여러 메서드 체이닝
df["열"] = df["열"].str.slice(0, 6).str.replace("!", ".", regex=False)

데이터 병합

merge를 이용한 테이블 결합

# 공통 열로 병합
result = pd.merge(df1, df2, on=["이름", "班级"], how="inner")

# 서로 다른 열 이름으로 병합
result = pd.merge(df1, df2, left_on="이름", right_index=True, how="inner")

인덱스의 이해

인덱스는 데이터 자체가 아니라 검색 속도를 위해 추가된 것이다:

  • 고유 인덱스: 해시 최적화로 O(1) 조회
  • 단조 증가 인덱스: 이진 검색으로 O(log n) 조회
  • 비고유 무순서 인덱스: 전체 스캔으로 O(n) 조회
# 인덱스 설정
df.set_index("날짜", inplace=True, drop=False)

# 인덱스 고유성 확인
print(df.index.is_unique)
print(df.index.is_monotonic_increasing)

데이터 추가

DataFrame 결합

# 동일 구조 DataFrame 결합
result = df1.append(df2)

# 상이한 구조 (없는 열은 NaN)
result = df1.append(df3, sort=False)

# 여러 DataFrame 결합
result = df1.append([df2, df3])

# Series 추가
series_data = pd.Series(['X0', 'X1', 'X2'], index=['A', 'B', 'C'])
result = df1.append(series_data, ignore_index=True)

# 사전으로 추가
dicts = [{'A': 1, 'B': 2}, {'A': 3, 'B': 4}]
result = df1.append(dicts, ignore_index=True, sort=False)

Excel 파일에 데이터 추가

# 기존 Excel 파일에 행 추가
existing_df = pd.read_excel('파일.xlsx')
row_count = existing_df.shape[0]

# 마지막 행 다음에 추가
existing_df.loc[row_count] = [값1, 값2, 값3]
existing_df.to_excel('파일.xlsx', index=False)

# 다른 시트에 쓰기
with pd.ExcelWriter('파일.xlsx', engine='openpyxl', mode='a') as writer:
    df.to_excel(writer, sheet_name='새시트', index=False)

여러 시트 처리

# ExcelFile로 시트 이름 확인
excel_file = pd.ExcelFile('파일.xlsx')
sheet_names = excel_file.sheet_names

# 각 시트 데이터 읽기
for sheet in sheet_names:
    df = pd.read_excel('파일.xlsx', sheet_name=sheet)
    # 처리 로직

성능 최적화 기법

데이터 타입 지정

# 읽기 시 데이터 타입 지정
dtypes = {'열1': 'int8', '열2': 'float32'}
df = pd.read_excel('파일.xlsx', dtype=dtypes)

카테고리 타입 사용

# 반복 값이 많은 열은 category 타입으로
df['열'] = df['열'].astype('category')

청크 단위 읽기

# 큰 파일을 청크로 읽기
chunksize = 10000
for chunk in pd.read_csv('파일.csv', chunksize=chunksize):
    # 각 청크 처리
    process(chunk)

필요한 열만 읽기

# 필요한 열만 선택
df = pd.read_excel('파일.xlsx', usecols=['열1', '열2', '열3'])

반복 처리 최적화

# iterrows보다 itertuples가更快
for row in df.itertuples():
    print(row.열1, row.열2)

# 가비지 컬렉션
import gc
del df
gc.collect()

7월 10일 21:12에 게시됨