Tech PostsTech Posts
기초와 계량경제학Basics & Econometrics · 02 / 24
02 기초와 계량경제학Basics & Econometrics

루프를 쓰고 싶어질 때가 신호다The Moment You Reach for a Loop Is the Signal

NumPy·Pandas·Statsmodels가 왜 이런 모양인지 알면 이 시리즈의 나머지 코드가 왜 그렇게 쓰였는지도 같이 풀립니다.Understanding why NumPy, Pandas and Statsmodels look the way they do explains why the rest of this series is written the way it is.

이 시리즈의 나머지 편들은 데이터를 파이썬으로 불러오고 원하는 모양으로 바꾸고 숫자 하나를 꺼낼 수 있다는 것을 전제합니다. 이 편은 그 바닥을 깝니다.

Every post that follows assumes you can get data into Python, reshape it, and get a number back out. This one lays that floor.

언어 입문서는 아닙니다. 다루는 것은 응용 계량경제 작업의 거의 전부를 떠받치는 네 라이브러리입니다. 그중에서도 스프레드시트나 Stata를 쓰던 감각과 다르게 움직이는 부분입니다.

It is deliberately not a language tutorial. The focus is the four libraries that carry almost all applied econometric work, and specifically the parts that behave differently from what a spreadsheet or a Stata background prepares you for.

스택이 이 모양인 이유Why the toolchain looks like this

파이썬 자체는 수치 연산에 느립니다. 100만 행을 순수 파이썬으로 순회하면 시간의 대부분이 인터프리터 오버헤드에 갑니다. 값마다 타입을 확인하고 중간 객체를 매번 할당합니다.

Python on its own is slow at numerical work. A loop over a million rows in pure Python spends most of its time on interpreter overhead: type-checking each value, allocating each intermediate object.

NumPy는 그 루프를 컴파일된 C로 옮겨서 이 문제를 풉니다. heights ** 2라고 쓰면 파이썬 수준의 반복은 일어나지 않습니다. C 루틴 하나가 연속된 메모리 블록을 훑습니다. 속도 차이는 보통 10~100배입니다. 과학 파이썬 스택 전체가 리스트가 아니라 NumPy 배열 위에 지어진 이유가 이것입니다.

NumPy solves this by moving the loop into compiled C. When you write heights ** 2, no Python-level iteration happens — a single C routine walks a contiguous block of memory. The speed difference is usually 10 to 100 times, and it is why the whole scientific Python stack is built on NumPy arrays rather than Python lists.

나머지는 그 위에 층으로 얹힙니다.

Everything else layers on that foundation.

  • NumPy는 타입이 정해진 연속 메모리 n차원 배열과 그 위의 원소별 연산을 줍니다.
  • Pandas는 그 배열에 이름표가 붙은 축을 씌웁니다. data[:, 3] 대신 df["wage"]라고 쓰고 위치가 아니라 키로 결합할 수 있습니다.
  • Matplotlib은 그것을 그립니다.
  • Statsmodels는 경제학자가 기대하는 추정량을 줍니다. 제대로 된 표준오차가 붙은 OLS, 패널 모형, 시계열을 회귀표와 함께 내놓습니다. .predict() 메서드 하나로 끝나지 않습니다.
  • NumPy provides the typed, contiguous n-dimensional array and elementwise operations over it.
  • Pandas wraps those arrays with labelled axes, so you write df["wage"] instead of data[:, 3] and join on keys rather than positions.
  • Matplotlib draws them.
  • Statsmodels provides the estimators an economist expects — OLS with proper standard errors, panel models, time series — with regression tables rather than just a .predict() method.

걸려 넘어지는 두 개념The two ideas that trip people up

벡터화. 연산이 배열 전체에 원소별로 한 번에 적용됩니다. bmi = weight / height ** 2 한 줄이 모든 사람의 BMI를 계산합니다. 루프가 없습니다.

Vectorisation. Operations apply elementwise across a whole array at once. bmi = weight / height ** 2 computes every person BMI in one statement, with no loop.

불리언 마스킹. 비교 연산도 원소별로 적용되어 True/False 배열을 만듭니다. 그 배열을 다시 인덱스로 넘기면 True인 행만 골라냅니다. bmi[bmi > 25]는 "25를 넘는 BMI 값들"로 읽힙니다. 이 스택에서 가장 자주 쓰는 패턴입니다. 뒤 편들의 필터링은 거의 전부 이 형태로 쓰여 있습니다.

Boolean masking. Comparisons also apply elementwise, producing an array of True/False. Passing that array back as an index selects the rows where it is True. So bmi[bmi > 25] reads as "the BMI values above 25". This is the single most useful pattern in the stack, and nearly all filtering in later posts is written this way.

python
import numpy as np
import pandas as pd

height = np.array([1.73, 1.68, 1.71, 1.89, 1.79])
weight = np.array([65.4, 59.2, 63.6, 88.4, 68.7])

bmi = weight / height ** 2          # vectorised: one C loop, no Python iteration
print(bmi)
print(bmi > 25)                     # elementwise comparison -> boolean array
print(bmi[bmi > 25])                # boolean mask -> selection
[21.85171573 20.97505669 21.75028214 24.7473475  21.44127836]
[False False False False False]
[]

두 줄짜리 예제지만 구조는 100만 행에서도 같습니다. 그리고 100만 행에서는 루프 버전과 20배쯤 차이가 납니다.

A two-line example, but the structure is identical at a million rows — where it also runs about twenty times faster than the loop version.

조용히 실패하는 버그The bug that fails silently

Pandas에서 가장 흔한 버그는 연쇄 인덱싱입니다. 오류를 내지 않고 아무 일도 하지 않기 때문에 위험합니다.

The most common Pandas bug is chained indexing. It is dangerous because it raises no error and simply does nothing.

python
df = pd.DataFrame({"wage": [8, 12, 15, 9], "hours": [40, 38, 45, 42]})

df[df.wage > 10]["hours"] = 0       # silently does nothing
print(df.hours.tolist())

df.loc[df.wage > 10, "hours"] = 0   # this is the one that works
print(df.hours.tolist())
[40, 38, 45, 42]
[40, 0, 0, 42]

같은 계열의 문제로 복사와 뷰의 구분이 있습니다. NumPy 배열을 슬라이싱하면 같은 메모리를 가리키는 가 나옵니다. 뷰를 수정하면 원본이 바뀝니다. 별도 객체로 작업할 생각이라면 .copy()를 명시하십시오. 그러길 바라지 말고요.

A related issue is the copy-versus-view distinction. Slicing a NumPy array gives a view into the same memory, so modifying it modifies the original. When you mean to work on a separate object, say .copy() explicitly rather than hoping.

추론과 예측은 다른 도구를 쓴다Inference and prediction use different tools

Statsmodels와 scikit-learn은 비슷해 보이지만 다른 질문에 답합니다. 응용 작업의 혼란 상당수가 잘못된 쪽을 집어 드는 데서 옵니다.

Statsmodels and scikit-learn look similar but answer different questions. A good share of the confusion in applied work comes from reaching for the wrong one.

Statsmodelsscikit-learn
목적계수 자체가 결과일 때표본 외 예측 정확도
출력계수·표준오차·t값·진단예측값. 표준오차는 아예 없음
이 시리즈에서인과추론 편 거의 전부교차검증·정규화 편
Statsmodelsscikit-learn
PurposeWhen the parameter is the resultOut-of-sample predictive accuracy
OutputCoefficients, standard errors, t-statistics, diagnosticsPredictions. No standard errors at all
In this seriesNearly all of the causal inference postsThe cross-validation and regularisation posts

뒤에 나오는 인과추론 편들이 Statsmodels에 거의 전적으로 기대는 이유가 이것입니다. 거기서 필요한 건 예측값이 아니라 계수와 그 불확실성입니다.

This is why the causal inference posts later in the series lean almost entirely on Statsmodels: what they need is a coefficient and its uncertainty, not a prediction.

실무에서In practice

  • 벡터화하되 추측하지 말고 재십시오. 루프를 피하려는 본능은 맞지만 무엇이 느린지는 짐작하면 틀립니다. 대표 구간에 %timeit을 걸면 몇 초 만에 논쟁이 끝납니다. 다음 편이 통째로 이 습관 이야기입니다.
  • dtype을 의식적으로 고르십시오. 결측값 하나가 섞인 정수 열은 조용히 float64가 됩니다. 반복되는 문자열을 object로 들고 있으면 category 대비 메모리를 50배까지 씁니다. 수백만 행을 넘어가면 메모리에 들어가느냐 마느냐의 차이가 됩니다.
  • 모든 노트북 맨 위에 난수 시드를 박으십시오. 이 시리즈의 시뮬레이션은 전부 그렇게 합니다. 시드가 없으면 실행할 때마다 숫자가 바뀌고 진짜 효과와 재표본 잡음을 구분할 수 없습니다.
  • 병합 전후로 모양을 확인하십시오. 유일하지 않은 키로 조인하면 행 수가 곱해집니다. df.duplicated(subset=key).sum()을 먼저 보고 병합 뒤 .shape를 다시 보십시오.
  • Prefer vectorised operations, then measure. The instinct to avoid loops is right, but do not guess at what is slow. %timeit on a representative slice takes seconds and settles the argument. The next post is entirely about this habit.
  • Choose your dtypes deliberately. A column of integers containing a single missing value silently becomes float64, and repeated strings stored as object can use fifty times the memory of the equivalent category. Past a few million rows this is the difference between fitting in memory and not.
  • Set a random seed at the top of every notebook. Every simulation in this series does. Without it your numbers change on each run and you cannot tell a real effect from resampling noise.
  • Check the shape around every merge. Joining on a non-unique key multiplies rows. Look at df.duplicated(subset=key).sum() first, then check .shape afterwards.

기초 섹션의 남은 편들은 이 도구를 씁니다. 알고리즘 비용 재기, 추정량 API로 데이터 흘리기, 교차검증으로 모형 고르기, OLS를 직접 구현하기, 그리고 모형이 틀렸을 때 무슨 일이 일어나는지 보기. 새 라이브러리는 더 나오지 않습니다.

The remaining posts in the Basics section put these tools to work: measuring algorithmic cost, pulling data through an estimator API, choosing a model with cross-validation, implementing OLS by hand, and seeing what happens when the model is wrong. None of them introduce new libraries.