SKN Family AI Bootcamp 24기 / 3주차 - 데이터분석 (Data Analysis)(2)

A.·2026년 2월 17일

SKN 24

목록 보기
12/15

통계 기초

데이터 분석 목적

  • 데이터 요약
    - 데이터 요약 척도: 대푯값, 산포도
  • 데이터 설명
    - 수학적 근거에 기반하여, 대상의 성질과 관계성을 묘사
    - 인과관계, 상관관계 등이 존재
  • 새 데이터 예측
    - 기존의 데이터를 근거로 경향성을 파악하여 새 데이터를 예측 -> 통계학의 역할

기술통계와 추론통계

  • 기술통계 (Descriptive Statistics)
    : 확보한 데이터에 집중하여 데이터 자체의 성질을 이해하는 것이 목표
    : 데이터를 요약하고 설명하는 데 사용되는 통계 기법. 평균, 중앙값, 최빈값, 표준편차 등이 포함
  • 추론통계 (Inferential Statistics)
    : 수집한 데이터로부터 발생원을 파악하여 미래를 예측하는 것이 목표
    : 표본 데이터를 사용하여 모집단에 대한 추론을 수행하는 통계 기법. 가설 검정, 신뢰 구간 등이 포함된다.
    : 확률모형: 현실의 상황을 수학적인 모델로 표현
    : 가설검정: 발생한 사건이 가설에 얼마나 부합하는가를 판단

모집단 (Population)

  • 통계 관찰의 대상이 되는 집단 전체
  • 크기에 따라 유한모집단과 무한모집단으로 분류
  • 유한모집단: 모집단 중 한정된 요소만 포함한 것
  • 무한모집단: 모집단 중 포함된 요소의 개수가 무한한 것

전수조사

  • 모집단의 성질을 아는 방법 중 하나로 모집단에 포함된 모든 요소를 조사하는 것
  • 획득한 데이터의 특징을 파악하고 기술하여 모집단의 성질을 설명
  • 막대한 시간 및 비용 소요와 더불어 시간에 따른 데이터 변질로 인해 현실적으로 수행하기 어려움

표본조사

  • 모집단의 성질을 아는 방법 중 하나
  • 모집단의 일부를 분석하여 모집단 전체의 설질을 추정하는 추론통계와 관련

표본 (Sample)

  • 모집단의 일부를 추출한 데이터를 의미하며, 모집단의 특성을 추정하는 데 사용
    ⇒ 통계 분석에서 모집단을 모두 조사하는 것은 어려우므로, 표본을 사용해 추론 통계를 수행한다.

통계 관련 NumPy활용

이산값(Discrete values)

중간에 소수점값이 존재할 수 없는 딱딱 끊어지는 숫자 (중간값이 존재하지 않음)

  • 범주형 데이터
  • countable (개수를 셀 때 사용)
  • '1개, 2개'는 가능하지만 1.5개는 불가능
  • 보통 정수로 표현
    e.g)
    우리가족인원수, 주사위 눈의 수, 사과개수/불량품 개수...
    얼마나 많은가? (how many)
    시각화 -> 막대그래프(bar chart)

연속값 (continuous values)

어떤 구간 내에서 어떤 값이라도 가질 수 있는 숫자. 숫자와 숫자 사이에 무한한 세분화 가능 (중간값 존재)
-측정할때 사용 (measurable)
-정확한 도구가 있다면 소수점 아래로 무한히 정밀해질수 있음
-그래프로 그리면 끊기지 않는 선의 형태
-실수형태
e.g)
키와 몸무게 (162.5cm, 53kg..)
현재 온도
시간
얼마나 큰가/긴가 (how much)
시각화 -> 히스토그램, 선 그래프 (line chart)

중심경향측도(Central Tendency): 데이터의 대표적인 위치

어떤 데이터 집합 전체를 대표할 수 있는 '중심적인 위치'가 어디인지를 나타내는 통계적 수치. 쉽게 말해 "데이터들이 보통 어느 값 근처에 모여 있는가?"를 보여주는 지표

  1. 평균 (Mean) - 모든 데이터 값을 더한 후 데이터의 개수로 나눈 값
arr = np.array([10, 20, 30, 40])
print(np.mean(arr))    # 25.0
  1. 중앙값 (Median) - 크기 순으로 정렬했을 때 한가운데 위치한 값
print(np.median(arr))  # 25.0
  1. 최빈값 (Mode) - 데이터집합에서 가장 자주 나타나는 값
from scipy import stats

arr = np.array([10, 20, 20, 30, 40])
print(stats.mode(arr))  # Mode: 20
  • 대표값으로 집약하면 데이터의 분포정도, 데이터에 포함된 최대값과 최소값 등의 정보를 알 수 없으므로 다른 정보도 확인 필요
  • 이상값(outlier): 극단적으로 큰 값이나 작은 값. 평균값은 이상값의 영향을 받기 쉬우나 중앙값은 이상값에 잘 영향을 받지 않음)
  • 대표값만으로는 알기 어려운 분포도 있기 때문에 대표값으로만 데이터를 이해하기보다 히스토그램을 그려서 대략적인 파악을 한 후, 대표값으로 적절하게 분포를 특징 지을 수 있는지 확인하는 것이 중요

산포 측도(Measures of Dispersion):

데이터가 얼마나 넓게 퍼져 있는가 or 얼마나 중심에 밀집해 있는가를 나타내는 지표

  1. 범위 (Range) - 데이터의 최대값과 최소값의 차이를 의미
print(np.max(arr) - np.min(arr))  # 30

# 범위 (range)
np.max(height) - np.min(height)
  1. 분산 (Variance) - 데이터가 평균으로부터 얼마나 떨어져있는지 / 값이 클수록 데이터가 퍼져있음을 의미.
print(np.var(arr))  # 125.0

# 분산 (Variance)
# (값 - 평균)^2의 평균
var1 = np.mean(np.square(height - np.mean(height)))  # ndarray - broadcasting 값
print(var1)

var2 = np.var(height)
print(var2)
  1. 표준편차 (Standard Deviation) - 분산의 제곱근, 데이터의 퍼짐 정도
print(np.std(arr))  # 11.1803

std1 = np.sqrt(var1)
print(std1)

std2 = np.std(height)
print(std2)
  1. 사분위수 범위 (IQR, Interquartile Range)

확률(probability) "얼마나 일어날까?"
:확률은 특정 사건이 일어날 가능성을 0(절대 안 일어남)에서 1(무조건 일어남) 사이의 숫자로 나타낸 것. 확률이 1에 가까울수록 해당 사건이 발생할 가능성이 크다.

확률분포(probability distribution) "전체적인 모양"
: 확률변수(Random Variable)가 어떤 값을 가질 확률이 어느정도인지 표나 그래프로 나타낸 지도. 데이터가 어디에 많이 모여있고 어디에 적게 퍼져있는지를 보여줌.

정규분포(Normal distribution)
: 종모양 (bell-curve) 의 분포.
모양: 가운데(평균)가 가장 높고 양옆으로 갈수록 낮아짐
관련 스케일러 - StandardScaler (데이터를 평균 0, 표준편차 1인 정규분포로 변환)

arr = np.random.randn(1000)  # 평균 0, 표준편차 1인 정규분포 난수 1000개
print(np.mean(arr), np.std(arr))
# 정규분포
# np.random.randn(shape): (평균이 0, 표준편차가 1인) 표준 정규 분포를 따르는 난수를 shape에 맞게 생성
std_arr = np.random.randn(1000)
plt.hist(std_arr, bins=100)
plt.show()
# 정규분포
# np.random.normal(loc, scale, size): 평균이 loc, 표준편차가 scale인 정규 분포를 따르는 난수를 size에 맞게 생성
# - loc: 평균
# - scale: 표준편차
# - size: shape (갯수)
nor_arr = np.random.normal(10, 1, 1000)
plt.hist(nor_arr, bins=10)
plt.show()

표준정규분포(standard normal distribution)
:모든 정규분포를 서로 비교하거나 확률을 쉽게 계산하기 위해 평균을 0으로, 표준편차를 1로 고정시킨 것
표준정규분포의 확률 (68-95-99.7 법칙)

*[ 68-95-99.7 규칙 ]

정규 분포를 나타내는 규칙으로, 경험적인 규칙(empirical rule)이라고도 한다. 

3시그마 규칙(three-sigma rule)이라고도 하는데 이 때는 평균에서 양쪽으로 3표준편차의 범위에 거의 모든 값들(99.7%)이 들어간다는 것을 나타낸다.

  • 약 68%의 값들이 평균에서 양쪽으로 1 표준편차 범위 μ±σ\mu \pm \sigma에 존재한다.
  • 약 95%의 값들이 평균에서 양쪽으로 2 표준편차 범위 μ±2σ\mu \pm 2\sigma에 존재한다.
  • 거의 모든 값들(실제로는 99.7%)이 평균에서 양쪽으로 3표준편차 범위 μ±3σ\mu \pm 3\sigma에 존재한다.
    .
    .

균등분포(uniform distribution)
: 모든 값이 동일한 확률로 발생하는 분포.
모양: 직사각형 모양 (평평함)
처음부터 꿑까지 높이가 일정함. 특정 값이 더 많이 나오지 않고 모든 구간에서 골고루 발생.
관련 스케일러 - MinMaxScaler (데이터를 0과 1 사이로 쫙 펼쳐서 균등하게 만듦)

arr = np.random.uniform(0, 1, 1000)  # 0과 1 사이의 균등분포 난수 1000개
print(np.mean(arr), np.std(arr))
# 균등분포
# np.random.rand(shape): 0.0 이상 1.0 미만 수에서 균등분포를 따르는 난수를 shape에 맞게 생성
arr = np.random.rand(1000)
plt.hist(arr, bins=10)
plt.show()
# 균등분포
# np.random.randint(start, end, size): start 이상 end 미만의 균등분포를 따르는 난수를 size에 맞게 생성
# - start: 시작값(포함)
# - end: 끝값(미만/포함X)
# - size: shape (갯수)

np.random.seed(0)   # 난수 계산 시작값 seed 고정 (재연 가능)

arr = np.random.randint(1, 100, 1000)
plt.hist(arr, bins=10)
plt.show()

ndarray 정렬

  1. 기본 배열 정렬
    NumPy 배열 정렬은 np.sort()함수를 사용하여 수행. 이 함수는 배열의 복사본을 생성하여 정렬된 배열을 반환하며, 기본적으로 오름차순으로 정렬됨.

(1) 1차원 배열 정렬 - 배열의 원소를 오름차순으로 정렬

# 1차원 배열 정렬
arr = np.random.randint(1, 11, 10) # 1이상 10미만의 균등분포
arr 

# np.sort(arr): 정렬된 ndarray를 "반환" (원본 ndarray는 영향을 받지 않음)
print(np.sort(arr)) # 오름차순
print(arr)

np.sort(arr)[::-1] # 내림차순

sorted_arr_desc = np.sort(arr)[::-1]
print(sorted_arr_desc)  # [50, 40, 30, 20, 10]

(2) 2차원 배열 정렬 - 2차원 배열은 각 행(row) 또는 열(column)을 기준으로 정렬할 수 있다. 기본적으로 각 행을 기준으로 정렬이 수행된다.

# 2차원 배열 정렬
arr = np.random.randint(10, size=(3, 4))  # 0 이상 10 미만
arr

print(np.sort(arr)) # 같은 행 안에서 열방향으로 정렬됨
                    # 기본값 axis=1

(3) 특정 축(axis)을 기준으로 정렬 - 축을 기준으로 정렬할 수 있으며, 축 0은 열(column) 을 기준으로, 축 1은 행(row) 을 기준으로 정렬

print(np.sort(arr, axis=0))  # 행 방향 정렬 (같은 열 안에서 정렬)
print(np.sort(arr, axis=1))  # 열 방향 정렬 (같은 행 안에서 정렬)

# axis=None: 축 무시 (행/열 구분없이 모든 값 정렬)
print(np.sort(arr, axis=None))
print(np.sort(arr, axis=None).shape)
  1. 인덱스를 반환하는 정렬
    np.argsort()
    배열을 정렬한 결과의 인덱스 배열을 반환
  • 배열의 값을 정렬했을 때, 각 값의 원래 인덱스를 반환
  • 정렬할 때 어떤 순서로 값을 배치해야 하는지 알려주는 인덱스를 반환
  • 배열의 값을 정렬한 후, 정렬된 순서에 해당하는 인덱스를 반환
# 1차원 배열에서 argsort 사용
arr = np.array([100, 400, 200, 300])
print(np.sort(arr))                 # [100 200 300 400]
print(np.argsort(arr))              # [0 2 3 1]

names = np.array(['리사', '로제', '제니', '지수'])
album_sales = np.array([900, 870, 1000, 670])

#argsort() -> 앨번판매량이 많은 순으로 멤버 이름을 출력
sales_desc = np.argsort(album_sales)[::-1]
print(names[sales_desc])

# 2차원 배열에서 argsort -> 특정 축을 기준으로 인덱스 반환 
**어떤 순서로 값을 배치해야 정렬이 되는가?**

arr = np.array([[2026, 1, 21], 
                [14, 43, 12]])

print(np.argsort(arr))           # 기본값 axis=1
[[1 2 0]
 [2 0 1]]
print(np.argsort(arr, axis=0))   # 열(세로) 방향으로 정렬 인덱스 추출
[[1 0 1]
 [0 1 0]]
print(np.argsort(arr, axis=1))   # 행(가로) 방향으로 정렬 인덱스 추출
[[1 2 0]
 [2 0 1]]

ndarray 병합 및 분할

배열병합: 여러 개의 배열을 연결하여 하나의 배열로 만드는 방법
NumPy에서 배열을 병합하는 방법: np.concatenate(), np.vstack(), np.hstack() 등의 함수를 사용

ndarray 요소 추가 및 삭제

  • ndarray는 고정 길이 배열 == 크기 변경이 불가 == 요소를 추가/삭제할 수 없음

(1)np.insert()

요소를 추가한 새로운 배열을 반환

# 1차원 배열에서 insert()
arr = np.array([10, 20, 30])

arr1 = np.insert(arr, 1, 15)    # insert(원본배열, 삽입할 위치 인덱스, 삽입할 요소)
arr2 = np.append(arr, 100)      # append(원본배열, 삽입할 요소) / 

※ numpy에서 append()는 병합 함수이므로, 아래 병합에서 다시 볼 것!

print(arr1)      # [10 15 20 30]
print(arr2)      # [ 10 20 30 100]

# is 연산자: 두 객체(인스턴스)가 동일한지 비교
print(arr1 is arr) 		# False
print(arr2 is arr) 		# False

# 2차원 배열에서 insert()
arr_2d = np.arange(1, 13).reshape(3, 4)  # 1 이상부터 13 미만
print(arr_2d)

print(np.insert(arr_2d, 1, 100))         # axis=None 축 무시 (1차원 배열로 변환 후 삽입)
print(np.insert(arr_2d, 1, 100).shape)  
# print(np.insert(arr_2d, 1, 999, axis=0))                  # axis=0: 행 기준
print(np.insert(arr_2d, 1, [999], axis=0))
# print(np.insert(arr_2d, 1, [777, 888], axis=0))           # 브로드캐스팅 연산이 가능한 스칼라값 또는 배열만 insert 가능
# print(np.insert(arr_2d, 1, [777, 888, 999], axis=0))      # 브로드캐스팅 연산이 가능한 스칼라값 또는 배열만 insert 가능
print(np.insert(arr_2d, 1, [777, 888, 999, 1010], axis=0))

# print(np.insert(arr_2d, 1, 999, axis=1))                  # axis=1: 열 기준
print(np.insert(arr_2d, 1, [999], axis=1))
# print(np.insert(arr_2d, 1, [777, 888], axis=1))           # 브로드캐스팅 연산이 가능한 스칼라값 또는 배열만 insert 가능
print(np.insert(arr_2d, 1, [777, 888, 999], axis=1))

(2)np.delete()
요소를 삭제한 새로운 배열 반환

1차원 배열에서 delete()

arr = np.arange(0, 15)

arr_del = np.delete(arr, 7) # delete(원본배열, 삭제할 위치 인덱스)

print(arr_del)
print(arr_del is arr)

2차원 배열에서 delete()

arr_2d = np.arange(1, 13).reshape(3, 4)
print(arr_2d)

print(np.delete(arr_2d, 1))         # axis=None: 축 무시 (1차원 배열로 변환 후 삭제)
print(np.delete(arr_2d, 1, axis=0)) # axis=0: 행 기준
print(np.delete(arr_2d, 1, axis=1)) # axis=1: 열 기준

ndarray 병합

  • np.append()
    - 인자로 넘어온 ndarray들을 병합
    - axis 파라미터의 기본값은 None으로 축을 무시함 (1차원으로 변환 후 병합)
  • np.vstack() & np.hstack()
    - np.vstack(): 수직으로 배열을 병합하는 함수, 여러 배열을 위아래로 병합할 수 있음
    • np.hstack(): 수평으로 배열을 병합하는 함수, 여러 배열을 좌우로 병합할 수 있음
      - 병합대상 ndarray를 튜플/리스트로 묶어서 전달해야 함
    • 묶어서 전달한 ndarray들을 vstack은 행(row) 기준, hstack은 열(column) 기준으로 병합
  • np.concatenate() - 축 기준 배열 병합
    - 주어진 축(axis)을 기준으로 배열을 병합하는 함수.
    - 병합 대상 ndarray를 튜플/리스트로 묶어서 전달해야 함
    - axis 파라미터의 기본값은 0으로 행 기준(방향)임
arr1 = np.arange(1, 13).reshape(3, 4)
arr2 = np.arange(101, 113).reshape(3, 4)
arr1, arr2

print(np.append(arr1, arr2))        # axis=None
print(np.append(arr1, arr2).shape)

print(np.append(arr1, arr2, axis=0))
print(np.append(arr1, arr2, axis=1))

print(np.vstack((arr1, arr2)))  # axis=0 병합
print(np.hstack([arr1, arr2]))  # axis=1 병합

# append(), hstack(), vstack()과의 공통점/차이점
# -> axis 설정 기본값
print(np.concatenate((arr1, arr2)))  # axis=0 기본값
# print(np.concatenate([arr1, arr2]))

print(np.concatenate((arr1, arr2), axis=0))
print(np.concatenate((arr1, arr2), axis=1))
print(np.concatenate((arr1, arr2), axis=None))


# 1차원 배열 병합 - 두 개 이상의 1차원 배열을 연결할 수 있다.
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
merged_arr = np.concatenate((arr1, arr2))
print(merged_arr)  # [1, 2, 3, 4, 5, 6]


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

# 2차원 배열 병합 (축 0) - 행을 기준으로 배열을 병합할 수 있다. 이 경우 배열들의 열 개수가 같아야 한다.
merged_arr = np.concatenate((arr1, arr2), axis=0)
print(merged_arr)
# [[1, 2],
#  [3, 4],
#  [5, 6],
#  [7, 8]]

# 2차원 배열 병합 (축 1) - 열을 기준으로 배열을 병합할 수 있다. 이 경우 배열들의 행 개수가 같아야 한다.
merged_arr = np.concatenate((arr1, arr2), axis=1)
print(merged_arr)
# [[1, 2, 5, 6],
#  [3, 4, 7, 8]]

# 새로운 축을 따라 배열 병합
```np.stack()```: 새로운 축을 따라 배열 병합 -> 기존 배열에 차원을 추가한 후 그 축을 따라 배열을 쌓을 수 있다.


arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

# 1. 0축을 따라 배열 병합
stacked_arr = np.stack((arr1, arr2), axis=0)
print(stacked_arr)
# [[1, 2, 3],
#  [4, 5, 6]]

# 2. 1축을 따라 배열 병합
stacked_arr = np.stack((arr1, arr2), axis=1)
print(stacked_arr)
# [[1, 4],
#  [2, 5],
#  [3, 6]]

ndarray 분할

np.split()

arr = np.concatenate((arr1, arr2))
print(arr.shape)

# split (원본배열, 분할할 개수, axis) / 기본값  axis=0
# np.split(arr, 2)
np.split(arr, 2, axis=0)
np.split(arr, 2, axis=1)

#※ 약수를 사용해서 분할할 개수를 정해야 함

# arr의 shape: (6, 4)
n = 2
print(np.split(arr, n, axis=0)) # 행 기준(방향) -> 6행: 1, 2, 3, 6
print(np.split(arr, n, axis=1)) # 열 기준(방향) -> 4열: 1, 2, 4

ndarray shape(형태) 변경

  • reshape(): 요소의 개수가 맞는 shape으로 변형 가능 (ndarray.size는 유지)
arr = np.arange(12) # 0 이상 12 미만 
print(arr.shape)                                   # (12,)

arr = arr.reshape(3, 4)  # 3행 4열 (3 * 4 = 12))
arr = arr.reshape(6, 2)  # 6행 2열 (6 * 2 = 12)
arr = arr.reshape(1, 12) # 1행 12열 (1 * 12 = 12)
# arr = arr.reshape(7, 1) 
# arr = arr.reshape(7, 2)
print(arr.shape)                                   # (1, 12)

arr = arr.reshape(3, 2, 2) # 3면 2행 2열 (3 * 2 * 2 = 12)
print(arr.shape)                                   # (3, 2, 2)
print(arr)   
# [[[ 0  1]
#   [ 2  3]]

#  [[ 4  5]
#   [ 6  7]]

#  [[ 8  9]
#   [10 11]]]
# 여기까지 arr의 shape = (3, 2, 2)

# reshape(-1) == 1차원 처리 (모든 숫자를 직접 계산하기 귀찮을 때 -1을 쓰면 NumPy가 알아서 계산)
arr = arr.reshape(-1)
print(arr)
print(arr.shape)
# 여기까지 arr의 shape = (12,)
# shape의 요소 중 하나만 -1 == -1 인 축은 자동 계산
arr = arr.reshape(4, -1)
arr = arr.reshape(-1, 6)
print(arr)
print(arr.shape)
  • np.ravel()
    1차원 변환
arr = np.arange(50).reshape(5, 5, 2) # -> 0부터 49까지 50개의 숫자를 생성하여 (5, 5, 2) 모양으로 재배열
print(arr) # 5개의 층(page)이 있고, 각 층은 5행2열인 구조
print(np.ravel(arr)) # 1차원으로 평평하게 펴기 (1차원 배열로 돌아감)
  • np.expand.dims()
    차원 추가
arr_1d = np.arange(10)  # (10,)

print(np.expand_dims(arr_1d, axis=0))   # (1, 10) 행(row)차원 추가 / 1행10열의 행 벡터
print(np.expand_dims(arr_1d, axis=1))   # (10, 1) 열(column)차원 추가 / 10행1열의 열 벡터
# print(np.expand_dims(arr_1d, axis=None))   # [[0 1 2 3 4 5 6 7 8 9]]
# print(np.expand_dims(arr_1d, axis=2))
[[0]
 [1]
 [2]
 [3]
 [4]
 [5]
 [6]
 [7]
 [8]
 [9]]
arr_2d = np.array([[1, 2], [3, 4]])   #(2, 2) 차원을 추가

print(np.expand_dims(arr_2d, axis=0).shape)       # (1, 2, 2)
print(np.expand_dims(arr_2d, axis=1).shape)       # (2, 1, 2)
print(np.expand_dims(arr_2d, axis=2).shape)       # (2, 2, 1)
  • np.squeeze()
  • 차원의 값(size)-해당 차원에 있는 요소의 개수-이 1일 때 제거
    • (+) np.squeeze()는 axis를 지정하지 않으면 차원 값이 1인걸 다 제거
    • (+) np.squeeze()에서 차원 값이 1이라도 남기고 싶으면, axis를 특정 차원으로 지정해야 함
arr = np.array([[1, 2]])  
print(arr.shape)                     #(1, 2) 

arr_sq = np.squeeze(arr)
arr_sq = np.squeeze(arr, axis=0)
# arr_sq = np.squeeze(arr, axis=1)
print(arr_sq, arr_sq.shape)          # [1 2] (2,)
arr = np.array([[1], [2]])  
print(arr.shape)                     #(2, 1)

arr_sq = np.squeeze(arr)
# arr_sq = np.squeeze(arr, axis=0)
arr_sq = np.squeeze(arr, axis=1)
print(arr_sq, arr_sq.shape)          # [1 2] (2,)
arr = np.array([[[[[[[[[[1, 2]]]]]]]]]])

print(arr.shape, arr.ndim)            # (1, 1, 1, 1, 1, 1, 1, 1, 1, 2) 10

print(np.squeeze(arr))                # [1 2]
print(np.squeeze(arr).shape)          # (2,)

[심화 파트]

NumPy 성능 비교 및 최적화

  1. Python 리스트와 NumPy 배열 성능 비교
  • NumPy는 내부적으로 C 언어로 구현되어 있어, Python의 기본 리스트보다 훨씬 더 빠르고 메모리 효율적이다.

  • Python 리스트의 성능 측정

import time
size = 1000000
python_list = list(range(size))

start_time = time.time()
python_list = [x * 2 for x in python_list]
print("Python 리스트 성능:", time.time() - start_time)
  • NumPy 배열의 성능 측정
import numpy as np
np_array = np.arange(size)

start_time = time.time()
np_array = np_array * 2
print("NumPy 배열 성능:", time.time() - start_time)

⇒ 대용량 데이터에서 NumPy 배열이 Python 리스트에 비해 얼마나 빠른지 성능 차이를 확인할 수 있다.

  1. 벡터화(Vectorization)로 성능 최적화
  • 반복문을 사용하지 않고 벡터화 연산으로 성능을 최적화

  • 반복문을 사용하는 방식

size = 1000000
python_list = list(range(size))
result = []

start_time = time.time()
for x in python_list:
    result.append(x * 2)
print("반복문 성능:", time.time() - start_time)
  • 벡터화를 사용하는 방식
np_array = np.arange(size)

start_time = time.time()
np_array = np_array * 2  # 벡터화 연산
print("벡터화 성능:", time.time() - start_time)

⇒ 벡터화 연산은 루프를 사용하지 않고, 데이터를 한 번에 처리하므로 훨씬 더 빠른 성능을 제공한다.

메모리 사용 최적화

  • 배열의 데이터 타입 변경
    • NumPy 배열의 데이터 타입은 배열이 사용하는 메모리 크기에 영향을 미친다. 적절한 데이터 타입을 선택하면 메모리 사용을 절감할 수 있다.
    • 기본 데이터 타입과 메모리 사용량 확인
arr = np.array([1, 2, 3], dtype='int64')
print(arr.nbytes)  # 배열이 사용하는 메모리 크기 (bytes)
  • 데이터 타입을 int32로 변경하여 메모리 절약
arr = np.array([1, 2, 3], dtype='int32')
print(arr.nbytes)  # 더 작은 메모리 사용

결론: 큰 데이터셋을 처리할 때는 데이터 타입을 적절히 조정해 메모리 효율성을 높일 수 있다.

대용량 데이터 처리

  1. 대용량 데이터로 연산 성능 테스트
  • 대규모 배열로 NumPy 성능을 테스트하고, 처리 성능을 최적화
  • 대용량 배열 생성 및 연산
size = 100000000  # 1억 개의 요소
arr = np.random.rand(size)  # 난수 배열 생성

start_time = time.time()
result = np.sum(arr)  # 배열의 합 계산
print("대용량 데이터 합계 성능:", time.time() - start_time)
  1. 메모리 맵핑을 통한 대용량 파일 처리
  • np.memmap()을 사용하면, 전체 데이터를 메모리에 올리지 않고 파일로부터 필요한 부분만 메모리에 로드해 작업을 수행할 수 있음
  • 메모리 맵핑을 사용하여 파일 읽기
# 1GB 크기의 파일을 생성 후 메모리 맵핑 방식으로 읽기
size = 10**9
arr = np.memmap('large_file.dat', dtype='float32', mode='w+', shape=(size,))
arr[:100] = np.random.rand(100)  # 일부 데이터를 파일에 쓰기

# 파일을 메모리 맵핑하여 읽기
memmap_arr = np.memmap('large_file.dat', dtype='float32', mode='r', shape=(size,))
print(memmap_arr[:10])  # 메모리에 로드된 일부 데이터만 접근

⇒ 메모리 크기를 초과하는 대용량 데이터를 처리할 때 np.memmap()을 사용하여 메모리 부담을 줄일 수 있다.

profile
코린이

0개의 댓글