포스트

[Data] Python 데이터 처리 입문 — pandas로 엑셀 자동화를 넘어서

왜 데이터 엔지니어에게 Python인가? pandas 기초(DataFrame, 읽기/쓰기, 필터·그룹·병합)부터 Polars 소개, 그리고 수작업 엑셀을 코드로 대체하는 실전 패턴까지.

[Data] Python 데이터 처리 입문 — pandas로 엑셀 자동화를 넘어서

[NextX_Data_Solution] · 주식회사 넥스트엑스(NEXT X) 정식 데이터 솔루션

“엑셀로 충분하지 않나요?” — 데이터가 1만 행을 넘고, 매월 같은 작업을 반복하고, 사람마다 다른 결과가 나온다면 코드로 전환할 타이밍입니다. 이 글에서는 Python과 pandas를 처음 접하는 실무자를 위해, 엑셀에서 코드로 넘어가는 전체 흐름을 다룹니다.


1. 왜 Python인가 – 엑셀의 한계, Python의 강점

엑셀이 한계에 부딪히는 순간

엑셀은 소규모 데이터 정리에는 최고의 도구입니다. 하지만 다음 상황이 반복된다면 엑셀만으로는 부족합니다.

상황엑셀에서의 문제Python이 해결하는 방식
10만 행 이상 데이터느려지고, 100만 행 한계메모리가 허용하는 한 무제한
매월 반복 작업사람이 매번 클릭·복붙스크립트 한 번 실행으로 끝
여러 파일 병합수작업 복사-붙여넣기pd.concat() 한 줄
결과 재현성누가 했느냐에 따라 다름코드 = 문서 = 동일 결과
데이터베이스 연동불가능 또는 매우 제한적read_sql() 한 줄로 연결
자동 스케줄 실행불가cron / Task Scheduler 연동

Python이 데이터 엔지니어의 표준이 된 이유

  1. 오픈소스 — 무료이며, 패키지 생태계가 방대합니다.
  2. pandas — 표 형태 데이터를 다루는 사실상의 표준 라이브러리.
  3. 자동화 친화적 — 스크립트를 짜면 매일 같은 시간에 실행할 수 있습니다.
  4. 확장성 — 데이터가 커지면 Polars, Spark로 코드 한 줄만 바꿔 전환 가능.
  5. 재현성 — 코드가 곧 문서이므로 “누가 해도 같은 결과”가 보장됩니다.

Python 엑셀 자동화(openpyxl)에 대한 기초는 Python 엑셀 자동화 가이드를 먼저 참고하세요.


2. pandas 핵심 개념 – DataFrame과 Series

pandas에는 두 가지 핵심 자료구조가 있습니다.

개념엑셀 대응설명
DataFrame시트 전체 (행 + 열)2차원 표 형태 데이터
Series하나의 열1차원 배열 (이름 있는 리스트)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pandas as pd

# DataFrame 직접 생성 — 엑셀 시트를 코드로 만드는 것과 같습니다
df = pd.DataFrame({
    "이름": ["김민수", "이수진", "박철호", "정다은"],
    "부서": ["영업", "개발", "영업", "인사"],
    "급여": [3200, 4100, 3500, 3800],
    "입사연도": [2021, 2019, 2022, 2020],
})

print(df)
#     이름  부서    급여  입사연도
# 0  김민수  영업  3200    2021
# 1  이수진  개발  4100    2019
# 2  박철호  영업  3500    2022
# 3  정다은  인사  3800    2020
1
2
3
4
# Series — DataFrame에서 열 하나를 꺼내면 Series가 됩니다
salary_series = df["급여"]
print(type(salary_series))  # <class 'pandas.core.series.Series'>
print(salary_series.mean()) # 3650.0

엑셀에서 시트 전체가 DataFrame, 한 열이 Series라고 기억하면 됩니다.

graph LR
    A["엑셀 파일<br/>.xlsx"] -->|"pd.read_excel()"| B["DataFrame"]
    B --> C["Series<br/>(열 단위)"]
    B --> D["필터 · 그룹 · 병합"]
    D --> E["to_excel() / to_csv()"]

3. 데이터 읽기 – CSV, Excel, JSON, SQL

설치

1
2
# pandas 설치 (Excel 지원 포함)
pip install pandas openpyxl xlsxwriter

CSV 읽기

1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd

# 기본 CSV 읽기
df = pd.read_csv("매출현황.csv", encoding="utf-8-sig")

# 인코딩이 다를 때 (Windows 엑셀에서 저장한 CSV)
df = pd.read_csv("매출현황.csv", encoding="cp949")

# 특정 열만 읽기 (메모리 절약)
df = pd.read_csv("매출현황.csv", usecols=["거래일", "금액", "고객명"])

# 날짜 열을 자동 파싱
df = pd.read_csv("매출현황.csv", parse_dates=["거래일"])

Excel 읽기

1
2
3
4
5
6
7
8
9
10
# 엑셀 파일 읽기 — 시트 이름 지정 가능
df = pd.read_excel("급여대장_2026_05.xlsx", sheet_name="5월")

# 여러 시트를 한 번에 읽기 (딕셔너리로 반환)
all_sheets = pd.read_excel("급여대장_2026.xlsx", sheet_name=None)
for sheet_name, sheet_df in all_sheets.items():
    print(f"시트: {sheet_name}, 행수: {len(sheet_df)}")

# 특정 범위만 읽기 (A1:D100에 해당)
df = pd.read_excel("급여대장.xlsx", usecols="A:D", nrows=100)

JSON 읽기

1
2
3
4
5
6
7
8
9
10
# API 응답이나 로그 파일에서 JSON 읽기
df = pd.read_json("api_response.json")

# 중첩(nested) JSON — normalize 사용
import json

with open("nested_data.json", "r", encoding="utf-8") as f:
    data = json.load(f)

df = pd.json_normalize(data["results"])

SQL 읽기

1
2
3
4
5
6
7
8
9
10
11
12
import sqlite3

# SQLite 연결 예시
conn = sqlite3.connect("company.db")
df = pd.read_sql("SELECT * FROM employees WHERE dept = '영업'", conn)
conn.close()

# PostgreSQL 연결 예시 (sqlalchemy 필요)
from sqlalchemy import create_engine

engine = create_engine("postgresql://user:pass@localhost:5432/mydb")
df = pd.read_sql("SELECT * FROM sales WHERE year = 2026", engine)

데이터 읽기 단계에서 인코딩 문제가 가장 흔합니다. Windows 엑셀에서 저장한 CSV는 cp949 또는 euc-kr, 그 외 대부분은 utf-8입니다.

함수용도필수 라이브러리
read_csv()CSV / TSV(내장)
read_excel().xlsx / .xlsopenpyxl
read_json()JSON 파일·문자열(내장)
read_sql()DB 쿼리 결과sqlalchemy / DB 드라이버
read_parquet()Parquet (대용량)pyarrow

4. 데이터 탐색 – 읽은 뒤 가장 먼저 할 일

데이터를 읽은 직후에는 구조를 파악해야 합니다. 엑셀에서 눈으로 훑는 것을 코드로 하는 단계입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 예시 데이터 생성
df = pd.DataFrame({
    "이름": ["김민수", "이수진", "박철호", "정다은", "최유리"],
    "부서": ["영업", "개발", "영업", "인사", "개발"],
    "급여": [3200, 4100, 3500, 3800, 4500],
    "입사일": pd.to_datetime(["2021-03-01", "2019-06-15", "2022-01-10",
                              "2020-09-01", "2018-11-20"]),
    "평가등급": ["B", "A", "B", "A", "S"],
})

# 상위 5행 미리보기 — 엑셀에서 스크롤 없이 첫 줄들만 보는 것
print(df.head())

# 행·열 개수 확인
print(df.shape)   # (5, 5) → 5행 5열

# 열 이름·타입·결측 개수 한눈에 보기
print(df.info())
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 5 entries, 0 to 4
# Data columns (total 5 columns):
#  #   Column  Non-Null Count  Dtype
# ---  ------  --------------  -----
#  0   이름      5 non-null      object
#  1   부서      5 non-null      object
#  2   급여      5 non-null      int64
#  3   입사일     5 non-null      datetime64[ns]
#  4   평가등급    5 non-null      object

# 수치 열 통계 요약 — 엑셀의 요약 통계를 한 줄로
print(df.describe())
#               급여
# count     5.000000
# mean   3820.000000
# std     502.991027
# min    3200.000000
# 25%    3500.000000
# 50%    3800.000000
# 75%    4100.000000
# max    4500.000000

# 열별 데이터 타입 확인
print(df.dtypes)

탐색 체크리스트

  • shape — 행·열 개수가 예상과 맞는가?
  • info() — 결측(Non-Null Count)이 있는 열은?
  • dtypes — 숫자여야 하는 열이 object(문자열)로 잡혔는가?
  • describe() — min/max에 이상치가 보이는가?

데이터 품질 점검의 상세 기준은 데이터 클렌징 실전을 참고하세요.


5. 필터링 – 원하는 행만 골라내기

Boolean Indexing (불리언 인덱싱)

1
2
3
4
5
6
7
8
9
10
11
# 급여가 3500 이상인 직원만
high_salary = df[df["급여"] >= 3500]
print(high_salary)

# 영업부이면서 급여 3500 이상 — 조건을 &(AND)로 연결
filtered = df[(df["부서"] == "영업") & (df["급여"] >= 3500)]
print(filtered)

# 개발부이거나 인사부 — 조건을 |(OR)로 연결
dev_or_hr = df[(df["부서"] == "개발") | (df["부서"] == "인사")]
print(dev_or_hr)

조건을 연결할 때 반드시 각 조건을 괄호로 감싸야 합니다. df[df["부서"] == "영업" & df["급여"] >= 3500]은 에러가 납니다.

query() 메서드

1
2
3
4
5
6
7
8
# SQL의 WHERE절과 유사한 문법
result = df.query("부서 == '영업' and 급여 >= 3500")
print(result)

# 변수를 참조할 때는 @ 접두사
min_salary = 4000
result = df.query("급여 >= @min_salary")
print(result)

isin() – 여러 값 중 하나에 해당하는지

1
2
3
4
5
6
7
8
# 부서가 "영업" 또는 "개발"인 행
target_depts = ["영업", "개발"]
result = df[df["부서"].isin(target_depts)]
print(result)

# 반대: 해당하지 않는 행
result = df[~df["부서"].isin(target_depts)]
print(result)
방법장점적합한 상황
Boolean Indexing가장 범용적복잡한 조건 조합
query()SQL과 유사해 읽기 쉬움단순 조건, 가독성 우선
isin()목록 기반 필터에 간결“A 또는 B 또는 C” 패턴

6. 집계 – groupby, agg, pivot_table

groupby + agg

엑셀의 피벗 테이블을 코드로 구현하는 가장 기본적인 방법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 부서별 급여 평균
dept_avg = df.groupby("부서")["급여"].mean()
print(dept_avg)
# 부서
# 개발    4300.0
# 영업    3350.0
# 인사    3800.0

# 부서별 여러 통계를 한 번에
dept_stats = df.groupby("부서")["급여"].agg(["mean", "sum", "count", "max"])
print(dept_stats)
#        mean   sum  count   max
# 부서
# 개발  4300  8600      2  4500
# 영업  3350  6700      2  3500
# 인사  3800  3800      1  3800
1
2
3
4
5
6
7
8
# 여러 열에 서로 다른 집계 적용
summary = df.groupby("부서").agg(
    평균급여=("급여", "mean"),
    인원수=("이름", "count"),
    최고급여=("급여", "max"),
    최근입사=("입사일", "max"),
)
print(summary)

pivot_table – 엑셀 피벗과 1:1 대응

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 예시: 부서·평가등급별 급여 평균
pivot = df.pivot_table(
    values="급여",
    index="부서",
    columns="평가등급",
    aggfunc="mean",
    fill_value=0,    # 비어있는 칸은 0으로
)
print(pivot)
# 평가등급     A       B       S
# 부서
# 개발     4100.0     0   4500.0
# 영업        0    3350.0     0
# 인사     3800.0     0       0
1
2
3
4
5
6
7
# 집계 함수를 여러 개 동시에
pivot_multi = df.pivot_table(
    values="급여",
    index="부서",
    aggfunc=["mean", "sum", "count"],
)
print(pivot_multi)
graph TD
    A["원본 DataFrame"] --> B["groupby('부서')"]
    B --> C["agg(mean, sum, count)"]
    C --> D["부서별 요약 테이블"]
    A --> E["pivot_table()"]
    E --> F["부서 x 평가등급<br/>교차 집계"]

7. 병합 – merge와 concat

merge – SQL JOIN과 같은 동작

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 직원 테이블
employees = pd.DataFrame({
    "사번": ["E001", "E002", "E003", "E004"],
    "이름": ["김민수", "이수진", "박철호", "정다은"],
    "부서코드": ["D10", "D20", "D10", "D30"],
})

# 부서 마스터 테이블
departments = pd.DataFrame({
    "부서코드": ["D10", "D20", "D30", "D40"],
    "부서명": ["영업부", "개발부", "인사부", "재무부"],
    "부서장": ["최부장", "김팀장", "이과장", "박차장"],
})

# INNER JOIN — 양쪽 모두에 있는 행만
merged = pd.merge(employees, departments, on="부서코드", how="inner")
print(merged)
#    사번   이름 부서코드  부서명   부서장
# 0  E001  김민수   D10  영업부  최부장
# 1  E002  이수진   D20  개발부  김팀장
# 2  E003  박철호   D10  영업부  최부장
# 3  E004  정다은   D30  인사부  이과장

# LEFT JOIN — 왼쪽 테이블 기준 (직원은 모두 유지)
merged_left = pd.merge(employees, departments, on="부서코드", how="left")

# 열 이름이 다를 때
merged = pd.merge(
    employees, departments,
    left_on="부서코드", right_on="부서코드",
    how="left",
)
merge how 옵션SQL 대응설명
innerINNER JOIN양쪽 모두 매칭되는 행만
leftLEFT JOIN왼쪽 전체 + 오른쪽 매칭
rightRIGHT JOIN오른쪽 전체 + 왼쪽 매칭
outerFULL OUTER JOIN양쪽 전체 (매칭 안 되면 NaN)

SQL JOIN에 대한 심화 내용은 SQL 실전 심화를 참고하세요.

concat – 같은 구조의 데이터를 위아래로 쌓기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5월, 6월 급여 데이터를 위아래로 결합
may_df = pd.read_excel("급여_2026_05.xlsx")
jun_df = pd.read_excel("급여_2026_06.xlsx")

# 위아래로 쌓기 (UNION ALL)
combined = pd.concat([may_df, jun_df], ignore_index=True)
print(f"5월: {len(may_df)}행, 6월: {len(jun_df)}행, 합계: {len(combined)}")

# 여러 파일을 한꺼번에 합치기
import glob

files = glob.glob("급여_2026_*.xlsx")
dfs = [pd.read_excel(f) for f in files]
all_data = pd.concat(dfs, ignore_index=True)
graph LR
    subgraph "merge (좌우 결합)"
        A1["직원 테이블"] --- M["merge()"]
        A2["부서 테이블"] --- M
        M --> R1["결합된 테이블"]
    end
    subgraph "concat (상하 결합)"
        B1["5월 데이터"] --- C["concat()"]
        B2["6월 데이터"] --- C
        C --> R2["전체 데이터"]
    end

8. 데이터 변환 – apply, map, replace, fillna

apply – 열 또는 행에 함수 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 급여에서 세금(3.3%) 공제 후 실수령액 계산
df["실수령액"] = df["급여"].apply(lambda x: int(x * 0.967))
print(df[["이름", "급여", "실수령액"]])
#     이름    급여  실수령액
# 0  김민수  3200    3094
# 1  이수진  4100    3964
# 2  박철호  3500    3384
# 3  정다은  3800    3674
# 4  최유리  4500    4351

# 직접 만든 함수를 적용
def salary_grade(salary):
    """급여 구간별 등급 부여"""
    if salary >= 4500:
        return ""
    elif salary >= 3500:
        return ""
    else:
        return ""

df["급여등급"] = df["급여"].apply(salary_grade)
print(df[["이름", "급여", "급여등급"]])

map – 값 매핑 (사전 기반 변환)

1
2
3
4
5
6
7
8
# 부서 코드를 부서명으로 변환
dept_map = {"영업": "Sales", "개발": "Development", "인사": "HR"}
df["부서영문"] = df["부서"].map(dept_map)
print(df[["이름", "부서", "부서영문"]])

# 평가등급을 점수로 변환
grade_to_score = {"S": 100, "A": 90, "B": 80, "C": 70}
df["평가점수"] = df["평가등급"].map(grade_to_score)

replace – 특정 값 치환

1
2
3
4
5
6
7
8
9
# 표기 통일: 여러 형태의 부서명을 하나로
df["부서"] = df["부서"].replace({
    "영업부": "영업",
    "영업팀": "영업",
    "Sales": "영업",
})

# 정규식으로 치환 — "(주)", "주식회사" 제거
df["회사명"] = df["회사명"].str.replace(r"(\(주\)|주식회사)\s*", "", regex=True)

표기 통일과 데이터 정제의 체계적인 방법론은 데이터 클렌징 실전엑셀 함수로 5분 정리를 참고하세요.

fillna – 결측값 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 결측값을 특정 값으로 채우기
df["비고"] = df["비고"].fillna("없음")

# 수치 열은 평균으로 채우기
df["급여"] = df["급여"].fillna(df["급여"].mean())

# 바로 앞 값으로 채우기 (시계열 데이터에 유용)
df["매출"] = df["매출"].ffill()

# 결측값 확인
print(df.isnull().sum())
# 이름      0
# 부서      0
# 급여      0
# 비고      2  ← 결측 2건
함수용도엑셀 대응
apply()사용자 정의 함수 적용수식 열 추가
map()딕셔너리 기반 값 매핑VLOOKUP
replace()특정 값 치환찾기-바꾸기
fillna()결측값 채우기IF(ISBLANK(…))
str.replace()문자열 패턴 치환SUBSTITUTE
astype()데이터 타입 변환VALUE(), TEXT()

9. 데이터 쓰기 – to_csv, to_excel, to_sql

처리가 끝난 데이터를 파일이나 데이터베이스로 저장합니다.

CSV로 저장

1
2
3
4
5
# 기본 저장 — 인덱스 제외 (엑셀에서 열기 편하게)
df.to_csv("결과_직원현황.csv", index=False, encoding="utf-8-sig")

# utf-8-sig를 쓰면 엑셀에서 한글이 깨지지 않습니다
# (BOM이 포함되어 엑셀이 인코딩을 자동 감지)

Excel로 저장

1
2
3
4
5
6
7
8
9
10
11
12
# 기본 저장
df.to_excel("결과_직원현황.xlsx", index=False, sheet_name="직원")

# 여러 시트에 나눠 저장
with pd.ExcelWriter("연간보고서.xlsx", engine="xlsxwriter") as writer:
    dept_summary.to_excel(writer, sheet_name="부서별요약")
    monthly_data.to_excel(writer, sheet_name="월별데이터")
    raw_data.to_excel(writer, sheet_name="원본", index=False)

# 기존 엑셀 파일에 시트 추가 (덮어쓰기 아님)
with pd.ExcelWriter("기존파일.xlsx", engine="openpyxl", mode="a") as writer:
    new_data.to_excel(writer, sheet_name="추가시트", index=False)

SQL 데이터베이스에 저장

1
2
3
4
5
6
7
8
9
10
11
import sqlite3

conn = sqlite3.connect("company.db")

# 테이블이 없으면 생성, 있으면 교체
df.to_sql("employees", conn, if_exists="replace", index=False)

# 기존 테이블에 데이터 추가
df.to_sql("employees", conn, if_exists="append", index=False)

conn.close()
함수포맷주요 옵션
to_csv()CSVencoding, index, sep
to_excel().xlsxsheet_name, engine
to_sql()DB 테이블if_exists, dtype
to_parquet()Parquetcompression, engine
to_json()JSONorient, force_ascii

데이터 파이프라인에서 쓰기 단계(Load)의 설계 원칙은 데이터 파이프라인이란?을 참고하세요.


10. pandas vs Polars – 대용량 데이터의 새로운 대안

pandas는 강력하지만, 100만 행 이상이면 속도와 메모리 한계가 느껴집니다. 이때 고려할 대안이 Polars입니다.

Polars란

  • Rust로 작성된 고속 DataFrame 라이브러리
  • Apache Arrow 기반 메모리 모델 (pandas보다 효율적)
  • Lazy evaluation — 실행 전 최적화 후 한 번에 처리
  • 멀티코어 병렬 처리 기본 지원

핵심 비교

항목pandasPolars
구현 언어C + PythonRust
메모리 효율보통높음 (Arrow 기반)
멀티코어기본 단일 스레드기본 병렬
Lazy 평가없음있음 (최적화)
API 성숙도매우 높음 (10년+)빠르게 성장 중
생태계방대 (호환 라이브러리 다수)성장 중
학습 곡선자료 풍부pandas 경험 필요
대용량 처리100만 행 이상 느려짐1억 행도 쾌적

Polars 코드 맛보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import polars as pl

# CSV 읽기 — pandas와 거의 동일한 인터페이스
df = pl.read_csv("매출현황.csv")

# 필터링
high_sales = df.filter(pl.col("금액") >= 1000000)

# 그룹 집계
summary = df.group_by("부서").agg([
    pl.col("금액").sum().alias("총매출"),
    pl.col("금액").mean().alias("평균매출"),
    pl.col("거래건수").count().alias("건수"),
])

# Lazy 평가 — 큰 데이터에서 성능 차이가 극적
result = (
    pl.scan_csv("대용량_10GB.csv")    # 파일을 즉시 읽지 않음
    .filter(pl.col("연도") == 2026)    # 조건을 모아둠
    .group_by("부서")                   # 집계를 모아둠
    .agg(pl.col("매출").sum())          # 모아둔 연산을
    .collect()                          # 여기서 한 번에 최적화 후 실행
)

pandas와 Polars 코드 비교

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ── pandas ─────────────────────────────
import pandas as pd

df_pd = pd.read_csv("sales.csv")
result_pd = (
    df_pd[df_pd["year"] == 2026]
    .groupby("dept")["amount"]
    .sum()
    .reset_index()
)

# ── Polars ─────────────────────────────
import polars as pl

result_pl = (
    pl.scan_csv("sales.csv")
    .filter(pl.col("year") == 2026)
    .group_by("dept")
    .agg(pl.col("amount").sum())
    .collect()
)

언제 무엇을 쓸까

graph TD
    A["데이터 처리 필요"] --> B{"데이터 크기?"}
    B -->|"100만 행 이하"| C["pandas 추천"]
    B -->|"100만 ~ 1억 행"| D["Polars 추천"]
    B -->|"1억 행 이상 / 분산"| E["Spark 추천"]
    C --> F["대부분의 실무에서 충분"]
    D --> G["단일 머신에서 최고 성능"]
    E --> H["클러스터 필요"]

처음 배운다면 pandas부터 시작하세요. 문서·레퍼런스·커뮤니티가 압도적으로 많고, Polars의 API도 pandas를 잘 아는 사람이 빠르게 적응할 수 있도록 설계되어 있습니다.


11. 실전 예제 – “5월/6월 급여 엑셀에서 통합 명부 자동 생성”

넥스트엑스 파트너스 매칭 매니저 업무에서 실제로 발생하는 시나리오입니다. 매월 파트너사별 급여 엑셀을 받아 통합 명부를 만드는 작업을, 수작업에서 코드로 전환합니다.

시나리오

  • 파트너사 3곳에서 매월 급여 엑셀을 받습니다.
  • 각 파일의 열 이름과 형식이 조금씩 다릅니다.
  • 최종 산출물: 통합 명부 엑셀 + 파트너사별 요약 시트

수작업 vs 코드 비교

단계수작업 (엑셀)Python 코드
파일 열기3개 파일 각각 열기glob + read_excel
열 이름 통일수동으로 열 이름 변경rename()
데이터 합치기복사-붙여넣기concat()
중복 확인COUNTIF로 확인duplicated()
결측값 처리눈으로 확인 후 수정fillna()
요약 테이블피벗 테이블 수동 생성groupby().agg()
결과 저장다른 이름으로 저장to_excel()
소요 시간약 30~40분약 10초 (최초 작성 후)

전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"""
급여 통합 명부 자동 생성 스크립트
- 파트너사별 급여 엑셀을 읽어 통합 명부를 생성합니다.
- 실행: python merge_salary.py
"""
import glob
import pandas as pd
from datetime import datetime

# ── 1단계: 파일 읽기 ──────────────────────
# 폴더에서 급여 엑셀 파일을 모두 찾습니다
files = glob.glob("input/급여_2026_*.xlsx")
print(f"발견된 파일: {len(files)}")

dfs = []
for filepath in files:
    df = pd.read_excel(filepath)

    # 파일명에서 월 정보 추출 (급여_2026_05.xlsx → "05")
    month = filepath.split("_")[-1].replace(".xlsx", "")
    df["기준월"] = month

    # 파일명에서 파트너사 정보 추출 (있는 경우)
    dfs.append(df)
    print(f"  - {filepath}: {len(df)}행 읽음")

# ── 2단계: 열 이름 통일 ───────────────────
# 파트너사마다 열 이름이 다를 수 있으므로 표준 이름으로 변환
column_map = {
    # 파트너사 A 형식
    "성명": "이름",
    "기본급(만원)": "기본급",
    "수당(만원)": "수당",
    # 파트너사 B 형식
    "직원명": "이름",
    "base_salary": "기본급",
    "allowance": "수당",
    # 파트너사 C 형식
    "name": "이름",
    "salary": "기본급",
    "bonus": "수당",
}

for i, df in enumerate(dfs):
    dfs[i] = df.rename(columns=column_map)

# ── 3단계: 데이터 합치기 ──────────────────
combined = pd.concat(dfs, ignore_index=True)
print(f"\n전체 합산: {len(combined)}")

# ── 4단계: 데이터 정제 ────────────────────
# 이름 공백 제거
combined["이름"] = combined["이름"].str.strip()

# 결측값 처리 — 수당이 없으면 0으로
combined["수당"] = combined["수당"].fillna(0)

# 총급여 계산
combined["총급여"] = combined["기본급"] + combined["수당"]

# 중복 확인 (같은 월에 같은 이름이 두 번 있으면 경고)
duplicates = combined[combined.duplicated(subset=["이름", "기준월"], keep=False)]
if len(duplicates) > 0:
    print(f"\n[경고] 중복 의심 {len(duplicates)}건:")
    print(duplicates[["이름", "기준월", "파트너사"]])

# ── 5단계: 요약 통계 ─────────────────────
# 파트너사별·월별 요약
summary = combined.groupby(["파트너사", "기준월"]).agg(
    인원수=("이름", "count"),
    총급여합계=("총급여", "sum"),
    평균급여=("총급여", "mean"),
    최고급여=("총급여", "max"),
).reset_index()

# 전체 요약
total_summary = pd.DataFrame({
    "항목": ["전체 인원", "전체 총급여", "전체 평균급여"],
    "": [
        len(combined),
        combined["총급여"].sum(),
        round(combined["총급여"].mean(), 1),
    ],
})

# ── 6단계: 결과 저장 ─────────────────────
today = datetime.now().strftime("%Y%m%d")
output_path = f"output/통합명부_{today}.xlsx"

with pd.ExcelWriter(output_path, engine="xlsxwriter") as writer:
    # 시트 1: 전체 명부
    combined.to_excel(writer, sheet_name="통합명부", index=False)

    # 시트 2: 파트너사별 요약
    summary.to_excel(writer, sheet_name="파트너사별요약", index=False)

    # 시트 3: 전체 요약
    total_summary.to_excel(writer, sheet_name="전체요약", index=False)

print(f"\n저장 완료: {output_path}")
print(f"  - 통합명부: {len(combined)}")
print(f"  - 파트너사별요약: {len(summary)}")

처리 흐름 다이어그램

graph TD
    A["파트너사 A<br/>급여_2026_05.xlsx"] --> D["glob.glob()로<br/>파일 목록 수집"]
    B["파트너사 B<br/>급여_2026_05.xlsx"] --> D
    C["파트너사 C<br/>급여_2026_05.xlsx"] --> D
    D --> E["read_excel()<br/>+ 열 이름 통일"]
    E --> F["concat()<br/>데이터 합치기"]
    F --> G["정제<br/>strip · fillna · 중복확인"]
    G --> H["groupby().agg()<br/>요약 통계"]
    H --> I["ExcelWriter<br/>멀티시트 저장"]
    I --> J["통합명부_20260724.xlsx"]

이 코드가 수작업보다 나은 이유

  1. 재현 가능 — 누가 돌려도 같은 결과
  2. 10초 — 최초 작성 후 매월 실행만 하면 됨
  3. 자동 검증 — 중복 감지가 코드에 포함
  4. 이력 관리 — 코드를 Git에 넣으면 변경 이력 추적 가능
  5. 확장 가능 — 파트너사가 늘어도 column_map만 추가

정리 – pandas 핵심 명령 치트시트

작업pandas 코드엑셀 대응
파일 읽기pd.read_csv() / pd.read_excel()파일 열기
미리보기df.head()첫 몇 행 스크롤
구조 확인df.info() / df.shape행·열 개수 세기
통계 요약df.describe()평균·합계 수식
필터링df[df["열"] > 값]필터(자동 필터)
정렬df.sort_values("열")정렬
그룹 집계df.groupby("열").agg()피벗 테이블
병합(좌우)pd.merge(df1, df2)VLOOKUP / INDEX-MATCH
결합(상하)pd.concat([df1, df2])복사-붙여넣기
열 변환df["열"].apply(함수)새 열에 수식
값 치환df["열"].replace()찾기-바꾸기
결측 처리df.fillna()IF(ISBLANK())
저장df.to_csv() / df.to_excel()다른 이름으로 저장

다음 단계

이 글에서 다룬 pandas 기초를 바탕으로, 다음 글들로 실무를 확장할 수 있습니다.

방향관련 글
데이터 품질 관리데이터 클렌징 실전
엑셀 함수로 소규모 정리엑셀 함수로 5분 정리
파이프라인 설계데이터 파이프라인이란?
엑셀 자동화 (openpyxl)Python 엑셀 자동화 가이드
SQL과의 연계SQL 실전 심화

pandas를 배우면 엑셀에서 30분 걸리던 반복 작업이 10초로 줄어듭니다. 그리고 그 코드가 문서가 되고, 자산이 됩니다.


NEXT X R&D · Data Engineering

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.