파이썬 데이터 분석 #2 데이터 불러오기: CSV, 엑셀, 그리고 첫 탐색

7 분 소요

1편에서 환경을 만들고 DataFrame이 무엇인지 봤습니다. 이번 편에서는 분석의 실제 출발점인 파일 불러오기를 다루겠습니다. 강좌용 데이터는 항상 깨끗하지만, 실무에서 받는 파일은 다릅니다. 인코딩이 다르고, 구분자가 탭이고, 첫 두 줄이 제목 장식이고, 숫자 컬럼에 콤마가 섞여 있습니다. 이 글에서는 그런 파일을 pandas로 읽는 법과, 읽은 직후 반드시 거쳐야 하는 확인 루틴을 잡아두겠습니다.

분석은 남이 만든 파일에서 시작합니다 #

데이터 분석 업무의 첫 입력은 대부분 내가 만들지 않은 파일입니다. 거래처가 보낸 엑셀, 사내 시스템에서 내려받은 CSV, API가 돌려준 JSON 같은 것들입니다. 이 파일들은 만든 사람의 환경을 그대로 담고 있어서, 읽는 쪽에서 형식을 맞춰줘야 합니다. pandas의 읽기 함수들이 인자를 수십 개씩 가진 이유가 이것입니다. 다 외울 필요는 없고, 자주 부딪히는 네 가지만 알면 대부분 해결됩니다.

read_csv 기본 #

가장 많이 쓰는 함수입니다. 잘 만들어진 CSV라면 한 줄로 끝납니다.

기본 읽기
import pandas as pd

df = pd.read_csv("sales.csv")

문제는 잘 만들어지지 않은 CSV입니다. 자주 쓰는 인자를 하나씩 보겠습니다.

encoding: 한국 환경 최대 함정 #

윈도우 엑셀에서 “CSV로 저장"한 파일은 UTF-8이 아니라 cp949(EUC-KR 확장)로 저장되는 경우가 많습니다. 이런 파일을 그대로 읽으면 이렇게 됩니다.

인코딩 에러
df = pd.read_csv("sales.csv")
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0 in position 0 ...

UnicodeDecodeError가 나면 거의 항상 cp949 파일입니다. encoding 인자로 해결합니다.

cp949로 읽기
df = pd.read_csv("sales.csv", encoding="cp949")

반대 경우도 있습니다. UTF-8 파일인데 한글이 깨져 보인다면 encoding="utf-8-sig"를 시도해 보세요. 파일 맨 앞에 BOM이라는 보이지 않는 표식이 붙은 UTF-8 파일을 처리하는 인코딩입니다.

sep: 구분자가 콤마가 아닐 때 #

이름은 CSV인데 실제로는 탭이나 세미콜론으로 구분된 파일이 흔합니다. 읽었는데 컬럼이 하나로 뭉쳐 나온다면 구분자 문제입니다.

구분자 지정
df = pd.read_csv("sales.tsv", sep="\t")       # 탭 구분
df = pd.read_csv("export.csv", sep=";")       # 세미콜론 구분

header: 첫 줄이 데이터 헤더가 아닐 때 #

시스템에서 내려받은 파일은 첫 줄에 “2026년 7월 판매 현황” 같은 제목이 들어 있는 경우가 있습니다. 실제 컬럼명이 세 번째 줄이라면 header=2로 지정합니다. 0부터 세는 점에 주의하세요.

헤더 위치 지정
df = pd.read_csv("report.csv", header=2)   # 3번째 줄을 컬럼명으로
df = pd.read_csv("raw.csv", header=None)   # 헤더가 아예 없는 파일

dtype: 타입을 미리 지정 #

상품 코드처럼 “숫자로 생겼지만 숫자가 아닌” 컬럼이 있습니다. 001234 같은 코드를 그냥 읽으면 pandas가 정수 1234로 바꿔 앞의 0을 날려버립니다. dtype으로 막습니다.

타입 지정
df = pd.read_csv("sales.csv", dtype={"product_code": str})

read_excel: 시트가 있는 파일 #

엑셀 파일은 read_excel로 읽습니다. CSV와 가장 다른 점은 시트라는 차원이 하나 더 있다는 것입니다.

엑셀 읽기
df = pd.read_excel("report.xlsx")                      # 첫 번째 시트
df = pd.read_excel("report.xlsx", sheet_name="7월")    # 이름으로 지정
df = pd.read_excel("report.xlsx", sheet_name=1)        # 순서로 지정 (0부터)

# 모든 시트를 한 번에: 시트명을 키로 하는 딕셔너리가 반환됨
sheets = pd.read_excel("report.xlsx", sheet_name=None)

openpyxl 패키지가 필요하므로 1편에서 만든 프로젝트에 uv add openpyxl로 추가하면 됩니다. 엑셀을 읽고 쓰고 서식까지 다루는 자동화 관점의 정리는 업무 자동화 시리즈 2편에 따로 있습니다. 이 시리즈에서는 “분석을 위해 DataFrame으로 가져온다"까지만 다룹니다.

read_json과 클립보드 #

API 응답을 저장한 JSON 파일은 read_json으로 읽습니다.

JSON 읽기
df = pd.read_json("data.json")

그리고 의외로 자주 쓰는 기능이 하나 있습니다. 엑셀이나 웹 페이지의 표를 복사한 상태에서 실행하면 클립보드 내용을 그대로 DataFrame으로 만들어 줍니다.

클립보드에서 읽기
df = pd.read_clipboard()

파일로 저장하기도 애매한 작은 표를 빠르게 확인할 때 유용합니다.

불러온 직후 확인 루틴 #

파일을 읽는 데 성공했다고 끝이 아닙니다. 읽자마자 데이터 상태를 확인하는 습관이 이 시리즈에서 가장 강조하고 싶은 부분입니다. 작은 판매 데이터를 예로 흐름을 따라가 보겠습니다.

샘플 데이터
df = pd.read_csv("sales.csv", encoding="cp949")
df.head()
head() 출력
         date product   region   qty    price
0  2026-04-01   키보드    서울      3    45000
1  2026-04-01   마우스    부산      5    12000
2  2026-04-02   모니터    서울      1  320,000
3  2026-04-02   키보드    대구      2    45000
4  2026-04-03   마우스    서울    NaN    12000

head()는 앞 5행, tail()은 뒤 5행을 보여줍니다. 출력만 봐도 벌써 두 가지가 보입니다. price에 콤마가 섞인 값이 있고, qty에 NaN(결측치)이 있습니다. 다음은 info()입니다.

info()
df.info()
info() 출력
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 12 entries, 0 to 11
Data columns (total 5 columns):
 #   Column   Non-Null Count  Dtype
---  ------   --------------  -----
 0   date     12 non-null     object
 1   product  12 non-null     object
 2   region   12 non-null     object
 3   qty      11 non-null     float64
 4   price    12 non-null     object
dtype: memory usage: 608.0+ bytes

info() 한 번에 세 가지를 읽을 수 있습니다.

  • 행 수: 전체 12행. 원본 파일의 행 수와 맞는지 비교합니다
  • 결측: qty가 11 non-null이므로 1개가 비어 있습니다
  • dtype: 각 컬럼의 타입. 여기서 이상 신호를 잡습니다

숫자 컬럼의 분포는 describe()로 확인합니다.

describe()
df.describe()
describe() 출력
             qty
count  11.000000
mean    2.818182
std     1.401298
min     1.000000
25%     2.000000
50%     3.000000
75%     3.500000
max     5.000000

평균, 최솟값, 최댓값을 훑으면 음수 수량이나 비정상적으로 큰 값 같은 이상치를 초기에 발견할 수 있습니다. 행과 열 개수만 빠르게 보려면 shape를 씁니다.

shape
df.shape
# (12, 5)

dtype이 틀려 있다는 신호 #

info() 출력에서 가장 중요한 발견은 price가 object라는 점입니다. 가격은 당연히 숫자여야 하는데 문자열로 읽혔습니다. 그리고 describe() 출력에 price가 아예 빠져 있습니다. 숫자 컬럼이 아니므로 통계 대상에서 제외된 것입니다.

원인은 head 출력에서 본 320,000입니다. 콤마가 섞인 값이 하나라도 있으면 pandas는 그 컬럼 전체를 문자열로 읽습니다. 공백이 섞인 경우(" 45000")도 같은 증상을 만듭니다. 고치는 방법은 두 가지입니다.

방법 1: 읽을 때 처리
df = pd.read_csv("sales.csv", encoding="cp949", thousands=",")

thousands=","는 천 단위 콤마를 구분 기호로 해석하라는 뜻입니다. 읽는 시점에 해결되므로 가장 깔끔합니다. 이미 읽어버린 뒤라면 문자열 처리 후 변환합니다.

방법 2: 읽은 후 변환
df["price"] = df["price"].str.replace(",", "").astype(int)
df.info()
# ...
#  4   price    12 non-null     int64

price가 int64로 바뀌었고, 이제 describe()에도 포함됩니다. 어느 쪽이든 변환 후 info()로 결과를 다시 확인하는 것까지가 한 세트입니다.

저장: to_csv와 to_excel #

정리한 DataFrame을 파일로 내보낼 때는 to_csv, to_excel을 씁니다. 여기에 함정이 하나 있습니다.

저장
df.to_csv("sales_clean.csv")

이렇게 저장하면 파일 첫 컬럼에 0, 1, 2, ... 인덱스가 같이 저장됩니다. 이 파일을 다시 읽으면 Unnamed: 0이라는 정체불명의 컬럼이 생깁니다. 다른 곳에서 받은 파일에 Unnamed: 0 컬럼이 있다면 십중팔구 이 실수의 흔적입니다. index=False를 기본 습관으로 두는 것이 좋습니다.

index=False로 저장
df.to_csv("sales_clean.csv", index=False, encoding="utf-8-sig")
df.to_excel("sales_clean.xlsx", index=False, sheet_name="정리본")

encoding="utf-8-sig"로 저장하면 윈도우 엑셀에서 더블클릭으로 열어도 한글이 깨지지 않습니다. 결과물을 엑셀 사용자에게 전달한다면 이 옵션까지 챙기는 편이 안전합니다.

정리 #

이번 편에서 다룬 흐름입니다.

  • 실무 데이터는 지저분한 파일에서 시작하고, 읽는 쪽에서 형식을 맞춥니다
  • read_csv의 핵심 인자 네 가지: encoding(cp949 함정), sep, header, dtype
  • read_excelsheet_name으로 시트를 지정하고, sheet_name=None이면 전체를 딕셔너리로 받습니다
  • 읽은 직후 루틴: head() / tail()info()(행 수·결측·dtype) → describe()shape
  • 숫자여야 할 컬럼이 object라면 콤마나 공백이 원인이고, thousands="," 또는 str.replace + astype으로 고칩니다
  • 저장은 index=False를 기본으로, 엑셀 전달용은 utf-8-sig까지 챙깁니다

다음 편(#3 선택과 필터)에서는 불러온 데이터에서 원하는 부분만 꺼내는 법을 다루겠습니다. 컬럼 선택, lociloc의 차이, 조건으로 행을 거르는 불리언 인덱싱까지가 범위입니다.

X