데이터 조작 — Pandas와 NumPy 활용
50 Python Interview Questions for Data Engineers (2026) — DataCamp (blog) Section 3: Data Manipulation (Pandas/NumPy) (pp.7-11); FAQ 보충 (p.16)
이 모듈을 다 읽으면
- 세트와 딕셔너리의 평균 O(1) 조회 성능을 활용해 리스트 기반 순회 로직의 시간 복잡도를 개선할 수 있다
- Series와 DataFrame의 구조적 차이, 결측치·이상치 처리 전략의 선택 기준을 설명할 수 있다
- .loc/.iloc의 차이와 프로덕션 코드에서 더 안전한 인덱싱 방법을 판단할 수 있다
- 메모리보다 큰 데이터셋을 처리하기 위한 청크 처리 기법과 2026년 현재의 대안 도구를 비교할 수 있다
세트/딕셔너리 기반 성능 최적화, Series/DataFrame, 결측치·이상치 처리, .loc/.iloc, NumPy 벡터화·브로드캐스팅, merge/groupby/pivot_table/apply-transform, 그리고 메모리보다 큰 데이터 처리를 다룬다. pandas의 chunksize 방식이 여전히 유효하지만, 2026년 현재는 Polars/DuckDB의 스트리밍 실행이 더 널리 쓰이는 대안임을 보충한다.
세트/딕셔너리 기반 성능 최적화
두 리스트의 교집합을 구하는 가장 효율적인 방법은 리스트를 집합(set)으로 바꿔 set(list_a) & set(list_b)를 쓰는 것이다. 이는 중첩 반복문의 O(N×M)을 평균적으로 O(N+M) 수준으로 줄여, 수십만 개의 ID를 다룰 때 성능 차이가 크다(단, 이는 해시 조회가 평균적으로 O(1)이라는 전제 하의 평균 시간 복잡도이며, 해시 충돌이 심한 최악의 경우에는 이보다 느려질 수 있다).
딕셔너리 조회는 해시 테이블을 쓰기 때문에 평균 O(1)이다. 카테고리 ID를 이름으로 치환하는 것 같은 '조회/매핑' 작업에서, 매 행마다 리스트나 DataFrame을 처음부터 훑는 것보다 딕셔너리로 조회하는 것이 훨씬 빠른 이유가 여기에 있다.
리스트에서 순서를 유지하며 중복을 제거하려면, Python 3.7부터 딕셔너리가 삽입 순서를 보장한다는 점(3.6에서는 CPython 구현 세부사항이었고, 3.7부터 언어 명세로 확정됨)을 이용해 list(dict.fromkeys(my_list))를 쓸 수 있다. 이는 'x not in seen' 방식의 O(N²) 반복문보다 O(N)으로 훨씬 효율적이다.
핵심 포인트
- set(list_a) & set(list_b)로 교집합을 구하면 평균 시간 복잡도가 O(N×M)에서 O(N+M)으로 줄어든다.
- 딕셔너리 조회는 해시 테이블 기반으로 평균 O(1)이며, 카테고리 ID→이름 매핑 같은 룩업 작업에 리스트/DataFrame 순회보다 훨씬 빠르다.
- Python 3.7+ 딕셔너리의 삽입 순서 보장을 이용한 list(dict.fromkeys(x))는 순서를 유지하며 O(N)으로 중복을 제거한다.
Series vs DataFrame, 결측치와 이상치 처리
Series는 값의 시퀀스와 그에 연결된 인덱스(레이블 배열)로 이뤄진 1차원 배열형 객체다. DataFrame은 레이블이 붙은 행과 열을 가진 2차원, 크기 가변, 이종(heterogeneous) 데이터를 담을 수 있는 표 형태 자료구조다. Series를 스프레드시트의 한 열, DataFrame을 스프레드시트 전체라고 생각하면 된다.
결측치는 .isna()로 식별한 뒤, .dropna()로 결측이 있는 행/열을 제거하거나 .fillna()로 특정 값(평균, 중앙값, "Unknown" 같은 플레이스홀더)을 채워 넣는다. 이 선택은 결측이 '무작위로 누락(missing at random)'된 것인지, 아니면 특정 시스템적 문제를 나타내는지에 따라 달라져야 한다.
이상치는 Z-score나 사분위범위(IQR) 같은 통계적 방법으로 탐지한다. 식별된 이상치는 최대/최소 임계값으로 '클리핑'하거나, 완전히 제거하거나, 상류(upstream) 소스의 데이터 품질 문제로 간주해 조사할 수 있다.
핵심 포인트
- Series는 1차원 레이블 배열, DataFrame은 2차원 표 — Series가 스프레드시트의 한 열, DataFrame이 스프레드시트 전체에 해당한다.
- 결측치는 .isna()로 찾고 .dropna()/.fillna()로 처리하되, '무작위 누락'인지 '시스템적 문제'인지에 따라 처리 전략이 달라야 한다.
- 이상치는 Z-score/IQR로 탐지 후 클리핑·제거·조사 중 상황에 맞는 방법을 택한다.
.loc vs .iloc, 날짜 파싱
.loc는 레이블 기반 인덱싱으로, 행과 열의 이름을 사용해 데이터에 접근한다. .iloc는 정수 위치 기반 인덱싱으로, 0부터 시작하는 숫자 인덱스로 접근한다. 프로덕션 스크립트에서는 소스 데이터의 컬럼 순서가 바뀌어도 코드가 깨지지 않기 때문에 일반적으로 .loc가 더 안전하고 가독성도 좋다.
보충: .loc로 행 범위를 슬라이싱할 때는(df.loc['a':'c']) 끝 레이블이 결과에 포함되는 반면, .iloc나 일반 Python 슬라이싱(df.iloc[0:3])은 끝 인덱스를 포함하지 않는다 — 원문에는 없지만 둘을 섞어 쓰다 자주 나는 실무 버그이므로 주의가 필요하다.
문자열 컬럼을 datetime으로 바꿀 때는 pd.to_datetime() 함수를 쓴다. format 파라미터(예: %Y-%m-%d)를 명시하는 것이 중요한데, 이는 파싱 속도를 높일 뿐 아니라 01/02/03처럼 모호한 날짜를 소스 시스템의 표기 규칙에 맞게 정확히 해석하도록 보장하기 위해서다.
핵심 포인트
- .loc는 레이블 기반, .iloc는 정수 위치 기반 — 컬럼 순서 변경에 안전한 .loc가 프로덕션 코드에서 일반적으로 더 권장된다.
- .loc 슬라이싱은 끝 레이블을 포함하지만 .iloc/일반 슬라이싱은 끝 인덱스를 포함하지 않는다는 비대칭성에 주의해야 한다.
- pd.to_datetime()에 format을 명시하면 파싱 속도와 모호한 날짜 해석의 정확성을 동시에 얻는다.
NumPy 벡터화와 브로드캐스팅
벡터화는 개별 원소가 아니라 배열 전체에 대해 연산을 수행하는 것을 뜻한다. NumPy는 이 루프를 고도로 최적화된 C 코드에 위임함으로써 이를 달성한다. 예를 들어 100만 개 원소를 가진 두 배열을 a + b로 더하는 것은 벡터화된 연산으로, 원소를 하나씩 더하는 Python for 루프보다 수십 배 빠르다.
브로드캐스팅은 서로 다른 shape을 가진 배열 사이에서도 NumPy가 산술 연산을 수행할 수 있게 해주는 규칙 집합이다. 예를 들어 100×100 행렬에 스칼라 값 하나를 곱하면, NumPy는 그 스칼라를 행렬의 모든 원소에 '브로드캐스트'한다. 이는 불필요한 메모리 복사를 피하고 코드를 더 간결하게 만들어준다.
핵심 포인트
- 벡터화는 원소 단위 반복 대신 배열 전체 연산을 C 레벨로 위임해 Python for 루프보다 훨씬 빠르다.
- 브로드캐스팅은 shape이 다른 배열 간 산술 연산을 불필요한 메모리 복사 없이 가능하게 하는 규칙이다.
병합·집계·재구조화 — merge, groupby, pivot_table, apply vs transform
두 DataFrame을 합칠 때는 pd.merge(df1, df2, on='key_column', how='join_type')를 쓴다. 조인 종류는 'inner'(양쪽에 모두 있는 키만), 'left'(첫 번째의 모든 키), 'right'(두 번째의 모든 키), 'outer'(양쪽의 모든 키)가 있으며, 이는 SQL 조인 로직을 그대로 반영한다.
groupby()는 '분할-적용-결합(split-apply-combine)' 과정이다. 어떤 기준에 따라 데이터를 그룹으로 나누고, 각 그룹에 독립적으로 함수(sum, mean, 커스텀 변환 등)를 적용한 뒤, 그 결과를 새로운 자료구조로 결합한다. 피벗 테이블(df.pivot_table())은 둘 이상의 차원에 걸쳐 값을 그룹화·요약함으로써 '긴(long)' 데이터를 '넓은(wide)' 데이터로 바꿔, 카테고리별 합계나 평균을 빠르게 파악하게 해준다.
apply()는 스칼라, Series, DataFrame 중 어떤 것이든 반환할 수 있어 유연하다. transform()은 더 제한적이어서, 입력과 동일한 shape의 결과를 반환해야만 한다. transform()은 그룹 평균을 각 행에서 빼는 '센터링(centering)'처럼 원본 인덱스를 유지하면서 그룹 단위 값을 각 행에 되돌려주는 연산에 특히 유용하다.
핵심 포인트
- pd.merge의 how 파라미터(inner/left/right/outer)는 SQL 조인 로직을 그대로 따른다.
- groupby는 분할-적용-결합 과정으로 데이터 집계의 기본이다.
- pivot_table은 long 데이터를 wide 데이터로 바꿔 카테고리별 요약을 쉽게 만든다.
- apply는 임의의 shape을 반환할 수 있지만, transform은 입력과 동일한 shape을 반환해야 하며 그룹 평균 빼기 같은 센터링에 적합하다.
메모리보다 큰 데이터 처리 — chunksize와 최신 대안
16GB RAM만 있는 환경에서 100GB짜리 CSV를 읽어야 한다면 pd.read_csv() 함수의 chunksize 파라미터를 쓴다. 이는 파일을 한 번에 100,000행처럼 더 작은 조각 단위로 불러오고 처리할 수 있는 순회 가능한(iterable) 객체를 반환하며, 어느 순간에도 전체 데이터의 일부만 메모리에 있도록 보장한다. 경험칙으로는, 가용 RAM의 50%를 넘는 양을 한 번에 로드하지 않아야 한다 — 프로덕션 환경은 메모리 제약이 더 빡빡한 경우가 많아 그 이상을 시도하면 OOM으로 이어지기 쉽다.
보충(2026년 현재 실무 대안): 원문은 pandas의 chunksize 방식만 소개하지만, 실무에서는 이 방식보다 Polars의 lazy API(scan_csv().collect(streaming=True) 또는 sink_parquet() 같은 스트리밍 실행)나 DuckDB의 아웃오브코어 SQL 엔진을 우선 고려하는 경우가 많다. pandas의 chunksize는 각 청크를 개발자가 직접 독립적으로 처리해야 해서, 청크 경계를 넘나드는 집계(예: 전체 데이터 기준 groupby)를 구현하기 번거롭다. 반면 Polars/DuckDB의 쿼리 플래너는 스트리밍 실행 계획을 자동으로 최적화해준다. 다만 '전체를 한 번에 메모리에 올리지 않는다'는 원칙 자체는 어떤 도구를 쓰든 동일하게 적용된다.
핵심 포인트
- pd.read_csv(chunksize=...)는 데이터를 작은 조각 단위로 순회하며 처리해 항상 일부만 메모리에 두게 한다.
- 가용 RAM의 50%를 넘게 한 번에 로드하지 않는 것이 프로덕션에서의 안전한 경험칙이다.
- 2026년 현재는 Polars의 스트리밍 lazy API나 DuckDB의 아웃오브코어 SQL 엔진이 청크 경계를 넘는 집계를 더 쉽게 다루는 대안으로 널리 쓰인다 — 보충.