Skip to main content

인덱싱과 슬라이싱

NumPy 배열에서 원하는 데이터를 효율적으로 추출하는 방법을 배웁니다. 기본 슬라이싱을 넘어 **팬시 인덱싱(Fancy Indexing)**과 **불리언 마스킹(Boolean Masking)**을 활용하면 복잡한 조건의 데이터 선택도 한 줄로 처리할 수 있습니다.

학습 목표

  • 기본 인덱싱과 슬라이싱의 동작 원리를 이해한다
  • 팬시 인덱싱으로 비연속적인 원소를 한 번에 선택할 수 있다
  • 불리언 마스킹으로 조건 기반 필터링을 수행할 수 있다
  • np.where()를 사용하여 조건부 값 대체를 할 수 있다

왜 중요한가

데이터 분석에서는 전체 데이터가 아닌 특정 조건을 만족하는 부분 집합을 다루는 경우가 대부분입니다. “결측치가 아닌 값만 선택”, “임계값 이상인 피처만 추출”, “특정 클래스에 해당하는 샘플만 분리” 등 모든 작업이 인덱싱과 마스킹으로 이루어집니다.

기본 인덱싱과 슬라이싱

import numpy as np

arr = np.array([10, 20, 30, 40, 50])

# 기본 인덱싱
print(arr[0])     # 10 — 첫 번째 원소
print(arr[-1])    # 50 — 마지막 원소

# 슬라이싱 (start:stop:step)
print(arr[1:4])   # [20 30 40] — 인덱스 1~3
print(arr[::2])   # [10 30 50] — 2칸씩 건너뛰기
print(arr[::-1])  # [50 40 30 20 10] — 역순

다차원 인덱싱

matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

# 단일 원소 접근
print(matrix[0, 2])     # 3 — 0행 2열

# 행 슬라이싱
print(matrix[0:2])      # [[1 2 3] [4 5 6]] — 0~1행

# 열 슬라이싱
print(matrix[:, 1])     # [2 5 8] — 모든 행의 1열

# 부분 행렬
print(matrix[0:2, 1:3]) # [[2 3] [5 6]] — 0~1행, 1~2열
슬라이싱 결과는 view입니다. 수정하면 원본도 변경됩니다. 원본을 보존하려면 .copy()를 사용하세요.

팬시 인덱싱

정수 배열을 인덱스로 사용하여 비연속적인 원소를 한 번에 선택합니다. 슬라이싱과 달리 항상 copy를 반환합니다.
arr = np.array([10, 20, 30, 40, 50])

# 정수 배열 인덱싱
indices = [0, 2, 4]
print(arr[indices])       # [10 30 50]

# 순서를 바꿔서 선택
print(arr[[4, 2, 0]])     # [50 30 10]

# 중복 선택도 가능
print(arr[[1, 1, 3, 3]])  # [20 20 40 40]

다차원 팬시 인덱싱

matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

# 특정 행 선택
rows = [0, 2]
print(matrix[rows])       # [[1 2 3] [7 8 9]]

# 특정 원소 선택 (행 인덱스, 열 인덱스 쌍)
row_idx = [0, 1, 2]
col_idx = [2, 0, 1]
print(matrix[row_idx, col_idx])  # [3 4 8] — (0,2), (1,0), (2,1)

# np.ix_로 부분 행렬 선택
rows = [0, 2]
cols = [0, 2]
print(matrix[np.ix_(rows, cols)])
# [[1 3]
#  [7 9]]

불리언 마스킹

조건식으로 생성한 불리언 배열을 인덱스로 사용합니다. 데이터 필터링에서 가장 많이 쓰이는 패턴입니다.
arr = np.array([15, 23, 8, 42, 11, 37])

# 조건식 → 불리언 배열
mask = arr > 20
print(mask)          # [False  True False  True False  True]

# 불리언 마스킹으로 필터링
print(arr[mask])     # [23 42 37] — 20 초과인 값만

# 한 줄로 작성
print(arr[arr > 20]) # [23 42 37]

복합 조건

arr = np.array([15, 23, 8, 42, 11, 37])

# AND 조건 — & 사용 (and 아님)
print(arr[(arr > 10) & (arr < 30)])   # [15 23 11]

# OR 조건 — | 사용 (or 아님)
print(arr[(arr < 10) | (arr > 35)])   # [ 8 42 37]

# NOT 조건 — ~ 사용 (not 아님)
print(arr[~(arr > 20)])               # [15  8 11]
불리언 연산에서 Python의 and, or, not이 아닌 비트 연산자 &, |, ~를 사용해야 합니다. 또한 각 조건을 괄호로 감싸야 연산자 우선순위 문제를 피할 수 있습니다.

실무 예시: 데이터 필터링

# 학생 성적 데이터
scores = np.array([85, 92, 67, 45, 78, 91, 53, 88])
names = np.array(['김', '이', '박', '최', '정', '강', '조', '윤'])

# 80점 이상인 학생
high_performers = names[scores >= 80]
print(high_performers)  # ['김' '이' '강' '윤']

# 60~80점 사이인 학생
mid_range = names[(scores >= 60) & (scores < 80)]
print(mid_range)  # ['박' '정']

# 과락(60점 미만) 학생 수
fail_count = np.sum(scores < 60)
print(f"과락 학생: {fail_count}명")  # 과락 학생: 2명

np.where — 조건부 값 선택

np.where(condition, x, y)는 조건이 True인 위치에서 x를, False인 위치에서 y를 반환합니다.
arr = np.array([1, -2, 3, -4, 5])

# 음수를 0으로 대체
result = np.where(arr > 0, arr, 0)
print(result)  # [1 0 3 0 5]

# 조건에 따라 레이블 부여
labels = np.where(arr > 0, '양수', '음수')
print(labels)  # ['양수' '음수' '양수' '음수' '양수']
# 실무 예시: 이상치 클리핑
data = np.array([2.1, 150.5, 3.4, -20.0, 4.7, 200.3])

# 상한/하한 설정
lower, upper = 0, 100
clipped = np.where(data < lower, lower, np.where(data > upper, upper, data))
print(clipped)  # [  2.1 100.    3.4   0.    4.7 100. ]

# np.clip()으로 더 간결하게
clipped2 = np.clip(data, lower, upper)
print(clipped2)  # [  2.1 100.    3.4   0.    4.7 100. ]

np.select — 다중 조건 선택

scores = np.array([95, 82, 67, 45, 73])

conditions = [
    scores >= 90,
    scores >= 80,
    scores >= 70,
    scores >= 60,
]
choices = ['A', 'B', 'C', 'D']

grades = np.select(conditions, choices, default='F')
print(grades)  # ['A' 'B' 'D' 'F' 'C']

AI/ML에서의 활용

  • 피처 선택: 상관계수가 높은 피처의 인덱스를 추출하여 피처 행렬에서 선택합니다
  • 클래스 분리: labels == 1과 같은 마스킹으로 특정 클래스의 샘플을 분리합니다
  • 이상치 처리: np.where()로 임계값을 벗어난 값을 대체합니다
  • 데이터 증강: 팬시 인덱싱으로 특정 샘플을 복제하여 클래스 불균형을 해소합니다
연속적인 범위는 슬라이싱(arr[1:5])이 더 빠릅니다(view 반환). 비연속적이거나 특정 인덱스만 선택해야 할 때 팬시 인덱싱(arr[[1,3,7]])을 사용합니다. 팬시 인덱싱은 항상 copy를 반환하므로 메모리를 더 사용합니다.
불리언 마스킹은 내부적으로 C 수준에서 처리되므로 Python 반복문보다 훨씬 빠릅니다. 100만 개 원소 배열에서도 밀리초 단위로 필터링이 완료됩니다.

체크리스트

  • 다차원 배열에서 행, 열, 부분 행렬을 슬라이싱으로 추출할 수 있다
  • 팬시 인덱싱으로 비연속적인 원소를 선택할 수 있다
  • 불리언 마스킹으로 조건에 맞는 데이터를 필터링할 수 있다
  • &, |, ~ 연산자로 복합 조건을 구성할 수 있다
  • np.where()로 조건부 값 대체를 수행할 수 있다

다음 문서