매일 아침 데이터 과학자는 같은 코드를 반복한다. iterrows()로 행을 순회하고, 중간 변수를 할당하고, merge()를 여러 번 호출한다. 결과는 나오지만 느리고 읽기 어렵다. 이 패턴들은 튜토리얼에서 배운 습관에서 비롯된다.
6가지 패턴이 일상 작업을 바꾼다
첫 번째는 메서드 체이닝이다. 중간 변수를 없애고 변환을 하나의 표현식으로 연결한다. assign() 안에서 람다(lambda)를 써야 현재 DataFrame 상태를 참조할 수 있다. inplace=True는 체인을 끊으므로 사용하지 않는다.
두 번째는 pipe() 패턴이다. 복잡한 변환을 별도 함수로 분리하고 체인 안에 유지한다. pipe()는 DataFrame을 첫 번째 인자로 전달한다. 각 함수는 개별 테스트가 가능하고, 함수 이름이 단계를 설명하므로 코드가 스스로 문서화된다. 디버깅 중 하나의 pipe() 호출을 주석 처리해도 나머지 체인은 정상 작동한다.
세 번째는 효율적인 조인이다. merge()의 가장 흔한 실수는 다대다 조인과 무음 행 증가다. 조인 키에 중복이 있으면 두 DataFrame의 카티전 곱이 발생한다. 500행의 users 테이블과 events 테이블이 합쳐져 수백만 행이 될 수 있다. 오류도 없다. 해결책은 validate 파라미터다:
merged = pd.merge(users, events, on='user_id', validate='many_to_one')이 코드는 다대일 가정이 깨지면 즉시 MergeError를 발생시킨다. indicator=True 파라미터는 _merge 열을 추가해 각 행이 left_only, right_only, both 중 어디서 왔는지 보여준다. 예상과 다른 조인을 즉시 발견할 수 있다. 두 DataFrame이 같은 인덱스를 공유하면 join()이 merge()보다 빠르다.
네 번째는 GroupBy 최적화다. transform()은 agg()와 달리 원본 DataFrame과 같은 형태를 반환한다. 그룹별 통계를 새 열로 추가할 때 merge() 없이 한 단계로 처리한다:
df['avg_revenue_by_segment'] = df.groupby('segment')['revenue'].transform('mean')이 코드는 세그먼트별 평균 매출을 각 행에 직접 추가한다. agg()로 같은 결과를 얻으려면 평균을 계산한 뒤 세그먼트 키로 다시 병합해야 한다. 범주형 groupby 열에는 항상 observed=True를 사용한다. 이 인자가 없으면 실제 데이터에 없는 조합까지 모든 범주에 대해 계산을 수행해 빈 그룹과 불필요한 연산이 발생한다.
다섯 번째는 벡터화 조건문이다. apply()에 람다를 넣는 방식은 각 행마다 파이썬 함수를 실행하므로 C 수준 연산을 활용하지 못한다. 이진 조건은 np.where()로 대체한다:
df['status'] = np.where(df['score'] >= 70, 'Pass', 'Fail')여러 조건은 np.select()가 처리한다:
conditions = [df['score'] >= 90, df['score'] >= 70, df['score'] >= 50]
choices = ['Excellent', 'Good', 'Average']
df['grade'] = np.select(conditions, choices, default='Poor')np.select()는 if/elif/else 구조를 벡터화 속도로 매핑한다. 백만 행 DataFrame에서 apply()보다 50~100배 빠르다. 숫자 구간 분할은 pd.cut()(동일 폭)과 pd.qcut()(분위수 기반)으로 조건문 자체를 대체한다.
여섯 번째는 성능 함정 회피다. 범주형 데이터는 category dtype을 사용해 메모리를 줄이고 연산을 가속한다. 불필요한 복사를 피하려면 copy=False를 설정한다. 큰 DataFrame을 필터링할 때는 query() 메서드가 불리언 인덱싱보다 빠르고 읽기 쉽다.
개발자가 바로 체감하는 변화는 실행 시간이다. iterrows() 루프를 np.where()로 바꾸면 1초 걸리던 작업이 0.01초로 줄어든다. merge()에 validate를 추가하면 데이터 오염을 사전에 차단한다. pipe()로 체인을 유지하면 디버깅 시간이 반으로 준다.
예전 방식과 달라진 기준점
예전에는 데이터 과학자가 코드 가독성과 성능 중 하나를 선택해야 했다. 이제는 메서드 체이닝과 pipe()가 둘 다 제공한다. 예전에는 그룹 통계를 추가하려면 agg() 후 merge()로 두 단계가 필요했다. transform()이 한 단계로 줄였다. 예전에는 조건문을 apply()로 작성했다. np.select()가 100배 빠른 대안이 되었다.
이번 패턴들이 바꾸는 건 개별 함수가 아니다. 데이터 과학 워크플로의 전체 지형이다. 체이닝은 중간 변수를 없애고, pipe()는 복잡한 로직을 테스트 가능한 단위로 분리하며, transform()은 병합 자체를 생략한다. 각 패턴은 코드의 밀도를 높이고 실행 경로를 단축한다.
판다스는 더 이상 데이터 과학의 병목이 아니다. 병목은 개발자가 모르는 패턴이다.




