4.4.6 다양한 슬라이싱과 배열 색인
[수학적 의미: 불연속적인 집합의 추출] 지금까지 연속된 선분 구간을 잘라내는 슬라이싱을 배웠다면, 팬시 인덱싱(Fancy Indexing)은 흩어져 있는 좌표 점 집합 \(A = \{x_1, x_3, x_8\}\)만 골라내어 콕콕 집어내는 이산 수학적 추출 작업입니다.
[비유로 이해하기: 즐겨찾는 아이템 다중 선택 (Fancy Indexing)]
슬라이싱은 행렬 덩어리를 빵 썰어내듯 자르지만, 팬시 인덱싱(Fancy Indexing)은 내가 원하는 특정 행과 열의 번호가 적힌 바구니 리스트([0, 2, 5])를 그대로 던져주어, 띄엄띄엄 떨어진 “즐겨찾기 항목”만 쏙쏙 끄집어내는 직관적이고 날카로운 타겟팅 기술입니다.
2차원 배열 a가 있다.
from numpy import arange
a = arange(40).reshape(5, 8)
a
출력:
array([[ 0, 1, 2, 3, 4, 5, 6, 7],
[ 8, 9, 10, 11, 12, 13, 14, 15],
[16, 17, 18, 19, 20, 21, 22, 23],
[24, 25, 26, 27, 28, 29, 30, 31],
[32, 33, 34, 35, 36, 37, 38, 39]])
다음 a[2:4, 3:7]를 통해 기본 색인으로 첨자 2행, 3행, 그리고 3열, 4열, 5열, 6열로 구성된 모양 (2, 4)의 2차원 배열이 반환된다.
a[2:4, 3:7]
출력:
array([[19, 20, 21, 22],
[27, 28, 29, 30]])
다음 코드는 고급 색인으로 2행과 3행으로 구성된 배열을 반환한다.
a[np.arange(2, 4)]
출력:
array([[16, 17, 18, 19, 20, 21, 22, 23],
[24, 25, 26, 27, 28, 29, 30, 31]])
이것은 a[2:4]와 유사해 보인다.
다음 코드로는 위의 반환 배열의 1열과 0열로 구성된 배열을 반환한다. 먼저 a[arange(2, 4)]로 2행과 3행으로 구성된 배열을 반환하고 다시 그 배열을 코드 [:, [1, 0]]로 1열과 0열로 구성된 배열을 반환한다.
a[np.arange(2, 4)][:, [1, 0]]
출력:
array([[17, 16],
[25, 24]])
다음 코드는 2, 3, 4행으로 구성된 배열에서 1열과 0열, 3열, 5열로 구성된 배열을 반환한다.
a[np.arange(2, 5)][:, [1, 0, 3, 5]]
출력:
array([[17, 16, 19, 21],
[25, 24, 27, 29],
[33, 32, 35, 37]])
다음 코드로 2, 3, 4 행으로 구성된 배열에서 첨자 -1(양수로 7) 열에서부터 -7(양수로 1)까지 열로 구성된 배열을 반환한다. 결과 배열이 원래의 행에서 원소 값이 역순이 된 것을 알 수 있다.
a[np.arange(2, 5)][:, np.arange(-1, -8, -1)]
출력:
array([[23, 22, 21, 20, 19, 18, 17],
[31, 30, 29, 28, 27, 26, 25],
[39, 38, 37, 36, 35, 34, 33]])
논리 연산자를 활용한 고급 조건 필터링 (Boolean Indexing)
[수학적 의미: 조건제시법에 의한 부분 집합 도출]
고등학교 수학 시간으로 돌아가 봅니다. 조건제시법 집합 \(B = \{ x \in A | x > 8 \}\) 처럼 어떤 본체의 데이터 내에서 특정한 논리적 수식을 통과한 진리값(True) 데이터들만 필터링하여 새로운 부분 집합을 만들어내는 수학적 절차입니다.
[비유로 이해하기: 조건 체(Mask)로 걸러내기]
수만 개의 타일 데이터 위에 “이 중에 값이 8보다 큰 애들만 빛나라!”라는 구멍 뚫린 그물망 마스크(True/False 배열)를 통째로 덮어씌우는 것과 같습니다. 조건식을 통과한 True 위치의 원소들만 체 밑으로 후두둑 떨어져 즉시 반환되는 파워풀한 전처리 기술입니다.
단순히 인덱스 숫자로 데이터를 추출하는 것을 넘어서서, 배열 내부에 직접 조건문을 삽입하여 원하는 조건의 데이터만 추출할 수도 있습니다.
import numpy as np
a = np.array([7, 20, 15, 11, 8, 7, 19, 11, 4])
# 4.4.6 ) 조건문 단독 필터링
print("8보다 큰 값들만 추출:", a[a > 8])
# 4.4.6 출력: [20 15 11 19 11]
# 4.4.6 ) 나머지(%) 연산자를 이용해 조건 패턴 만들기 (홀수번째 값 추출)
# 4.4.6 부터 배열 길이만큼의 인덱스 배열을 만들어 2로 나눈 나머지가 1인 곳만 선택
# 4.4.6 결과적으로 첫 번째, 세 번째, 다섯 번째 등 홀수 인덱스의 값들만 나타남
odd_positions = a[np.arange(1, len(a)+1) % 2 == 1]
print("홀수 번째에 위치한 값들 추출:", odd_positions)
# 4.4.6 출력: [ 7 15 8 19 4]
조건을 혼합해서 쓸 때는 앰퍼샌드 & (AND 기호)와 파이프 | (OR 기호)를 활용하여 다중 조건을 겁니다. 반드시 소괄호 ()로 각각의 조건을 묶어주어야 합니다.
a = np.array([1, 2, 3, 4, 16, 17, 18])
# 4.4.6 이거나 15 초과인 값만 추출 (| 연산자)
result1 = a[(a == 4) | (a > 15)]
print("OR 조건 추출결과:", result1)
# 4.4.6 출력: [ 4 16 17 18]
# 4.4.6 슬라이싱을 활용해 조건에 맞는 원소들 일괄 치환하기
# 4.4.6 배열 a에서 10 이상인 원소를 전부 10으로 하향 평준화(클리핑) 처리
a[a >= 10] = 10
print("10 이상 값들 10으로 강제 하향:", a)
# 4.4.6 출력: [ 1 2 3 4 10 10 10]
조건을 만족하는 위치(Index) 탐색, np.where()
만약 데이터 값을 추출하는 것이 아니라, 조건을 만족하는 값들이 대체 어디(몇 번째 인덱스)에 있는지 위치를 알고 싶다면 np.where() 함수를 활용합니다.
x = np.array([1, 5, 7, 8, 10])
# 4.4.6 보다 작은 데이터들이 들어있는 좌표(인덱스) 반환
idx = np.where(x < 7)
print("7보다 작은 값들의 위치 인덱스:", idx)
# 4.4.6 출력: (array([0, 1]),) -> 0번째 원소(1)와 1번째 원소(5)에 위치
np.where는 데이터 전처리 과정에서 엉뚱한 결측치나 아웃라이어(이상치)가 어느 행 몇 번째 줄에 박혀 있는지 구출 작전을 펼칠 때 대단히 유용하게 활용되는 핵심 함수입니다.