# 2. NumPy 배열 인덱싱과 슬라이싱

## 📚 학습 목표
- NumPy 배열의 인덱싱 방법 이해
- 슬라이싱을 통한 부분 배열 추출
- 불리언 인덱싱과 팬시 인덱싱 활용
- 다차원 배열의 인덱싱 기법 습득

## 🔍 문제 설명

NumPy 배열에서 특정 원소나 부분 배열을 추출하는 것은 데이터 분석의 핵심 기술입니다. 인덱싱과 슬라이싱을 통해 원하는 데이터만 선택적으로 처리할 수 있습니다.

### 실전 활용 사례
- **데이터 전처리**: 특정 조건을 만족하는 데이터만 선택
- **이미지 처리**: 특정 영역의 픽셀 값 추출
- **시계열 분석**: 특정 기간의 데이터만 분석
- **머신러닝**: 특성 선택 및 데이터 분할

## 📝 문제

### 문제 2-1: 기본 인덱싱
1. 1차원 배열 `[10, 20, 30, 40, 50]`에서 세 번째 원소를 추출하세요.
2. 2차원 배열 `[[1, 2, 3], [4, 5, 6], [7, 8, 9]]`에서 (1, 2) 위치의 원소를 추출하세요.
3. 3차원 배열에서 특정 원소에 접근하는 방법을 연습하세요.

### 문제 2-2: 슬라이싱
4. 1차원 배열에서 인덱스 1부터 4까지의 원소를 추출하세요.
5. 2차원 배열에서 첫 번째 행의 모든 원소를 추출하세요.
6. 2차원 배열에서 마지막 열의 모든 원소를 추출하세요.

### 문제 2-3: 고급 인덱싱
7. 불리언 인덱싱을 사용하여 5보다 큰 원소만 추출하세요.
8. 팬시 인덱싱을 사용하여 특정 인덱스의 원소들을 추출하세요.
9. 조건부 인덱싱을 사용하여 짝수 원소만 추출하세요.

## 💡 해답 및 설명

### 해답 2-1: 기본 인덱싱

```python
import numpy as np

# 1. 1차원 배열에서 세 번째 원소 추출
arr_1d = np.array([10, 20, 30, 40, 50])
third_element = arr_1d[2]  # 인덱스는 0부터 시작
print("세 번째 원소:", third_element)  # 출력: 30

# 2. 2차원 배열에서 (1, 2) 위치의 원소 추출
arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
element_1_2 = arr_2d[1, 2]  # 또는 arr_2d[1][2]
print("(1, 2) 위치의 원소:", element_1_2)  # 출력: 6

# 3. 3차원 배열 인덱싱
arr_3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print("3차원 배열 형태:", arr_3d.shape)  # 출력: (2, 2, 2)
element_3d = arr_3d[0, 1, 0]
print("(0, 1, 0) 위치의 원소:", element_3d)  # 출력: 3
```

### 해답 2-2: 슬라이싱

```python
# 4. 1차원 배열 슬라이싱
arr_1d = np.array([10, 20, 30, 40, 50, 60, 70])
slice_1d = arr_1d[1:5]  # 인덱스 1부터 4까지 (5는 포함되지 않음)
print("인덱스 1~4의 원소:", slice_1d)  # 출력: [20 30 40 50]

# 5. 2차원 배열에서 첫 번째 행 추출
arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
first_row = arr_2d[0, :]  # 또는 arr_2d[0]
print("첫 번째 행:", first_row)  # 출력: [1 2 3]

# 6. 2차원 배열에서 마지막 열 추출
last_column = arr_2d[:, -1]
print("마지막 열:", last_column)  # 출력: [3 6 9]

# 추가 슬라이싱 예제
print("\n슬라이싱 추가 예제:")
print("첫 번째 행과 두 번째 행:", arr_2d[0:2, :])
print("첫 번째 열과 두 번째 열:", arr_2d[:, 0:2])
print("2x2 부분 배열 (좌상단):", arr_2d[0:2, 0:2])
```

### 해답 2-3: 고급 인덱싱

```python
# 7. 불리언 인덱싱
arr = np.array([1, 3, 5, 7, 9, 2, 4, 6, 8, 10])
boolean_mask = arr > 5
elements_greater_than_5 = arr[boolean_mask]
print("5보다 큰 원소:", elements_greater_than_5)  # 출력: [7 9 6 8 10]

# 8. 팬시 인덱싱
fancy_indices = [0, 2, 4, 6, 8]
selected_elements = arr[fancy_indices]
print("선택된 인덱스의 원소:", selected_elements)  # 출력: [1 5 9 4 8]

# 9. 조건부 인덱싱 (짝수 원소만)
even_elements = arr[arr % 2 == 0]
print("짝수 원소:", even_elements)  # 출력: [2 4 6 8 10]

# 2차원 배열에서의 불리언 인덱싱
arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
mask_2d = arr_2d > 5
print("\n2차원 배열에서 5보다 큰 원소:")
print("불리언 마스크:")
print(mask_2d)
print("조건을 만족하는 원소:", arr_2d[mask_2d])
```

## 🔧 주요 인덱싱 기법

### 기본 인덱싱
- `array[index]`: 1차원 배열에서 특정 위치의 원소
- `array[row, col]`: 2차원 배열에서 특정 위치의 원소
- `array[i, j, k]`: 3차원 배열에서 특정 위치의 원소

### 슬라이싱
- `array[start:stop:step]`: 범위 지정 슬라이싱
- `array[start:]`: 시작 위치부터 끝까지
- `array[:stop]`: 처음부터 끝 위치까지
- `array[::step]`: 스텝 간격으로 슬라이싱
- `array[::-1]`: 역순으로 슬라이싱

### 불리언 인덱싱
- `array[boolean_array]`: 조건을 만족하는 원소만 선택
- `array[array > value]`: 특정 값보다 큰 원소 선택
- `array[(array > min_val) & (array < max_val)]`: 범위 조건

### 팬시 인덱싱
- `array[index_array]`: 인덱스 배열로 원소 선택
- `array[[0, 2, 4]]`: 특정 인덱스의 원소들 선택

## 📊 추가 연습문제

### 연습문제 1: 기본 인덱싱
```python
# 다음 배열을 생성하고 문제를 풀어보세요:
arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])

# 1. 두 번째 행의 세 번째 원소를 추출하세요.
# 2. 첫 번째 열의 모든 원소를 추출하세요.
# 3. 마지막 행의 마지막 원소를 추출하세요.
```

### 연습문제 2: 슬라이싱
```python
# 다음 배열을 사용하여 문제를 풀어보세요:
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 1. 인덱스 2부터 7까지의 원소를 추출하세요.
# 2. 짝수 인덱스의 원소만 추출하세요 (0, 2, 4, 6, 8).
# 3. 배열을 역순으로 출력하세요.
```

### 연습문제 3: 고급 인덱싱
```python
# 다음 배열을 사용하여 문제를 풀어보세요:
arr = np.array([15, 23, 8, 42, 17, 31, 5, 19, 28, 12])

# 1. 20보다 큰 원소만 추출하세요.
# 2. 10의 배수인 원소만 추출하세요.
# 3. 인덱스 1, 3, 5, 7, 9의 원소를 추출하세요.
```

## ⚠️ 주의사항 및 팁

1. **인덱스 범위**: Python 인덱스는 0부터 시작하며, 슬라이싱에서 끝 인덱스는 포함되지 않습니다.
2. **뷰(View) vs 복사(Copy)**: 슬라이싱은 원본 배열의 뷰를 반환하지만, 팬시 인덱싱은 복사를 반환합니다.
3. **불리언 인덱싱**: 조건을 만족하는 원소만 선택하므로 결과 배열의 크기가 달라질 수 있습니다.
4. **다차원 인덱싱**: 각 차원별로 인덱스를 지정할 수 있습니다.
5. **음수 인덱스**: -1은 마지막 원소, -2는 뒤에서 두 번째 원소를 의미합니다.

## 🎯 실전 활용 예제

### 예제 1: 이미지 데이터 처리
```python
# 28x28 이미지 데이터에서 특정 영역 추출
image = np.random.randint(0, 256, (28, 28))
print("전체 이미지 형태:", image.shape)

# 중앙 14x14 영역 추출
center_region = image[7:21, 7:21]
print("중앙 영역 형태:", center_region.shape)

# 특정 밝기 이상의 픽셀만 추출
bright_pixels = image[image > 200]
print("밝은 픽셀 개수:", len(bright_pixels))
```

### 예제 2: 시계열 데이터 분석
```python
# 100일간의 주가 데이터
np.random.seed(42)
stock_prices = np.random.normal(100, 10, 100).cumsum()

# 최근 30일 데이터만 추출
recent_30_days = stock_prices[-30:]
print("최근 30일 데이터 형태:", recent_30_days.shape)

# 상승한 날의 데이터만 추출
up_days = stock_prices[1:][stock_prices[1:] > stock_prices[:-1]]
print("상승한 날의 수:", len(up_days))
```

### 예제 3: 조건부 데이터 필터링
```python
# 학생들의 점수 데이터
scores = np.array([85, 92, 78, 96, 88, 75, 91, 83, 79, 94])
grades = np.array(['A', 'A', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A'])

# A등급 학생들의 점수만 추출
a_grade_scores = scores[grades == 'A']
print("A등급 학생들의 점수:", a_grade_scores)

# 90점 이상 학생들의 등급
high_scores_grades = grades[scores >= 90]
print("90점 이상 학생들의 등급:", high_scores_grades)
```

## 📚 다음 단계

이제 NumPy 배열의 인덱싱과 슬라이싱을 익혔습니다. 다음 단계에서는:
- 배열 연산과 브로드캐스팅
- 조건부 필터링과 마스킹
- 배열 변형과 재구성
- 집계 함수와 통계 계산

을 학습하게 됩니다.

---

**참고 자료:**
- [NumPy 인덱싱 가이드](https://numpy.org/doc/stable/user/basics.indexing.html)
- [NumPy 슬라이싱 튜토리얼](https://numpy.org/doc/stable/reference/arrays.indexing.html) 