인지야공/인공 지능 공부 치트 시트 정리/1번째 글
NumPy 치트시트를 다시 쓴다 — 축, 뷰, 그리고 지금은 에러가 나는 것들
치트시트를 그대로 두고 보면 시험장에서 안 나온다. 함수는 알파벳순으로 늘어서 있는데 막히는 지점은 함수 이름이 아니라 축(axis)과 뷰(view) 여서 그렇다. 그래서 목록을 버리고 틀리는 자리를 기준으로 다시 묶었다.
여기 적은 것은 전부 직접 돌려 보고 옮겼다. 확인한 환경은 NumPy 1.26.4 다. ADP 시험장은 1.21.6 이라 다르게 도는 것이 몇 개 있는데, 그건 마지막 절에 따로 모아 두었다.
축은 “사라지는 축”을 적는 것이다
집계 함수의 axis 는 그 축을 따라 계산한다가 아니라 그 축이 사라진다로 외우는 편이
안 헷갈린다. (2, 3) 배열에 axis=0 을 주면 결과가 (3,) 이 된다 — 0번 축이 없어졌다.
b = np.array([(1.5, 2, 3),
(4, 5, 6)]) # 모양 (2, 3)
b.sum(axis=0) # [5.5, 7., 9.] ← 행이 사라진다 = 열별 합. 결과 모양 (3,)
b.sum(axis=1) # [6.5, 15.] ← 열이 사라진다 = 행별 합. 결과 모양 (2,)
b.sum() # 16.5 ← 축을 다 없앤다
표로 쓰는 데이터는 행이 관측치, 열이 변수다. 그러니 “변수별로 평균” 은 언제나
axis=0 이다. 표준화 코드가 X.mean(axis=0) 인 이유가 이것이다.
만들기 — arange 와 linspace 를 나눠 쓴다
np.zeros((3, 4)) # 0 으로 채운 (3,4)
np.ones((2, 3, 4), dtype=np.int16)
np.full((2, 2), 7) # 상수로 채운다
np.eye(2) # 단위행렬
np.empty((3, 2)) # 값을 채우지 않는다. 안에 든 것은 쓰레기값이다
np.random.random((2, 2)) # [0, 1) 균등난수
np.arange(10, 25, 5) # [10, 15, 20] ← 끝값 미포함, 간격을 지정
np.linspace(0, 2, 9) # 0 부터 2 까지 9 개 ← 끝값 포함, 개수를 지정
np.empty 는 빠른 대신 초기화를 하지 않는다. 0 이 들어 있다고 믿고 누적하면 틀린다.
누적할 자리는 np.zeros 를 쓴다.
간격이 실수일 때 arange 를 쓰면 개수가 흔들린다. 부동소수 오차 때문에 끝값을 넘겨 버리는
일이 실제로 생긴다.
len(np.arange(0, 0.3, 0.1)) # 3 ← 기대한 대로
len(np.arange(1, 1.3, 0.1)) # 4 ← 끝값 1.3 이 1.3000000000000003 으로 딸려 들어왔다
그래서 개수가 중요하면 linspace, 정수 간격이면 arange 로 나눠 쓴다.
살펴보기
a.shape # 모양. 디버깅의 8할은 이것만 찍어 봐도 끝난다
a.ndim # 축의 개수
a.size # 전체 원소 수
a.dtype # 원소의 자료형
a.astype(int) # 형 변환. 제자리가 아니라 새 배열을 돌려준다
len(a) # 첫 축의 길이일 뿐이다. 전체 개수가 아니다 — size 와 헷갈리지 않는다
인덱싱 — 뷰인가 복사인가
치트시트가 한 덩어리로 묶어 놓은 세 가지가 동작이 서로 다르다. 이걸 모르면 원본이 말없이 바뀌거나, 반대로 바꿨는데 원본에 반영이 안 된다.
| 방식 | 예 | 결과 |
|---|---|---|
| 기본 슬라이싱 | a[0:2], b[:1], a[::-1] | 뷰 — 고치면 원본이 같이 바뀐다 |
| 팬시 인덱싱 | b[[1, 0, 1], [0, 1, 2]] | 복사 — 고쳐도 원본은 그대로 |
| 불리언 인덱싱 | a[a < 2] | 복사 |
a = np.arange(6)
s = a[1:3] # 뷰다. 새 배열이 아니다
s[0] = 99
a # [0, 99, 2, 3, 4, 5] ← 원본이 바뀌었다
f = a[[1, 2]] # 팬시 인덱싱은 복사다
f[0] = -1
a # 그대로다
원본을 지키고 싶으면 슬라이스 뒤에 .copy() 를 붙인다. 전처리 함수에 배열을 넘길 때
습관으로 붙여 두면 사고가 없다.
h = a.view() # 같은 데이터를 다른 배열 객체로 본다
h = a.copy() # 데이터까지 따로 뜬다. 이쪽이 안전하다
연산 — * 는 행렬곱이 아니다
a * b # 원소별 곱. [1,2] * [3,4] → [3, 8]
a @ b # 행렬곱(내적). [1,2] @ [3,4] → 11
a.dot(b) # @ 와 같다. 옛날 코드에서 자주 본다
a - b # np.subtract(a, b) 와 같다. 연산자 쪽이 읽기 쉽다
np.exp(b); np.sqrt(b); np.log(a); np.sin(a) # 원소마다 적용된다
np.subtract, np.add 같은 이름은 연산자를 못 쓰는 자리(예: out= 으로 결과 배열을
지정하거나 reduce 를 붙일 때)를 위한 것이다. 평소에는 -, + 로 쓴다.
브로드캐스팅
모양이 다른 배열끼리 계산할 때, NumPy 는 뒤쪽 축부터 맞춰 보고 길이가 1 인 축을 늘린다. 표준화가 한 줄로 써지는 것이 이 규칙 덕분이다.
X = np.array([[1., 2.],
[3., 4.],
[5., 6.]]) # (3, 2) — 행이 관측치, 열이 변수
X.mean(axis=0) # (2,) 변수별 평균
(X - X.mean(axis=0)) / X.std(axis=0)
# (3,2) 와 (2,) → 뒤 축이 2 로 같으니 (2,) 를 세 줄로 늘려 뺀다
행 방향으로 브로드캐스팅하고 싶으면 축을 하나 세워 준다 — X - X.mean(axis=1)[:, None].
[:, None] 은 (3,) 을 (3, 1) 로 만드는 관용구다.
모양 바꾸기 — 넷이 다 다르다
| 함수 | 원본을 건드리나 | 반환 |
|---|---|---|
a.reshape(3, -1) | 아니다 | 되도록 뷰 |
a.ravel() | 아니다 | 가능하면 뷰, 아니면 복사 |
a.flatten() | 아니다 | 항상 복사 |
a.resize((2, 6)) | 제자리로 바꾼다 | None |
g.reshape(3, -1) # -1 은 "나머지는 알아서 계산해라". 치트시트의 -2 는 오타에 가깝다
a.ravel() # 펴서 뷰로 준다 → 고치면 원본이 바뀔 수 있다
a.T.ravel() # 전치한 것은 메모리가 연속이 아니라 이때는 복사가 나온다
h.resize((2, 6)) # 반환값은 None 이다. `h = h.resize(...)` 라고 쓰면 h 가 None 이 된다
i = b.T # 전치. np.transpose(b) 와 같다
ravel 이 때에 따라 뷰가 되기도 복사가 되기도 한다는 점 때문에, 원본을 지켜야 하면
flatten 을 쓰는 편이 마음이 편하다.
붙이고 쪼개기
np.concatenate((a, d), axis=0) # 기본. 축을 직접 고른다
np.vstack((a, b)) # 위아래로 (행 추가). concatenate(axis=0) 과 같은 뜻
np.hstack((e, f)) # 옆으로 (열 추가)
np.column_stack((a, d)) # 1차원 여러 개를 열로 세워 표로 만든다
np.r_[e, f]; np.c_[a, d] # vstack / column_stack 의 짧은 표기
np.hsplit(a, 3) # 3 등분
np.vsplit(c, 2)
np.append, np.insert, np.delete 는 매번 배열 전체를 새로 만든다. 반복문 안에서
np.append 로 한 줄씩 붙이면 데이터가 커질수록 급격히 느려진다. 파이썬 리스트에 모았다가
마지막에 한 번 np.array(...) 로 바꾸는 게 정석이다.
통계 — ddof 와 결측
a.sum(); a.min(); b.max(axis=0); b.cumsum(axis=1)
a.mean(); np.median(b); np.corrcoef(a, d)
np.std(b) # 기본이 ddof=0 이다 (모집단 표준편차, n 으로 나눈다)
np.std(b, ddof=1) # 표본 표준편차 (n-1 로 나눈다)
NumPy 의 기본은 ddof=0, pandas .std() 의 기본은 ddof=1 이다. 같은 데이터에서
두 값이 다르게 나오는 이유가 이것이지 계산 오류가 아니다. 왜 표본에서 n-1 로 나누는지는
자유도 이야기에 적어 두었다.
y = np.array([1.0, np.nan, 3.0])
y.mean() # nan ← 하나라도 섞이면 전부 nan 이 된다
np.nanmean(y) # 2.0 ← 결측을 빼고 계산한다. nanstd, nansum 도 같다
np.nan == np.nan # False! 같은지 비교로는 절대 못 찾는다
np.isnan(y) # [False, True, False] ← 이것으로 찾는다
치트시트에 있지만 지금은 에러가 나는 것들
원본 치트시트를 그대로 따라 치면 에러가 나는 줄이 여럿 있다. 오래된 자료라서 그렇다. 1.26.4 에서 하나씩 돌려 확인한 결과다.
| 치트시트의 표기 | 지금 | 대신 |
|---|---|---|
np.bool np.int np.float np.object np.complex | AttributeError (1.24 에서 제거) | 파이썬 내장 bool int float, 또는 np.bool_ np.int64 np.float64 |
np.string_ np.unicode_ | 1.26 까지는 돈다. NumPy 2.0 에서 제거 | np.bytes_ np.str_ |
b.median() | AttributeError — 배열의 메서드가 아니다 | np.median(b) |
a.corrcoef() | AttributeError — 마찬가지다 | np.corrcoef(a, d) |
h.resize((2,6)) 가 “새 배열을 돌려준다” | 설명이 틀렸다. 제자리로 바꾸고 None 을 준다 | 새 배열이 필요하면 h.reshape(2, 6) |
g.reshape(3, -2) | 돌기는 한다 | 관례는 -1 이다 |
여기서 방향에 주의할 것이 하나 있다. 집에서는 에러가 나는데 시험장에서는 도는 경우다.
np.float 같은 옛 별칭은 1.20 에서 경고, 1.24 에서 제거였으므로 시험장 환경(1.21.6)에서는
경고만 뜨고 그냥 돈다. 시험장 환경 글에 적은 것과 같은
함정이 여기서도 반복된다 — 낡은 자료와 낡은 환경, 그리고 최신 환경이 서로 어긋난다.
출처
DataCamp 의 Python For Data Science Cheat Sheet — NumPy Basics 를 보고 다시 쓴 것이다. 원본은 DataCamp 치트시트 페이지에서 받을 수 있다. 여기 있는 문장과 코드, 확인 결과는 내가 쓰고 내가 돌려 본 것이다.