4. Pandas 데이터 조작

4. Pandas 데이터 조작

이전: Pandas 기초 | 다음: Pandas 고급

개요

Pandas의 핵심 데이터 조작 기법인 필터링, 정렬, 그룹화, 병합에 대해 다룹니다. 이 기법들은 실무 데이터 분석에서 가장 많이 사용됩니다.


1. 데이터 필터링

1.1 조건 필터링

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'age': [25, 30, 35, 28, 32],
    'department': ['Sales', 'IT', 'IT', 'HR', 'Sales'],
    'salary': [50000, 60000, 70000, 55000, 65000]
})

# 단일 조건
print(df[df['age'] > 30])
print(df[df['department'] == 'IT'])

# 복합 조건 (AND: &, OR: |)
print(df[(df['age'] > 25) & (df['salary'] >= 60000)])
print(df[(df['department'] == 'IT') | (df['department'] == 'Sales')])

# NOT 조건
print(df[~(df['department'] == 'HR')])

# 범위 조건
print(df[df['age'].between(25, 30)])  # 25 <= age <= 30

1.2 isin을 이용한 필터링

# 여러 값 중 하나와 일치
departments = ['IT', 'Sales']
print(df[df['department'].isin(departments)])

# 일치하지 않는 경우
print(df[~df['department'].isin(departments)])

1.3 문자열 조건

# 문자열 포함
print(df[df['name'].str.contains('a', case=False)])

# 시작/끝 문자열
print(df[df['name'].str.startswith('A')])
print(df[df['name'].str.endswith('e')])

# 정규 표현식
print(df[df['name'].str.match(r'^[A-C]')])  # A, B, C로 시작

1.4 query 메서드

# SQL 스타일 쿼리
print(df.query('age > 30'))
print(df.query('department == "IT"'))
print(df.query('age > 25 and salary >= 60000'))

# 변수 사용
min_age = 30
print(df.query('age >= @min_age'))

# 인덱스 참조
df_indexed = df.set_index('name')
print(df_indexed.query('index == "Alice"'))

1.5 결측값 필터링

df_with_na = pd.DataFrame({
    'A': [1, 2, None, 4],
    'B': [None, 2, 3, 4],
    'C': [1, 2, 3, None]
})

# 결측값이 있는 행
print(df_with_na[df_with_na['A'].isna()])

# 결측값이 없는 행
print(df_with_na[df_with_na['A'].notna()])

# 모든 열에 결측값이 없는 행
print(df_with_na.dropna())

# 특정 열에 결측값이 없는 행
print(df_with_na.dropna(subset=['A', 'B']))

2. 정렬

2.1 값 기준 정렬

df = pd.DataFrame({
    'name': ['Charlie', 'Alice', 'Bob', 'Diana'],
    'age': [35, 25, 30, 25],
    'score': [85, 95, 75, 90]
})

# 단일 열 정렬
print(df.sort_values('age'))
print(df.sort_values('age', ascending=False))

# 여러 열 정렬
print(df.sort_values(['age', 'score']))
print(df.sort_values(['age', 'score'], ascending=[True, False]))

# 결측값 위치
df_na = df.copy()
df_na.loc[0, 'age'] = None
print(df_na.sort_values('age', na_position='first'))  # 결측값 맨 앞
print(df_na.sort_values('age', na_position='last'))   # 결측값 맨 뒤

# inplace 정렬
df.sort_values('age', inplace=True)

2.2 인덱스 정렬

df = pd.DataFrame({
    'value': [10, 20, 30, 40]
}, index=['d', 'b', 'c', 'a'])

# 인덱스 오름차순
print(df.sort_index())

# 인덱스 내림차순
print(df.sort_index(ascending=False))

# 열 인덱스 정렬
df_wide = pd.DataFrame({
    'C': [1, 2],
    'A': [3, 4],
    'B': [5, 6]
})
print(df_wide.sort_index(axis=1))

2.3 순위 매기기

df = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'score': [85, 95, 85, 90]
})

# 기본 순위 (동점 시 평균)
df['rank'] = df['score'].rank(ascending=False)
print(df)

# 순위 방법
# method='average': 평균 순위 (기본값)
# method='min': 최소 순위
# method='max': 최대 순위
# method='first': 먼저 나온 순서대로
# method='dense': 밀집 순위 (간격 없음)

df['rank_min'] = df['score'].rank(ascending=False, method='min')
df['rank_dense'] = df['score'].rank(ascending=False, method='dense')
print(df)

3. 그룹화 (GroupBy)

3.1 기본 그룹화

df = pd.DataFrame({
    'department': ['Sales', 'IT', 'IT', 'HR', 'Sales', 'HR'],
    'name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank'],
    'salary': [50000, 60000, 70000, 55000, 65000, 52000],
    'bonus': [5000, 8000, 10000, 6000, 7000, 5500]
})

# 그룹화
grouped = df.groupby('department')

# 그룹 확인
print(grouped.groups)
print(grouped.ngroups)  # 그룹 수

# 특정 그룹 가져오기
print(grouped.get_group('IT'))

3.2 집계 함수

# 단일 집계
print(df.groupby('department')['salary'].mean())
print(df.groupby('department')['salary'].sum())

# 여러 열 집계
print(df.groupby('department')[['salary', 'bonus']].mean())

# 여러 집계 함수
print(df.groupby('department')['salary'].agg(['mean', 'sum', 'count']))

# 사용자 정의 함수
print(df.groupby('department')['salary'].agg(lambda x: x.max() - x.min()))

3.3 agg 메서드

# 열마다 다른 집계
agg_result = df.groupby('department').agg({
    'salary': ['mean', 'max'],
    'bonus': 'sum',
    'name': 'count'
})
print(agg_result)

# 이름 지정
agg_result = df.groupby('department').agg(
    avg_salary=('salary', 'mean'),
    max_salary=('salary', 'max'),
    total_bonus=('bonus', 'sum'),
    employee_count=('name', 'count')
)
print(agg_result)

3.4 transform과 apply

# transform: 원본과 같은 크기의 결과
df['dept_avg_salary'] = df.groupby('department')['salary'].transform('mean')
print(df)

# 그룹 내 정규화
df['salary_normalized'] = df.groupby('department')['salary'].transform(
    lambda x: (x - x.mean()) / x.std()
)

# apply: 유연한 그룹 연산
def top_n(group, n=2, column='salary'):
    return group.nlargest(n, column)

print(df.groupby('department').apply(top_n, n=1))

3.5 여러 열로 그룹화

df = pd.DataFrame({
    'year': [2022, 2022, 2023, 2023, 2022, 2023],
    'quarter': ['Q1', 'Q2', 'Q1', 'Q2', 'Q1', 'Q1'],
    'sales': [100, 150, 120, 180, 110, 130]
})

# 다중 열 그룹화
print(df.groupby(['year', 'quarter'])['sales'].sum())

# 결과를 DataFrame으로
print(df.groupby(['year', 'quarter'])['sales'].sum().reset_index())

# 언스택
print(df.groupby(['year', 'quarter'])['sales'].sum().unstack())

3.6 필터링

df = pd.DataFrame({
    'department': ['Sales', 'IT', 'IT', 'HR', 'Sales'],
    'salary': [50000, 60000, 70000, 55000, 65000]
})

# 조건을 만족하는 그룹만 필터링
# 평균 급여가 55000 이상인 부서
result = df.groupby('department').filter(lambda x: x['salary'].mean() >= 55000)
print(result)

4. 데이터 병합

4.1 merge (SQL 스타일 조인)

# 예제 데이터
employees = pd.DataFrame({
    'emp_id': [1, 2, 3, 4],
    'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'dept_id': [10, 20, 10, 30]
})

departments = pd.DataFrame({
    'dept_id': [10, 20, 40],
    'dept_name': ['Sales', 'IT', 'Marketing']
})

# 내부 조인 (기본값)
result = pd.merge(employees, departments, on='dept_id')
print(result)

# 왼쪽 조인
result = pd.merge(employees, departments, on='dept_id', how='left')
print(result)

# 오른쪽 조인
result = pd.merge(employees, departments, on='dept_id', how='right')
print(result)

# 외부 조인
result = pd.merge(employees, departments, on='dept_id', how='outer')
print(result)

4.2 다른 열 이름으로 조인

employees = pd.DataFrame({
    'emp_id': [1, 2, 3],
    'name': ['Alice', 'Bob', 'Charlie'],
    'department': [10, 20, 10]
})

departments = pd.DataFrame({
    'id': [10, 20],
    'dept_name': ['Sales', 'IT']
})

# 다른 열 이름으로 조인
result = pd.merge(employees, departments,
                  left_on='department', right_on='id')
print(result)

4.3 인덱스 기반 조인

employees = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Charlie'],
    'salary': [50000, 60000, 70000]
}, index=[1, 2, 3])

bonuses = pd.DataFrame({
    'bonus': [5000, 8000, 10000]
}, index=[1, 2, 4])

# 인덱스로 조인
result = pd.merge(employees, bonuses, left_index=True, right_index=True, how='outer')
print(result)

# join 메서드 (인덱스 기반)
result = employees.join(bonuses, how='outer')
print(result)

4.4 concat (연결)

df1 = pd.DataFrame({
    'A': [1, 2],
    'B': [3, 4]
})

df2 = pd.DataFrame({
    'A': [5, 6],
    'B': [7, 8]
})

# 수직 연결 (행 방향)
result = pd.concat([df1, df2])
print(result)

# 인덱스 재설정
result = pd.concat([df1, df2], ignore_index=True)
print(result)

# 수평 연결 (열 방향)
result = pd.concat([df1, df2], axis=1)
print(result)

# 키 추가
result = pd.concat([df1, df2], keys=['first', 'second'])
print(result)

4.5 append (행 추가) - deprecated in pandas 2.0

# concat 사용 권장
new_row = pd.DataFrame({'A': [9], 'B': [10]})
result = pd.concat([df1, new_row], ignore_index=True)

5. 피벗과 멜트

5.1 pivot

df = pd.DataFrame({
    'date': ['2023-01', '2023-01', '2023-02', '2023-02'],
    'city': ['Seoul', 'Busan', 'Seoul', 'Busan'],
    'sales': [100, 80, 120, 90]
})

# 피벗 테이블
pivot = df.pivot(index='date', columns='city', values='sales')
print(pivot)
#          Busan  Seoul
# date
# 2023-01     80    100
# 2023-02     90    120

5.2 pivot_table

df = pd.DataFrame({
    'date': ['2023-01', '2023-01', '2023-01', '2023-02'],
    'city': ['Seoul', 'Seoul', 'Busan', 'Seoul'],
    'category': ['A', 'B', 'A', 'A'],
    'sales': [100, 150, 80, 120]
})

# 집계 함수 적용
pivot = pd.pivot_table(df, values='sales', index='date',
                       columns='city', aggfunc='sum')
print(pivot)

# 여러 집계 함수
pivot = pd.pivot_table(df, values='sales', index='date',
                       columns='city', aggfunc=['sum', 'mean'])
print(pivot)

# 여러 인덱스
pivot = pd.pivot_table(df, values='sales',
                       index=['date', 'category'],
                       columns='city',
                       aggfunc='sum',
                       fill_value=0)
print(pivot)

# 마진 추가
pivot = pd.pivot_table(df, values='sales', index='date',
                       columns='city', aggfunc='sum', margins=True)
print(pivot)

5.3 melt (언피벗)

df_wide = pd.DataFrame({
    'name': ['Alice', 'Bob'],
    'math': [90, 85],
    'english': [80, 95],
    'science': [85, 90]
})

# Wide → Long 변환
df_long = pd.melt(df_wide,
                  id_vars=['name'],
                  value_vars=['math', 'english', 'science'],
                  var_name='subject',
                  value_name='score')
print(df_long)
#     name  subject  score
# 0  Alice     math     90
# 1    Bob     math     85
# 2  Alice  english     80
# 3    Bob  english     95
# 4  Alice  science     85
# 5    Bob  science     90

5.4 stack과 unstack

df = pd.DataFrame({
    'A': [1, 2],
    'B': [3, 4]
}, index=['x', 'y'])

# stack: 열을 행으로
stacked = df.stack()
print(stacked)
# x  A    1
#    B    3
# y  A    2
#    B    4

# unstack: 행을 열로
unstacked = stacked.unstack()
print(unstacked)

6. 중복 처리

df = pd.DataFrame({
    'A': [1, 1, 2, 2, 3],
    'B': ['a', 'a', 'b', 'c', 'c'],
    'C': [10, 10, 20, 30, 40]
})

# 중복 확인
print(df.duplicated())
print(df.duplicated(subset=['A', 'B']))
print(df[df.duplicated(keep=False)])  # 모든 중복 행

# 중복 개수
print(df.duplicated().sum())

# 중복 제거
print(df.drop_duplicates())
print(df.drop_duplicates(subset=['A']))
print(df.drop_duplicates(subset=['A'], keep='last'))  # 마지막 유지
print(df.drop_duplicates(subset=['A'], keep=False))   # 모든 중복 제거

7. 교차 테이블

df = pd.DataFrame({
    'gender': ['M', 'F', 'M', 'F', 'M', 'F'],
    'department': ['Sales', 'IT', 'IT', 'Sales', 'HR', 'IT'],
    'salary': [50000, 60000, 55000, 58000, 52000, 62000]
})

# 빈도 교차 테이블
ct = pd.crosstab(df['gender'], df['department'])
print(ct)

# 마진 추가
ct = pd.crosstab(df['gender'], df['department'], margins=True)
print(ct)

# 정규화
ct = pd.crosstab(df['gender'], df['department'], normalize=True)
print(ct)

# 집계 함수 적용
ct = pd.crosstab(df['gender'], df['department'],
                 values=df['salary'], aggfunc='mean')
print(ct)

연습 문제

문제 1: 그룹별 통계

부서별 평균 급여와 직원 수를 구하세요.

df = pd.DataFrame({
    'department': ['Sales', 'IT', 'IT', 'HR', 'Sales'],
    'name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'salary': [50000, 60000, 70000, 55000, 65000]
})

# 풀이
result = df.groupby('department').agg(
    avg_salary=('salary', 'mean'),
    count=('name', 'count')
)
print(result)

문제 2: 데이터 병합

두 DataFrame을 조인하여 직원의 부서명을 포함하세요.

employees = pd.DataFrame({
    'emp_id': [1, 2, 3],
    'name': ['Alice', 'Bob', 'Charlie'],
    'dept_id': [10, 20, 10]
})

departments = pd.DataFrame({
    'dept_id': [10, 20],
    'dept_name': ['Sales', 'IT']
})

# 풀이
result = pd.merge(employees, departments, on='dept_id')
print(result)

문제 3: 피벗 테이블

월별, 카테고리별 매출 합계를 피벗 테이블로 만드세요.

sales = pd.DataFrame({
    'month': ['Jan', 'Jan', 'Feb', 'Feb', 'Jan', 'Feb'],
    'category': ['A', 'B', 'A', 'B', 'A', 'A'],
    'amount': [100, 150, 120, 180, 110, 130]
})

# 풀이
pivot = pd.pivot_table(sales, values='amount',
                       index='month', columns='category',
                       aggfunc='sum')
print(pivot)

요약

기능 함수/메서드
필터링 df[condition], query(), isin()
정렬 sort_values(), sort_index(), rank()
그룹화 groupby(), agg(), transform(), apply()
병합 merge(), join(), concat()
피벗 pivot(), pivot_table(), melt(), stack(), unstack()
중복 duplicated(), drop_duplicates()
교차표 crosstab()
to navigate between lessons