✏️ 데이터 사이언스 스쿨에서 공부한 내용입니다.
카테고리 확률변수(Categorical random variable)는 1부터 까지 개 정숫값 중 하나가 나온다. 이 정숫값을 범주값, 카테고리(category) 혹은 클래스(class)라고 한다. 예를 들어, 주사위를 던져 나오는 눈금 수는 인 카테고리 분포다.
주의할 점은 원래 카테고리는 스칼라값이지만 카테고리 확률변수는 다음과 같이 1과 0으로만 이루어진 다차원 벡터를 출력한다. (벡터는 원래 세로 열로 표시해야하지만 여기에서는 편의상 가로 행으로 표시하였다.)
숫자를 이렇게 변형하는 것을 원핫인코딩(One-Hot-Encoding) 이라고 한다. 따라서 확률변수의 값도 다음처럼 벡터로 표시한다.
이 벡터를 구성하는 원소 , , , , , 에는 다음과 같은 제한 조건이 붙는다.
첫 번째 제한 조건은 값으로 0 또는 1 만 가능하다는 것이고, 두 번째 제한 조건은 여러 중 단 하나만 1일 수 있다는 것이다. 원소값 는 베르누이 확률변수로 볼 수 있기 때문에 각각 1이 나올 확률을 나타내는 모수 를 가진다. 따라서 전체 카테고리 분포의 모수는 다음과 같이 벡터로 나타낸다.
이 모수 벡터도 다음과 같이 제한 조건을 가진다.
첫 번째 제한 조건은 모수 가 0과 1 사이의 값만 가질 수 있다는 점을 가리킨다. 두 번째 제한 조건은 의 합이 1이 된다는 것이다. 모든 경우의 확률의 합은 1이 되어야 하므로 이 또한 당연하다. 다만 0 아니면 1만 되어야 하는 와는 달리 는 0부터 1 사이의 어떤 실숫값이든 가질 수 있다.
카테고리 확률변수의 확률분포인 카테고리 확률분포는 출력 벡터 , 모수 벡터 를 사용하여 표현한다.
인 카테고리 분포의 확률질량함수가 베르누이분포의 확률질량함수와 같음을 보여라.
✏️
베르누이 분포의 pmf는 다음과 같다.
이는 카테고리 분포의에서 K=2를 대입한 경우와 같다.
카테고리 분포는 표본값이 벡터이므로 기댓값과 분산도 벡터이다. 기댓값과 분산을 구하는 공식은 다음과 같다.
scipy는 카테고리 분포를 위한 별도의 클래스를 제공하지 않는다. 하지만 뒤에서 설명할 다항분포를 위한 multinomial
클래스에서 시행 횟수를 1로 설정하면 카테고리 분포가 된다. 다음은 사기 주사위에 해당하는 모수가 인 카테고리 분포를 시뮬레이션한 것이다. 카테고리 분포의 출력은 벡터값으로 pmf()
메서드의 인수로도 원핫인코딩 벡터를 넣기 위해 pd.get_dummies
를 사용한다.
import scipy as sp
import scipy.stats
mu = [0.1, 0.1, 0.1, 0.1, 0.3, 0.3]
rv = sp.stats.multinomial(1, mu)
xx = np.arange(1, 7)
# 대각 성분은 True, 나머지는 False인 6x6 데이터 프레임
# 비슷한 방법으로, np.eye(len(mu)) 으로 단위 행렬을 만들 수 있다.
xx_ohe = pd.get_dummies(xx)
plt.bar(xx, rv.pmf(xx_ohe.values))
plt.ylabel("P(x)")
plt.xlabel("표본값")
plt.title("카테고리 분포의 확률질량함수")
plt.show()
rvs()
메서드로 생성한 값도 원핫인코딩 벡터다.
np.random.seed(1)
X = rv.rvs(100)
X[:5]
array([[0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1], [0, 0, 1, 0, 0, 0]])
100개의 표본값을 생성하는 시뮬레이션 결과는 다음과 같다.
y = X.sum(axis=0) / float(len(X))
plt.bar(np.arange(1, 7), y)
plt.title("카테고리 분포의 시뮬레이션 결과")
plt.xlabel("표본값")
plt.ylabel("비율")
plt.show()
이론적인 확률분포와 시뮬레이션 결과를 비교하면 다음과 같다.
df = pd.DataFrame({"이론": rv.pmf(xx_ohe.values), "시뮬레이션": y},
index=np.arange(1, 7)).stack()
df = df.reset_index()
df.columns = ["표본값", "유형", "비율"]
df.pivot("표본값", "유형", "비율")
df
표본값 | 유형 | 비율 | |
---|---|---|---|
0 | 1 | 이론 | 0.1 |
1 | 1 | 시뮬레이션 | 0.1 |
2 | 2 | 이론 | 0.1 |
3 | 2 | 시뮬레이션 | 0.15 |
4 | 3 | 이론 | 0.1 |
5 | 3 | 시뮬레이션 | 0.13 |
6 | 4 | 이론 | 0.1 |
7 | 4 | 시뮬레이션 | 0.07 |
8 | 5 | 이론 | 0.3 |
9 | 5 | 시뮬레이션 | 0.33 |
10 | 6 | 이론 | 0.3 |
11 | 6 | 시뮬레이션 | 0.22 |
import seaborn as sns
sns.barplot(x="표본값", y="비율", hue="유형", data=df)
plt.title("카테고리 분포의 이론적 분포와 시뮬레이션 분포")
plt.show()
카테고리 확률분포의 모수가 다음과 같을 경우에 각각 표본을 생성한 후 기댓값과 분산을 구하고 앞의 예제와 같이 확률밀도함수와 비교한 바 플롯을 그린다. 표본이 10개인 경우와 1000개인 경우에 대해 각각 위의 계산을 한다.
(1)
(2)
✏️
# !pip install koreanize-matplotlib
import koreanize_matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import scipy as sp
import scipy.stats
import seaborn as sns
params = [[0.25,0.25,0.25,0.25], [0.3,0.3,0.2,0.2]]
sample_sizes = [10, 1000]
fig, axes = plt.subplots(2, 2, figsize=(12, 8)) # Create subplots
for i, mu in enumerate(params):
for j, sample_size in enumerate(sample_sizes):
# Create Categorical random variable
rv = sp.stats.multinomial(1, mu)
# Generate samples
x = rv.rvs(sample_size, random_state=i+j)
# Calculate mean and variance
mu_hat = x.mean(axis=0)
var_hat = np.round(x.var(axis=0), 3)
print(f"문제:{i+1}, 표본:{sample_size}, mu_hat: {mu_hat}, var_hat: {var_hat}")
# Theoretical probabilities (pmf)
# 단위행렬을 이용한 one-hot-encoding 사용
theoretical_probs = rv.pmf(np.eye(len(mu)))
# Simulated probabilities
simulated_probs = x.sum(axis=0) / float(len(x))
# Create DataFrame for plotting
df = pd.DataFrame({"Theoretical": theoretical_probs, "Simulated": simulated_probs})
df.index = np.arange(1, 5)
df2 = df.stack().reset_index()
df2.columns = ["sample", "type", "rate"]
# Plot
sns.barplot(x="sample", y="rate", hue="type", data=df2, ax=axes[i,j])
axes[i,j].set_title(f"카테고리 분포({i+1}번 문제, 표본:{sample_size}개)")
plt.tight_layout()
plt.show()
문제:1, 표본:10, mu_hat: [0.4 0.2 0.2 0.2], var_hat: [0.24 0.16 0.16 0.16] 문제:1, 표본:1000, mu_hat: [0.232 0.285 0.244 0.239], var_hat: [0.178 0.204 0.184 0.182] 문제:2, 표본:10, mu_hat: [0.1 0.4 0.4 0.1], var_hat: [0.09 0.24 0.24 0.09] 문제:2, 표본:1000, mu_hat: [0.273 0.3 0.222 0.205], var_hat: [0.198 0.21 0.173 0.163]
스팸메일 필터링과 같은 이진분류문제에서는 베르누이분포를 사용했다. 예측할 범주값이 두 가지보다 많은 다중분류문제(multi-class classification)에서는 카테고리 분포를 사용하여 범주값 데이터 모형을 만들 수 있다. 다음은 사이킷런 패키지에서 제공하는 붓꽃 데이터의 품종값을 시각화한 것이다. 0, 1, 2 세 가지의 범주값을 가지는 붓꽃 데이터의 품종값은 카테고리 인 카테고리 분포를 이룬다. 꽃잎 폭이라는 하는 특징에 따라 카테고리 분포의 모양이 달라지는 것을 볼 수 있다.
from sklearn.datasets import load_iris
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df["품종"] = pd.Series(iris.target, dtype="category")
df1 = df[df["petal width (cm)"] > 1.5]
df2 = df[df["petal width (cm)"] <= 1.5]
fig, ax = plt.subplots(1, 2)
sns.countplot(x="품종", data=df1, ax=ax[0]).set_title("꽃잎 폭 > 1.5cm")
sns.countplot(x="품종", data=df2, ax=ax[1]).set_title("꽃잎 폭 <= 1.5cm")
plt.tight_layout()
plt.show()
베르누이 확률변수의 데이터가 복수이면 이 데이터의 합이 이항 분포를 이루는 것처럼 카테고리 확률변수의 데이터가 여럿 있으면 이 데이터의 합은 다항분포(Multinomial distribution) 가 된다. 비유를 들면 동전을 번 던져 앞면이 나오는 횟수의 분포가 이항 분포이고 주사위를 번 던져 각 면이 나오는 횟수 집합의 분포가 다항분포다.
다항분포는 카테고리가 개인 카테고리 확률변수의 표본 데이터를 개 얻었을 때, 각각의 카테고리 가 각각 번 나올 확률분포 즉, 표본값이 벡터 가 되는 확률분포를 말한다.
예를 들어 은 6개의 숫자가 나올 수 있는 주사위를 10번 던져서 1인 면이 1번, 2인 면이 2번, 3인 면이 1번, 4인 면이 2번, 5인 면이 3 번, 6인 면이 1번 나왔다는 뜻이다.
다항분포의 확률질량함수는 와 같이 표기하며 다음과 같은 수식을 따른다.
이 식에서 조합 기호는 다음과 같이 정의된다.
인 다항분포의 확률질량함수가 이항분포의 확률질량함수와 같음을 보여라.
✏️
k=2 대입하면 같다.
사이파이는 다항분포를 위한 multinomial
클래스를 지원한다. 인수로는 시행 횟수 과 모수 벡터 를 받는다. 이 시뮬레이션은 5와 6이 다른 수보다 잘 나오게 만든 조작된 주사위를 30번 던졌을 때 나올 수 있는 여러 경우를 살펴본 것이다.
N = 30
mu = [0.1, 0.1, 0.1, 0.1, 0.3, 0.3]
rv = sp.stats.multinomial(N, mu)
np.random.seed(0)
X = rv.rvs(100)
X[:10]
array([[ 3, 4, 3, 3, 8, 9], [ 3, 3, 5, 6, 6, 7], [ 4, 3, 3, 5, 5, 10], [ 1, 0, 5, 5, 12, 7], [ 7, 4, 2, 3, 5, 9], [ 3, 1, 6, 3, 8, 9], [ 2, 4, 3, 3, 5, 13], [ 3, 3, 3, 6, 8, 7], [ 2, 3, 4, 1, 11, 9], [ 4, 2, 1, 2, 10, 11]])
다음은 이 시뮬레이션 결과를 시본 패키지의 스웜 플롯(swarm plot) 명령과 바이올린 플롯(violin plot) 명령을 사용하여 시각화한 것이다. 스웜 플롯은 각 카테고리에 해당하는 실숫값 데이터 집합을 하나하나 점으로 나타낸 것이고 바이올린 플롯은 이 데이터의 분포를 커널밀도(kernel density)라는 부드러운 곡선으로 표현한 것이다.
df = pd.DataFrame(X).stack().reset_index()
df.columns = ["시도", "클래스", "표본값"]
sns.violinplot(x="클래스", y="표본값", data=df, inner="quartile")
sns.swarmplot(x="클래스", y="표본값", data=df, color=".3", size=1)
plt.title("다항분포의 시뮬레이션 결과")
plt.show()
맷플롯립 패키지의 박스 플롯(box plot) 명령으로 더 단순하게 시각화할 수도 있다. boxplot
명령은 박스-휘스커 플롯(box-whisker Plot) 혹은 간단히 박스 플롯(box-plot) 이라 부르는 차트를 그려준다. 박스 플롯은 상자(box)와 상자 바깥의 선(whisker)으로 이루어진다.
상자는 실숫값 분포에서 1사분위수(Q1)와 3사분위수(Q3)를 뜻하고 이 3사분위수와 1사분수의 차이(Q3 - Q1)를 IQR(inter-quartile range) 이라고 한다. 상자 내부의 가로선은 중앙값을 나타낸다. 박스 외부의 세로선은 1사분위 수보다 1.5IQR만큼 낮은 값과 3사분위 수보다 1.5IQR만큼 높은 값의 구간을 기준으로 그 구간의 내부에 있는 가장 큰 데이터와 가장 작은 데이터를 잇는 선분이다. 그 바깥의 점은 아웃라이어(outlier) 라고 부르는데 점으로 표시한다.
plt.boxplot(X)
plt.title("다항분포의 시뮬레이션 결과")
plt.xlabel("클래스")
plt.ylabel("표본값")
plt.show()
다항분포의 표본 하나는 같은 모수를 가진 카테고리 분포를 표본 여럿을 합쳐놓은 것이므로 다항분포의 표본이 이루는 분포의 모양은 카테고리 분포와 비슷해진다.