7, 9장 바로가기
5장 사이파이를 사용한 기본 확률 및 통계 분석
예산이 제한된다면 데이터도 제한 될 수 밖에 없고, 데이터와 투입 가능한 자원 간 절충이 현대 통계의 핵심.
통계의 목적 : 데이터 크기가 제한된 경우에서도 데이터에서 숨겨진 의미를 찾는 것
5.1 사이파이로 데이터와 확률 간 관계 탐색하기
사이파이
from scipy import stats
- 과학적 파이썬의 줄인말
- 과학적 분석에 유용한 여러 기능을 제공
- 확률과 통계 문제 해결용으로 만들어진 전용 모듈 scipy.stats를 포함함
- scipy.stats 모듈은 데이터의 임의성 평가에 매우 유용
- stats.binom_test 메서드 : 이항 분포, 확률을 측정할 수 있음.
num_heads = 16
num_flips = 20
prob_head = 0.5
prob = stats.binomtest(num_heads, num_flips, prob_head)
print(f"15개 이상의 동전 앞면 또는 뒷면이 관찰될 확률은 {prob.pvalue:.17f}입니다.")
- 정확히 앞면 16개가 관측될 확률을 구하려면 stats.binom.pmf 메서드를 사용해야한다.
- stats.binom.pmf 메서드는 이항 분포의 확률 질량 함수를 표현한다.
- 확률 질량 함수는 입력 정수 값들을 각 값들이 발생할 확률에 매핑한다.
prob_16_heads = stats.binom.pmf(num_heads, num_flips, prob_head)
print(f"{num_heads}번의 앞면 중 {num_flips}번이 관측될 확률은 {prob_16_heads}입니다.")
- 정확히 동전 앞면이 16번 관측될 확률 구하기 위해 stats.binom.pmf 메서드를 사용함.
- 하지만 이 메서드는 여러 확률을 동시에 계산할 수도 있다.
probabilities = stats.binom.pmf([4, 16], num_flips, prob_head)
assert probabilities.tolist() == [prob_16_heads] * 2
- stats.binom.pmf([4, 16], num_flips, prob_head)를 실행하면 동일한 두 요소를 가진 배열이 반환됨.
- 앞면 4번과 뒷면 16번을 관측할 확률 = 뒷면 4번, 앞면 16번을 볼 확률과 동일하기 때문임.
interval_all_counts = range(21)
probabilities = stats.binom.pmf(interval_all_counts, num_flips, prob_head)
total_prob = probabilities.sum()
print(f"확률의 총합은 {total_prob:.14f}입니다.")
- interval_all_counts에 따른 확률을 그래프로 나타내면 분포도를 얻을 수 있음.
import matplotlib.pyplot as plt
plt.plot(interval_all_counts, probabilities)
plt.xlabel('동전 앞면의 등장 횟수')
plt.ylabel('확률')
plt.show()

- stats.binom.pmf 메서드 덕분에 분포도를 시각화 할 수 있었다.
flip_counts = [20, 80, 140, 200]
linestyles = ['-', '--', '-.', ':']
colors = ['b', 'g', 'r', 'k']
for num_flips, linestyle, color in zip(flip_counts, linestyles, colors):
x_values = range(num_flips + 1)
y_values = stats.binom.pmf(x_values, num_flips, 0.5)
plt.plot(x_values, y_values, linestyle=linestyle, color=color,
label=f'{num_flips} 회의 동전 뒤집기')
plt.legend()
plt.xlabel('동전 앞면의 등장 횟수')
plt.ylabel('확률')
plt.show()

- 각 이항 분포의 최대 확률 지점은 동전 뒤집기를 많이 시행할수록 오른쪽으로 이동함.
- 분포도 중심이 오른쪽으로 이동할수록 그들의 분포는 중심에서 더욱 분산되는 것을 알 수 있음.
- 이런 분포 차이를 정량화 할 필요가 있음.
- 중심성 및 분산에 구체적인 수치를 부여하여 분포도마다 각 수치가 변하는 방식을 파악해야함.
5.2 중심성의 척도로서 평균
import numpy as np
measurements = np.array([80, 77, 73, 61, 74, 79, 81])
- measurements.sort() 메서드를 호출해 측정값들을 순서대로 정렬
- 정렬된 측정치를 그래프로 나타내어 중심 값을 판단해볼 수 있음.
measurements.sort()
number_of_days = measurements.size
plt.plot(range(number_of_days), measurements)
plt.scatter(range(number_of_days), measurements)
plt.ylabel('온도')
plt.show()

- 중심온도가 대충 70도인 것을 추정 가능
difference = measurements.max() - measurements.min()
midpoint = measurements.min() + difference / 2
assert midpoint == (measurements.max() + measurements.min()) / 2
print(f"중간 온도는 {midpoint}도 입니다 ")
- 최저 온도와 최고 온도의 중앙값을 정량적으로 파악하는 방법은 최저 및 최고 온도 값을 더한 뒤 2로 나누어 얻거나 최고 온도에서 최저 온도를 뺀 뒤 다시 최저 온도를 더하는 방식으로 알 수 있음.
plt.plot(range(number_of_days), measurements)
plt.scatter(range(number_of_days), measurements)
plt.axhline(midpoint, color='k', linestyle='--')
plt.ylabel('온도')
plt.show()
- 중앙값 : 측정치들을 균등하게 두 부분으로 나눌 수 있는 값을 말한다.
- 다음 그래프는 최저 및 최고 온도의 중간 값을 나타냄

median = measurements[3]
print(f"온도의 중앙값은 {median}도 입니다")
plt.plot(range(number_of_days), measurements)
plt.scatter(range(number_of_days), measurements)
plt.axhline(midpoint, color='k', linestyle='--', label='중간지점')
plt.axhline(median, color='g', linestyle='-.', label='중앙값')
plt.legend()
plt.ylabel('온도')
plt.show()

- 77도의 중앙값은 온도를 반으로 나누고, 장앙값은 낮은 온도 세 개보다 높은 온도 세 개에 더 가까워 균형이 맞지 않는 것처럼 보임.
- 여기에 페널티를 부여하면 이 불균형을 맞춰줄 수있음.
def squared_distance(value1, value2):
return (value1 - value2) ** 2
possible_centers = range(measurements.min(), measurements.max() + 1)
penalties = [squared_distance(center, 61) for center in possible_centers]
plt.plot(possible_centers, penalties)
plt.scatter(possible_centers, penalties)
plt.xlabel('가능한 중앙값')
plt.ylabel('페널티')
plt.show()
- 페널티는 두 값 간의 차이를 제곱한 거리의 제곱으로 구현할 수 있음
- 거리의 제곱은 두 값이 멀어질수록 네 제곱으로 증가함.

- 최솟값에서 떨어진 거리를 기준으로 가능한 중심 범위에 걸친 페널티를 보여줌
def sum_of_squared_distances(value, measurements):
return sum(squared_distance(value, m) for m in measurements)
penalties = [sum_of_squared_distances(center, measurements)
for center in possible_centers]
plt.plot(possible_centers, penalties)
plt.scatter(possible_centers, penalties)
plt.xlabel('가능한 중심값')
plt.ylabel('페널티')
plt.show()
- 측정치 일곱 개에 대한 제곱 거리를 기반으로 각 잠재적인 중심에 페널티를 부여해야함.
- 이를 위해 특정 값과 측정된 값들의 배열 간 제곱 거리를 모두 더하는 함수를 정의
- 이 함수가 새로 부여될 페널티를 결정할 것임.
- 가능한 중심과 페널티를 비교해 그래프를 그리면 페널티가 최소화되는 중심을 찾을 수 있음.

least_penalized = 75
assert least_penalized == possible_centers[np.argmin(penalties)]
plt.plot(range(number_of_days), measurements)
plt.scatter(range(number_of_days), measurements)
plt.axhline(midpoint, color='k', linestyle='--', label='중간지점')
plt.axhline(median, color='g', linestyle='-.', label='중앙값')
plt.axhline(least_penalized, color='r', linestyle='-', label='패널티가 가장 낮은 중심')
plt.legend()
plt.ylabel('온도')
plt.show()
- 75도에 가장 낮은 페널티가 부여됨. 이를 페널티가 가장 낮은 중심이라 부르겠다.
- 여기서 저 페널티가 가장 낮은 중심은 중앙값에 비해 최저 온도에 더 가까운 거리를 제공하면서 동시에 데이터를 균형 있게 분할함.
- 페널티가 가장 낮은 중심은 중심성을 나타내는 좋은 척도이다.
- 중심과 모든 값 사이의 거리를 균형 잡히게 만들어 준다.

assert measurements.sum() / measurements.size == least_penalized
- 측정값을 모두 더한 뒤 배열 크기로 나누는 방식 → 페널티가 가장 낮은 중심을 바로 계산할 수 있음.
mean = measurements.mean()
assert mean == least_penalized
assert mean == np.mean(measurements)
assert mean == np.average(measurements)
- 배열 값들을 모두 더한 뒤 배열 크기로 나누는 것을 공식적으로 산술 평균이라고 함.
- 배열의 평균이라고도 함.
- 평균은 넘파이 배열의 mean 메서드를 호출하면 쉽게 계산할 수 있음.
equal_weights = [1] * 7
assert mean == np.average(measurements, weights=equal_weights)
unequal_weights = [100] + [1] * 6
assert mean != np.average(measurements, weights=unequal_weights)
- np.mean or np.average 메서드로 평균 계산할 수도 있음.
- np.mean과 np.average 메서드는 다름. 별도의 weights 라는 매개변수를 선택적으로 입력할 수 있기 때문
- 이 매개변수는 각 측정값이 다른 측정값에 비해 상대적으로 얼마나 중요한지 나타내는 가중치 리스트
- 모든 가중치가 균등하다면 np.mean와 np.average의 결과는 동일할 것이다.
weighted_mean = np.average([75, 77], weights=[9, 1])
print(f"평균은 {weighted_mean}입니다.")
assert weighted_mean == np.mean(9 * [75] + [77]) == weighted_mean
- 가중치 매개변수는 중복된 값들의 평균을 계산하는데 유용함.
- 중복된 값이 있는 상황에서 가중된 평균을 계산하면 일반적인 평균을 더 빠르게 얻을 수 있음.
- 고윳값들에 대한 상대적인 비율은 가중치 비율로 표현됨.
assert weighted_mean == np.average([75, 77], weights=[900, 100])
assert weighted_mean == np.average([75, 77], weights=[0.9, 0.1])
- 확률을 가중치로 취급할 수 있는 것. 따라서 어떤 확률 분포에서도 평균을 계산할 수 있다고 해석됨.
5.2.1 확률 분포의 평균 구하기
num_flips = 20
interval_all_counts = range(num_flips + 1)
probabilities = stats.binom.pmf(interval_all_counts, 20, prob_head)
mean_binomial = np.average(interval_all_counts, weights=probabilities)
print(f"이항 분포의 평균은 동전 앞면이 {mean_binomial:.2f}번 등장할 때입니다.")
plt.plot(interval_all_counts, probabilities)
plt.axvline(mean_binomial, color='k', linestyle='--')
plt.xlabel('동전의 앞면 수')
plt.ylabel('확률')
plt.show()

- 가장 높은 확률은 분포의 평균과 어떻게 다를까? np.average메서드의 가중치 매개변수에 확률들이 담긴 배열을 입력하면 평균을 구할 수 있음.
- 해당 평균을 분포를 가로지르는 수직선으로 그래프에 표현 가능
- 사이파이는 단순히 stats.binom.mean을 호출하는 것만으로도 모든 이항 분포의 평균을 쉽게 구함.
- stats.binom.mean 메서드를 호출할 때는 동전 뒤집기 횟수 및 동전 앞면이 나올 확률을 매개변수로 입력해야 함.
assert stats.binom.mean(num_flips, 0.5) == 10
- stats.binom.mean 메서드를 사용하면 이항 분포의 중심성과 동전 뒤집기 횟수 사이의 관계를 엄격히 분석 할 수 있음.
means = [stats.binom.mean(num_flips, 0.5) for num_flips in range(500)]
plt.plot(range(500), means)
plt.xlabel('동전 뒤집기 횟수')
plt.ylabel('평균')
plt.show()
- 동전 뒤집기 횟수와 분포 평균은 선형적인 관계
- 동전이 앞면으로 떨어질 균등한 확률은 베르누이 분포의 평균과 같음.
num_flips = 1
assert stats.binom.mean(num_flips, 0.5) == 0.5

num_flips = 1000
assert stats.binom.mean(num_flips, 0.5) == 500
interval_all_counts = range(num_flips)
probabilities = stats.binom.pmf(interval_all_counts, num_flips, 0.5)
plt.axvline(500, color='k', linestyle='--')
plt.plot(interval_all_counts, probabilities)
plt.xlabel('동전의 앞면 수')
plt.ylabel('확률')
plt.show()

- 관측된 선형 관계를 사용하면 동전을 1000번 뒤집은 분포의 평균을 예측할 수 있음.
5.3 흩어진 정도를 측정하는 분산
- 흩어짐은 일부 중심 값 주변에 데이터가 흩어진 것을 의미
- 흩어진 정도가 작을수록 데이터에 대한 예측 가능성이 높아짐.
- 반면 흩어진 정도가 크다면 데이터의 출렁거림이 심할 것임.
- 캘리포니아주와 켄터키주의 여름 기온을 측정하는 시나리오 가정
california = np.array([52, 77, 96])
kentucky = np.array([71, 75, 79])
print(f"캘리포니아 주의 평균 온도는 {california.mean()}입니다.")
print(f"켄터키 주의 평균 온도는 {california.mean()}입니다.")
- 각 주의 평균 온도
plt.plot(range(3), california, color='b', label='캘리포니아')
plt.scatter(range(3), california, color='b')
plt.plot(range(3), kentucky, color='r', linestyle='-.', label='켄터키')
plt.scatter(range(3), kentucky, color='r')
plt.axhline(75, color='k', linestyle='--', label='평균')
plt.legend()
plt.show()

- 두 측정 배열을 그래프로 표현해 이러한 분산 차이를 시각화
- 수평선을 그려서 평균도 구분
def sum_of_squares(data):
mean = np.mean(data)
return sum(squared_distance(value, mean) for value in data)
california_sum_squares = sum_of_squares(california)
print(f"캘리포니아 온도의 제곱합은 {california_sum_squares}입니다.")
- 측정된 값과 평균 사이의 제곱 거리 합을 계산
- 평균에서 거리의 제곱합을 단순히 제곱합이라 함.
- 따라서 sum_of_squares라는 함수를 정의 이를 캘리포니아주에서 측정된 온도에 적용
kentucky_sum_squares = sum_of_squares(kentucky)
print(f"켄터키 온도의 제곱합은 {kentucky_sum_squares}입니다.")
- 캘리포니아주의 제곱합은 974가 나옴.
- 켄터키는 32가 나옴.
- 제곱합은 이 흩어진 정도를 측정하는 데 유용함. 다만 측정이 완벽하지는 않음.
california_duplicated = np.array(california.tolist() * 2)
duplicated_sum_squares = sum_of_squares(california_duplicated)
print(f"복제된 캘리포니아 온도의 제곱합은 {duplicated_sum_squares}입니다.")
assert duplicated_sum_squares == 2 * california_sum_squares
- 배열 복제 후 제곱합으로 계산
value1 = california_sum_squares / california.size
value2 = duplicated_sum_squares / california_duplicated.size
assert value1 == value2
- 제곱합은 입력된 배열 크기에 영향을 받기에 흩어진 정도를 측정하기 좋은 수단은 아님.
- 하지만 다행히 배열 크기로 제곱합을 나누어 이러한 영향을 쉽게 제거 가능함.
def variance(data):
mean = np.mean(data)
return np.mean([squared_distance(value, mean) for value in data])
assert variance(california) == california_sum_squares / california.size
- 제곱을 측정된 배열 크기로 나누는 것 = 분산임
- 분산 : 평균으로부터의 평균 제곱 거리
assert variance(california) == variance(california_duplicated)
california_variance = variance(california)
kentucky_variance = variance(kentucky)
print(f"캘리포니아의 온도에 대한 분산은 {california_variance}입니다.")
print(f"켄터키의 온도에 대한 분산은 {kentucky_variance}입니다.")
- 분산은 흩어진 정도를 측정하기 좋은 수단임.
- 파이썬 리스트 또는 넘파이 배열에 대해 np.var 함수를 호출하면 쉽게 계산 가능
- 넘파이 배열을 사용할 때는 내장된 var 메서드로도 계산 가능
assert california_variance == california.var()
assert california_variance == np.var(california)
- 분산은 평균에 따라 달라짐 가중된 평균을 계산한다면 분산 또한 가중된 분산을 계산해야함.
- 가중된 분산은 단순히 가중된 평균으로부터의 모든 거리 제곱의 평균을 계산하면 구할 수 있음.
def weighted_variance(data, weights):
mean = np.average(data, weights=weights)
squared_distances = [squared_distance(value, mean) for value in data]
return np.average(squared_distances, weights=weights)
assert weighted_variance([75, 77], [9, 1]) == np.var(9 * [75] + [77])
- 데이터 리스트와 가중치를 매개변수로 입력받으며, np.average 함수로 평균으로부터의 거리 제곱에 대한 가중 평균을 계산
5.3.1 확률 분포의 분산 구하기
공정하게 20번 시행한 동전 뒤집기에 대한 이항분포의 분산을 계산해보자.
interval_all_counts = range(21)
probabilities = stats.binom.pmf(interval_all_counts, 20, prob_head)
variance_binomial = weighted_variance(interval_all_counts, probabilities)
print(f"이항 분포의 분산은 동전 앞면이{variance_binomial:.2f}번 등장했을 때입니다")
- 이항 분포의 분산은 5, 이는 이항 분포 평균의 절반에 해당
- 사이파이의 stats.binom.var 함수를 사용하면 이 분산을 더욱 빠르고 직접적으로 계산 가능
assert stats.binom.var(20, prob_head) == 5.0
assert stats.binom.var(20, prob_head) == stats.binom.mean(20, prob_head) / 2
- stats.binom.var함수를 사용하면 이항 분포의 흩어진 정도와 동전을 뒤집은 횟수 사이의 관계를 엄격히 분석할 수 있음.

stats.binom.var(num_flips, prob_head)
variances = []
for num_flips in range(500):
variances.append([stats.binom.var(num_flips, prob_head)])
plt.plot(range(500), variances)
plt.xlabel('동전 뒤집기 횟수')
plt.ylabel('분산')
plt.show()

assert stats.binom.var(1, 0.5) == 0.25
assert stats.binom.var(1000, 0.5) == 250
- 분산은 동전 뒤집기 횟수의 1/4에 해당, 따라서 동전을 뒤집기 횟수가 1인 베르누이 분포의 분산은 0.25
data = [1, 2, 3]
standard_deviation = np.std(data)
assert standard_deviation ** 2 == np.var(data)
- 표준 편차는 분산의 제곱근임, np.std 함수로 쉽게 계산 가능하다.
- 이를 제곱하면 다시 분산을 얻을 수 있다
- 단위를 더 쉽게 추적하는 수단으로 분산 대신 표준 편차를 많이 사용한다.
- 평균과 표준 편차는 매우 유용한 값으로 수치형 데이터셋비교/확률분포 비교/ 수치형 데이터셋과 확률 분포 비교 등 가능하다.
6장 사이파이와 중심 극한 정리로 예측하기
- 정규 분포는 3장에서 소개된 종 모양의 곡선
- 중심 극한 정리 때문에 임의의 데이터 샘플링에서 자연스럽게 발생하는 것
- 반복적으로 샘플링된 빈도가 정규 분포 곡선의 모양을 만들어 낸다
- 이 정리는 각 빈도 표본의 크기가 커질수록 곡선의 폭은 좁아진다고 예측함.
- 분포의 표준 편차는 표본 크기가 증가할수록 줄어든다.
6.1 사이파이로 정규 분포 다루기
- 표본은 배열을 가지고, 배열 길이는 표본 크기이다.
- 동전 뒤집기 샘플에 대한 히스토그램을 그려 정규 분포 곡선을 생성해 보자.
np.random.seed(0)
sample_size = 10000
sample = np.array([np.random.binomial(1, 0.5) for _ in range(sample_size)])
head_count = sample.sum()
head_count_frequency = head_count / sample_size
assert head_count_frequency == sample.mean()
- 임의의 단일 샘플에 대한 동전 앞면 빈도를 계산하고 그 빈도와 평균의 관계를 확인
np.random.seed(0)
frequencies = np.random.binomial(sample_size, 0.5, 100000) / sample_size
- 샘플 10만 번에 대한 동전 앞면 빈도를 코드 한 줄로 계산 할 수도 있다.
sample_means = frequencies
likelihoods, bin_edges, _ = plt.hist(sample_means, bins='auto', edgecolor='black', density=True)
plt.xlabel('구간별 샘플 평균')
plt.ylabel('상대적 확률')
plt.show()

- 히스토그램을 이용해 sample_means에 담긴 데이터를 시각화
- 히스토그램이 정규 분포와 같은 모양을 띄는 것을 알 수 있음
mean_normal = np.average(bin_edges[:-1], weights=likelihoods)
var_normal = weighted_variance(bin_edges[:-1], likelihoods)
std_normal = var_normal ** 0.5
print(f"평균은 약 {mean_normal:.2f}입니다.")
print(f"표준 편차는 약 {std_normal:.3f}입니다.")
- 해당 분포의 평균과 표준 편차를 계산함.
import math
peak_x_value = bin_edges[likelihoods.argmax()]
print(f"평균은 약 {peak_x_value:.2f}입니다.")
peak_y_value = likelihoods.max()
std_from_peak = (peak_y_value * (2* math.pi) ** 0.5) ** -1
print(f"표준 편차는 약 {std_from_peak:.3f}입니다.")
- 분포의 평균은 약 0.5, 표준 편차는 0.005이다.
- 정규 분포에서 이 값들은 분포의 가장 높은 지점을 통해 즉시 계산될 수 있음.
fitted_mean, fitted_std = stats.norm.fit(sample_means)
print(f"평균은 약 {fitted_mean:.2f}입니다.")
print(f"표준 편차는 약 {fitted_std:.3f}입니다.")
- 평균과 표준 편차는 stats.norm.fit(sample_means)를 호출해 간단히 계산할 수 있음.
- 주어진 데이터로 형성된 정규 분포를 다시 생성할 수 있는 두 값을 반환한다.
normal_likelihoods = stats.norm.pdf(bin_edges, fitted_mean, fitted_std)
plt.plot(bin_edges, normal_likelihoods, color='k', linestyle='--', label='정규 확률 곡선')
plt.hist(sample_means, bins='auto', alpha=0.2, color='r', density=True)
plt.legend()
plt.xlabel('샘플 평균')
plt.ylabel('상대적 확률')
plt.show()
- stats.norm.pdf() 함수로 상대적 확률을 계산한 뒤 샘플링된 동전 뒤집기의 히스토그램과 함께 그래프로 표현

- 계산된 평균 및 표준 편차를 이용하면 정규 분포 곡선을 재현할 수 있음
- 간단히 stats.norm.pdf(bin_edges, fitted_mean, fitted_std)를 호출하기만 하면 됨.
- 사이파이의 stats.norm.pdf() 함수는 정규 분포의 확률 밀도 함수를 나타냄
- 확률밀도 함수는 확률 질량 함수와 유사하지만 확률을 반환하지 않는다는 주요 차이점이 있음. 그 대신 상대적 확률을 반환함.
adjusted_likelihoods = stats.norm.pdf(bin_edges, fitted_mean + 0.01, fitted_std / 2)
plt.plot(bin_edges, adjusted_likelihoods, color='k', linestyle='--')
plt.hist(sample_means, bins='auto', alpha=0.2, color='r', density=True)
plt.xlabel('샘플 평균')
plt.ylabel('상대적 확률')
plt.show()

- 해당 최대 지점의 x,y좌표는 fitted_mean 및 fitted_std 함수의 결과와 같음.
- 최대 지점을 오른쪽으로 0.001 만큼 이동, 그 크기를 늘리는 예시가 위의 코드
- 입력 평균을 fitted_mean+0.01로 조정, 높이는 표준편차에 반비례하기 때문에 fitted_std /2 로 최대 지점에서 높이의 2배를 얻을 수 있음.
6.1.1 샘플링된 정규 분포 곡선 두 개 비교하기
np.random.seed(0)
new_sample_size = 40000
new_head_counts = np.random.binomial(new_sample_size, 0.5, 100000)
new_mean, new_std = stats.norm.fit(new_head_counts / new_sample_size)
new_likelihoods = stats.norm.pdf(bin_edges, new_mean, new_std)
plt.plot(bin_edges, normal_likelihoods, color='k', linestyle='--', label='A: 샘플 수 10K')
plt.plot(bin_edges, new_likelihoods, color='b', label='B: 샘플 수 40K')
plt.legend()
plt.xlabel('샘플 평균')
plt.ylabel('상대적 확률')
plt.show()
- 사이파이를 사용하면 입력된 매개변수를 바탕으로 정규 분포 모양을 탐색, 조정할 수 있음
- 입력 매개변수의 값은 임의의 데이터를 샘플링하는 방식에 따라 다름.
- 정규 분포의 전(A) 후(B) 모양을 그래프로 그려 비교함.

- 두 정규 분포의 중심 = 평균인 0.5 부근
- 표본 수가 더 많은 분포가 중심에서 더 좁게 퍼져 있음. = 표본 수가 증가함에 따라 최대 지점의 위치는 일정하게 유지되는 반ㅁ면 그 주변 영역의 폭은 줄어드는 것 관측 가능함.
- 최대 지점에 따라 퍼진 영역이 좁아지면 신뢰 구간은 감소함.
- 신뢰 구간 : 동전 앞면에 대한 실제 확률을 포함한 가능한 값 범위
- 표본 평균을 사용 시 동전 앞면의 확률을 구할 수 있음.
mean, std = new_mean, new_std
start, end = stats.norm.interval(0.95, mean, std)
print(f"이항 분포로 샘플링된 참 평균은 {start:.3f}와 {end:.3f} 사이에 있습니다")
- 정규 분포 B를 사용해 참 베르누이 평균에 대한 95% 신뢰 구간을 계산함
- 사이파이의 stats.norm.interval(0.95, mean, std)를 호출하면 그 범위를 자동으로 추출할 수 있다.
- 이 함수는 평균 및 표준 편차로 정의되는 정규 분포의 95% 영역을 포함하는 구간을 반환함.
assert stats.binom.mean(1, 0.5) == 0.5
- 정규 분포 곡선을 바탕으로 베르누이 분포의 분산을 추정함.
- 분포 A의 최대 지점 높이가 분포 B보다 약 2배 더 큼, 이 높이는 표준 편차에 반비례하므로 분포 B의 표준 편차는 분포 A의 표준 편차를 반으로 나눈 것과 같음.
- 표준 편차는 분산에 제곱근을 씌운 것, 분포 B 분산이 분포 A 분산의 1/4 이라는 것 유추 가능
variance_ratio = (new_std ** 2) / (fitted_std ** 2)
print(f"분산의 비율은 약 {variance_ratio:.2f}입니다")
- 실제로 분산의 비율을 출력해보면 0.25라고 나오는 것 확인 가능하다.
np.random.seed(0)
reduced_sample_size = 2500
head_counts = np.random.binomial(reduced_sample_size, 0.5, 100000)
_, std = stats.norm.fit(head_counts / reduced_sample_size)
variance_ratio = (std ** 2) / (fitted_std ** 2)
print(f" 분산의 비율은 약 {variance_ratio:.1f}입니다")
- 분산이 표본 크기에 반비례하는 것으로 보임.
- 위 코드 실행 시 분산의 비율 = 4.0인 것을 확인 가능.
estimated_variance = (fitted_std ** 2) * 10000
print(f"샘플 크기가 1인 경우, 추정 분산은 {estimated_variance:.2f} 입니다")
- 표본 크기가 4배 감소 시 분산은 4배 증가함.
assert stats.binom.var(1, 0.5) == 0.25
- 표본 크기 1에 대한 예측 분산 확인
정규 분포를 사용해 샘플링한 베르누이 분포의 분산과 평균을 계산하고 결과를 도출하는 과정
- 베르누이 분포에서 무작위로 1과 0을 샘플링
- sample_size 1과 0의 각 시퀀스를 단일 샘플로 그룹화
- 모든 표본의 평균을 계산
- 표본 평균은 정규 곡선을 생성
- 평균과 표준 편차 구함
- 정규 곡선의 분산에 표본 크기를 곱했을 때 베르누이 분포의 분산과 같았음.
다른 분포 사용하는 경우
- 푸아송 분포 : 시간당 스토어 방문 고객 수 , 초당 온라인 광고 클릭 수
- 감마 분포 : 한 지역의 월별 강우량, 대출 규모에 따른 은행 대출 불이행 건수
- 로그 정규 분포 : 주가 변동, 전염병의 잠복기
정규 곡선을 샘플링한 뒤에는 이를 사용해 기본 분포를 분석할 수 있음.
정규 곡선의 평균은 기본 분포의 평균에 근사함.
정규 곡선의 분산에 표본 크기를 곱하면 기본 분포의 분산에 근사치가 됨.
6.2 무작위 샘플링으로 모집단의 평균 및 분산 결정하기
np.random.seed(0)
population_ages = np.random.randint(1, 85, size=50000)
- 마을의 인구 수가 5만이라 할 때 한 마을에 사는 사람들의 평균 나이를 구하는 작업이 주어졌을 때.
population_mean = population_ages.mean()
population_variance = population_ages.var()
- 마을의 인구 평균과 인구 분산을 빠르게 계산해봄
- 그런데 실제로 얻어야하는 데이터가 엄청나게 많다면?
np.random.seed(0)
sample_size = 10
sample = np.random.choice(population_ages, size=sample_size)
sample_mean = sample.mean()
- 간단히 평균을 구할 수 있는 방법 = 마을에서 무작위로 사람들을 열 명 선정해 인터뷰하는 것
- np.random.choice 메서드에서 무작위로 열 명의 나이를 추출
- np.random.choice(population_ages, size=sample_size)를 실행하면 무작위로 샘플링된 나이 배열을 열 개 반환함.
- 샘플링이 완료되면 결과 요소 열 개로 구성된 배열 평균을 계산함.
percent_diff = lambda v1, v2: 100 * abs(v1 - v2) / v2
percent_diff_means = percent_diff(sample_mean, population_mean)
print(f"평균에 대해 {percent_diff_means:.2f}%의 차이가 있습니다.")
- 표본 평균과 모집단 평균을 비교했을 때.표본 평균과 모집단 평균 사이에 약 27% 차이가 있음.
- 표본이 충분치 않아 더 많은 표본을 수집해야함.
- 지원자들이 각각 인터뷰를 해서 데이터를 가져오는 병렬 느낌? 으로 진행하면 빠르게 많은 데이터를 수집 가능할 것이다.
np.random.seed(0)
sample_means = [np.random.choice(population_ages, size=sample_size).mean() for _ in range(100)]
- 1000명의 표본 평균을 계산한 것.
- 중심 극한 정리에 따르면, 표본 평균의 히스토그램은 정규 분포와 유사해야한다.
- 정규 분포 평균은 모집단 평균에 가까워야 한다.
- 표본 평균을 정규 분포에 맞추면 이것이 사실임을 확인 가능하다.
likelihoods, bin_edges, _ = plt.hist(sample_means, bins='auto', alpha=0.2, color='r', density=True)
mean, std = stats.norm.fit(sample_means)
normal_likelihoods = stats.norm.pdf(bin_edges, mean, std)
plt.plot(bin_edges, normal_likelihoods, color='k', linestyle='--')
plt.xlabel('샘플 평균')
plt.ylabel('상대적 확률')
plt.show()

- 히스토그램은 데이터 포인트를 100개만 처리했기에 매끄럽지 않음.
print(f"실제 모집단 평균은 약 {population_mean:.2f} 입니다.")
percent_diff_means = percent_diff(mean, population_mean)
print(f"평균에 대해 {percent_diff_means:.2f}% 차이가 있습니다.")
- 그렇지만 히스토그램 모양은 여전히 정규 분포에 가까움. 이 분포 평균을 인쇄해 모집단 평균과 비교
normal_variance = std ** 2
estimated_variance = normal_variance * sample_size
- 표준 편차를 제곱하면 분포의 분산을 구할 수 있음. 이 분산을 이용하여 마을의 나이 분산을 추정할 수 있음.
- 계산된 분산에 표본 크기를 곱하기만 하면 됨.
print(f"추정 분산은 약 {estimated_variance:.2f} 입니다.")
print(f"실제 모집단 분산은 약 {population_variance:.2f} 입니다.")
percent_diff_var = percent_diff(estimated_variance, population_variance)
print(f"분산에 대해 {percent_diff_var:.2f}% 차이가 있습니다.")
- 추정분산 = 576.73, 모집단 분산 584.33, 분산 간 차이는 1.3% 이다.
- 분산이 비교적 정확하게 추정된 것을 확인 가능해졌다.
6.3 평균과 분산을 이용하여 예측하기
- 만 선생님이 25년간 5학년 학생들을 가르쳤고, 총 500명을 지도했다고 가정한다.
- 각 학생의 성적은 매년 학업 평가 시험으로 측정되며, 0점에서 100점까지 점수가 매겨짐.
- 데이터베이스에는 각 시험의 연도나 시험 회차 정보가 없어 특정 시험 성적을 직접 비교하기는 어렵습니다.
가정
선생님이 과거에 평균 성적이 89% 이상인 학급을 가르친 적이 있었는가?
- 이를 위해 데이터베이스를 조회한다고 가정한다.
- 과거 학생들의 개별 성적은 알 수 없지만, 전체 500명의 평균은 84점, 분산도 주어졌다고 가정한다.
- 이 값들(모집단 평균과 분산)을 바탕으로 선생님이 가르친 학급이 예외적으로 성적이 높은 집단인지 예측해보자는 내용.
population_mean = 84
population_variance = 25
- 모집단 평균은 84
- 모집단 분산은 25라고 할때
mean = population_mean
population_std = population_variance ** 0.5
sem = population_std / (20 ** 0.5)
grade_range = range(101)
normal_likelihoods = stats.norm.pdf(grade_range, mean, sem)
plt.plot(grade_range, normal_likelihoods)
plt.xlabel('20명 학생의 평균 성적 (%)')
plt.ylabel('상대적 확률')
plt.show()
- 평균 표준 오차 : 분산의 제곱근을 구하면 나오는 곡선의 표준 편차
- 평균 표준 오차는 모집단 표준 편차를 표본 크기의 제곱근으로 나눈 값과 같음.
- 곡선 매개변수를 계산하고 정규 곡선을 그림

- 표시된 곡선 아래 면적은 89%에보다 높은 값에서 0에 가까워짐.
- 이 면적은 주어진 관측치 확률과도 같지만, 90% 이상 평균 성적이 관측될 확률은 매우 낮음.
- 확실히 하려면 실제 확률을 계산해야함.
6.3.1 정규 곡선 아래 면적 계산하기
- 법선 곡선은 직사각형으로 분해되지 않음.
- 한 가지 해결책 : 법선 곡선을 작은 사다리꼴 단위로 세분화하는 것 = 사다리꼴 규칙
total_area = np.sum([normal_likelihoods[i: i + 2].sum() / 2 for i in range(normal_likelihoods.size - 1)])
assert total_area == np.trapz(normal_likelihoods)
print(f"곡선 아래 예상 면적은 {total_area}입니다.")
- 사다리꼴 규칙을 정규 분포에 적용한 것

- 예상 면적은 1.0에 매우 가깝지만 정확히 1.0과 같지는 않다. 살짝 더 큼.
assert stats.norm.sf(0, mean, sem) == 1.0
- 정확한 계산이 필요하다.
- stats.norm.sf 메서드를 사용해 수학적으로 정확한 솔루션에 접근할 수 있다.
- 이 방법은 정규 곡선의 생존 함수를 나타냄.
- 즉, 생존 함수는 np.trapz(normal_likelihoods[mean:])로 근사한 면적에 대한 정확한 해법임.

- 평균이 정규 곡선을 동일한 절반 두 개로 완벽히 나누기에 stats.norm.sf(0, mean, sem) 0.5가 될 것이라 예상한다.
assert stats.norm.sf(mean, mean, sem) == 0.5
estimated_area = np.trapz(normal_likelihoods[mean:])
print(f"평균을 초과하는 추정 면적은 {estimated_area} 입니다")
- np.trapz(normal_likelihoods[mean:])는 0.5에 가깝지만 완전히 같지는 않음.
area = stats.norm.sf(90, mean, sem)
print(f"20명의 학생이 시험에 합격할 확률은 {area} 입니다.")
- area = stats.norm.sf(90, mean, sem)를 실행했을 때 이 함수는 90%를 초과하는 값의 간격에 대한 영역을 반환한 것을 알 수 있다.
6.3.2 계산된 확률 해석하기
몇몇 데이터가 제공되지 않는 경우 최선을 다했지만 불확실성이 남아있을 수 있다.
통계학자들은 이런 경우, 제한된 기록에 의존해 중대한 결정을 내려야 하는 경우가 많다.
따라서 불완전한 정보에서 결론을 도출할 때는 매우 신중해야 한다.
7 주차 바로 보기
https://marble-shovel-9f2.notion.site/7-9-1c9cbc174299809da8d2da849d404a70?pvs=4
'소프트웨어 > 데이터 분석' 카테고리의 다른 글
실전 데이터 분석 2주차 (3) | 2025.04.08 |
---|