🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

A

AI Generated

2025. 12. 16. · 48 Views

복잡한 데이터셋 구조 파악 완벽 가이드

대규모 데이터셋을 처음 받았을 때 막막하신가요? 컬럼이 수백 개, 데이터 타입도 제각각인 복잡한 데이터를 체계적으로 파악하고 분석하는 실전 노하우를 배워봅니다. 초급 개발자도 쉽게 따라할 수 있는 단계별 가이드입니다.


목차

  1. 대규모 데이터셋 로드하기
  2. 컬럼 의미 파악하기
  3. 데이터 타입별 분류
  4. 수치형 vs 범주형 데이터
  5. 중첩 데이터 구조 이해
  6. 메모리 사용량 최적화
  7. 샘플링으로 빠른 탐색

1. 대규모 데이터셋 로드하기

입사 첫날, 김개발 씨는 선배로부터 5GB 크기의 CSV 파일을 받았습니다. "이 데이터 좀 분석해볼래요?" 파일을 열어보려 했지만 엑셀이 멈춰버렸습니다.

어떻게 해야 할까요?

대규모 데이터셋을 다룰 때는 메모리 효율적인 로딩이 핵심입니다. 마치 두꺼운 책을 한 번에 다 읽는 게 아니라 필요한 챕터만 펼쳐보는 것처럼, 데이터도 필요한 부분만 먼저 확인할 수 있습니다.

이렇게 하면 시스템 자원을 아끼면서도 데이터 구조를 빠르게 파악할 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd

# 먼저 처음 5줄만 확인합니다
sample_df = pd.read_csv('large_dataset.csv', nrows=5)
print("데이터 미리보기:")
print(sample_df.head())

# 전체 컬럼 정보 확인
print("\n컬럼 목록:")
print(sample_df.columns.tolist())

# 메모리 사용량 확인
print(f"\n메모리 사용량: {sample_df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

김개발 씨는 당황했습니다. 5GB짜리 파일을 어떻게 열어봐야 할지 감이 잡히지 않았습니다.

엑셀은 100만 행까지만 지원하는데, 이 파일은 분명 그보다 훨씬 클 것 같았습니다. 옆 자리의 박시니어 씨가 조용히 다가왔습니다.

"처음부터 전체 파일을 열려고 하면 안 돼요. 일단 맛보기만 해보는 거죠." 대규모 데이터셋을 다루는 첫 번째 원칙은 바로 점진적 접근입니다.

두꺼운 책을 산다고 생각해봅시다. 서점에서 책을 고를 때 처음부터 끝까지 다 읽지는 않습니다.

목차를 보고, 몇 페이지 펼쳐보고, 책의 구성을 파악합니다. 데이터 분석도 마찬가지입니다.

과거에는 어땠을까요? 예전 개발자들은 대용량 파일을 열 때마다 메모리 부족 오류와 싸워야 했습니다.

파일을 열려고 시도하면 컴퓨터가 멈추기 일쑤였습니다. 데이터가 어떤 구조인지 알아보려면 일단 전체를 로드해야 했고, 그 과정에서 많은 시간과 자원이 낭비되었습니다.

하지만 이제는 다릅니다. nrows 파라미터를 사용하면 파일의 처음 몇 줄만 선택적으로 읽을 수 있습니다.

전체 100만 행짜리 파일이라도 처음 5줄만 읽으면 불과 몇 초면 충분합니다. 이렇게 하면 데이터의 구조, 컬럼 이름, 데이터 타입을 빠르게 확인할 수 있습니다.

코드를 살펴보겠습니다. 첫 번째 줄에서 pandas를 임포트합니다.

pandas는 파이썬에서 데이터 분석을 할 때 가장 많이 사용하는 라이브러리입니다. 다음으로 read_csv 함수에 nrows=5 옵션을 줍니다.

이것이 핵심입니다. 전체 파일이 아니라 처음 5행만 읽어오는 것입니다.

head() 메서드로 데이터를 출력하면 실제로 어떤 값들이 들어있는지 확인할 수 있습니다. columns 속성으로 모든 컬럼 이름을 리스트로 받아올 수 있습니다.

마지막으로 memory_usage() 함수로 이 작은 샘플이 얼마나 메모리를 차지하는지 계산합니다. 실무에서는 어떻게 활용할까요?

예를 들어 전자상거래 회사의 주문 데이터를 분석한다고 가정해봅시다. 수년간 쌓인 주문 데이터는 수십 GB에 달할 수 있습니다.

이럴 때 처음 몇 줄만 읽어서 "주문번호, 고객ID, 상품코드, 수량, 가격" 같은 컬럼이 있다는 것을 확인하고, 그 다음 단계를 계획할 수 있습니다. 주의할 점도 있습니다.

처음 5줄만 보고 전체 데이터를 판단하면 안 됩니다. 데이터셋의 첫 부분과 뒷부분이 다른 특성을 가질 수 있기 때문입니다.

예를 들어 초반에는 결측치가 없다가 중간부터 결측치가 많아질 수도 있습니다. 따라서 샘플링은 탐색의 시작일 뿐, 전체 분석의 끝이 아닙니다.

김개발 씨는 박시니어 씨가 알려준 대로 코드를 실행했습니다. 놀랍게도 5GB 파일의 구조를 1초 만에 파악할 수 있었습니다.

"와, 이렇게 간단한 방법이 있었네요!" 대규모 데이터를 다룰 때는 항상 작게 시작하세요. 전체를 한 번에 로드하려고 하지 말고, 먼저 구조를 파악한 다음 필요한 부분만 처리하는 전략을 세우는 것이 현명합니다.

실전 팁

💡 - nrows=1000 정도로 시작해서 데이터 패턴을 먼저 파악하세요

  • skiprows 옵션으로 중간 부분도 확인하면 데이터 일관성을 검증할 수 있습니다
  • usecols 파라미터로 필요한 컬럼만 선택하면 메모리를 더욱 절약할 수 있습니다

2. 컬럼 의미 파악하기

데이터를 로드했더니 컬럼이 무려 150개나 됩니다. "cust_id", "prod_cd", "trx_amt" 같은 축약어만 가득합니다.

김개발 씨는 한숨을 쉬었습니다. 이 컬럼들이 도대체 무엇을 의미하는 걸까요?

컬럼 메타데이터 분석은 데이터 이해의 첫걸음입니다. 마치 외국어 사전을 펼치듯이, 각 컬럼이 무엇을 담고 있는지 체계적으로 정리해야 합니다.

컬럼명, 데이터 타입, 샘플 값을 한눈에 보면 전체 그림이 그려집니다.

다음 코드를 살펴봅시다.

import pandas as pd

# 데이터 로드
df = pd.read_csv('sales_data.csv', nrows=100)

# 컬럼별 상세 정보 확인
def analyze_columns(df):
    for col in df.columns:
        print(f"\n{'='*50}")
        print(f"컬럼명: {col}")
        print(f"데이터 타입: {df[col].dtype}")
        print(f"유니크 값 개수: {df[col].nunique()}")
        print(f"결측치 개수: {df[col].isna().sum()}")
        print(f"샘플 값:\n{df[col].head(3).tolist()}")

analyze_columns(df)

김개발 씨는 컬럼 목록을 출력했습니다. 화면에는 알 수 없는 약자들이 주르륵 나타났습니다.

"이게 다 무슨 뜻이지?" 막막했습니다. 박시니어 씨가 커피를 한 잔 건네며 말했습니다.

"컬럼 이름만 봐서는 모르죠. 실제 데이터를 봐야 해요.

탐정처럼 단서를 찾는 거예요." 데이터 분석은 때로 탐정 작업과 비슷합니다. 범죄 현장에 도착한 탐정을 생각해봅시다.

처음에는 어지럽게 흩어진 증거물만 보입니다. 하지만 하나하나 살펴보면서 패턴을 찾고, 연결고리를 발견하고, 결국 전체 스토리를 재구성합니다.

데이터 컬럼도 마찬가지입니다. 옛날 방식은 어땠을까요?

예전에는 데이터 사전이라는 별도 문서를 찾아야 했습니다. 하지만 많은 경우 문서가 없거나 오래되어 실제 데이터와 맞지 않았습니다.

개발자들은 데이터베이스 관리자를 찾아가 "이 컬럼이 뭔가요?"라고 물어봐야 했습니다. 시간도 오래 걸리고, 때로는 아는 사람조차 없는 레거시 컬럼도 있었습니다.

이제는 데이터 자체가 말해줍니다. dtype으로 데이터 타입을 확인하면 숫자인지 문자인지 알 수 있습니다.

**nunique()**로 고유값 개수를 세어보면 카테고리 변수인지 연속형 변수인지 추측할 수 있습니다. **isna().sum()**으로 결측치를 확인하면 데이터 품질도 파악됩니다.

무엇보다 실제 샘플 값을 보면 컬럼의 의미가 명확해집니다. 코드를 단계별로 살펴보겠습니다.

먼저 analyze_columns라는 함수를 정의합니다. 이 함수는 모든 컬럼을 순회하면서 각각의 특성을 출력합니다.

반복문 안에서 컬럼명을 출력하고, dtype으로 데이터 타입을 확인합니다. nunique()는 중복을 제거한 고유값의 개수를 알려줍니다.

예를 들어 "gender" 컬럼이 있고 유니크 값이 2개라면 아마도 남/여를 나타낼 것입니다. "customer_id"가 있고 유니크 값이 10만 개라면 고객 식별자일 가능성이 높습니다.

이런 식으로 추론해 나갑니다. 실무에서는 이렇게 활용합니다.

금융회사에서 거래 데이터를 받았다고 가정해봅시다. "trx_amt"라는 컬럼이 있습니다.

샘플 값을 보니 "15000, 23500, 8900" 같은 숫자입니다. 아, 이것은 거래금액(transaction amount)이구나!

dtype을 보니 float64입니다. 소수점이 있을 수 있다는 뜻입니다.

결측치가 0개라면 모든 거래에 금액이 기록되어 있다는 의미입니다. 주의해야 할 점도 있습니다.

컬럼명이 오해를 일으킬 수 있습니다. "date"라는 컬럼이 있어도 dtype이 object라면 문자열로 저장되어 있을 수 있습니다.

이런 경우 날짜 연산을 하려면 타입 변환이 필요합니다. 또한 샘플 값만 보고 판단하면 안 됩니다.

처음 100행에는 정상 값만 있어도, 나중에 이상한 값이 섞여 있을 수 있기 때문입니다. 김개발 씨는 함수를 실행했습니다.

화면에 각 컬럼의 상세 정보가 주르륵 출력되었습니다. "cust_id"의 샘플 값을 보니 "C00001, C00002, C00003" 같은 패턴이었습니다.

"아, 고객 ID구나!" 하나씩 의미가 파악되기 시작했습니다. 컬럼의 의미를 파악하는 것은 데이터 분석의 기초 중의 기초입니다.

이 단계를 건너뛰면 나중에 잘못된 분석을 하게 될 수 있습니다. 시간이 걸리더라도 차근차근 확인하세요.

실전 팁

💡 - value_counts()로 빈도를 확인하면 카테고리 변수의 분포를 한눈에 볼 수 있습니다

  • describe()는 숫자형 컬럼의 통계 요약을 제공하여 범위와 분포를 파악하는 데 유용합니다
  • 컬럼명에 규칙이 있는지 찾아보세요(예: _id로 끝나면 식별자, _dt로 끝나면 날짜)

3. 데이터 타입별 분류

컬럼이 150개나 되니 하나하나 확인하기가 힘듭니다. 김개발 씨는 더 효율적인 방법이 없을까 고민했습니다.

그때 박시니어 씨가 말했습니다. "타입별로 묶어서 보면 훨씬 쉬워요."

데이터 타입별 분류는 데이터셋을 논리적으로 구조화하는 방법입니다. 마치 서류를 종류별로 파일에 정리하듯이, 숫자형 컬럼, 문자형 컬럼, 날짜형 컬럼을 분류하면 전체 구조가 한눈에 들어옵니다.

이렇게 하면 각 타입에 맞는 분석 전략을 세울 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd

# 데이터 로드
df = pd.read_csv('customer_data.csv', nrows=1000)

# 타입별로 컬럼 분류
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
datetime_cols = df.select_dtypes(include=['datetime64']).columns.tolist()

print("숫자형 컬럼:", len(numeric_cols))
print(numeric_cols[:5])  # 처음 5개만

print("\n문자형 컬럼:", len(categorical_cols))
print(categorical_cols[:5])

print("\n날짜형 컬럼:", len(datetime_cols))
print(datetime_cols)

김개발 씨는 150개 컬럼을 하나씩 확인하다가 지쳐갔습니다. 한참을 들여다보다가 패턴을 발견했습니다.

"어? 숫자 컬럼들이 비슷한 특성을 가지고 있네요." 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 그래서 타입별로 묶는 거예요.

같은 종류끼리 모으면 처리하기 쉽죠." 데이터 타입 분류는 정리정돈과 같습니다. 집에 물건이 어질러져 있다고 상상해봅시다.

옷, 책, 전자제품이 뒤섞여 있으면 찾기도 어렵고 관리도 힘듭니다. 하지만 옷장에는 옷을, 책장에는 책을, 서랍에는 전자제품을 넣으면 일목요연해집니다.

데이터도 마찬가지입니다. 과거에는 어떻게 했을까요?

옛날 데이터 분석가들은 컬럼 목록을 손으로 적어가며 분류했습니다. "이건 숫자, 이건 문자, 이건 날짜..." 시간도 오래 걸리고 실수하기도 쉬웠습니다.

특히 컬럼이 수백 개인 경우에는 분류 작업만으로도 하루가 걸렸습니다. 하지만 pandas는 이 작업을 자동화해줍니다.

select_dtypes() 메서드는 데이터 타입을 기준으로 컬럼을 선택합니다. include 파라미터에 원하는 타입을 지정하면 해당하는 컬럼만 골라냅니다.

숫자형은 int64와 float64로, 문자형은 object로, 날짜형은 datetime64로 자동 분류됩니다. 코드를 자세히 들여다보겠습니다.

첫 번째 줄에서 숫자형 컬럼을 선택합니다. include=['int64', 'float64']라고 지정하면 정수와 실수 컬럼을 모두 가져옵니다.

columns 속성으로 컬럼 이름을 얻고, **tolist()**로 리스트로 변환합니다. 두 번째 줄에서는 문자형 컬럼을 선택합니다.

pandas에서 문자열은 object 타입으로 저장됩니다. 카테고리 변수나 ID 같은 텍스트 데이터가 여기 포함됩니다.

세 번째 줄은 날짜형 컬럼입니다. 다만 날짜가 문자열로 저장되어 있다면 미리 변환이 필요합니다.

실무에서는 이렇게 사용합니다. 마케팅 캠페인 데이터를 분석한다고 가정해봅시다.

숫자형 컬럼에는 "클릭수, 전환율, 매출액" 같은 지표가 있을 것입니다. 이들은 평균, 합계, 중앙값 같은 통계 분석이 가능합니다.

문자형 컬럼에는 "캠페인명, 고객등급, 지역" 같은 범주가 있습니다. 이들은 그룹화와 빈도 분석에 사용됩니다.

한 번에 분류하면 각 타입에 맞는 처리를 일괄적으로 할 수 있습니다. 예를 들어 모든 숫자형 컬럼의 결측치를 평균으로 채우고, 모든 문자형 컬럼의 결측치를 "Unknown"으로 채울 수 있습니다.

주의할 점이 있습니다. 때로는 pandas가 타입을 잘못 추론할 수 있습니다.

예를 들어 "001, 002, 003" 같은 고객 ID를 숫자로 인식할 수 있습니다. 하지만 이것은 카테고리 변수로 다뤄야 합니다.

따라서 자동 분류 후에 반드시 결과를 확인하고, 필요하면 수동으로 조정해야 합니다. 김개발 씨는 코드를 실행했습니다.

"숫자형 컬럼: 47개, 문자형 컬럼: 98개, 날짜형 컬럼: 5개" 결과가 나왔습니다. "오, 이제 뭘 어떻게 분석해야 할지 감이 잡히네요!" 데이터 타입별 분류는 복잡한 데이터셋을 이해하는 강력한 도구입니다.

분류만 잘해도 분석의 절반은 끝난 것이나 다름없습니다.

실전 팁

💡 - include 대신 exclude를 사용하면 특정 타입을 제외할 수도 있습니다

  • convert_dtypes() 메서드로 pandas가 더 적절한 타입을 자동 추론하게 할 수 있습니다
  • 분류 후에는 각 타입별로 describe()를 실행하여 요약 통계를 확인하세요

4. 수치형 vs 범주형 데이터

김개발 씨는 "age"라는 컬럼을 발견했습니다. 숫자니까 당연히 수치형이겠죠?

그런데 박시니어 씨가 고개를 저었습니다. "항상 그런 건 아니에요.

문맥을 봐야 해요."

수치형과 범주형 데이터를 구분하는 것은 올바른 분석의 핵심입니다. 마치 사과와 오렌지를 구분하듯이, 숫자로 표현되어 있어도 의미에 따라 다르게 다뤄야 합니다.

연속형 수치는 연산이 가능하지만, 범주형 코드는 세는 것만 의미가 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd

# 샘플 데이터 생성
data = {
    'customer_id': [101, 102, 103, 104, 105],
    'age': [25, 34, 29, 45, 38],
    'income': [45000, 67000, 52000, 89000, 71000],
    'region_code': [1, 2, 1, 3, 2],  # 1=서울, 2=부산, 3=대구
    'satisfaction': [3, 5, 4, 5, 2]  # 1~5점 척도
}
df = pd.DataFrame(data)

# 수치형: 평균이 의미 있음
print("평균 나이:", df['age'].mean())
print("평균 수입:", df['income'].mean())

# 범주형: 빈도가 의미 있음
print("\n지역별 고객 수:")
print(df['region_code'].value_counts())

김개발 씨는 자신 있게 말했습니다. "나이는 숫자니까 평균을 내면 되겠네요!" 박시니어 씨가 웃으며 물었습니다.

"그럼 고객 ID도 숫자인데, 평균을 낼 건가요?" 김개발 씨는 멈칫했습니다. "아, 그건...

ID는 그냥 구분용이니까..." 수치형과 범주형의 차이는 의미의 차이입니다. 식당에서 테이블 번호를 생각해봅시다.

1번 테이블, 2번 테이블, 3번 테이블이 있습니다. 숫자로 표현되어 있지만 "1번과 2번의 평균은 1.5번 테이블"이라고 말하면 이상합니다.

테이블 번호는 단지 구분하기 위한 코드일 뿐입니다. 반면 각 테이블의 매출액은 진짜 숫자입니다.

평균을 내고 합계를 구하는 것이 의미가 있습니다. 옛날 분석가들의 실수는 무엇이었을까요?

과거에는 컴퓨터가 숫자를 더 효율적으로 처리하기 때문에 모든 것을 숫자로 코딩했습니다. 성별을 1, 2로, 지역을 1, 2, 3으로 저장했습니다.

그러다 보니 실수로 "평균 성별"을 계산하는 일이 생겼습니다. 남자(1)와 여자(2)의 평균은 1.5인데, 이게 무슨 의미일까요?

아무 의미도 없습니다. 이제는 명확히 구분합니다.

연속형 수치 데이터는 산술 연산이 의미 있는 데이터입니다. 나이, 수입, 온도, 무게 같은 것들입니다.

평균, 표준편차, 상관관계를 계산할 수 있습니다. 반면 범주형 데이터는 카테고리를 나타냅니다.

지역 코드, 등급, ID 같은 것들입니다. 빈도를 세고, 비율을 구하는 것이 의미 있습니다.

코드를 단계별로 살펴보겠습니다. 샘플 데이터에서 age와 income은 진짜 수치입니다.

평균 나이 34.2세, 평균 수입 64,800원 같은 계산이 의미가 있습니다. 하지만 region_code는 다릅니다.

1, 2, 3은 단지 서울, 부산, 대구를 나타내는 코드입니다. **value_counts()**로 지역별 고객 수를 셉니다.

"1번 지역에 2명, 2번 지역에 2명, 3번 지역에 1명" 이런 빈도 분석이 유의미합니다. 하지만 "평균 지역 코드는 1.8"이라고 하면 아무 의미가 없습니다.

흥미로운 것은 satisfaction 같은 경우입니다. 만족도가 1~5점 척도로 되어 있습니다.

이것은 순서가 있는 범주형(ordinal)입니다. 평균을 낸다면?

"평균 만족도 3.8점"은 어느 정도 의미가 있습니다. 하지만 엄밀히 말하면 1점과 2점의 차이가 4점과 5점의 차이와 정확히 같다고 보장할 수 없습니다.

이런 경우는 신중하게 판단해야 합니다. 실무에서는 이렇게 활용합니다.

전자상거래 데이터를 분석한다면, 상품 가격은 수치형이므로 평균 가격, 가격대별 분포를 분석합니다. 하지만 상품 카테고리 코드는 범주형이므로 카테고리별 판매량을 집계합니다.

이 구분을 명확히 해야 올바른 시각화와 통계 분석이 가능합니다. 주의사항이 있습니다.

pandas는 숫자로 저장된 것을 자동으로 수치형으로 인식합니다. 따라서 범주형 코드를 숫자로 저장했다면 명시적으로 **astype('category')**로 변환해야 합니다.

그래야 pandas가 적절한 처리를 합니다. 김개발 씨는 깨달았습니다.

"아, 겉으로는 다 숫자지만 의미가 다르구나. 연산이 의미 있는지 생각해봐야겠어요." 박시니어 씨가 만족스럽게 웃었습니다.

"그렇죠. 데이터 분석은 기계적인 작업이 아니라 의미를 이해하는 작업이에요." 수치형과 범주형을 제대로 구분하는 것은 데이터 리터러시의 핵심입니다.

이것만 잘해도 많은 분석 오류를 방지할 수 있습니다.

실전 팁

💡 - 숫자로 된 ID나 코드는 문자열로 저장하는 것이 더 안전합니다

  • pandas의 category 타입을 활용하면 메모리도 절약하고 의미도 명확해집니다
  • 순서가 있는 범주(ordinal)와 순서가 없는 범주(nominal)도 구분하면 더 정교한 분석이 가능합니다

5. 중첩 데이터 구조 이해

김개발 씨는 "orders" 컬럼을 출력했습니다. 그런데 이상한 값이 나왔습니다.

리스트 안에 딕셔너리가 들어있습니다. "이건 또 뭐죠?" 박시니어 씨가 다가왔습니다.

"JSON 형태로 저장된 중첩 구조네요."

중첩 데이터 구조는 복잡한 현실 세계를 표현하기 위한 방법입니다. 마치 러시아 인형처럼 데이터 안에 데이터가 들어있는 형태입니다.

한 고객이 여러 주문을 하고, 각 주문에 여러 상품이 있는 경우처럼 계층적 관계를 담을 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import json

# 중첩 구조 샘플 데이터
data = {
    'customer_id': [1, 2],
    'name': ['김철수', '이영희'],
    'orders': [
        '[{"order_id": 101, "amount": 5000}, {"order_id": 102, "amount": 3000}]',
        '[{"order_id": 201, "amount": 7000}]'
    ]
}
df = pd.DataFrame(data)

# JSON 파싱하여 중첩 구조 확인
df['orders_parsed'] = df['orders'].apply(json.loads)
print("첫 번째 고객의 주문:")
print(df.loc[0, 'orders_parsed'])

# 평탄화(flattening) - 각 주문을 별도 행으로
df_exploded = df.explode('orders_parsed')
print("\n평탄화된 데이터:")
print(df_exploded)

김개발 씨는 당황했습니다. 지금까지 본 데이터는 모두 단순한 값이었습니다.

숫자, 문자열, 날짜... 그런데 이 컬럼은 리스트가 통째로 들어있습니다.

박시니어 씨가 설명했습니다. "현실 세계는 복잡해요.

한 고객이 여러 주문을 할 수 있죠. 그걸 표현하려면 중첩 구조가 필요합니다." 중첩 데이터는 상자 속의 상자와 같습니다.

선물 상자를 받았다고 상상해봅시다. 상자를 열면 작은 상자들이 여러 개 들어있고, 각 작은 상자 안에 또 다른 물건들이 있습니다.

데이터도 비슷합니다. 고객 정보라는 큰 상자 안에 주문 목록이 있고, 각 주문 안에 주문 상세가 있습니다.

과거에는 이런 구조를 어떻게 다뤘을까요? 관계형 데이터베이스에서는 테이블을 분리했습니다.

고객 테이블, 주문 테이블, 주문상세 테이블을 따로 만들어서 ID로 연결했습니다. 이것이 정규화입니다.

하지만 요즘은 NoSQL이나 JSON 데이터가 많아지면서 중첩 구조를 그대로 저장하는 경우가 늘었습니다. API 응답, 로그 파일, 웹 크롤링 데이터는 대부분 JSON 형태입니다.

이런 데이터를 분석하려면 중첩 구조를 이해하고 다룰 줄 알아야 합니다. 코드를 자세히 살펴보겠습니다.

샘플 데이터에서 orders 컬럼은 문자열로 저장된 JSON입니다. 첫 번째 단계는 **json.loads()**로 파싱하는 것입니다.

apply() 함수를 사용하여 각 행의 JSON 문자열을 파이썬 리스트로 변환합니다. 변환 후에는 파이썬 객체처럼 다룰 수 있습니다.

df.loc[0, 'orders_parsed']를 출력하면 첫 번째 고객의 주문 리스트가 나옵니다. 리스트 안에 딕셔너리 두 개가 들어있습니다.

하지만 이 상태로는 분석하기 어렵습니다. pandas는 기본적으로 2차원 테이블을 다루기 때문입니다.

여기서 explode() 메서드가 등장합니다. explode()는 리스트의 각 요소를 별도의 행으로 펼쳐줍니다.

김철수의 주문이 2개면 김철수가 2행으로 복제됩니다. 이렇게 **평탄화(flattening)**하면 일반적인 데이터프레임 연산을 적용할 수 있습니다.

실무에서는 이렇게 사용합니다. 전자상거래 로그를 분석한다고 가정해봅시다.

사용자의 클릭 이벤트가 JSON으로 기록되어 있습니다. 한 사용자가 한 세션에서 여러 페이지를 클릭했다면, 클릭 목록이 배열로 저장되어 있을 것입니다.

이것을 평탄화해서 "페이지별 방문 횟수"를 집계할 수 있습니다. 주의할 점도 있습니다.

중첩이 여러 단계로 깊어지면 복잡해집니다. 예를 들어 주문 안에 상품 목록이 있고, 각 상품에 옵션 목록이 있다면?

여러 번 파싱하고 여러 번 평탄화해야 합니다. 이럴 때는 json_normalize() 같은 도구를 사용하면 자동으로 평탄화할 수 있습니다.

또한 explode()를 사용하면 데이터가 중복됩니다. 김철수의 이름과 ID가 주문 개수만큼 반복됩니다.

메모리를 더 많이 사용하게 되므로, 대용량 데이터에서는 주의해야 합니다. 김개발 씨는 explode() 결과를 보고 놀랐습니다.

"와, 자동으로 펼쳐지네요! 이제 일반 데이터프레임처럼 다룰 수 있겠어요." 박시니어 씨가 덧붙였습니다.

"맞아요. 하지만 원본 구조를 이해하는 것도 중요해요.

평탄화는 분석을 위한 수단일 뿐이니까요." 중첩 데이터를 다루는 능력은 현대 데이터 분석가에게 필수입니다. JSON이 표준이 된 세상에서 이것을 피할 수 없기 때문입니다.

실전 팁

💡 - pd.json_normalize()를 사용하면 복잡한 중첩 JSON을 자동으로 평탄화할 수 있습니다

  • explode() 후에는 reset_index()로 인덱스를 재정렬하는 것이 좋습니다
  • 중첩 구조가 복잡하면 먼저 작은 샘플로 파싱 로직을 테스트하세요

6. 메모리 사용량 최적화

데이터를 로드했더니 컴퓨터가 느려졌습니다. 작업 관리자를 보니 메모리가 거의 꽉 찼습니다.

김개발 씨는 걱정이 되었습니다. "이 데이터를 계속 다룰 수 있을까요?" 박시니어 씨가 웃으며 말했습니다.

"메모리를 줄이는 방법이 있어요."

메모리 최적화는 대용량 데이터를 다룰 때 필수입니다. 마치 여행 짐을 꾸릴 때 불필요한 것을 빼고 압축하듯이, 데이터도 적절한 타입으로 변환하고 필요한 컬럼만 유지하면 메모리를 크게 절약할 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 샘플 데이터 로드
df = pd.read_csv('large_data.csv', nrows=10000)

# 메모리 사용량 확인
def check_memory(df):
    memory_mb = df.memory_usage(deep=True).sum() / 1024**2
    print(f"메모리 사용량: {memory_mb:.2f} MB")
    return memory_mb

print("최적화 전:")
before = check_memory(df)

# 정수형 다운캐스팅
for col in df.select_dtypes(include=['int64']).columns:
    df[col] = pd.to_numeric(df[col], downcast='integer')

# 실수형 다운캐스팅
for col in df.select_dtypes(include=['float64']).columns:
    df[col] = pd.to_numeric(df[col], downcast='float')

# 범주형 변환
for col in df.select_dtypes(include=['object']).columns:
    if df[col].nunique() / len(df) < 0.5:  # 유니크 비율이 50% 미만
        df[col] = df[col].astype('category')

print("\n최적화 후:")
after = check_memory(df)
print(f"절약률: {(1 - after/before)*100:.1f}%")

김개발 씨의 노트북 팬이 빠르게 돌아가기 시작했습니다. 프로그램이 점점 느려졌습니다.

"왜 이러지?" 당황했습니다. 박시니어 씨가 작업 관리자를 열어보더니 말했습니다.

"메모리를 16GB나 쓰고 있네요. 데이터가 비효율적으로 저장되어 있어요." 메모리 최적화는 짐 정리와 비슷합니다.

여행을 간다고 생각해봅시다. 큰 가방에 옷을 그냥 넣으면 공간을 많이 차지합니다.

하지만 압축팩을 사용하고, 작은 물건은 작은 주머니에 넣고, 필요 없는 것은 빼면 같은 가방에 훨씬 많은 것을 넣을 수 있습니다. 데이터도 마찬가지입니다.

과거에는 메모리가 부족하면 어떻게 했을까요? 옛날 개발자들은 서버를 업그레이드하거나 데이터를 쪼개서 처리했습니다.

하지만 근본적인 해결책은 아니었습니다. 데이터가 커지면 또다시 메모리 부족에 시달렸습니다.

비용도 많이 들었습니다. 하지만 이제는 타입 최적화로 해결할 수 있습니다.

pandas는 기본적으로 안전한 타입을 사용합니다. 정수는 int64(8바이트), 실수는 float64(8바이트)로 저장됩니다.

하지만 실제 데이터가 작은 범위라면? 예를 들어 나이가 0~100 사이라면 int8(1바이트)로도 충분합니다.

8분의 1로 줄어드는 것입니다. 코드를 단계별로 살펴보겠습니다.

먼저 **memory_usage(deep=True)**로 현재 메모리 사용량을 확인합니다. deep=True는 문자열 같은 객체 타입도 정확히 측정하라는 의미입니다.

다음으로 downcast 옵션을 사용합니다. to_numeric() 함수에 downcast='integer'를 주면 pandas가 자동으로 가장 작은 정수 타입을 선택합니다.

값이 -128127 범위면 int8, -3276832767이면 int16 식으로 적절히 선택합니다. 실수형도 마찬가지입니다.

downcast='float'를 사용하면 float64를 float32로 줄일 수 있습니다. 정밀도가 약간 떨어지지만 대부분의 경우 문제없습니다.

가장 효과적인 것은 category 타입입니다. 문자열 컬럼이 있고, 중복된 값이 많다면 category로 변환하면 엄청난 메모리 절약이 가능합니다.

예를 들어 100만 행의 데이터에 "서울", "부산", "대구" 세 값만 반복된다면? object 타입은 각 셀마다 문자열을 저장하지만, category는 내부적으로 정수 코드로 저장하고 매핑 테이블을 따로 둡니다.

코드에서는 유니크 비율이 50% 미만인 경우만 category로 변환합니다. 왜냐하면 거의 모든 값이 유니크하다면 category의 이점이 없기 때문입니다.

실무에서는 이렇게 활용합니다. 고객 로그 데이터를 분석한다고 가정해봅시다.

100만 명의 고객이 있고, 각각 평균 10개의 로그가 있으면 천만 행입니다. 여기서 "device_type" 컬럼이 "mobile", "desktop", "tablet" 세 값만 가진다면 category로 변환하여 메모리를 90% 이상 절약할 수 있습니다.

주의할 점도 있습니다. 다운캐스팅을 잘못하면 데이터 손실이 생길 수 있습니다.

예를 들어 int8은 -128~127만 표현할 수 있습니다. 나이가 보통 이 범위에 있지만, 만약 데이터 오류로 999가 들어있다면?

잘못된 값으로 변환될 수 있습니다. 따라서 최적화 전에 데이터 범위를 확인해야 합니다.

또한 category 타입은 모든 연산에서 빠른 것은 아닙니다. 유니크 값이 매우 많으면 오히려 느려질 수 있습니다.

김개발 씨는 최적화를 실행했습니다. "와!

16GB에서 4GB로 줄었어요. 75% 절약이네요!" 컴퓨터 팬도 조용해졌습니다.

박시니어 씨가 만족스럽게 고개를 끄덕였습니다. "이제 훨씬 큰 데이터도 다룰 수 있을 거예요.

메모리 최적화는 습관이 되어야 해요." 메모리 최적화는 대용량 데이터 분석의 기본기입니다. 이것만 잘해도 서버 비용을 아끼고 분석 속도를 높일 수 있습니다.

실전 팁

💡 - 데이터 로드 시 dtype 파라미터로 미리 타입을 지정하면 더 효율적입니다

  • 필요 없는 컬럼은 usecols로 제외하여 아예 로드하지 않는 것이 최선입니다
  • 대용량 데이터는 chunk 단위로 읽어서 처리하면 메모리 피크를 낮출 수 있습니다

7. 샘플링으로 빠른 탐색

김개발 씨는 1억 행 데이터를 분석해야 합니다. 코드를 돌려봤더니 한 번 실행하는 데 10분이 걸립니다.

"이렇게 느리면 시행착오를 어떻게 하죠?" 박시니어 씨가 말했습니다. "전체 데이터가 아니라 샘플로 먼저 해보세요."

샘플링은 전체 데이터의 축소판으로 빠르게 탐색하는 기법입니다. 마치 음식 맛을 보기 위해 한 숟가락만 먹어보듯이, 데이터도 대표성 있는 일부만 추출하여 패턴을 파악할 수 있습니다.

이렇게 하면 개발 속도가 10배 이상 빨라집니다.

다음 코드를 살펴봅시다.

import pandas as pd

# 대용량 데이터에서 샘플 추출
# 방법 1: 무작위 샘플 (1%)
df_sample = pd.read_csv('huge_data.csv',
                        skiprows=lambda i: i > 0 and np.random.rand() > 0.01)

# 방법 2: 계층적 샘플링 (그룹별 비율 유지)
df = pd.read_csv('customer_data.csv', nrows=100000)
df_stratified = df.groupby('region', group_keys=False).apply(
    lambda x: x.sample(frac=0.1)  # 각 지역에서 10% 샘플
)

print(f"원본 크기: {len(df)}")
print(f"샘플 크기: {len(df_stratified)}")
print("\n지역별 비율 비교:")
print("원본:", df['region'].value_counts(normalize=True))
print("샘플:", df_stratified['region'].value_counts(normalize=True))

김개발 씨는 좌절했습니다. 코드를 조금만 수정해도 10분씩 기다려야 합니다.

하루에 몇 번 시도해보지도 못했습니다. "이러다가 마감을 못 맞추겠어요." 박시니어 씨가 조언했습니다.

"개발 단계에서는 전체 데이터가 필요 없어요. 샘플로 충분해요.

로직이 완성되면 그때 전체 데이터로 돌리면 돼요." 샘플링은 시식 코너와 같습니다. 대형 마트에 가면 시식 코너가 있습니다.

통째로 사기 전에 작은 조각을 맛봅니다. 맛이 좋으면 사고, 별로면 다른 제품을 봅니다.

한 조각만 먹어봐도 전체 맛을 알 수 있습니다. 데이터 분석도 마찬가지입니다.

과거 데이터 분석가들은 어떻게 했을까요? 옛날에는 샘플링 기법이 제한적이었습니다.

파일의 처음 N줄만 읽거나, 매 K번째 행만 읽는 단순한 방법을 썼습니다. 하지만 데이터가 시간 순으로 정렬되어 있으면 초반부만 보게 되어 편향이 생겼습니다.

계절성이 있는 데이터라면 1월 데이터만 보고 전체를 판단하는 오류를 범했습니다. 이제는 통계적 샘플링이 가능합니다.

**무작위 샘플링(random sampling)**은 모든 행이 동일한 확률로 선택되도록 합니다. 1억 행에서 1%를 무작위로 뽑으면 100만 행이 됩니다.

데이터 크기는 100분의 1이지만 통계적 특성은 거의 동일합니다. 평균, 분산, 상관관계 같은 지표가 전체와 비슷하게 나옵니다.

더 정교한 방법은 **계층적 샘플링(stratified sampling)**입니다. 여론조사를 생각해봅시다.

무작위로 사람을 뽑으면 우연히 특정 연령대가 많이 뽑힐 수 있습니다. 그래서 연령대별로 비율을 맞춰서 뽑습니다.

데이터도 마찬가지입니다. 지역별, 고객등급별로 같은 비율로 샘플링하면 대표성이 높아집니다.

코드를 자세히 들여다보겠습니다. 첫 번째 방법은 read_csv()의 skiprows 파라미터를 활용합니다.

람다 함수로 각 행을 읽을지 말지 결정합니다. np.random.rand()가 0.01보다 크면 건너뛰므로, 결과적으로 1%만 읽힙니다.

이 방법은 전체 파일을 스캔하지만 메모리에는 일부만 로드합니다. 두 번째 방법은 계층적 샘플링입니다.

**groupby()**로 지역별로 그룹을 나누고, 각 그룹에서 **sample(frac=0.1)**로 10%씩 추출합니다. 이렇게 하면 원본에서 서울이 40%, 부산이 30%, 대구가 30%였다면 샘플에서도 같은 비율이 유지됩니다.

**value_counts(normalize=True)**로 비율을 비교하면 원본과 샘플이 거의 같다는 것을 확인할 수 있습니다. 실무에서는 이렇게 활용합니다.

머신러닝 모델을 개발한다고 가정해봅시다. 전체 데이터로 학습하면 몇 시간이 걸립니다.

하지만 개발 단계에서는 샘플 데이터로 빠르게 실험합니다. 피처 엔지니어링, 하이퍼파라미터 튜닝을 샘플로 하고, 최종 모델만 전체 데이터로 학습하면 시간을 크게 절약할 수 있습니다.

주의사항이 있습니다. 샘플이 너무 작으면 통계적 오차가 커집니다.

특히 희귀한 패턴은 샘플에 포함되지 않을 수 있습니다. 예를 들어 사기 거래가 전체의 0.1%라면 1% 샘플에서는 거의 나타나지 않습니다.

이런 경우는 불균형 샘플링을 사용하여 희귀 케이스를 더 많이 포함시켜야 합니다. 또한 샘플로 개발한 코드가 전체 데이터에서도 잘 작동하는지 최종 검증은 필수입니다.

샘플에서는 빠르게 실행되던 코드가 전체 데이터에서는 메모리 부족으로 실패할 수도 있습니다. 김개발 씨는 10만 행 샘플로 작업을 시작했습니다.

실행 시간이 10분에서 5초로 줄었습니다. "와, 120배나 빨라졌어요!" 빠르게 여러 가지를 시도해보며 로직을 다듬었습니다.

며칠 후, 완성된 코드를 전체 데이터에 적용했습니다. 샘플에서 검증한 덕분에 한 번에 성공했습니다.

박시니어 씨가 웃으며 말했습니다. "샘플링은 개발자의 시간을 절약하는 최고의 방법이에요.

전체를 다루기 전에 작게 검증하는 습관을 들이세요." 샘플링은 데이터 과학자의 핵심 스킬입니다. 큰 그림을 잃지 않으면서도 빠르게 움직일 수 있게 해줍니다.

실전 팁

💡 - 개발 단계에서는 10만~100만 행 샘플이면 대부분 충분합니다

  • 시계열 데이터는 무작위 샘플링보다 기간을 나눠서 샘플링하는 것이 좋습니다
  • sample() 메서드의 random_state 파라미터로 재현 가능한 샘플을 만들 수 있습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#Pandas#DataFrame#DataAnalysis#DataStructure

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스