728x90
반응형

대푯값(Representative value)과 비대칭도(Skewness)

 앞서 학습였던, 중앙값(Median), 최빈값(Mode), 평균(Mean) 등과 같은 대푯값과 표준편차(Standard Deviation)를 사용하면, 데이터가 대략 어떻게 생겼는지를 파악할 수 있다.

 이번 포스트에서는 대푯값이 무엇인지, 대푯값을 사용하여 어떻게 데이터의 형태를 파악할 수 있는지에 대해 알아보도록 하자.

 

 

 

 

1. 대푯값(Representative value)

  • 대푯값은 말 그대로 어떠한 데이터를 단 하나의 값으로 표현할 수 있는 값을 말한다.
  • 데이터 어디에 모여있는지(중심경향치): 평균(Mean), 중앙값(Median), 최빈값(Mode), 절사 평균(Trimmed mean)
  • 데이터를 정렬하고 일정 크기로 나눴을 때의 간격 점: 사분위수(Quartile), 백분위수(Percentile)
  • 정상 범위에서 벗어난 값: 이상값(Outlier)
  • 확률상 기대되는 대푯값: 기댓값(Expected value)

 

1.1. 대푯값을 이용하여, 데이터의 대략적인 분포를 알 수 있다.

  • 산포도는 대푯값이 아니지만, 대푯값을 이용해서 산포도를 알 수도 있다.
  • 사분위수를 이용하면, 사분위 범위를 알 수 있고, 사분위 범위를 알면, 상자 수염 그림(Box-and-Whisker plot)을 그릴 수 있다.
  • 평균과 중앙값, 최빈값을 이용하여, 데이터의 대략적인 개형을 알 수 있다.

 

 

 

 

 

2. 대푯값을 이용한 데이터의 대략적인 개형 알아보기

  • 집중화 경향치인 평균, 중앙값, 최빈값은 데이터 분포에 따라 아래와 같은 경향을 갖는다.

  • 위 그림은 조금 과장을 한 것이긴 하지만, 데이터는 위와 같은 경향을 띄며, 그 이유는 다음과 같다.
  1. 최빈값: 가장 많이 등장한 관찰 값이 최빈값이 된다.
  2. 중앙값: 관찰 값의 빈도에 상관없이 무조건 가운데에 위치한다.
  3. 평균: 이상치의 영향을 크게 받는다.
  • 간단한 도수분포표를 만들어서, 실제로는 어떻게 돌아가는지 확인해보자.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 부드러운 그래프를 그린다.
from scipy.interpolate import make_interp_spline

plt.rc('font', family='NanumGothic')
# 최빈값, 중앙값, 평균 계산
def Rep_V(data, column):

    raw_list = []

    for i in range(len(data)):

        X_list = [data.loc[i, "X"]] * data.loc[i, column]
        raw_list = raw_list + X_list

    median = np.median(raw_list)
    mean = np.mean(raw_list)
    std = np.std(raw_list)
    mode = np.bincount(raw_list).argmax()

    return mode, median, mean, std



# 그래프 그리기
def draw_Ske(data, column, title):
    """
    도수분포다각형을 부드럽게 그리고, 최빈값, 중앙값, 평균을 점으로 찍고, 선을 그린다.
    """
    x = data["X"].to_numpy()
    y = data[column].to_numpy()

    fig = plt.figure(figsize=(10, 6))

    # 부드러운 그래프를 그린다.
    model=make_interp_spline(x,y)
    xs=np.linspace(np.min(index_list),np.max(index_list),100)
    ys=model(xs)

    plt.plot(xs, ys)

    # 최빈값, 중앙값, 평균 그리기
    mode, median, mean, std = Rep_V(data, column)
    plt.scatter(mode, 1, c = "r", s = 100)
    plt.axvline(x=mode, c = "r", linewidth = 1, alpha = 0.5)
    plt.annotate('최빈값', xy=(mode+1, 1), xytext=(mode + 10, 0.5), fontsize = 15,
                 arrowprops=dict(facecolor='black', width=1, headwidth=8))

    plt.scatter(median, 3, c = "g", s = 100)
    plt.axvline(x=median, c = "g", linewidth = 1, alpha = 0.5)
    plt.annotate('중앙값', xy=(median+1, 3), xytext=(median + 10, 3.2), fontsize = 15,
                 arrowprops=dict(facecolor='black', width=1, headwidth=8))

    plt.scatter(mean, 5, c = "y", s = 100)
    plt.axvline(x=mean, c = "y", linewidth = 1, alpha = 0.5)
    plt.annotate('평균', xy=(mean+1, 5), xytext=(mean + 10, 6), fontsize = 15,
                 arrowprops=dict(facecolor='black', width=1, headwidth=8))



    plt.title(title, fontsize = 20, pad = 15)
    plt.xlabel("$X$", fontsize = 15, labelpad = 10)
    plt.ylabel("$f$", rotation = 0, fontsize = 15, labelpad = 10)
    plt.show()
  • 함수 Rep_V(data, column)는 도수분포표를 본래 데이터 셋으로 되돌리고, 최빈값, 중앙값, 평균과 같은 대푯값을 계산하는 함수다.
  • 데이터의 양이 지나치게 많아, 컴퓨터가 이를 감당하지 못하는 경우에는 의도적으로 도수분포표를 만들어 대푯값을 계산할 수도 있으나, 위와 같이 데이터의 양이 적은 경우, 도수분포표를 본래 데이터 셋으로 되돌리는 것이 더 쉽다.
index_list = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
freq1_list = [1, 3, 6, 11, 17, 24, 17, 11, 6, 3, 1]
freq2_list = [1, 2, 3, 6, 10, 14, 20, 25, 15, 3, 1]
freq3_list = [1, 3, 15, 25, 20, 14, 10, 6, 3, 2, 1]

DF = pd.DataFrame({"X":index_list, "f1":freq1_list, "f2":freq2_list, "f3":freq3_list})
DF

2.1. 대칭 분포

draw_Ske(DF, "f1", "1. 대칭분포")

  • 대칭 분포인 경우, "평균 = 중앙값 = 최빈값"인 것을 볼 수 있다.

2.2. 왼쪽 꼬리 분포

draw_Ske(DF, "f2", "2. 왼쪽꼬리분포")

  • 왼쪽 꼬리 분포인 경우, "평균 < 중앙값 < 최빈값"이 되는 것을 볼 수 있다.

2.3. 오른쪽 꼬리 분포

  • 오른 꼬리 분포인 경우, "평균 > 중앙값 > 최빈값"이 되는 것을 볼 수 있다.

 

 

 

 

 

3. 피어슨의 비대칭 계수(Pearson's skewness coefficients)

 칼 피어슨(Karl Pearson)이 만든, 피어슨의 비대칭 계수는, 위에서 본 분포가 좌, 우로 치우치는 경우 평균, 중앙값, 최빈값이 일정한 크기로 순서를 갖는 성질을 이용해서 만든 것이다.

  • 피어슨의 비대칭 계수는 평균에 대하여, 최빈값이나 중앙값을 뺀 값을 표준 편차로 나눠서 구한다.
  • 피어슨의 비대칭 계수를 구하는 방법은 크게 3가지가 있다.

◎ 피어슨의 제1 비대칭도 계수: $SK_1 = \frac{\bar{X} - mode}{S}$

◎ 피어슨의 제2 비대칭도 계수: $SK_2 = \frac{3(\bar{X} - mode)}{S}$

◎ 피어슨의 제3 비대칭도 계수: $SK_3 = \frac{3(\bar{X} - median)}{S}$


  • 위 비대칭도 계수들은 평균과 다른 대푯값의 거리를 얼마나 민감하게 볼 것인가에 대하여 다르게 만들어진 것임을 볼 수 있다.
  • 일반적으로 사용하는 것은 피어슨의 제3 비대칭도 계수다.
  • 단순한 해석 방법은 3가지 모두 동일하다.

 

3.1. 피어슨의 비대칭 계수 보는 방법

  • 비대칭도 계수가 0이 나오는 경우, 평균과 중앙값(최빈값)이 같으므로, 분포가 치우쳐지지 않음을 알 수 있다.
  • 비대칭도 계수가 양수인 경우, 평균보다 중앙값이 작으므로, 오른쪽 꼬리 분포이다.
  • 비대칭도 계수가 음수인 경우, 평균보다 중앙값이 크므로, 왼쪽 꼬리 분포이다.
  • 비대칭도 계수가 클수록, 한쪽으로 치우친 정도가 크다는 소리다.
  • 비대칭도 계수가 0~1 사이인 경우, 1 표준편차보다 분자(평균과 대푯값의 거리에 3을 곱한 값)가 작다는 의미이므로, 치우친 정도가 작다는 소리다.
  • 이를 이용하여, 서로 다른 분포에 대하여, 치우친 정도를 비교할 수도 있다(표준편차로 나누었으므로, 단위도 사라진다.).

 

3.2. 파이썬으로 피어슨의 비대칭 계수를 구현해보자.

  • 기존 도수분포표에 보다 치우친 새로운 도수를 추가하여 비교해보자.
index_list = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
freq1_list = [1, 3, 6, 11, 17, 24, 17, 11, 6, 3, 1]
freq2_list = [1, 2, 3, 6, 10, 14, 20, 25, 15, 3, 1]
freq3_list = [1, 3, 15, 25, 20, 14, 10, 6, 3, 2, 1]
freq4_list = [1, 1, 2, 3, 5, 7, 10, 16, 23, 26, 6]

DF = pd.DataFrame({"X":index_list, "f1":freq1_list, "f2":freq2_list, "f3":freq3_list, "f4":freq4_list})
DF

  • 피어슨의 비대칭도 계수를 구하는 함수를 만들고, 각 변수의 도수들을 비교해보자.
  • 각 칼럼의 최빈값, 중앙값, 평균, 표준편차를 출력해보자.
>>> for column in ["f1", "f2", "f3", "f4"]:

>>>     mode, median, mean, std = Rep_V(DF, column)
>>>     print(f"column: {column} \n 최빈값:{mode}, 중앙값: {median}, 평균: {mean}, 표준편차: {np.round(std,2)}")
>>>     print("----"*20)
    
    
column: f1 
 최빈값:50, 중앙값: 50.0, 평균: 50.0, 표준편차: 19.39
--------------------------------------------------------------------------------
column: f2 
 최빈값:70, 중앙값: 60.0, 평균: 58.8, 표준편차: 19.2
--------------------------------------------------------------------------------
column: f3 
 최빈값:30, 중앙값: 40.0, 평균: 41.2, 표준편차: 19.2
--------------------------------------------------------------------------------
column: f4 
 최빈값:90, 중앙값: 80.0, 평균: 71.9, 표준편차: 21.06
--------------------------------------------------------------------------------

 

  • 1번부터 3번까지 피어슨의 비대칭 계수를 계산하는 함수를 만들고, 계수를 계산해보자.
def P_SK(data, column, key):
    
    mode, median, mean, std = Rep_V(DF, column)
    
    if key == 1:
        result = (mean - mode)/std
        
    elif key == 2:
        result = 3*(mean - mode)/std
        
    elif key == 3:
        result = 3*(mean - median)/std
        
    return result
>>> for column in ["f1", "f2", "f3", "f4"]:
    
>>>     print(f"column: {column}")
>>>     for key in range(1,4):
        
>>>         P_SK_value = P_SK(DF, column, key)
>>>         print(f"피어슨의 제 {key} 비대칭도 계수 = {np.round(P_SK_value, 4)}")
        
>>>     print("----"*20)


column: f1
피어슨의 제 1 비대칭도 계수 = 0.0
피어슨의 제 2 비대칭도 계수 = 0.0
피어슨의 제 3 비대칭도 계수 = 0.0
--------------------------------------------------------------------------------
column: f2
피어슨의 제 1 비대칭도 계수 = -0.5834
피어슨의 제 2 비대칭도 계수 = -1.7502
피어슨의 제 3 비대칭도 계수 = -0.1875
--------------------------------------------------------------------------------
column: f3
피어슨의 제 1 비대칭도 계수 = 0.5834
피어슨의 제 2 비대칭도 계수 = 1.7502
피어슨의 제 3 비대칭도 계수 = 0.1875
--------------------------------------------------------------------------------
column: f4
피어슨의 제 1 비대칭도 계수 = -0.8596
피어슨의 제 2 비대칭도 계수 = -2.5787
피어슨의 제 3 비대칭도 계수 = -1.154
--------------------------------------------------------------------------------
  • 대칭 분포인 f1은 비대칭도 계수가 0이 나왔다.
  • 왼쪽 꼬리 분포인 f2는 비대칭도 계수가 모두 음수가 나왔다.
  • 오른쪽 꼬리 분포인 f3는 비대칭도 계수가 모두 양수가 나왔다.
  • 새로 만든 f2의 비대칭도 계수는 모두 음수가 나왔다.
  • 수치가 제법 다르므로, f2와 f4를 시각화하여, 직접 비교해보자.

 

3.3. 시각화하여 비대칭도 비교

  • 위에서 만든 도수분포와 최빈값, 중앙값, 평균을 그려주는 함수인 draw_Ske를 약간 변형하여, 4개 칼럼에 대해 subplot으로 그려보도록 하자.
data = DF
x = data["X"].to_numpy()
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(16,12))
ax = axes.ravel()
column_list = ["f1", "f2", "f3", "f4"]

for i, column in enumerate(column_list):
    

    y = data[column].to_numpy()
    
    # 부드러운 그래프를 그린다.
    model=make_interp_spline(x,y)
    xs=np.linspace(np.min(index_list),np.max(index_list),100)
    ys=model(xs)
    
    ax[i].plot(xs, ys)
    title = column_list[i] + "의 분포"
    ax[i].set_title(title, fontsize=20, fontweight="bold")
    
    
    # 최빈값, 중앙값, 평균 그리기
    mode, median, mean, std = Rep_V(data, column)
    ax[i].scatter(mode, 1, c = "r", s = 100)
    ax[i].axvline(x=mode, c = "r", linewidth = 1, alpha = 0.5)
    ax[i].annotate('최빈값', xy=(mode+1, 1), xytext=(mode + 10, 0.5), fontsize = 15,
                 arrowprops=dict(facecolor='black', width=1, headwidth=8))

    ax[i].scatter(median, 3, c = "g", s = 100)
    ax[i].axvline(x=median, c = "g", linewidth = 1, alpha = 0.5)
    ax[i].annotate('중앙값', xy=(median+1, 3), xytext=(median + 10, 3.2), fontsize = 15,
                 arrowprops=dict(facecolor='black', width=1, headwidth=8))

    ax[i].scatter(mean, 5, c = "y", s = 100)
    ax[i].axvline(x=mean, c = "y", linewidth = 1, alpha = 0.5)
    ax[i].annotate('평균', xy=(mean+1, 5), xytext=(mean + 10, 6), fontsize = 15,
                 arrowprops=dict(facecolor='black', width=1, headwidth=8))
    
plt.tight_layout()

  • 위에서 만들었던, 피어슨의 3 비대칭도 계수로 f2와 f4를 비교해보자.
# 치우침이 비교적 작은 f2의 피어슨 비대칭도 계수
>>> P_SK(DF, "f2", 3)
-0.18752034836405185

# 치우침이 비교적 큰 f4의 피어슨 비대칭도 계수
>>> P_SK(DF, "f4", 3)
-1.154019976254971
  • f2와 f4의 피어슨 비대칭도 계수를 절댓값으로 비교해보면, f4가 더 큰 것을 알 수 있다.
  • 위 결과는 평균과 중앙값의 거리가 f2보다 f4가 더 멀다는 것을 의미하며, 이는 f4가 f2보다 한쪽에 더 치우쳐져 있음을 보여준다.
  • 위 그래프를 보면 알 수 있듯, f2의 분포보다 f4의 분포가 오른쪽으로 더 데이터가 몰려있음을 알 수 있다.

 

 

 

 

 

4. 왜도(Skewness)

  • 왜도는 위에서 본 피어슨의 비대칭 계수(Pearson's skewness coefficients)와 같은 분포가 어디에 치우쳐져 있는 지를 보는 보다 더 대표적인 방법이다.
  • 사실 왜도를 보다 간략화시킨 것이 피어슨의 비대칭 계수로, 왜도를 보는 방법은 피어슨의 비대칭 계수와 동일하다.
  • 음의 왜도(Negative skewness): 왜도에서는 왼쪽에 긴 꼬리를 갖는 왼쪽 꼬리 분포를, 음의 왜도나 좌비대칭형(Skewed left)이라 부른다.
  • 양의 왜도(positive skewness): 왜도에서는 오른쪽에 긴 꼬리를 갖는 오른쪽 꼬리 분포를, 양의 왜도나 우비대칭형(Skewed right)이라 부른다.
  • 왜도는 각 관찰 값의 편차에 표준편차를 나눈 값에 3 제곱을 하여 합친 값이다.
  • 왜도는 모집단의 왜도와 표본 왜도를 구하는 공식이 약간 다르다.

◎ 표본 왜도

$$b_1 = \frac{m_3}{S^3} = \frac{\frac{1}{n}\sum_{i=1}^{n}(x_i - \bar{x})^3}{[\frac{1}{n-1}\sum_{i=1}^{n}(x_i - \bar{x})^2]^{\frac{3}{2}}} = \frac{\sum_{i=1}^{n}(x_i - \bar{x})^3}{nS^3}$$

◎ 모집단의 왜도

$$G_1 = \frac{n^2}{(n-1)(n-2)}b_1 = \frac{n}{(n-1)(n-2)}\sum_{i=1}^{n}(\frac{x_i - \bar{x}}{S})^3$$


  • 모집단의 평균, 표준편차 등을 알 수 없기 때문에 표본집단을 이용하여, 모집단의 왜도를 추정한다.

 

4.1. 파이썬을 사용하여 왜도를 구해보자.

  • 위에서 만든 DF에 대하여 왜도를 구해보도록 하자.
  • 모집단의 왜도를 구하도록 해보자.
# 도수분포표의 도수를 list로 변환한다.
def freqT2list(data, column):
    
    result = []

    for i in range(len(data)):

        X_list = [data.loc[i, "X"]] * data.loc[i, column]
        result = result + X_list
        
    return result
    
    
# 왜도 계산
def skewness(data, column):
    
    target = np.array(freqT2list(data, column))

    n = len(target)
    mean = np.mean(target)
    std = np.std(target, ddof=1)

    return n/((n-1)*(n-2))*np.sum(((target - mean)/std)**3)
>>> for column in ["f1", "f2", "f3", "f4"]:
    
>>>     skew = skewness(DF, column)
>>>     print(f"column:{column}, 왜도: {skew}")


column:f1, 왜도: 1.0985509210885902e-16
column:f2, 왜도: -0.704757133477093
column:f3, 왜도: 0.7047571334770931
column:f4, 왜도: -1.1553356711140326
  • 대칭 분포인 f1의 왜도는 1.0985509210885902e-16로 나왔다.
  • 이는 0.0000000000000001098으로, 소수점 아래 16번째 자리에서 처음으로 e 앞에있는 숫자가 나온다는 의미로, 매우 작은 수로, 파이썬의 계산 방식 등으로 인해 이런 문제가 발생할 수 있으니, 깔끔하게 소숫점 아래 6자리에서 반올림을 해서 다시 보자.
>>> for column in ["f1", "f2", "f3", "f4"]:
    
>>>     skew = np.round(skewness(DF, column), 6)
>>>     print(f"column:{column}, 왜도: {skew}")


column:f1, 왜도: 0.0
column:f2, 왜도: -0.704757
column:f3, 왜도: 0.704757
column:f4, 왜도: -1.155336
  • 왜도 역시, 피어슨의 비대칭 계수처럼 음과 양으로 데이터가 어디에 모여있는지 알 수 있다. 간단하게 음수면 긴 꼬리가 음수의 방향, 양수면 긴 꼬리가 양수의 방향에 있다고 생각하자.
  • 왜도가 커질수록 분포가 크게 치우친 것이다.

 

4.2. Scipy로 왜도를 구해보자

  • scipy의 stats 모듈에 있는 skew 함수를 사용해서 더 쉽게 왜도를 구할 수 있다.
from scipy import stats
>>> for column in ["f1", "f2", "f3", "f4"]:
    
>>>     target = np.array(freqT2list(DF, column))
>>>     skew = stats.skew(target)
>>>     print(f"column:{column}, 왜도: {skew}")

column:f1, 왜도: 0.0
column:f2, 왜도: -0.6941414183237987
column:f3, 왜도: 0.694141418323799
column:f4, 왜도: -1.137932918011734
  • scipy.stats.skew(array): 왜도를 구한다.
  • scipy로 구한 왜도와 직접 만든 공식과 그 값이 꽤 다른 것을 알 수 있다.
  • 이는, scipy의 왜도는 표본 왜도를 구하며, 사용하는 공식이 약간 다르므로, 표준편차를 모 표준편차로 구했기 때문이다.
  • 이에 맞게 공식을 고쳐서 출력해보자.
# 왜도 계산
def skewness(data, column):
    
    target = np.array(freqT2list(data, column))

    n = len(target)
    mean = np.mean(target)
    std = np.std(target, ddof=0)

    return 1/n*np.sum(((target - mean)/std)**3)
>>> for column in ["f1", "f2", "f3", "f4"]:
    
>>>     skew = np.round(skewness(DF, column), 6)
>>>     print(f"column:{column}, 왜도: {skew}")


column:f1, 왜도: 0.0
column:f2, 왜도: -0.694141
column:f3, 왜도: 0.694141
column:f4, 왜도: -1.137933
  • 왜도가 돌아가는 방식에 대해 알 필요는 있으나, 개인적으로는 이 부분을 심화시키는 것은 시간낭비라고 판단된다.
  • 왜도는 분포의 형태를 대략적으로 아는 도구이므로, 보다 정확한 도구를 사용하는 것보단, 동일한 도구를 이용해서 평가하는 것이 중요하다고 판단되므로, scipy.stats의 skew함수를 사용하도록 하자.

 

 

 

 

 

5. 첨도(Kurtosis)

  • 왜도가 데이터가 어느 쪽에 쏠려 있는지를 보는 용도라면, 첨도는 데이터가 얼마나 모여있는지를 가리킨다.
  • 첨도가 클수록 데이터는 한 곳에 매우 많이 모여 있고, 첨도가 작을수록 데이터가 흩어져 있다.
  • 첨도 = 0: 표준 정규분포다.
  • 첨도 > 0: 표준 정규분포보다 평균에 데이터가 더 많이 모여 있는 뾰족한 형태다.
  • 첨도 < 0: 표준 정규분포보다 평균에서 데이터가 더 흩어진 완만한 형태다.
  • 첨도가 크다할지라도, 분산이 더 큰 경우, 완만한 그래프가 나타날 수 있으므로, 분산이 동일한 분포끼리 비교해야 한다.
  • 첨도는 각 관찰 값과 표본 평균의 편차에 표본 표준편차를 나눈 값에 4 제곱을 해서 구한다.
  • 첨도의 기준은 정규분포가 되며, 정규분포의 첨도는 3이지만, 이를 보기 쉽도록, 3을 빼서, 0으로 보정해준다.

◎ 표본 첨도

$$g_2 = \frac{m_4}{m_{2}^2} - 3 = \frac{\frac{1}{n}\sum_{i=1}^{n}(x_i - \bar{x})^4}{[\frac{1}{n}\sum_{i=1}^{n}(x_i - \bar{x})^2]^2} - 3 = \frac{n}{(n-1)^2}\sum_{i=1}^{n}(\frac{x_i-\bar{x}}{S})^4 - 3$$

◎ 모집단의 첨도(불편추정량)

$$G_2 = \frac{n-1}{(n-2)(n-3)}[(n+1)g_2 + 6]) = \frac{n(n+1)}{(n-1)(n-2)(n-3)}\sum_{i=1}^{n}(\frac{x_i - \bar{x}}{S})^4 - 3\frac{(n-1)^2}{(n-2)(n-3)}$$


 

5.1. 파이썬으로 첨도를 구현해보자.

  • 넘파이를 이용하여, 평균은 0이고, 표준편차 1인 표준 정규분포를 만들어보자.
np.random.seed(1234)
Normal_Dist = np.random.normal(loc = 0, scale = 1, size = 10000)
plt.rc('font', family='monospace')
  • 위에서 한글을 사용하기 위해 폰트를 나눔 고딕으로 설정하는 경우 음수의 "-" 부호가 깨지게 되므로, 폰트를 바꿔주었다.
fig = plt.figure(figsize=(10, 6))

plt.hist(Normal_Dist, bins = 60, density=True)
plt.show()

 

5.2. 첨도 함수를 만들어보자.

5.2.1. Numpy 라이브러리의 함수를 사용해서 만들어보자.

# 첨도
def Kurt(array):
    
    mean = np.mean(array)
    std = np.std(array, ddof = 1)
    n = len(array)

    g2 = n/(n-1)**2*np.sum(((array - mean)/std)**4) - 3

    G2 = ((n-1)/((n-2)*(n-3)))*((n+1)*g2 + 6)
    
    return G2
>>> Kurt(Normal_Dist)
-0.06547331601746852

 

5.2.2. scipy.stats 모듈의 kurtosis() 함수를 사용해서 만들어보자.

from scipy import stats
>>> stats.kurtosis(Normal_Dist)
-0.06604052394826398
  • scipy와 numpy로 직접 만든 첨도의 결과가 약간 다른 것을 알 수 있다.
  • 이는, scipy에서는 Kurt함수의 G2가 아닌 g2로 계산되기 때문으로, 즉 모집단의 첨도가 아닌 표본 첨도가 계산된다는 소리다.
def Kurt_g2(array):
    
    mean = np.mean(array)
    std = np.std(array, ddof = 1)
    n = len(array)

    g2 = n/(n-1)**2*np.sum(((array - mean)/std)**4) - 3
    
    return g2
>>> Kurt_g2(Normal_Dist)
-0.06604052394826398

>>> stats.kurtosis(Normal_Dist) == Kurt_g2(Normal_Dist)
True

 

5.3. 무작위로 생성한 표준정규분포의 난수를 더 표준정규분포에 가깝게 해 보자.

  • 위에서 만든 표준정규분포의 난수는 크기가 10,000으로, 실제 표준정규분포와 거리가 있다.
  • 이번에는 난수의 크기를 1,000,000으로 해서 실제 표준정규분포에 더 가깝게 만들어보자.
    (데이터의 양이 엄청나기 때문에 컴퓨터에 부담이 가는 경우 수를 조금 줄이자)
np.random.seed(1234)
Normal_Dist = np.random.normal(loc = 0, scale = 1, size = 1000000)
fig = plt.figure(figsize=(10, 6))

plt.hist(Normal_Dist, bins = 60, density=True)
plt.show()

>>> Kurt_g2(Normal_Dist)
0.0006377812832605301
  • 난수의 크기가 10,000일 때보다 표준정규분포에 근사하게 되니 첨도가 0에 더 가까워지는 것을 볼 수 있다.
  • 그러나, 난수를 이용해서인지 크기를 늘린다고 해서 선형으로 0에 더 가깝게 가지는 않는다.

 

5.4. 표준 편차가 1로 동일하지만, 크기가 매우 작아 표준 정규분포와 거리가 먼 난수를 생성해보자.

np.random.seed(1234)
Normal_Dist = np.random.normal(loc = 0, scale = 1, size = 10)
fig = plt.figure(figsize=(10, 6))

plt.hist(Normal_Dist, density=True)
plt.show()

>>> Kurt_g2(Normal_Dist)
-0.6112182077094723
  • 이번에 생성한 데이터는 평균이 1, 표준편차가 1로 이전의 난수와 동일하지만, 크기가 10으로 매우 작아, 분산만 동일하지 정규분포로 보기 힘든 데이터를 생성해보았다.
  • 이 경우, 첨도는 -0.611로 기존에 생성한 분포보다 더 완만하다는 것을 알 수 있다.

 

5.5. 표준편차가 다른 분포들을 만들고, 첨도를 비교해보자.

np.random.seed(1234)
Normal_Dist0 = np.random.normal(loc = 0, scale = 0.4, size = 100000)
Normal_Dist1 = np.random.normal(loc = 0, scale = 1, size = 100000)
Normal_Dist2 = np.random.normal(loc = 0, scale = 2, size = 100000)
fig = plt.figure(figsize=(10, 6))

plt.hist(Normal_Dist0, density=True, bins=60, alpha=0.5, label="N(0, 0.5)")
plt.hist(Normal_Dist1, density=True, bins=60, alpha=0.5, label="N(0, 1)")
plt.hist(Normal_Dist2, density=True, bins=60, alpha=0.5, label="N(0, 2)")
plt.legend(loc="upper right")
plt.show()

>>> Ku0 = Kurt_g2(Normal_Dist0)
>>> Ku1 = Kurt_g2(Normal_Dist1)
>>> Ku2 = Kurt_g2(Normal_Dist2)

>>> print(f"표준편차가 0.5인 분포의 첨도:{Ku0}")
>>> print(f"표준편차가 1인 분포의 첨도:{Ku1}")
>>> print(f"표준편차가 2인 분포의 첨도:{Ku2}")

표준편차가 0.5인 분포의 첨도:-0.030292593794574607
표준편차가 1인 분포의 첨도:-0.00581814290437066
표준편차가 2인 분포의 첨도:0.01995196078021788
  • 위 분포만 본다면, 표준 정규분포에 가장 가까운 노란색보다 더 뾰족하게 솟은 파란색 분포가 값이 더 커야 할 것으로 보이지만, 보다 음수에 가까운 것을 알 수 있다.
  • 이는 첨도와 표준편차는 별개로 움직이기 때문으로, 위와 같이 표준편차가 다른 경우, 그래프의 개형이 보다 더 뾰족하거나 완만하다 할지라도, 첨도가 별개로 작동하여, 잘못된 판단을 할 위험이 있다.
  • 즉, 첨도는 분산이 동일한 상태에서 사용해야, 데이터가 평균에 모인 정도에 대해 제대로 된 비교를 할 수 있다.
728x90
반응형
728x90
반응형

편차(Deviation)

 이전 포스트에서는 관측값과 평균의 차이인 편차를 이용해서 구하는 대표적인 산포도 "분산(Varience)"와 "표준편차(Standard Deviation)에 대해 학습해보았다.

 이번 포스트에서는 편차를 이용하는 다른 산포도인 절대 편차(Absolute deviation)와 변동 계수(Coefficient of variation)에 대해 알아보자.

 

 

 

 

 

1. 절대 편차(Absolute deviation)

  • 절대 편차는 관측값에서 평균(Mean) 또는 중앙값(Median)을 빼고, 그 편차에 절댓값을 취하고, 평균을 구한 것으로, 분산에서 제곱 대신 절댓값을 취한 것이다.

$$ AAD = \frac{1}{N}\sum_{i=1}^{N}\left | x_i - \mu \right | = \frac{\left | x_1 - \mu \right | + \left | x_2 - \mu \right | + \cdots + \left | x_N - \mu \right |}{N} $$


  • 평균 절대 편차(Average Absolute Deviation, AAD): 각 관측값에 평균을 빼고, 편차 절댓값의 평균을 구하는 경우로, 평균 편차(Mean Deviation), 절대 편차(Absolute Deviation)라고도 한다.
  • 중앙값 절대 편차(Median Absolute Deviation, MAD): 각 관측값에 중앙값을 빼고, 편차 절댓값의 평균을 구하는 방식이다.
  • 최소 절대 편차(Least Absolute Deviation, LAD): 회귀분석에서 최소제곱법(LSA)에 대응되는 기법으로, 절대 오차의 합이 최소가 되는 해를 찾는 기법이다.
  • 분산은 이상치가 존재하는 경우, 편차를 제곱시키므로 편차가 증폭될 수 있다. 절대 편차를 하는 경우, 제곱을 하지 않으므로, 이상치 존재로 인한 편차 증폭 문제를 보완할 수 있다.
  • 예) 평균 $100$, 이상치가 $10000$인 경우, $9,900^2 = 9,8010,000$이 되어 값이 매우 커지게 된다.

 

1.1. 파이썬과 절대 편차

Data_for_study.csv
3.39MB

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

 

1.1.1. numpy를 사용하여 절대 편차(AD) 만들기

  • 데이터 분석에 주로 사용되는 라이브러리인 numpy나 pandas는 따로 해당 함수를 제공하지 않는다.
  • 그러나, numpy를 사용해서 만들면, 난이도도 쉽고, numpy 기본 함수의 속도가 매우 빨라, 성능도 나쁘지 않다.
def AD(data, column, way, dof = 1):
    
    target_array = data[column].to_numpy()
    
    if way == "mean":
        key_value = np.mean(target_array)
        
    elif way == "median":
        key_value = np.median(target_array)
        
    return np.sum(np.abs(target_array - key_value))/(len(target_array) - dof)
  • way로 "mean"이 들어오는지, "median"이 들어오는지에 따라 평균 절대 편차나 중앙값 절대 편차가 수행된다.
  • np.abs(array): array에 있는 모든 원소를 양수로 만든다.
  • 표본 집단에 대한 분석을 가정하여, 자유도로 나눈다.
>>> AD(Rawdata, "키", "mean")
7.0767022252239355

>>> AD(Rawdata, "키", "median")
7.046750497784634

>>> AD(Rawdata, "몸무게", "mean")
10.159191145859118

>>> AD(Rawdata, "몸무게", "median")
10.00470697974779

 

 

 

 

 

2. 절대편차(AAD)가 아닌 표준편차(Standard Deviation)를 사용하는 이유

  • 절대편차를 사용하지 않는 이유는 총 4가지가 있다.
  1. 편차란 유클리디안 거리(Euclidean distance)에서, 각 관측값이 특정값(평균)으로부터 떨어져 있는 거리를 의미하기 때문이다.
  2. 절대편차의 최솟값은 평균이 아닌 중앙값이다.
  3. 절대편차는 미분되지 않는다.
  4. 절댓값을 연산하기 위해선 공식이 복잡해진다.

 

2.1. 편차와 유클리디안 거리(Euclidian distance)

  • 절대편차를 사용하지 않는 이유를 알기 위해선, 편차의 의미에 대해 알 필요가 있다.
  • "편차는, 관측값이 특정값(평균)으로부터 떨어진 거리"인데, 이를 구체적으로 보기 위해 고등학교 때 배웠던, 두점 사이의 거리 공식을 보도록 하자.

  • 위 두 점 사이의 거리는 2차원에서의 거리다. 이번에는 n차원의 공간에서 두 점간의 거리를 알아내는 공식인 유클리디안 거리(Euclidan distance)에 대해 알아보자. 위 2차원에서의 거리 공식과 개념이 매우 유사하다.

  • 위 유클리디안 거리에서 점 $B_i$를 평균인 $\mu$로 만든다면, 우리가 아는 분산(Variance) 공식이 나온다.
  • 그러나, 위 유클리디안 거리는 n차원에서의 두 점 사이의 거리를 의미한다. 때문에 1차원에서라면, 절대 평균 편차 공식을 사용해도 되지 않는가 하는 의문이 들 수 있다.

 

2.2. 절대 편차의 최솟값은 평균이 아닌 중앙값이다.

  • "편차는 관측값들이 특정값으로부터 떨어진 정도(거리)"라고 하였다.
  • 이 특정값을 우리는 대푯값(Representative value)라고 하며, 대푯값은 어떤 데이터를 대표하는 값이다.
  • 이번에는 관측값의 집합$X = (1, 2, 5)$가 있다고 가정하고, 이것을 분산으로 구할 때와, 절대편차로 구할 때에 따라, 임의의 대푯값 $R$이 분산($\sigma^2$)과 절대편차에 대하여 어떠한 함수의 형태를 그리는지 보도록 하자.
# 분산인 경우
def f(x):
    
    return((1-x)**2 + (2-x)**2 + (5-x)**2)/3
    
 
 
# 절대 편차인 경우
def h(x):
    
    return(np.abs(1-x) + np.abs(2-x) + np.abs(5-x))/3
def draw_Function(f, key):

    # 대푯값에 대한 시각화를 해보자.
    R = np.arange(0, 10, 0.001)
    y = f(R)

    # 최솟값을 구한다.
    min_y = y.min()
    min_x = R[np.where(y == min_y)][0]
    
    # 그래프 그리기
    plt.rc('font', family='NanumGothic')
    fig = plt.figure(figsize=(8, 6))

    # 함수 f의 시각화
    plt.plot(R, y)

    # y 값에서의 최솟값
    plt.scatter(min_x, min_y, color="r",s = 100)

    # 그래프 꾸며주기
    plt.title(f"{key}에 대한 대푯값", fontsize = 20, pad = 30)
    plt.xlabel("대푯값", fontsize = 15, labelpad = 10)
    plt.ylabel(f"{key}", rotation = 0, fontsize = 15, labelpad = 20)


    plt.show()
    
    
    print(f"({np.round(min_x, 3)}, {np.round(min_y, 3)})에서 함수 f(x)는 최솟값을 갖는다.")

 

2.2.1. 분산에 대한 대푯값의 그래프

>>> draw_Function(f, "분산")
(2.667, 2.889)에서 함수 f(x)는 최솟값을 갖는다.

  • 대푯값 R에 대한 분산($\sigma^2$)의 그래프를 그렸을 때, 이차 함수 형태를 그리는 것을 볼 수 있다.
  • 분산의 최솟값은 2.889이며, 이는 대푯값(R) 2.667이다.
  • 관측값 X = [1, 2, 5]의 평균은 다음과 같다.
>>> np.mean([1,2,5])
2.6666666666666665

 

  • 관측값의 평균과 분산이 최솟값을 갖는 대푯값은 일치한다.
    (약간의 차이가 있는 이유는, 그래프 생성 시, np.arange(start, end, by)에 대하여, by의 간격이 0.001로 이산된 부분이 생겼기 때문이다. by를 작게 할 수도록 평균과 근사해진다.)
  • 즉, 분산에 있어 대푯값은 평균이다.

 

2.2.2. 절대편차에 대한 대푯값의 그래프

>>> draw_Function(h, "절대편차")
(2.0, 1.333)에서 함수 f(x)는 최솟값을 갖는다.

  • 절대편차의 개형은 꺾은선 그래프와 같은 형태를 그린다.
  • 절대편차에서의 최솟값은 대푯값 2.0에서 존재한다.
  • 대푯값 2.0은 관측값의 집합 X=[1,2,5]에서 중앙값(Median)인 2이다.

 

2.2.3. 절대편차에 대한 대푯값의 그래프에서 집합의 원소가 짝수일 때

  • X = [1, 2, 5, 7]이 관측값의 집합이라 가정해보자.
def h1(x):
    
    return(np.abs(1-x) + np.abs(2-x) + np.abs(5-x) + np.abs(7-x))/4
>>> draw_Function(h1, "절대편차")
(2.0, 2.25)에서 함수 f(x)는 최솟값을 갖는다.

  • 집합의 원소가 짝수인 경우, 그래프의 개형에서 아래쪽이 평평해진다.
  • 대푯값 R이 한 점이 아닌 선이 되었으므로, 선에 대한 대푯값으로 중앙값을 구해보자.
>>> R = np.arange(0, 10, 0.001)
>>> y = h1(R)

>>> min_y = y.min()
>>> min_x = R[np.where(y == min_y)]

>>> print("절대편차에 대한 대푯값의 중앙값:", str(np.median(min_x)))
절대편차에 대한 대푯값의 중앙값: 3.5

>>> print("집합 X의 중앙값:", str(np.median([1,2,5,7])))
집합 X의 중앙값: 3.5
  • 집합의 개수가 짝수일 때도, 대푯값이 중앙값인 것을 알 수 있다.

 

2.2.4. 정리

  • 위 결과를 볼 때, 절대 편차는 관측값으로부터 중앙값까지의 거리의 평균인 것임을 알 수 있다.
  • 즉, 차원이 1개라고 가정한다 할지라도, 절대 편차의 대푯값은 중앙값이므로, 분산처럼 평균에서 각 원소까지의 거리 제곱의 평균을 구할 수 있는 것이 아니라, 중앙값에서 각 원소까지의 거리의 절댓값의 평균이 되게 되어, 의미가 달라지게 된다.
  • 또한, 절대 편차를 사용할 때, 중앙값 절대 편차(MAD)를 사용하게 되는 이유가 바로 위와 같다.

 

2.3. 절대편차는 미분되지 않는다.

  • 위에서 만든 절대편차의 대푯값 그래프를 가지고 와보자.

  • 미분을 위한 기본 조건은 좌 미분과 우 미분이 같아야 한다는 것인데, 보시다시피 절대편차는 매끈한 곡선이 아니므로, 좌 미분과 우 미분이 동일하지 않은 점이 반드시 존재한다.
  • 미분이 되지 않는다는 것은 산포도의 관점에서는 그다지 중요한 이유가 아니지만, 손실함수의 미분을 통한 최적해를 찾아가는 과정인 딥러닝의 학습 부분에서는 상당히 중요한 이유가 된다.

 

2.4. 절댓값 연산을 위해선 연산이 복잡해진다.

  • 절댓값 연산을 위해선, 각 편차의 부호를 확인하는 과정이 필요하다.
  • 반면에 분산은 편차를 일괄적으로 제곱하여 모든 부호가 양으로 만들기 때문에 연산이 아주 간단해진다.

 

2.5. 정리

  • 위 내용을 줄여보면, 산포도라는 대상을 위해 절대 편차를 사용하는 것에는 문제가 없으나, 그 기준을 평균이 아닌 중앙값(Median)으로 잡는 것이 좋으며, 중간값 절대 편차(MAD)를 사용한다 할지라도, 태생적인 한계점이 많으므로, 중간값 절대 편차보다는 분산을 사용하는 것이 좋다.

 

 

 

 

 

3. 변동 계수(Coefficient of Variation, CV)

  • 변동 계수는 표준 편차를 표본 평균이나 모 평균과 같은 산술 평균으로 나눈 값이다.

$$ CV = \frac{S}{\bar{X}} $$


  • 변동 계수는 상대 표준 편차(Relaative standard deviation, RSD)라고도 한다.
  • 변동 계수는 측정 단위가 서로 다른 데이터의 산포도를 비교할 때 사용한다.
  • 표준편차와 산술 평균 모두 단위가 있으나, 이 둘을 나눔으로써 단위가 사라지게 된다.
  • 평균과 표준편차는 단위가 존재하며, 서로 다른 데이터끼리 그 흩어져있음을 비교하기 위해선 변동 계수를 만들어, 단위를 없애야만 서로 비교해볼 수 있다.
  • 변동 계수가 클수록 상대적인 산포도가 크다고 할 수 있다.

 

3.1. 예시 및 파이썬 구현

  • A반의 키는 평균 164cm, 표준편차는 12cm가 나왔다.
  • A반의 몸무게는 평균 57 kg, 표준편차 10kg이 나왔다.
  • 키와 몸무게는 서로 다른 변수이므로, 단위 역시 다르기 때문에 이들의 표준편차를 단순하게 비교해서는 안된다.
def CV(mean, std):
    
    return std/mean
# 키의 변동계수
>>> CV(164, 12)
0.07317073170731707

# 몸무게의 변동계수
>>> CV(57, 10)
0.17543859649122806
  • 위 결과를 보면, 몸무게의 변동 계수는 0.1754, 키의 변동 계수는 0.731로 몸무게의 변동 계수가 더 크므로, 몸무게가 키보다 산포도가 크다는 것을 알 수 있다.
  • 변동 계수는 그 공식이 매우 단순하므로, 위처럼 평균과 표준편차를 출력하여 바로 구할 수도 있다.
def CV(data, column):
    
    target_array = data[column].to_numpy()
    
    return np.std(target_array)/np.mean(target_array)
>>> CV(Rawdata, "키")
0.05189104256446505

>>> CV(Rawdata, "몸무게")
0.2196611158371381
  • 또는 위 함수처럼 numpy로 평균과 표준편차를 직접 생성하여, 구할 수도 있다.
728x90
반응형
728x90
반응형

편차(Deviation)


◎ 편차(Deviation): 관측값들이 특정값(평균)으로부터 떨어진 정도(거리)이다.

$$ Dev = x_i - \mu $$


  • 이전 포스트에서 학습하였던, 범위, 사분위간 범위는 관측값 간의 간격을 사용해 산포도를 나타냈다. 이번에는 평균과 관측값의 차이인 편차(Deviation)를 이용해 산포도를 나타내는 방법에 대해 알아보겠다.
  • 편차를 이용해 산포도를 나타내는 방법은 분산(Varience), 표준 편차(Stadard deviation), 절대 편차(Absolute deviation), 변동 계수(Coefficient of variation) 등이 있다.
  • 편차는 양수, 음수 모두 가능하며, 평균보다 크면 양수, 작으면 음수가 된다.
  • 편차의 크기는 관측값이 평균으로부터 떨어진 거리를 말한다.
  • 모집단 평균에서의 편차는 오류(Error)라고 하며, 표본 집단 평균에서의 편차는 잔차(Observed value)라고 한다.

 

 

 

 

 

1. 분산(Varience)과 표준편차(Standard Deviation)


◎ 분산(Varience): 편차 제곱의 평균으로, 평균으로부터 관찰값들이 떨어진 거리의 제곱 평균이다.

◎ 표준편차(Standard Deviation): 분산의 양의 제곱근이다.


  • 중심경향치에서 평균이 제일 많이 쓰이듯, 산포도에서 제일 많이 쓰이는 분산과 표준편차가 나오게 된 개념은 편차의 평균을 구하려는 시도에서 시작되었다.
  • 편차는 각 관측값이 평균으로부터 떨어진 거리이므로, 그 평균을 알 수 있다면, 관측값들이 평균으로부터 떨어진 정도를 한 값으로 알 수 있다.
  • 편차 평균의 공식은 다음과 같다.

$$\frac{1}{N}\sum (x_i - \mu)$$

  • 그러나, 위 공식은 무조건 결괏값이 0이 나온다.

  • 모든 편차의 합이 0이 되는 것을 막기 위해, 편차에 제곱을 해줘서 모든 편차를 양수로 만들고, 이의 평균을 구한 것이 바로 분산(Varience)이다.
  • 본래 우리가 구하고자 했던 값은 편차의 평균이었다. 그러나, 편차의 합은 0이 나오기 때문에 제곱을 해주었고, 그로 인해 편차의 증폭과 단위의 제곱이 일어났다.
  • 위 문제를 해결하고자, 분산에 양의 제곱근을 씌워, 제곱으로 인한 편차의 증폭과 단위를 원상 복귀하고자, 분산에 양의 제곱근을 씌운 것이 표준편차(Standard deviation)다.

 

 

 

 

 

2. 모집단과 표본집단의 분산과 표준편차

  • 분산의 모수와 통계량은 계산 방법과 표기 방법이 달라진다.
  • 모집단의 분산과 표준편차

모집단의 분산: $\sigma ^2 = \frac{\sum(X_i - \mu)^2}{N}$

모집단의 표준편차: $\sigma = \sqrt{\frac{\sum(X_i - \mu)^2}{N}}$


  • 표본집단의 분산과 표준편차

표본집단의 분산: $S ^2 = \frac{\sum(X_i - \bar{X})^2}{n-1}$

표본집단의 표준편차: $S = \sqrt{\frac{\sum(X_i - \bar{X})^2}{n-1}}$


  • 단순 기술통계량을 구할 땐, 모집단의 분산과 표준편차를 구하는 방식으로 하면 된다.
  • 관찰 값들의 차이가 클수록 편차가 커지므로, 분산 $\sigma^2$은 커진다.
  • 분산은 편차의 제곱이므로, 평균으로부터 멀어질수록 그 차이가 증폭되게 된다.
  • 분산은 편차의 제곱이므로, 단위 역시 제곱된다.
  • 위 문제를 해결하기 위해 양의 제곱근을 씌워서 표준편차를 만든다.

 

 

 

 

 

3. 표본 분산에 $(n-1)$을 나눠주는 이유.

 표본 분산에 $(n-1)$을 나눠주는 이유는 꽤 복잡하기 때문에 기초통계학에서는 이를 다루지 않는다. 그러나, 이 부분을 그냥 넘어가게 된다면, 앞으로 이와 비슷한 경우가 등장할 때마다, 수식을 이해하는 것이 아닌, 수식을 암기만 하고 넘어갈 위험이 있다.

 표본 분산에 $(n-1)$을 나눠준 이유를 알기 위해서는 먼저 "자유도(Degree of freedom)"라는 개념에 대해 알아야 한다. 자유도의 개념은 꽤나 모호하고, 국내에서는 이를 명확히 설명하는 글을 찾기 어렵다. 위키피디아 영문판에서는 자유도를 무엇인지 대략적인 개념을 이해해보자.

 

3.1. 자유도(Degree of freedom)


◎ 자유도(Degree of freedom):

  • In statistics, the number of degrees of freedom is the number of values in the final calculation of a statistic that are free to vary.
    ("Degrees of Freedom". Glossary of Statistical Terms. Animated Software. Retrieved 2008-08-21.)
  • Estimates of statistical parameters can be based upon different amounts of information or data. The number of independent pieces of information that go into the estimate of a parameter are called the degrees of freedom. In general, the degrees of freedom of an estimate of a parameter are equal to the number of independent scores that go into the estimate minus the number of parameters used as intermediate steps in the estimation of the parameter itself 
    (Lane, David M. "Degrees of Freedom". HyperStat Online. Statistics Solutions. Retrieved 2008-08-21.)
  • 출처: en.wikipedia.org/wiki/Degrees_of_freedom_(statistics)

 위 자유도의 정의를 해석해보면, 자유도는 다음과 같은 성질을 갖는다.

  1. 통계의 최종 계산에서 자유롭게 변경 가능한 값의 개수
  2. "통계의 매개 변수의 추정치(Estimates of statistical parameters)"는 정보나 데이터에서 다른 값의 개수를 기반으로 하며, 매개 변수 추정치에 들어가는 독립적인 정보의 수를 자유도라 한다.
    일반적으로, 모수 추정의 자유도는 추정에 들어가는 독립된 값의 개수에 모수 추정의 중간 단계로 사용되는 모수의 수를 뺀 값과 같다.

 

 위 내용을 보다 쉽게 풀어써보면 다음과 같다.

  • 자유도는 독립된 값의 개수와 이를 통해 모수를 추정할 때, 사용되는 결정된 정보의 수를 뺀 것으로, 더 쉽게 말하면, 독립적인 미지수의 개수에 그 독립적인 미지수에 의해 자동으로 결정되어 버리는 값의 수를 빼는 것이다.
  • 표본집단의 각 원소는 완전 무작위 표본 추출 방법으로 복원 추출하여 실시한다면, 각 원소는 독립적지만, 그 독립된 원소들이 결정되면, 평균, 표준편차 등의 통계량들은 자동으로 결정되므로, 이들 통계량은 독립적이지 않다고 할 수 있다.
  • 그러므로, 자유도는 표본 집합의 원소의 수에 모수 추정 과정에서 표본 집합이 결정되면, 자동으로 결정되는 평균, 표준편차와 같은 파라미터의 개수를 빼서 구하게 된다.

 

3.2. 자유도를 사용해서 표본 분산을 계산하는 이유

  • 표본집단은 모집단에 비해, 표본의 수가 매우 적으므로, 데이터가 편향(Bias)되어 있다.
  • 편향되어 있기 때문에 표본집단의 크기는 모집단의 크기보다 작으며, 이를 보정하여, 통계량의 값을 크게 만들어줘서 불편추정량(Unbiased estimate)으로 만들어줘야 한다.
  • 이때, 독립적인 정보의 수인 n이 아니라, 자유도로 나눠준다면, 표본 집단의 통계량의 기댓값이 모집단의 통계량의 기댓값과 같아져 불편추정량이 된다.
  • 다른 값이 아니라 자유도를 사용하는 이유는, 자유도로 나눴을 때, 불편추정량이 만들어지기 때문이며, 이는 최소분산불편추정량(Uniformaly Minimum Variance Unbiased Estimator, UMVUE)를 통해 증명할 수 있다.
  • 모 분산과 표본 분산의 기댓값에 대하여, 표본 분산을 자유도로 나눴을 때, 어떤 결과가 나오는지 보도록 하자.

  • 위 결과를 보듯, 표본 분산의 기댓값은 $n$이 아니라, 자유도인 $n-1$로 나눴을 때, 모분산과 같게 된다.
  • 그러므로, 표본 분산에는 $n$이 아닌 자유도 $n-1$로 나눠줘야 한다.

 

3.3. 자유도를 무시하는 경우

  • 위 자유도의 개념을 알고 나니, 표본 집단을 이용하여, 모집단을 추정할 때는 불편추정량을 만들어주기 위해, 자유도의 개념이 필요하다는 것을 알 수 있다.
  • 그러나, 위 개념을 모르는 상태에서 현장에서 데이터 분석을 해본 사람들은 지금까지 큰 문제가 없었을 것인데, 이는 앞서 말했던, 불편추정량이 생기는 원인 때문이다.
  • 불편추정량은 표본 집단의 양이 작기 때문에 생기는 현상인데, 만약 표본 집단의 양이 매우 많다면 어떻겠는가
  • 자 표본 분산을 구하는데, 표본 집단의 크기가 10개라고 생각해보자, 자유도 9와 원래 값인 10은 꽤 큰 차이가 있다. 그러나 표본 집단의 크기가 10,000개라고 해보자, 10,000개와 9,999개는 거의 차이가 없다.
  • 즉, 표본의 크기가 매우 크다면, 자유도를 무시하고 n으로 계산해도 아무 문제가 없다.

 

 

 

 

 

4. 파이썬으로 표준편차를 구해보자.

 위에서 봤던, 표준편차와 분산의 개념과 달리 파이썬으로 표준편차를 구하는 것은 매우 쉽다. 아래 데이터를 이용해서, 양적 데이터인 "키", "몸무게"의 표준편차를 뽑아보도록 하자.

Data_for_study.csv
3.39MB

import numpy as np
import pandas as pd
Rawdata = pd.read_csv("Data_for_study.csv")

 

4.1. 위 식대로 함수를 만들어보자

# 모 표준편차
def Pop_standard_deviation(data, column):
    
    target_array = data[column].to_numpy()
    result = np.sqrt(np.sum(((target_array - np.mean(target_array)) ** 2))/len(target_array))
    
    return result


# 표본 표준편차
def Sample_standard_deviation(data, column):
    
    target_array = data[column].to_numpy()
    result = np.sqrt(np.sum((target_array - np.mean(target_array)) ** 2)/(len(target_array)-1))
    
    return result
# 키의 모표준편차
>>> Pop_standard_deviation(Rawdata, "키")
8.589382382344954

# 키의 표본표준편차
>>> Sample_standard_deviation(Rawdata, "키")
8.589459420964639

# 데이터의 크기
>>> len(Rawdata)
55748
  • 데이터의 크기가 55,784로 꽤 크다 보니, 모표준편차와 표본표준편차가 거의 유사하다는 것을 알 수 있다.

 

4.2 Pandas와 Numpy를 사용하여 표준편차를 구하자.

# pandas 함수로 구하기
>>> Rawdata.키.std()
8.589459420964502

>>> Rawdata.몸무게.std()
12.935373134509032


# numpy 함수로 구하기
>>> np.std(Rawdata.키.to_numpy())
8.589382382344954

>>> np.std(Rawdata.몸무게.to_numpy())
12.9352571175121
  • pd.Series.std(): 시리즈의 value를 대상으로 표준편차를 구한다. - 표본표준편차
  • np.std(array): 배열을 대상으로 표준편차를 구한다. - 모표준편차
  • 몸무게의 표준편차가 키의 표준편차보다 크므로, 몸무게의 데이터가 키의 데이터보다 평균에서 멀리 떨어진 것을 알 수 있다.
  • 위 결과를 보면, Pandas와 Numpy의 표준편차 결과가 약간이긴 하지만, 차이가 나는 것을 알 수 있다.
  • Pandas와 Numpy의 표준편차 계산 방식의 차이를 보기 위해, 표본을 아주 조금만 뽑아서 표본의 크기가 커져서, 자유도의 보정 영향력이 줄어드는 걸 줄여서 봐보자.
# 무작위로 데이터를 30개만 sampling 하자.
>>> sample_data = Rawdata.sample(30)


# 공식대로 만든 모표준편차
>>> Pop_standard_deviation(sample_data, "키")
7.172806672116262

# 공식대로 만든 표본표준편차
>>> Sample_standard_deviation(sample_data, "키")
7.295427634334816

# numpy의 std 함수
>>> np.std(sample_data.키.to_numpy())
7.172806672116262

# pandas의 std 함수
>>> sample_data.키.std()
7.295427634334816
  • 위 결과를 보면, numpy의 표준편차는 기본적으로 모표준편차를 구하는 공식으로 돌아가는 것을 알 수 있다.
  • pandas의 표준편차는 기본적으로 표본표준편차를 구하는 공식으로 돌아간다.
  • 기본적으로 numpy의 성능이 더 좋기 때문에, numpy를 사용하여 표본표준편차를 구하고자 하는 경우도 많을 텐데, numpy로 표본표준편차를 구하고자 한다면, 다음 파라미터를 추가해줘야 한다.
# numpy를 사용해서 표본표준편차 구하기
>>> np.std(sample_data.키.to_numpy(), ddof = 1)
7.295427634334816
  • ddof 파라미터는 자유도에서 빼 줄 독립적이지 않은 파라미터의 크기를 말한다.
  • 표본 데이터를 대상으로 numpy를 연산한다면, 자유도의 존재를 잊지 말고 꼭 반영해주자.
  • 물론, 데이터의 양이 굉장히 크다면, 굳이 불편추정량을 만들기 위해, 자유도로 나눠주지 않아도 괜찮다.

 

 

 

 

 

5. 도수분포표를 이용한 표준편차 계산

  • 앞서 도수분포표를 사용해서 평균, 중앙값 등을 구했듯, 도수분포표를 사용해서 표준편차를 계산할 수도 있다.
  • 앞서 말했듯, 원시자료를 가지고 있다면, 굳이 도수분포표로 표준편차를 구하지 않아도 되지만, 도수분포표밖에 없다면, 도수분포표를 사용해서 표준편차를 유추해야 한다.
  • 이전에 만들었던, 연속형 데이터를 범주형 데이터로 만드는 함수를 사용해서 도수분포표를 만들고, 해당 도수분포표를 이용해서 표준편차를 추론해보자.
def make_freq_table(data, column):
    """
    -------------------------------------------------------------------------------
    지정된 변수의 관찰값이 20개보다 많은 경우, 10개의 등급을 가진 데이터를 반환한다.
    10개의 등급은 동일한 간격으로 자른 것이다.
    -------------------------------------------------------------------------------
    Input: DataFrame, str
    Output: DataFrame
    """
    # array 생성
    target_array = data[column].to_numpy()

    # class의 수가 20개보다 많은 경우 10개로 줄인다.
    class_array = np.unique(target_array)

    if len(class_array) > 20:

        min_key = class_array.min()
        max_key = class_array.max()
        split_key = np.linspace(min_key, max_key, 10)

        a0 = str(round(split_key[0], 2)) + " 이하"
        a1 = str(round(split_key[0], 2)) + " ~ " + str(round(split_key[1], 2))
        a2 = str(round(split_key[1], 2)) + " ~ " + str(round(split_key[2], 2))
        a3 = str(round(split_key[2], 2)) + " ~ " + str(round(split_key[3], 2))
        a4 = str(round(split_key[3], 2)) + " ~ " + str(round(split_key[4], 2))
        a5 = str(round(split_key[4], 2)) + " ~ " + str(round(split_key[5], 2))
        a6 = str(round(split_key[5], 2)) + " ~ " + str(round(split_key[6], 2))
        a7 = str(round(split_key[6], 2)) + " ~ " + str(round(split_key[7], 2))
        a8 = str(round(split_key[7], 2)) + " ~ " + str(round(split_key[8], 2))
        a9 = str(round(split_key[8], 2)) + " 이상"
        new_index = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]


        target_array= np.where(target_array <= split_key[0], 0,
                               np.where((target_array > split_key[0]) & (target_array <= split_key[1]), 1,
                                        np.where((target_array > split_key[1]) & (target_array <= split_key[2]), 2,
                                                 np.where((target_array > split_key[2]) & (target_array <= split_key[3]), 3,
                                                          np.where((target_array > split_key[3]) & (target_array <= split_key[4]), 4,
                                                                   np.where((target_array > split_key[4]) & (target_array <= split_key[5]), 5,
                                                                            np.where((target_array > split_key[5]) & (target_array <= split_key[6]), 6,
                                                                                     np.where((target_array > split_key[6]) & (target_array <= split_key[7]), 7,
                                                                                              np.where((target_array > split_key[7]) & (target_array <= split_key[8]), 8, 9)))))))))


    # 도수분포표 생성
    freq_table = pd.DataFrame(pd.Series(target_array).value_counts(), columns = ["freq"])
    freq_table.index.name = column

    freq_table.sort_index(inplace = True)
    freq_table["ratio"] = freq_table.freq / sum(freq_table.freq)
    freq_table["cum_freq"] = np.cumsum(freq_table.freq)
    freq_table["cum_ratio"] = np.round(np.cumsum(freq_table.ratio), 2)
    freq_table["ratio"] = np.round(freq_table["ratio"], 2)

    if "new_index" in locals():
        freq_table.index = new_index
        freq_table.index.name = column

    return freq_table
Rawdata = pd.read_csv("Data_for_study.csv")
make_freq_table(Rawdata, "키")

  • 위 도수분포표에서 사용해야 할 것은 범주화된 $X_i$인 키의 중앙값과 freg다.
  • 표준편차의 식을 다음과 같이 유도해보자.

$$\sqrt{\frac{\sum(X_i - \bar{X})^2}{n}} = \sqrt{\frac{\sum fX_i^2}{n} - \left ( \frac{\sum fX_i}{n} \right )^2}$$


# 중앙값 생성
키_도수분포표 = make_freq_table(Rawdata, "키")
키_도수분포표.reset_index(drop=False, inplace=True)

# 1 부터 8번 행까지 처리
키1_8_array = 키_도수분포표[1:9].키.str.split(" ~ ", expand=True).values.astype("float")
키1_8_array_mid = (키1_8_array[:,0] + 키1_8_array[:,1])/2

# 0, 9 행 처리
키0_9_array = 키_도수분포표.loc[[0,9]].키.str.partition(" ")[0].to_numpy().astype("float")

# 중앙값 생성
키1_8_array_mid = np.insert(키1_8_array_mid, 0, 키0_9_array[0])
키_도수분포표["중앙값"] = np.insert(키1_8_array_mid, len(키1_8_array_mid), 키0_9_array[1])
키_도수분포표

키_도수분포표["$fX$"] = 키_도수분포표["freq"] * 키_도수분포표["중앙값"]
키_도수분포표["$fX^2$"] = 키_도수분포표["freq"] * 키_도수분포표["중앙값"]**2
키_도수분포표

# 도수분포표로 추론한 표준편차
>>> 앞부분 = 키_도수분포표["$fX^2$"].sum()/키_도수분포표["freq"].sum()
>>> 뒷부분 = (키_도수분포표["$fX$"].sum()/키_도수분포표["freq"].sum())**2
>>> np.sqrt(앞부분 - 뒷부분)
8.771635569514544
# 실제 표준편차
>>> np.std(Rawdata.키)
8.589382382344816
  • 자유도가 아닌 총 빈도 수인 $n$으로 나눠 모 표준편차를 구하였다.
  • 표본 표준편차를 구하고자 하는 경우, 위 공식에서도 $n$이 아니라 $n-1$로 나눠주면 된다.
  • 실제 표준편차는 8.5893이 나왔으며, 도수분포표로 추론한 표준편차는 8.7716이 나왔다.
  • 앞서 말했듯, 이 방법은 도수분포표만 사용 가능한 경우에 쓸 수 있는 방법이다.
728x90
반응형
728x90
반응형

산포도(Dispersion)


◎ 산포도: 분산도(Degree of dispersion), 변산성(Variability)이라고도 하며, 관찰된 데이터가 흩어져 있는 정도를 말한다. 


  • 통계학은 기본적으로 데이터가 어디에 모여있고, 얼마나 흩어져 있는지를 통해 데이터를 파악한다고 하였다.
  • 지금까지 데이터가 어디에 모이는지에 대한 중심경향치(Center Tendency)를 알아보았으므로, 이번에는 데이터가 얼마나 흩어져있는지를 알 수 있는 산포도(Dispersion)에 대해 알아보겠다.
  • 산포도에는 범위(Range), 사분위간 범위(Interquartile range), 분산(Varience), 표준 편차(Standard deviation), 절대 편차(Absolute deviation), 변동 계수(Coefficient of variation) 등이 있다.
  • 학습 데이터(참고 포스트: "통계 분석을 위한 데이터 준비")

Data_for_study.csv
3.39MB

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

Rawdata = pd.read_csv("Data_for_study.csv")

 

 

 

 

 

1. 범위(Range)


◎ 범위(Range): 관찰 값에서 최댓값과 최솟값의 차이다.

$$ range = max - min $$


  • 데이터가 흩어진 정도를 보는 가장 간단한 방법으로, 관찰 값이 시작되는 최솟값에서 관찰값이 끝나는 최댓값의 차이다.
  • 범위는 구하기 쉽고, 해석하기도 쉬우나, 간단한만큼 많은 단점을 가지고 있다.
  • 범위는 이상치(다른 데이터에 비해 지나치게 크거나, 작은 값)의 영향을 너무 심하게 받는다.
  • 최댓값에서 최솟값의 차이만 구하므로, 같은 범위를 갖는다고 할지라도 데이터의 분포가 완전히 다를 수 있다.
  • 학습 데이터에서 "학업성적", "건강인지"의 범위를 구해보자.
def cal_range(data, column):
    
    target_series = data[column]
    
    return target_series.max() - target_series.min()
  • pd.Series.min(): 시리즈의 최솟값을 구한다.
  • pd.Series.max(): 시리즈의 최댓값을 구한다.
>>> cal_range(Rawdata, "학업성적")
4.0

>>> cal_range(Rawdata, "건강인지")
4.0
  • "학업성적", "건강인지"의 도수분포표를 이용해, 도수분포다각형(Frequency distribution polygon)을 그려보자.
def FDPolygon(data, column):
    
    freq_table = data[column].value_counts().sort_index()
    ratio_x = freq_table.values/sum(freq_table.values)

    # 도수분포다각형 그리기
    plt.plot(freq_table.index, ratio_x, label = column)

    plt.xticks(np.arange(1, len(freq_table.index) + 1))
plt.rc('font', family='NanumGothic')
fig = plt.figure(figsize=(8, 6))

FDPolygon(Rawdata, "건강인지")
FDPolygon(Rawdata, "학업성적")

# 그래프 꾸미기
plt.legend(loc = "upper right")
plt.title("건강인지 & 학업성적 도수분포다각형", fontsize = 20, pad = 30)
plt.xlabel("관측값", fontsize = 15, labelpad = 10)
plt.ylabel("비율", rotation = 0, fontsize = 15, labelpad = 20)
plt.show()

  • "건강인지", "학업성적" 변수의 범위는 동일하지만, 데이터의 분포는 상당히 다른 것을 알 수 있다.
  • 즉, 범위는 한 변수 안의 데이터의 관측값의 시작 값(최솟값)에서 끝 값(최댓값)까지의 간격을 알 수 있지만, 데이터가 어떻게 분포해 있는지 알 수는 없다.

 

 

 

 

 

2. 사분위간 범위(Interquartile range, IQR)


◎ 사분위간 범위:  데이터를 25% 단위로 4개의 구간으로 나누는 관측값을 사분위수(Quartile)라 하며, 이 사분위수에서 제1사분위수(Q1)와 제3사분위수(Q3)의 범위가 사분위간 범위다.

$$ IQR = Q3 - Q1 $$


  • 사분위수(Quartile):
    전체 데이터를 25% 단위로 나눌 수 있는, Q1(1사분위수), Q2(2사분위수), Q3(3사분위수)를 말한다.
  • 각 사분위수는 각 값의 하위 범위에 대하여, Q1은 25% 이하의 데이터가 존재하는 관측값, Q2는 50% 이하의 데이터가 존재하는 관측값(중위수), Q3는 75% 이하의 데이터가 존재하는 관측값을 말한다.
  • 즉, Q1은 제 25 백분위수, Q2는 제 50 백분위수, Q3는 제 75 백분위수이다.
  • Q4는 제4사분위로 제 100 백분위수다.
  • 사분위간 범위는 산포도이며, 사분위수는 대푯값에 해당한다.
  • 중위수, 사분위간 범위는 빈도를 이용해 계산되며, 이상치(지나치게 크거나 작은 값)의 영향을 받는 양 극단의 값 역시, 단순히 빈도로 보므로 이상치의 영향을 받지 않는다.

 

2.1. 파이썬을 이용해서 사분위수 구하기

2.1.1. pandas 라이브러리 활용

  • 키에 대하여 사분위수를 구해보자.
>>> Rawdata.키.quantile(0)
136.0

>>> Rawdata.키.quantile(0.25)
159.1

>>> Rawdata.키.quantile(0.5)
165.0

>>> Rawdata.키.quantile(0.75)
172.0

>>> Rawdata.키.quantile(1)
196.0
  • pd.Series.quantile(float): 0에서 1 사이의 비율에 대하여, 그 비율의 위치에 해당하는 관찰 값을 반환한다.
  • 하위 빈도의 비율에 대해 구하므로, 사분위수가 아닌 하위 60, 80%와 같은 값도 쉽게 구할 수 있다.
>>> Rawdata.키.quantile(0.6)
167.8

>>> Rawdata.키.quantile(0.8)
173.2

 

2.1.2. Pandas의 기본적인 기술 통계량을 반환하는 함수로도 구할 수 있다.

>>> Rawdata.키.describe()

count    55748.000000
mean       165.527266
std          8.589459
min        136.000000
25%        159.100000
50%        165.000000
75%        172.000000
max        196.000000
Name: 키, dtype: float64
  • pd.DataFrame.describe(): 데이터 요약을 위한 메서드로, 빈도, 평균, 표준편차, 최솟값, 사분위수, 최댓값을 시리즈로 출력한다.
>>> 키_기술통계량 = Rawdata.키.describe()
>>> 키_기술통계량.loc["25%"]
159.1

>>> 키_기술통계량.loc["75%"]
172.0

>>> 키_기술통계량.loc["mean"]
165.5272655521264

>>> 키_기술통계량.loc["std"]
8.589459420964502
  • 시리즈로 출력되므로, index를 이용해, 그 인데스에 해당하는 값을 반환하는 loc를 사용하면, 내가 원하는 값만 가지고 올 수 있다.

 

2.2.2. Numpy를 이용하여 사분위수 구하기

>>> target_array = Rawdata.키.to_numpy()
>>> np.percentile(target_array, 25)
159.1

>>> np.percentile(target_array, 50)
165.0

>>> np.percentile(target_array, 75)
172.0
  • np.percentile(array, q): q번째 백분위 수를 계산하여, 배열의 q번째 백분위 수를 반환한다.

 

2.3. 사분위간 범위 구하기

def cal_Interquartile_range(data, column):
    
    target_array = data[column].to_numpy()
    
    return np.percentile(target_array, 75) - np.percentile(target_array, 25)
>>> cal_Interquartile_range(Rawdata, "키")
12.900000000000006

 

 

 

 

 

3. 사분위수와 상자 수염 그림


◎ 상자 수염 그림(Box-and-whisker plot): 상자 그림(Box plot)이라고도 하며, 다섯 숫자 요약(최솟값, 제1사분위수, 중앙값, 제3사분위수, 최댓값)으로 데이터의 특성을 요약하는 그래프다.


  • 다섯 숫자 요약(Five-number summary): 최솟값(min), 제1사분위수(Q1), 중앙값(median), 제3사분위수(Q3), 최댓값(max)으로 전체 데이터를 요약한 것이다.
  • 사분위간 범위(IQR)로 몸통을 구성하고, 근접 값들로 꼬리를 구성한다.
  • 단위 척도(Step): 1.5 * IQR
  • 안 울타리(Inner fence)는 Q1에서 최솟값의 방향으로 1 step만큼 이동한 것이고, Q3에서 최댓값의 방향으로 1 step만큼 이동한 곳에 그린다.
  • 바깥 울타리(Outer fence)는 Q1에서 최솟값의 방향으로 2 step만큼 이동한 것이고, Q3에서 최댓값의 방향으로 2 step만큼 이동한 곳에 그린다.

 

3.1. 상자 수염 그림과 이상값(Outlier)


◎ 이상값(Outlier): 정상 범주에서 벗어난 값으로, 정상 관측값에서 벗어난 값이거나, 지나치게 크거나 작아 정상범위에서 벗어난 값을 의미한다.


  • 상자 수염 그림을 이용하면, 이상값을 찾아낼 수 있다.
  • 근접값(Adjacent value): 안 울타리 안쪽 값 중 안 울타리에 가장 가까운 값
  • 보통 이상값(Mild Outlier): 한쪽 방향에 대하여, 바깥 울타리, 안 울타리 사이의 값
  • 극단 이상값(Extreme Outlier): 바깥 울타리 밖에 있는 값
  • 근접값은 이상값에 가장 가까운 정상 범위 내 최대·최솟값이며, 이상값은 정상 범위(사분위간 범위)에서 크게 벗어난 값이므로, 연구자의 관점에 따라 데이터에서 제거, 혹은 집단의 재구성 등을 통한 이상값 처리를 해주는 것을 추천한다.

 

3.2. 상자 수염 그림 그리기

  • 주어진 데이터 "키" 변수에 대하여, 상자 수염 그림을 그려보자.
fig = plt.figure(figsize=(10, 8))

plt.boxplot(Rawdata.키.to_numpy())

plt.title("키의 상자 수염 그림", fontsize = 20, pad = 30)
plt.ylabel("키", rotation = 0, fontsize = 15, labelpad = 20)
plt.show()

  • 위 상자 수염 그림에서 상자(Box)는 사분위간 범위이다.
  • 상자(Box) 안의 주황색 선은 중앙값이다.
  • 상자 위아래에 달려 있는 'ㅜ', 'ㅗ' 모양의 선은 수염(Whisker)이다.
  • 양쪽 수염의 끝은 정상 범위의 최대·최솟값이다.
  • 수염 바깥의 동그란 점은 보통 이상값(mild outlier)이다.
  • 위 데이터에는 극단 이상값(Extream outlier)이 존재하지 않으므로, 바깥 울타리는 보이지 않는다.

 

3.3. 상자 수염 그림에서의 정상범위의 최솟값과 최댓값 찾기

  • 상자 수염 그림에서 정상범위의 최솟값과 최댓값을 뽑으면, 이상값을 구분할 수 있는 영역을 찾을 수 있다.
  • 상자 수염 그림에서 정삼 범위 최솟값은 $Q1 - 1.5*IQR$이며, 최댓값은 $Q3 + 1.5*IQR$이다.
def Outlier_checker(data, column):
    
    target_array = data[column].to_numpy()
    Q1 = np.percentile(target_array, 25)
    Q3 = np.percentile(target_array, 75)
    IQR =  Q3 - Q1
    
    norm_min = Q1 - 1.5*IQR
    norm_max = Q3 + 1.5*IQR
    
    return norm_min, norm_max
>>> Outlier_checker(data, "키")
(139.75, 191.35000000000002)
  • 위 결과를 볼 때, 키에서 139.75 cm 미만, 191.35cm 초과는 이상값임을 알 수 있다.
728x90
반응형
728x90
반응형

중심경향치(Center Tendency)

 이전 포스트에서는 최빈값, 중앙값을 구하는 방법에 대해 알아보았다. 이번 포스트에서는 가장 자주 사용되는 중심경향치인 산술 평균과 기하 평균, 조화 평균에 대해서 알아보도록 하자.

 

 

 

1. 산술 평균(Arithmetic mean)

  • 주어진 수의 합을 수의 개수로 나눈 값인 산술 평균은, 통계학에서 가장 많이 사용되는 중심경향치로, 단순하게 평균(Mean)이라고 부르기도 한다.
  • 중앙값처럼 산술 평균 역시, 양적 변수를 대상으로만 사용할 수 있다.

 

1.1. 산술평균의 정의

  • $N$ 개로 구성된 모집단의 관찰값 $X_1, X_2, X_3, ..., X_N$이 있다고 할 때, 모집단의 평균 $\mu$는 다음과 같이 구한다.

$$\mu = \frac{X_1 + X_2 + X_3 + \cdots X_N}{N} = \frac{1}{N}\sum_{i=1}^{N}X_i$$


  • 통계학에서 같은 값을 구한다고 할지라도 모집단을 대상(모수)으로 하는지, 표본집단을 대상(통계량)으로 하는지에 따라, 공식이 약간 달라지게 된다.
  • 평균은 모수와 통계량 모두 공식이 동일하지만, 사용하는 기호가 $\mu$에서 $\bar{x}$로 달라진다.

 

1.2 모평균과 표본평균이 동일한 이유.

 앞서 산술평균에서 단순하게 모수와 통계량의 공식은 동일하지만, 모수는 $\mu$로 통계량은 $\bar{x}$로 사용하는 기호가 다르다고 했다. 이는, 모집단의 평균과 표본 집단의 평균값이 같다는 소리인데, 어떻게 이 것이 가능한 것일까?

  • 먼저, 표본 집단을 완전 무작위로 추출한다고 가정해보자.
  • 이 무작위 추출은 복원 추출(뽑은 값을 또 뽑을 수 있음)을 가정한다.
  • 복원 추출을 가정한 완전 무작위 표본 추출이므로, 원소들이 뽑히는 사건은 서로 독립이다.

  • 위 증명을 보면, 아주 쉽게 표본평균의 기댓값이 모평균과 같다는 것을 찾아내었다.
  • 중간에 나온 원소가 1개인 표본 집합의 기댓값이 모평균과 같다는 이유는 무엇일까?

  • 위와 같은 이유로, 원소가 1개인 표본평균의 분산이 모집단과 동일하다.
  • 즉, 표본을 무수히 많이 뽑게 된다면, 표본 평균의 기댓값은 모평균과 동일하게 된다.

 

 

 

 

 

2. 파이썬으로 산술 평균 구하기.

2.1. 도수분포표로 평균 구하기

  • 중앙값과 마찬가지로, 도수분포표로 평균을 추정할 수는 있으나, 실제 평균과 일치하지는 않는다.
  • 원시 자료를 가지고 있는 상황이라면, 굳이 도수분포표를 구하고, 부정확한 평균을 추론할 필요는 없으나, 혹시나 구할 수 있는 데이터가 도수분포표밖에 없는 상황이 있을 수도 있으므로, 이를 가정하여, 도수분포표로 평균을 구하는 방법에 대해 알아보도록 하겠다.
  • 사용할 데이터와 양적 변수의 범주화된 도수분포표 변환 함수는 다음과 같다.

Data_for_study.csv
3.39MB

import pandas as pd
import numpy as np
def make_freq_table(data, column):
    """
    -------------------------------------------------------------------------------
    지정된 변수의 관찰값이 20개보다 많은 경우, 10개의 등급을 가진 데이터를 반환한다.
    10개의 등급은 동일한 간격으로 자른 것이다.
    -------------------------------------------------------------------------------
    Input: DataFrame, str
    Output: DataFrame
    """
    # array 생성
    target_array = data[column].to_numpy()

    # class의 수가 20개보다 많은 경우 10개로 줄인다.
    class_array = np.unique(target_array)

    if len(class_array) > 20:

        min_key = class_array.min()
        max_key = class_array.max()
        split_key = np.linspace(min_key, max_key, 10)

        a0 = str(round(split_key[0], 2)) + " 이하"
        a1 = str(round(split_key[0], 2)) + " ~ " + str(round(split_key[1], 2))
        a2 = str(round(split_key[1], 2)) + " ~ " + str(round(split_key[2], 2))
        a3 = str(round(split_key[2], 2)) + " ~ " + str(round(split_key[3], 2))
        a4 = str(round(split_key[3], 2)) + " ~ " + str(round(split_key[4], 2))
        a5 = str(round(split_key[4], 2)) + " ~ " + str(round(split_key[5], 2))
        a6 = str(round(split_key[5], 2)) + " ~ " + str(round(split_key[6], 2))
        a7 = str(round(split_key[6], 2)) + " ~ " + str(round(split_key[7], 2))
        a8 = str(round(split_key[7], 2)) + " ~ " + str(round(split_key[8], 2))
        a9 = str(round(split_key[8], 2)) + " 이상"
        new_index = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]


        target_array= np.where(target_array <= split_key[0], 0,
                               np.where((target_array > split_key[0]) & (target_array <= split_key[1]), 1,
                                        np.where((target_array > split_key[1]) & (target_array <= split_key[2]), 2,
                                                 np.where((target_array > split_key[2]) & (target_array <= split_key[3]), 3,
                                                          np.where((target_array > split_key[3]) & (target_array <= split_key[4]), 4,
                                                                   np.where((target_array > split_key[4]) & (target_array <= split_key[5]), 5,
                                                                            np.where((target_array > split_key[5]) & (target_array <= split_key[6]), 6,
                                                                                     np.where((target_array > split_key[6]) & (target_array <= split_key[7]), 7,
                                                                                              np.where((target_array > split_key[7]) & (target_array <= split_key[8]), 8, 9)))))))))


    # 도수분포표 생성
    freq_table = pd.DataFrame(pd.Series(target_array).value_counts(), columns = ["freq"])
    freq_table.index.name = column

    freq_table.sort_index(inplace = True)
    freq_table["ratio"] = freq_table.freq / sum(freq_table.freq)
    freq_table["cum_freq"] = np.cumsum(freq_table.freq)
    freq_table["cum_ratio"] = np.round(np.cumsum(freq_table.ratio), 2)
    freq_table["ratio"] = np.round(freq_table["ratio"], 2)

    if "new_index" in locals():
        freq_table.index = new_index
        freq_table.index.name = column

    return freq_table
  • 키에 대하여 범주화된 도수분포표를 뽑아보자.
Rawdata = pd.read_csv("Data_for_study.csv")
make_freq_table(Rawdata, "키")

  • 범주화된 도수분포표를 이용한 평균 계산은 각 등급(Class)의 중앙값에 각 빈도를 곱해 평균을 계산한다.
  • 위와 같이 136 이하, 189.33 이상이 존재하는 경우, 두 등급에 속하는 빈도가 전체에서 차지하는 비중이 매우 작으므로, 단순하게 우리가 알고 있는 136, 189.33에 대하여 빈도를 곱하거나, 이상치로 보고 제거해도 된다.
  • 애초에 도수분포표를 이용해 중심경향치를 계산하는 것은 정확한 값을 얻기 위해서가 아닌, 대략적인 값을 얻기 위해서이므로, 이번에는 저 두 값에 그냥 빈도를 곱하도록 하겠다.
>>> ((136.0*1)+((142.67+136.0)/2*70)+((149.33+142.67)/2*869)+((156.0+149.33)/2*7079)+
    ((162.67+156.0)/2*14131)+((169.33+162.67)/2*14640)+((176.0+169.33)/2*12492)+
    ((182.67+176.0)/2*5118)+((189.33+182.67)/2*1241)+(189.33*107))/55748
    
165.47919010547466
  • 위 코드처럼 값을 하나하나 쓰기 싫은 사람을 위해, pandas의 str 모듈을 사용해 계산하는 방법도 보도록 하겠다.
>>> 키_도수분포표 = make_freq_table(Rawdata, "키")
>>> 키_도수분포표.reset_index(drop=False, inplace=True)

# 1 부터 8번 행까지 처리
>>> 키1_8_array = 키_도수분포표[1:9].키.str.split(" ~ ", expand=True).values.astype("float")
>>> 키1_8_sum = np.sum(((키1_8_array[:,0] + 키1_8_array[:,1])/2) * 키_도수분포표[1:9].freq.to_numpy())

# 0, 9 행 처리
>>> 키0_9_array = 키_도수분포표.loc[[0,9]].키.str.partition(" ")[0].to_numpy().astype("float")
>>> 키0_9_sum = np.sum(키0_9_array * 키_도수분포표.loc[[0,9]].freq.to_numpy())

>>> 평균키 = (키1_8_sum + 키0_9_sum)/키_도수분포표.freq.sum()
>>> 평균키
165.4791901054746
  • 0번행과 9번행은 1~8번행까지와 구간의 패턴이 다르므로, 이를 분리하여, 원하는 값만 추출하여 평균을 구했다.
  • 위에서 숫자를 일일이 복사 붙여넣기한 것과 같은 결과가 나왔다.

 

2.2. 파이썬으로 산술 평균 구하기

  • 원시자료를 가지고 있는 경우, 위 같이 번거로운 과정 없이 아주 편하게 평균을 구할 수 있다.
>>> Rawdata.키.mean()
165.5272655521264

>>> np.mean(Rawdata.키.to_numpy())
165.52726555212743
  • Series.mean()은 pandas 함수로 평균을 구하는 것이다.
  • np.mean(array)는 numpy 함수로 평균을 구하는 것이다.
  • 실제 평균과 위에서 도수분포표를 이용해서 추론한 평균이 꽤 유사한 것을 알 수 있다.

 

 

 

 

 

3. 기타 산술 평균

3.1. 가중 평균(Weighted mean)

  • 서로 다른 집단에 대하여, 다른 가중치를 부여하여 평균을 내는 방법
  • 집단의 크기가 서로 다르거나, 다른 점수 배점을 가질 때, 사용하는 방법이다.
  • 예를 들어, A반은 10명, B반은 40명일 때, A반과 B반의 평균을 동일하게 생각하고 $\bar{X_A}$와 $\bar{X_B}$를 더하고 평균을 내면 안 된다.
  • 이 경우, A반은 전체에서 차지하는 비중인 $\frac{10}{50}$만큼 평균 $\bar{X_A}$ 곱해주고, B반은 전체에서 차지하는 비중인 $\frac{40}{50}$만큼 평균 $\bar{X_B}$에 곱해주고 이 둘을 더해줘야 한다.
  • 즉, A반의 평균이 $\bar{X_A} = 60$이고, B반의 평균이 $\bar{X_B} = 70$이라면, 두 반의 평균은 $\frac{60+70}{2}=65$이 아닌, $0.2\bar{X_A} + 0.8\bar{X_B}= 0.2 * 60 + 0.8 * 70 = 68$점이 된다.

3.2. 절삭 평균(Trimmed mean)

  • 이상치인 극단적으로 크거나 작은 값을 제거하고 평균을 내는 방법이다.
  • 예를 들어, A라는 마을의 실제 평균 월급은 250만 원인데, 그 동네에 빌 게이츠가 살아서 평균 월급이 1,000만 원이 나와버린다면, 그 평균은 실제 데이터가 모여 있는 곳을 보여준다고 할 수 없다.
  • 물론, 이상치를 제거한다면, 이상치가 제거된 것을 집단의 크기에도 반영해야 한다.

 

 

 

 

4. 기하 평균(Geometric mean)

  • 기하 평균은 $n$개의 양수 값을 모두 곱하고, $n$ 제곱근을 해준 것이다.
  • 기하 평균은 지금까지 우리가 다뤘던 변수들과 달리 곱셈으로 계산하는 값에서 사용된다.
  • 변수가 비율로 구성된 경우, 기하 평균을 사용하면 된다.
  • $n$ 개로 구성된 모집단의 관찰 값 $a_1, a_2, a_3, ..., a_n$이 있다고 할 때, 기하 평균은 다음과 같이 구한다.

$$(\prod_{i=1}^{n}a_i)^{\frac{1}{n}} = (a_1\cdot a_2 \cdot a_3\cdots a_n)^{\frac{1}{n}}=\sqrt[n]{a_1\cdot a_2 \cdot a_3\cdots a_n}$$


  • 앞서 보았던 산술평균은 합의 평균이고, 기하 평균은 곱의 평균이다.
  • 기하평균의 대상인 변수의 모든 관찰 값은 양수여야 한다(제곱근을 사용하므로).
  • 로그 항등식을 사용하여 기하평균 공식을 변환시키면, $f(n) = ln x$일 때의 일반화된 f-평균이 만들어진다.

$$ln(a_1\cdot a_2 \cdot a_3\cdots a_n)^{\frac{1}{n}} = \frac{lna_1 + lna_2 + lna3 + \cdots + lna_n}{n}$$


 

4.1 파이썬으로 기하평균 구하기

  • 아래와 같은 데이터가 있다고 가정해보자.
salary = [0.85, 1.1, 1.5, 0.95, 2.0, 1.05]
  • 해당 데이터는 월급 200만 원을 1로 놓고, 6명의 월급의 비율을 본 것이다.
  • 즉, $a_1$은 $200*0.85=170$만원을 월급으로 받고, $a_2$는 $200*1.1=220$만원을 월급으로 받는다는 의미다.
  • 위 데이터의 기하 평균은 다음과 같다.
>>> (0.85*1.1*1.5*0.95*2.0*1.05)**(1/6)
1.187064439031504

 

4.2. Numpy로 구하기

  • 반복문을 사용하는 방법 말고 기하평균을 Numpy로 구하는 방법은 크게 2가지가 있다.
  1. array의 모든 원소를 곱하고, 출력된 스칼라에 n 제곱근 씌우기
  2. array를 모두 로그 치환하고, 그에 대한 산술 평균을 구한 후, 지수로 원 상태로 만들어주기

4.2.1. 방법 1.

# 방법 1.
>>> salary_A = np.array(salary)
>>> np.prod(salary_A)**(1/len(salary_A))
1.187064439031504
  • np.prod(array): 배열의 모든 인자들을 곱한다.

4.2.2. 방법 2.

  • 자연로그의 평균을 구하고, 아래 로그의 성질을 사용하여, 기하 평균을 구하는 방법도 있다.

$$\log_xy = A \rightarrow x^A = y$$

# 방법 2
>>> np.exp(1) ** (np.mean(np.log(salary_A)))
1.1870644390315037
  • np.exp(1): 자연로그의 밑인 2.718281...이다.
  • np.log(array): array의 모든 원소를 자연로그로 변환시킨다.

 

4.3. statistics 라이브러리 사용하기

>>> import statistics as stat
>>> stat.geometric_mean(salary_A)
1.187064439031504
  • statistics 라이브러리는 굉장히 많은 수학적 통계 계산 함수를 제공한다.
  • 개인적으로는 statistics 함수를 사용하기보다는 numpy가 성능이 더 좋으므로, numpy를 사용하길 추천한다.

 

 

 

 

 

5. 조화 평균(Harmonic mean)

  • 조화 평균은 주어진 값들의 역수의 산술 평균의 역수다.
  • 평균 변화율을 구할 때 주로 사용한다.
  • $n$ 개로 구성된 모집단의 관찰 값 $a_1, a_2, a_3, ..., a_n$이 있다고 할 때, 조화 평균은 다음과 같이 구한다.

$$H = \frac{n}{\frac{1}{a_1} + \frac{1}{a_2} + \frac{1}{a_3} + \cdots + \frac{1}{a_n}}$$


  • 조화평균은 속도와 같은 변화율의 평균을 구할 때, 매우 유용하다.
  • 예를 들어, 전체 거리의 절반은 40km/h로 이동하고, 나머지 절반을 60km/h로 이동하였다면, 평균 속력은 산술 평균인 50km/h가 아닌 조화 평균인 48km/h가 된다.
  • 동일한 거리에 대하여 소모된 시간이 다르므로, 단순하게 산술 평균을 구한다면, 평균 속력을 구할 수 없다.
  • 위와 같은 경우엔, 시간의 차원에서 평균을 구해야하므로, 시간에 대하여 값을 바꿔주고, 평균을 구한 후, 다시 속력으로 바꿔보자.
  • 거리를 $S$라 가정하고, 속도를 $V$라고 가정 하자.

 

5.1. 변화율(Rate of change)

  • 두 변수의 변화 정도를 비율로 나타낸 것이다.
  • 미분에서 $\frac{dy}{dx}$라는 단어가 나오는데, 이는 변수 $x$에 대한 변수 $y$의 변화율을 의미한다.
  • 평균변화율(Average rate of change):
    어떤 함수 $f(x)$가 있다고 할 때, 두 점 $A(a, f(a)), B(b, f(b))$에 대하여(단, $a<b$이다.), 두 점을 이어 만든 직선의 기울기를 말한다.
  • 함수 $y=f(x)$에서 $x$의 값이 $a$부터 $a + \bigtriangleup x $까지 변한다고 하면, 아래 식을 $[a, a+\bigtriangleup x]$에서의 $y$의 평균변화율이라 한다.

$$\frac{\bigtriangleup y}{\bigtriangleup x} = \frac{f(a+\bigtriangleup x) - f(a)}{\bigtriangleup x}$$

 

5.2. 파이썬으로 조화 평균을 구해보자

V_array = np.array([20, 30, 50, 30, 60, 40, 80]) 
  • 다음과 같은 속도 데이터가 있다고 가정해보자.
  • 조화 평균을 사용하여, 평균 속력을 구해보자.

5.2.1.  Numpy로 구해보자.

  • 조화 평균은 역수 평균의 역수이므로, numpy의 Broadcast를 사용하면 쉽게 구할 수 있다.
>>> 1/(np.mean(1/V_array))
36.68122270742358

5.2.2. statistics 라이브러리 사용하기

>>> stat.harmonic_mean(V_array)
36.68122270742358

 

 

 

 

 

6. 기하 평균과 조화 평균의 주의사항

  • 0인 표본 값이 존재하는 경우, 사용할 수가 없다.
    • 기하평균은 곱의 평균이므로, 원소에 0이 있는 경우 곱하여 0이 된다.
    • 조화 평균은 역수의 평균의 역수인데, 0의 역수는 무한대로 발산한다.
  • 표본 값이 모두 양수여야 한다.
    • 기하평균은 비율, 배수에 대한 것인데, 음수가 나올 수가 없다.
    • 조화수열의 대상인 변화율의 음수는 벡터로써 방향이 반대를 의미한다.
  • 변수가 비율 혹은 배수이지만, 각 표본 값이 독립적일 때 사용할 수 있다.
    • 표본 값끼리 곱했을 때, 어떠한 의미도 갖지 않아야 서로 독립적이라 할 수 있다.

 

728x90
반응형
728x90
반응형

중심경향치(Center Tendency)


◎ 중심경향치: 관찰된 자료들이 어디에 집중되어 있는지를 나타낸다.


 통계학은 기본적으로 데이터가 어디에 모이고, 얼마나 흩어지는지를 통해서 데이터의 성격을 파악한다. 중심경향치는 데이터가 어디에 모이는지를 보는 것으로, 최빈값, 중앙값, 평균 등의 다양한 지표를 이용하여, 데이터가 모인 곳을 찾아낸다.

 그렇다면, 그 데이터가 모이고 흩어진다는 것이 대체 무슨 말일까? 이를 알기 위해, 이전 포스트에서 학습했던 내용을 바탕으로 이를 눈으로 확인해보자.

 

 

 

 

0. 데이터가 모이고 흩어진다는 것은 무엇일까?

  • 이전 포스트에서 사용했던 데이터를 가지고 오고, 모든 변수들을 히스토그램으로 시각화해보자.

Data_for_study.csv
3.39MB

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
Rawdata = pd.read_csv("Data_for_study.csv")
plt.rc('font', family='NanumGothic')
Rawdata.hist(figsize=(16,25), layout=(5,3), grid=False,density=True)

plt.show()

  • 위 코드는 pandas와 matplotlib.pyplot 두 가지를 사용하여, DataFrame 내에 있는 변수들을 시각화해주며, 히스토그램은 그 변수가 무엇이든 간에, 그 변수의 빈도를 이용해 그래프를 그린다. 또한, bins 파라미터를 통해, 키, 몸무게 같은 비율 척도는 실제 형태보다 단순화시켜, 모든 변수들의 추이를 쉽게 파악할 수 있다.
  • 위 히스토그램들을 보면, "주중_인터넷이용시간"은 주로 0 ~ 100 사이에 가장 많은 데이터가 모여 있으며, 흩어진 정도는 그리 크지 않다는 것을 알 수 있다.
  • 이러한 변수별 데이터가 어디에 모여있는지를 하나의 값으로 확인할 수 있는 방법이 바로 중심경향치다.

 

 

 

 

1. 최빈값(Mode)


◎ 최빈값(Mode): 빈도수가 가장 큰 관찰값


  • 도수분포표에서 가장 값이 많이 모여 있는 관찰 값을 의미한다.
  • 양적 자료, 질적 자료에서 모두 사용되나, 일반적으로 질적 자료에서 더 자주 사용된다.
  • 위에서 불러온 데이터에서 명목변수: "흡연경험", 등간변수(리커트 5점 척도): "스트레스인지", 비율 변수: "몸무게"에 대하여 최빈값을 구해보자.

 

1.1. 도수분포표를 사용하여 최빈값 구하기

  • 최빈값을 구하는 방법은 도수분포표를 구하고, 가장 빈도수가 높은 관찰 값을 선택하는 방법이 있다.
  • 연속형 변수를 구간 화하는 것은 꽤 귀찮은 일이므로, 20개 이상의 관찰 값을 갖는 경우, 10개의 구간을 갖는 변수로 변환하여 도수분포표를 출력하는 함수를 만들었다.
def make_freq_table(data, column):
    """
    -------------------------------------------------------------------------------
    지정된 변수의 관찰값이 20개보다 많은 경우, 10개의 등급을 가진 데이터를 반환한다.
    -------------------------------------------------------------------------------
    Input: DataFrame, str
    Output: DataFrame
    """
    # array 생성
    target_array = data[column].to_numpy()

    # class의 수가 20개보다 많은 경우 10개로 줄인다.
    class_array = np.unique(target_array)

    if len(class_array) > 20:

        min_key = class_array.min()
        max_key = class_array.max()
        split_key = np.linspace(min_key, max_key, 10)

        a0 = str(round(split_key[0], 2)) + " 이하"
        a1 = str(round(split_key[0], 2)) + " ~ " + str(round(split_key[1], 2))
        a2 = str(round(split_key[1], 2)) + " ~ " + str(round(split_key[2], 2))
        a3 = str(round(split_key[2], 2)) + " ~ " + str(round(split_key[3], 2))
        a4 = str(round(split_key[3], 2)) + " ~ " + str(round(split_key[4], 2))
        a5 = str(round(split_key[4], 2)) + " ~ " + str(round(split_key[5], 2))
        a6 = str(round(split_key[5], 2)) + " ~ " + str(round(split_key[6], 2))
        a7 = str(round(split_key[6], 2)) + " ~ " + str(round(split_key[7], 2))
        a8 = str(round(split_key[7], 2)) + " ~ " + str(round(split_key[8], 2))
        a9 = str(round(split_key[8], 2)) + " 이상"
        new_index = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]


        target_array= np.where(target_array <= split_key[0], 0,
                               np.where((target_array > split_key[0]) & (target_array <= split_key[1]), 1,
                                        np.where((target_array > split_key[1]) & (target_array <= split_key[2]), 2,
                                                 np.where((target_array > split_key[2]) & (target_array <= split_key[3]), 3,
                                                          np.where((target_array > split_key[3]) & (target_array <= split_key[4]), 4,
                                                                   np.where((target_array > split_key[4]) & (target_array <= split_key[5]), 5,
                                                                            np.where((target_array > split_key[5]) & (target_array <= split_key[6]), 6,
                                                                                     np.where((target_array > split_key[6]) & (target_array <= split_key[7]), 7,
                                                                                              np.where((target_array > split_key[7]) & (target_array <= split_key[8]), 8, 9)))))))))


    # 도수분포표 생성
    freq_table = pd.DataFrame(pd.Series(target_array).value_counts(), columns = ["freq"])
    freq_table.index.name = column

    freq_table.sort_index(inplace = True)
    freq_table["ratio"] = freq_table.freq / sum(freq_table.freq)
    freq_table["cum_freq"] = np.cumsum(freq_table.freq)
    freq_table["cum_ratio"] = np.round(np.cumsum(freq_table.ratio), 2)
    freq_table["ratio"] = np.round(freq_table["ratio"], 2)

    if "new_index" in locals():
        freq_table.index = new_index
        freq_table.index.name = column

    return freq_table
  • np.linspace(start, end, num): start부터 end까지 num개를 일정한 간격으로 자르는 함수로, 아주 간편하게, 연속형 데이터를 범주화할 수 있다.
  • 흡연경험의 도수분포표
make_freq_table(Rawdata, "흡연경험")

  • 명목 변수인 흡연경험의 최빈값은 1.0인 것을 알 수 있다. 청소년건강행태조사 이용지침서 참고 시, "없다"가 88%로 가장 많이 등장하였다.
make_freq_table(Rawdata, "스트레스인지")

  • 등간 변수(리커트 척도)인 스트레스인지의 최빈값은 3.0인 것을 알 수 있다. 청소년건강행태조사 이용지침서 참고 시, "조금 느낀다"가 가장 많이 등장하였다.
make_freq_table(Rawdata, "몸무게")

  • 비율 변수인 몸무게에서 제일 많이 등장한 등급(Class)는 45.87 ~ 56.3 kg인 것을 알 수 있다. 표본집단인 중·고등학생 중 36%가 해당 구간에 존재한다.

 

1.2. 파이썬을 이용하여 최빈값 구하기

  • 도수분포표를 일일이 구하고, 최빈값을 구하는 일은 꽤 번거로운 일이다.
  • 데이터 분석에서 기본적으로 사용되는 라이브러리 중 하나인 pandas는 다양한 기본 함수를 제공하여, 이러한 문제를 쉽게 해결할 수 있게 해 준다.
  • Series.mode(): 최빈값을 출력한다.
>>> 흡연경험_최빈값 = Rawdata.흡연경험.mode()
>>> 흡연경험_최빈값
0    1.0
dtype: float64


>>> 스트레스인지_최빈값 = Rawdata.스트레스인지.mode()
>>> 스트레스인지_최빈값
0    3.0
dtype: float64


>>> 몸무게_최빈값 = Rawdata.몸무게.mode()
>>> 몸무게_최빈값
0    60.0
dtype: float64
  • Series.mode()는 Series로 결과를 출력한다.
  • 양적 변수라 할지라도, 바로 최빈값을 찾는 경우, 굳이 도수분포표를 만드는 수고를 할 필요가 없으므로, 구간을 만드는 수고를 하지 않아도 된다.
  • 이번에는, 최빈값에 해당하는 빈도수를 출력해보자.
# 최빈값과 최빈값 도수 출력
def mode_value_printer(data, column):
    
    mode_ = data[column].mode().values[0]
    freq =len(data[data[column] == mode_])
    
    print(f"{column} - 최빈값: {mode_}, 도수: {freq}")
>>> mode_value_printer(Rawdata, "흡연경험")
흡연경험 - 최빈값: 1.0, 도수: 48995

>>> mode_value_printer(Rawdata, "스트레스인지")
스트레스인지 - 최빈값: 3.0, 도수: 22915

>>> mode_value_printer(Rawdata, "몸무게")
몸무게 - 최빈값: 60.0, 도수: 2350
  • 보시다시피 pandas 기본 함수를 사용하면, 아주 쉽게 최빈값과 그에 해당하는 도수를 찾을 수 있다.
  • 그러나, 양적 변수, 그중에서도 관찰 값이 매우 많은 변수는 범주화를 시키는 것과, 단순하게 가장 많이 등장한 관찰 값을 찾는 것이 다른 결과를 가져온다.
  • 때문에 양적 변수에서는 중심경향치를 확인하고자 할 때, 최빈값보다는 평균, 중위수와 같은 다른 값을 추출하는 경우가 더 많다(물론, 연구자의 의도에 따라 최빈값 역시 필요할 수 있으므로, 절대 양적 변수에 최빈값을 사용하지는 않는다고 생각해선 안된다).

 

 

 

 

2. 중앙값(Median)


◎ 중앙값(Median): 수치로 된 자료를 크기 순서대로 나열할 때, 가장 가운데에 위치하는 관찰값을 말한다.

$$Md = \frac{(n+1)}{2}$$


  • 중앙값은 순서, 일정한 간격을 가지고 있는 양적 변수에 대해서만 사용 가능하며, 말 그대로 한 변수 내 모든 관찰값들의 중앙에 있는 값을 가리킨다.
  • 중앙값에서 이슈라고 할 수 있는 것은 관찰값의 수가 짝수인지 홀수인지로, 아래 예시를 보자.

$$ A = {1, 3, 4, 7, 8, 10, 11, 15, 16}$$

  • 위 예시 같이 집합 내 원소의 수가 홀수인 경우에는 그냥 $\frac{9+1}{2}=5$에 있는 관찰값을 중앙값으로 하면 되지만, 짝수인 경우는 조금 다르다.

$$ B = {2, 4, 6, 7, 9, 10} $$

  • 위 예시 같이 집합 내 원소의 수가 짝수인 경우에는 $\frac{6+1}{2} = 3.5$가 되어, 3.5번째에 있는 관찰값을 중앙값으로 사용해야 하나, 3.5번째 관찰값은 존재할 수 없다.
  • 이 때는, 3번째 관찰값인 6과 4번째 관찰값인 7의 평균을 중앙값으로 사용한다. 즉, $\frac{6+7}{2} = 6.5$가 중앙값이 된다.

 

2.1. 도수분포표를 이용하여 연속형 데이터의 중앙값 구하기

  • 중앙값은 관찰값들의 중앙에 있는 값이므로, 도수분포표를 사용하지 않고 구할 수 있고, 그것이 정석이다.
  • 그러나, 항상 모든 관찰값들을 알 수 있는 것이 아니고, 때에 따라서는 도수분포표를 사용해서 중앙값을 유추해야할 필요도 있다.
    (물론, 원시자료를 손 델 수 있다면, 굳이 그럴 필요는 없지만!)
  • 이번에는 범주화된 연속형 데이터의 도수분포표를 이용해서 중앙값을 구해보자.
  • 위에서 만든 make_freq_table함수를 이용해서 키에 대한 도수분포표를 만들어보자.
make_freq_table(Rawdata, "키")

  • 총데이터의 양은 55748개이며, 55748의 중앙값은 $\frac{55748+1}{2} = 27874.5$이다. 즉, 27,874.5번째에 있는 값이 있는 구간이 중앙값이다.
  • 누적빈도를 볼 때, 27,874.5는 162.67 ~ 169.33 구간에 존재하므로, 중앙값이 있는 구간은 162.67 ~ 169.33임을 알 수 있다.
  • 이 구간 안에서 비율을 사용해서 중앙값을 유추해보자

  • 위 방법처럼 관찰 값의 비율과 빈도의 비율을 이용하면, 중위수를 유추해낼 수 있다.
  • 실제 중위수랑 비교해보자.
>>> Rawdata.키.median()
165.0
  • 실제 중위수와 도수분포표를 사용해서 유추한 중위수가 상당히 유사한 것을 알 수 있다.

 

2.2. 파이썬을 이용하여 중위수 구하기

  • 파이썬을 이용해 중위수를 구하는 것은 정말 단순하다.
>>> Rawdata.스트레스인지.median()
3.0

>>> Rawdata.몸무게.median()
57.0
  • Series.median()을 사용하면, 중위수를 구할 수 있다.
  • numpy 함수를 사용하는 경우는 다음과 같다.
>>> np.median(Rawdata.스트레스인지.to_numpy())
3.0

>>> np.median(Rawdata.몸무게.to_numpy())
57.0
  • np.median(array)를 사용해서 중위수를 구하면 된다.

 

 

 

 지금까지 최빈값과 중앙값을 구해보았다. 다음 포스트에서는 가장 대표적인 중심경향치인 평균에 대해 자세히 알아보도록 하겠다.

728x90
반응형
728x90
반응형

도수분포표와 시각화

 앞서 도수분포표에서 봤듯, 도수분포표는 수집한 데이터의 분포를 알기 위해 사용한다. 그러나 여전히 숫자만으로 데이터를 파악하기 때문에 데이터의 분포를 명확하게 이해하기 어려울 수도 있다. 도수분포표를 시각화한다면, 보다 쉽게 데이터의 분포를 파악할 수 있다.

  • 도수분포표의 시각화이므로, 들어간 데이터의 도수(빈도)를 시각화 하는 것이다.
  • 파이썬으로 도수분포표를 시각화하는 방법은 크게 두가지가 있다.
  1. 범주화된 데이터를 사용해서 히스토그램을 만들기
  2. 도수분포표 생성 후, 도수분포표를 기반으로 그래프 그리기
  • 개인적으로 추천하는 방법은 도수분포표를 먼저 생성하고, 그래프를 그리는 것으로, 도수분포표 연산이 한 번 이루어지고 나면, 나머지 과정은 자원을 거의 먹지 않는다.
  • 도수분포표만 만들면, 이를 이용해서 히스토그램(막대그래프 사용), 도수분포다각형, 누적도수분포곡선 3가지를 모두 쉽게 그릴 수 있다.

 

 

 

 

1. 히스토그램

  • 히스토그램은 가장 대표적인 도수분포표의 시각화 방법이다.
  • 위에서 설명한 파이썬으로 도수분포표 시각화를 하는 첫 번째 방법으로, 원본 데이터(범주화가 된)를 사용해서 히스토그램을 그리는 것이다.
  • 히스토그램은 막대그래프로 한 변수를 구성하는 각 집단의 빈도를 이용하여, 막대 그래프를 그리는 것이다.
  • 이전 데이터에서 사용했던, 청소년건강행태조사 2019년 데이터에서 일부 변수만 추려낸 데이터를 사용해보자.

Data_for_study.csv
3.39MB

  • "건강인지"를 히스토그램으로 나타내보자.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
Rawdata = pd.read_csv("Data_for_study.csv")
# 한글 사용
plt.rc('font', family='NanumGothic')

# 전체 그래프 크기
fig = plt.figure(figsize=(8, 6))

# 히스토그래프 그리기
plt.hist(Rawdata.건강인지.to_numpy(), bins=9)

# x축 ticks 지정
plt.xticks(np.arange(1, 6), labels=["매우 좋음", "좋음", "보통", "나쁨", "매우 나쁨"])
plt.tick_params(axis="x", direction="in", labelsize = 12, pad = 20)

# title, xlabel, ylabel 지정
plt.title("건강인지 히스토그램", fontsize = 30, pad = 30)
plt.xlabel('건강인지', fontsize = 20, loc = 'center', labelpad = 20)
plt.ylabel('도수', fontsize = 20, rotation = 0, loc='center', labelpad = 30)

plt.show()

  • plt.hist(density=True)로 지정해주면, 빈도가 아닌, 비율로 나타낼 수도 있다.
  • 그러나, 이 비율은 히스토그램 전체를 1로 하는 비율이 아닌, 최대 빈도를 1로 하는 비율이다.
fig = plt.figure(figsize=(8, 6))

plt.hist(Rawdata.건강인지.to_numpy(), bins=9, density=True)
plt.xticks(np.arange(1, 6), labels=["매우 좋음", "좋음", "보통", "나쁨", "매우 나쁨"])
plt.tick_params(axis="x", direction="in", labelsize = 12, pad = 20)

plt.title("건강인지 히스토그램", fontsize = 30, pad = 30)

plt.xlabel('건강인지', fontsize = 20, loc = 'center', labelpad = 20)
plt.ylabel('도수', fontsize = 20, rotation = 0, loc='center', labelpad = 30)


plt.show()

  • 입력된 데이터는 숫자지만, 실제로는 문자인 Factor이므로, label을 문자로 입력하였다.
>>> Rawdata.건강인지.to_numpy()
array([1., 1., 2., ..., 4., 3., 3.])
  • 실제 정의에 맞게 하려면, 다음과 같이 숫자를 원래 형태인 문자로 바꿔주고, 문자 배열에 대한 도수를 히스토그램으로 그려야 한다.
건강인지 = Rawdata.건강인지.to_numpy()
건강인지 = np.where(건강인지==1, "매우 좋음",
                np.where(건강인지==2, "좋음",
                         np.where(건강인지==3, "보통",
                                  np.where(건강인지==4, "나쁨", "매우 나쁨"))))

 

 

 

 

2. 도수분포다각형

  • 도수분포다각형은 히스토그램의 각 중간점을 이어서 그린 것으로, 데이터가 연속적인 경우에 사용한다.
  • 도수분포다각형을 사용하면, 분포의 윤곽이 보다 명확하게 보이며, 빈도의 증감을 보다 명확히 볼 수 있다.
  • 또한, 서로 다른 집단에 대한 도수분포를 같은 그림 위에서 비교할 수 있다는 장점이 있다.
  • 도수분포다각형, 누적도수분포곡선은 도수분포표를 기반으로 그리는 것이 훨씬 쉽다(히스토그램 역시, 이미 생성된 도수분포표를 기반으로 막대그래프로 그리는 것이 추가 연산 시간이 없으므로 쉽다).
  • 이전 포스트에서 만들었던, 16세 남성의 키와 16세 여성의 키를 10cm 간격으로 범주화한 도수분포표를 대상으로 해서 만들어보도록 하자.
  • 총합은 도수분포표를 이해하기 좋게 만든 것이므로, 제거하고, 필요한 각 클래스별 값만 유지하겠다.
def cat_height(array):
    cat_array = np.where(array<=140, "140 이하",
                     np.where((array>140) & (array<=150), "140~150",
                              np.where((array>150) & (array<=160), "150~160",
                                       np.where((array>160) & (array<=170), "160~170",
                                                np.where((array>170) & (array<=180), "170~180",
                                                         np.where((array>180) & (array<=190), "180~190", "190 이상"))))))
    return cat_array
    
    
def Freq_table(array):

    freq_table = pd.DataFrame(pd.Series(array).value_counts(), columns=["freq"])
    freq_table.sort_index(inplace = True)
    freq_table["ratio"] = freq_table.freq / sum(freq_table.freq)
    freq_table["cum_freq"] = np.cumsum(freq_table.freq)
    freq_table["cum_ratio"] = np.round(np.cumsum(freq_table.ratio), 2)
    freq_table["ratio"] = np.round(freq_table["ratio"], 2)

    return freq_table
    
남자_16세_키 = Rawdata[(Rawdata["연령"] == 16) & (Rawdata["성별"] == 1.0)]["키"].to_numpy()
여자_16세_키 = Rawdata[(Rawdata["연령"] == 16) & (Rawdata["성별"] == 2.0)]["키"].to_numpy()

M_DF = Freq_table(cat_height(남자_16세_키))
F_DF = Freq_table(cat_height(여자_16세_키))
fig = plt.figure(figsize=(8, 6))

plt.plot(F_DF.index, F_DF.ratio, label = "Female, 16 years old")
plt.plot(M_DF.index, M_DF.ratio, linestyle = "--", label = "Male, 16 years old")

plt.title("16세 남성 & 여성 키 도수분포다각형",fontsize = 20, pad = 20)
plt.xlabel("키", fontsize = 15)
plt.ylabel("비율", fontsize = 15, rotation = 0, labelpad = 30)
plt.legend(loc="upper right")

plt.show()

  • 생성된 두 집단의 도수분포표를 상대적으로 비교하기 위해, 상대 빈도인 비율을 사용하여 그래프를 그렸다.
  • 도수분포다각형이 히스토그램의 꼭짓점을 연결한 것임을 보기 위해 히스토그램도 뒤에 연하게 그려보자.
fig = plt.figure(figsize=(8, 6))


plt.plot(F_DF.index, F_DF.ratio, label = "Female, 16 years old")
plt.plot(M_DF.index, M_DF.ratio, linestyle = "--", label = "Male, 16 years old")

plt.bar(F_DF.index, F_DF.ratio, alpha = 0.4, color = "blue")
plt.bar(M_DF.index, M_DF.ratio, alpha = 0.4, color = "yellow")

plt.title("16세 남성 & 여성 키 도수분포다각형",fontsize = 20, pad = 20)
plt.xlabel("키", fontsize = 15)
plt.ylabel("비율", fontsize = 15, rotation = 0, labelpad = 30)
plt.legend(loc="upper right")

plt.show()

  • 동일한 비율을 사용하기 위해 plt에서 histogram 함수가 아닌 막대그래프인 bar를 가지고 왔다.
  • 히스토그램과 막대그래프는 본질이 다르지만, 도수분포표를 사용해서 그린다면 히스토그램과 막대그래프는 동일한 결과를 가지고 온다.
    (히스토그램은 데이터가 한 차원만 들어가고, 그 빈도로 그래프를 그린다. 막대그래프는 두 차원의 데이터가 필요하며, 그 두 차원의 데이터를 이용해서 그래프를 그린다)
  • 가지고 있는 전체 데이터를 이용해서 그래프를 그린다면 히스토그램을 사용하고, 이미 도수분포표를 만들었다면, 막대그래프를 그리길 추천한다.

 

 

 

 

3. 누적도수분포곡선

  • 누적도수분포곡선은 위에서 도수분포다각형을 그렸던 방법에서, y축에 들어가는 데이터만 누적 도수로 바꾸면 된다.
  • 이번에도 두 집단을 비교하기 쉽도록, 비율로 구해보도록 하겠다.
fig = plt.figure(figsize=(8, 6))


plt.plot(F_DF.index, F_DF.cum_ratio, label = "Female, 16 years old")
plt.plot(M_DF.index, M_DF.cum_ratio, linestyle = "--", label = "Male, 16 years old")

plt.bar(F_DF.index, F_DF.cum_ratio, alpha = 0.4, color = "blue")
plt.bar(M_DF.index, M_DF.cum_ratio, alpha = 0.4, color = "yellow")

plt.title("16세 남성 & 여성 키 누적도수분포곡선",fontsize = 20, pad = 20)
plt.xlabel("키", fontsize = 15)
plt.ylabel("비율", fontsize = 15, rotation = 0, labelpad = 30)
plt.legend(loc="lower right")

plt.show()

 

 

 

 지금까지 파이썬을 사용해서 도수분포표의 시각화를 해보았다. 개인적으로는 도수분포표를 먼저 구하고, 그 도수분포표를 바탕으로 시각화를 진행하길 바란다.

728x90
반응형
728x90
반응형

도수분포표(Frequency Distribution Table)


◎ 도수분포표: 수집된 데이터를 분류한 후, 각 분류에 해당하는 데이터의 빈도, 비율 등으로 정리한 표를 말한다.


 앞서 통계학은 크게 기술통계학(Descriptive statistics)과 추론통계학(Inferential statistics) 이 두 가지로 나뉘며, 이 둘은 별개의 존재가 아니라, 기술통계학 > 추론통계학순으로 순차적으로 이루어진다고 하였다.

 기술통계학은 말 그대로 데이터가 가지고 있는 정보를 기술(Describe)하는 것이며, 도수분포표는 데이터의 각 범주별 빈도수와 비율을 이용하여, 데이터를 설명하는 방법으로 기술통계학의 기초가 되는 기법이다.

 백 마디 말보다, 한 번 실제 만들어보는 것이 가장 좋은 방법이니, 실제로 도수분포표를 만들어보고, 도수분포표가 어떻게 생겼고, 도수분포표를 이용해서 무엇을 할 수 있기에 기술통계학의 기초가 되는 것인지 알아보도록 하자.

  • 도수분포표를 학습할 때, 집단을 잘 구분하는 것이 중요하다.
  • 한 변수 안에는 $m$개의 데이터가 존재하는데, 이 $m$개의 데이터를 중복을 제거하면 $n$개의 데이터가 남게 된다($m$ ≥ $n$).
  • 이 중복이 없는 $n$의 데이터는 한 원소 안의 집단(Group), 등급(Class) 두 가지 용어로 부를 수 있는데, 본 포스트에서는 이해하기 쉽도록, 선택된 군은 집단(Group)으로, 한 변수 안의 중복이 제거된 데이터의 군은 등급(Class)라 부르겠다.

 

 

 

 

1. Python을 사용하여, 기본적인 도수분포표를 만들어보자.

1.1. 데이터 가지고 오기

 이전 포스트(참고: "통계 분석을 위한 데이터 준비")에서 생성한 데이터를 기반으로 통계 분석을 진행하도록 하겠다. 해당 데이터는 청소년건강행태조사 2019년 데이터에서 대표적인 16개 변수만 선택하고, 간단하게 결측값을 처리한 데이터다. 보다 상세한 설명이 필요한 경우 위 참고를 보길 바란다.

Data_for_study.csv
3.69MB

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
  • 데이터 분석을 할 때, 기본적으로 사용하는 라이브러리다.
  • pandas는 DataFrame을 이용해, 데이터를 관리하는 모든 과정을 굉장히 편하게 해 준다.
  • numpy는 빠른 수학 연산에 필수인 라이브러리다.
  • matplotlib.pyplot은 시각화에 필수인 라이브러리다.
Rawdata = pd.read_csv("Data_for_study.csv")
Rawdata
  • pd.read_csv("파일이름.csv"): csv 파일을 DataFrame으로 불러온다.

  • 변수(Column)의 수는 총 14개이며, 55,748개의 대상이 들어가 있는 데이터가 불러와졌다.

 

1.2. 질적 변수 도수분포표 만들기

  • 출력된 위 데이터만으로 데이터가 어떻게 생겼고, 그 안에 숨어있는 정보를 찾아내는 것은 불가능에 가깝다.
  • 위 데이터를 가장 쉽게 정리하는 방법이 바로 도수분포표(Frequency distribution table)이다.
  • 성별에 대한 도수분포표를 만들어보겠다.
>>> Rawdata.성별.value_counts()
1.0    29059
2.0    26689
Name: 성별, dtype: int64
  • value_counts(): 시리즈로 빈도표를 출력한다
  • 1은 남자, 2는 여자이므로, 총 55,748명 중 남자가 29,059명, 여자가 26,689명임을 알 수 있다.
  • 비율을 추가해보자.
성별_빈도표 = pd.DataFrame(Rawdata.성별.value_counts())
성별_빈도표.columns = ["freq"]
성별_빈도표["ratio"] = np.round(성별_빈도표.freq/sum(성별_빈도표.freq),2)
성별_빈도표
  • pd.DataFrame.columns: DataFrame의 컬럼명을 조작한다.
  • np.round(array, n): array의 값들을 n의 자리에서 반올림한다.

  • 대한민국의 중, 고등학생을 모집단으로 하는 데이터의 표본집단에서 남성이 차지하는 비중은 52%, 여성이 차지하는 비중은 48% 임을 쉽게 알 수 있다.

 

1.3. 양적 변수 도수분포표 만들기

  • 이번엔 동일한 방법으로 양적 변수인 키에 대해 도수분포표를 만들어보겠다.
키_빈도표 = pd.DataFrame(Rawdata.키.value_counts())
키_빈도표.columns = ["freq"]
키_빈도표["ratio"] = np.round(키_빈도표.freq/sum(키_빈도표.freq),2)
키_빈도표

  • 데이터를 보기 쉽게 만드려고 했는데, 여전히 보기가 어렵다.
  • 키와 같은 양적 변수는 들어갈 수 있는 값이 매우 다양하여 등급(Class)이 많기 때문에 이를 바로 도수분포표로 만들면, 여전히 보기 어렵다. 이를 범주화(Categorization)시켜 단순화해보자.
>>> 키_array = Rawdata.키.to_numpy()
>>> print("min:", 키_array.min())
>>> print("max:", 키_array.max())
min: 136.0
max: 196.0
  • DataFrame.column.to_numpy(): DataFrame의 특정 열 column을 numpy의 array로 변환시킨다.
  • array.min(): 배열의 최솟값
  • array.max(): 배열의 최댓값
  • 최솟값이 136.0cm, 최댓값이 196.0cm이므로, "140 이하", "140~150", "150~160", "160~170", "170~180", "180~190", "190 이상"으로 데이터를 범주화해보자.
키_범주 = np.where(키_array<=140, "140 이하",
                np.where((키_array>140) & (키_array<=150), "140~150",
                         np.where((키_array>150) & (키_array<=160), "150~160",
                                  np.where((키_array>160) & (키_array<=170), "160~170",
                                           np.where((키_array>170) & (키_array<=180), "170~180",
                                                    np.where((키_array>180) & (키_array<=190), "180~190", "190 이상"))))))
                                                    
키_도수분포표 = pd.DataFrame(pd.Series(키_범주).value_counts(), columns=["freq"])
키_도수분포표.sort_index(inplace=True)
키_도수분포표
  • np.where(조건, a, b): 조건에 해당하는 경우 a로 해당하지 않는 경우 b를 반환
  • DataFrame.sort_index(): index로 정렬함.

  • 비율을 추가해보자.
키_도수분포표["ratio"] = np.round(키_도수분포표.freq / sum(키_도수분포표.freq), 2)
키_도수분포표

 

1.4. 누적 빈도, 누적 비율 추가

  • 이번에는 빈도(freq)와 비율(ratio)을 첫 집단부터 차근차근 누적시키는 누적 빈도(Cumulative frequency)와 누적 비율(Cumulative ratio)을 보자.
키_도수분포표["cum_freq"] = np.cumsum(키_도수분포표.freq)
키_도수분포표["cum_ratio"] = np.cumsum(키_도수분포표.ratio)
키_도수분포표

  • np.cumsum(array): array를 주어진 순서대로 누적합

 

 

 

 

2. 도수분포표의 개념 정리

 위에서 만든 도수분포표를 기반으로, 도수분포표의 개념을 정리해 보자.

  • 질적 변수는 일반적으로 구성하고 있는 등급(Class)의 수가 그리 많지 않기 때문에 바로 도수분포표를 생성해도 가시성이 높다. 물론, 질적 변수 역시 구성하고 있는 등급(Class)의 수가 매우 많다면, 재 범주화하여, 그 등급(Class)의 수를 줄여야한다.
    예) 읍, 면, 동 단위로 지역명이 들어가 있는 변수는, 이를 시 단위로 재범주화 할 수 있음
  • 양적 변수로 구성된 데이터는 중복을 제거하더라도, 구성하고 있는 등급(Class)가 매우 많아 도수분포표로 만들더라도, 가시성이 매우 떨어지므로, 변수의 성격에 따라 그 변수의 데이터 분포를 가장 잘 보여줄 수 있는 구간으로 데이터를 범주화시켜 도수분포표를 만든다.

 

  • 각 등급(Class)에서 관찰된 객체의 수를 빈도수 또는 도수(Frequency)라고 하며, $f$로 나타낸다.
  • 각 변수를 구성하는 데이터의 빈도, 비율 등을 파악하기 때문에 이 과정을 빈도 분석(Frequency Analysis)라 하며, 출력된 도수분포표를 빈도표라고도 한다.
  • 빈도 분석을 통해 생성하는 도수분포표 자체도 통계 분석에서 목적이 될 정도로 중요하지만, 일반적으로는 데이터의 형태를 파악하는 기초 자료로 생성하는 경우가 많다.

 

  • 누적 빈도(Cumulative frequency):
    어떤 등급(Class)에 해당하는 빈도를 포함해, 그 이하 또는 그 이상에 있는 모든 빈도를 합친 것
  • 누적 빈도를 사용하게 되면, 특정 등급(Class)에 있는 대상이 전체 데이터에서 차지하는 위치를 알 수 있다. 위 도수분포표를 보면, 중·고등학생 전체 집단에서 키가 170~180에 있는 집단이 전체 대상에서 96%에 위치하고 있음을 알 수 있다.
  • 누적 빈도는 질적 변수에 대해서는 사용하지 않는 것을 추천한다. 양적 변수는 데이터에 순서가 있고, 값과 값 사이의 간격이 일정하므로, 누적 빈도를 통해 대상 등급(Class)의 객관적인 위치를 알 수 있다. 그러나, 명목 변수는 순서가 없고, 순서가 존재하는 서열 변수라 할지라도, 간격이 일정하지 않기 때문에, 대상 등급(Class)이 전체 등급의 어디에 위치하는지 알 수 없다.

 

 

 

 

3. 상대 빈도(Relative frequency) = 비율(Ratio)

  • 위에서 생성한 키에 대한 도수분포표는 표본 집단인 중·고등학교에 재학 중인 청소년을 대상으로 한 것이다.
>>> print("만연령 min:", Rawdata.연령.min())
>>> print("만연령 max:", Rawdata.연령.max())
만연령 min: 12.0
만연령 max: 18.0
  • 대상 집단의 연령 범위는 만 12세~18세이며, 성별이 남, 녀 두 가지가 들어 있기 때문에 단순하게 위 도수분포표를 보고, 해당 집단의 특성을 파악한다면 아래와 같은 정보 전달의 오류가 발생할 위험이 있다.
  • 키 170cm는 12세 여성이란 집단에서는 굉장히 큰 키이지만, 18세 남성이란 집단에서는 그리 큰 키가 아니다. 위 도수분포표는 만 12세 ~ 18세 중·고등학생 표본집단을 대상으로 하였기 때문에 위 도수분포표만으로는 둘을 같게 볼 여지가 있다.
    (물론, 대상을 만 12~18세라고 미리 설명해놨다면, 이런 문제는 발생하지 않는다.)
  • 보다 정밀한 데이터 파악을 위해선, 연구자의 의도를 가장 잘 보여줄 수 있는 도수분포표(빈도표) 생성이 필요하다.
  • 이번에는 만 16세인 사람으로 한정하여, 남성과 여성의 키를 보도록 하자.
def cat_height(array):
    cat_array = np.where(array<=140, "140 이하",
                     np.where((array>140) & (array<=150), "140~150",
                              np.where((array>150) & (array<=160), "150~160",
                                       np.where((array>160) & (array<=170), "160~170",
                                                np.where((array>170) & (array<=180), "170~180",
                                                         np.where((array>180) & (array<=190), "180~190", "190 이상"))))))
    return cat_array
    
    
def Freq_table(array):

    freq_table = pd.DataFrame(pd.Series(array).value_counts(), columns=["freq"])
    freq_table.sort_index(inplace = True)
    freq_table["ratio"] = freq_table.freq / sum(freq_table.freq)
    freq_table["cum_freq"] = np.cumsum(freq_table.freq)
    freq_table["cum_ratio"] = np.round(np.cumsum(freq_table.ratio), 2)

    # 반올림 및 총합 생성
    freq_table.loc["총합"] = [sum(freq_table.freq), sum(freq_table.ratio), "", ""]
    freq_table["ratio"] = np.round(freq_table["ratio"], 2)

    return freq_table
  • 위에서 만들었던, 양적 변수의 범주화(키)와 도수분포표를 출력하는 코드들을 정리하여 별 개의 함수로 만들어놓았으며, 추가로 총합이 계산되도록 코드를 추가하였다.
  • 이러한 코드 함수화를 통해, 코드의 재활용성, 가시성, 유지보수의 용이함 등의 이점을 얻을 수 있다.
남자_16세_키 = Rawdata[(Rawdata["연령"] == 16) & (Rawdata["성별"] == 1.0)]["키"].to_numpy()
여자_16세_키 = Rawdata[(Rawdata["연령"] == 16) & (Rawdata["성별"] == 2.0)]["키"].to_numpy()
Freq_table(cat_height(남자_16세_키))

Freq_table(cat_height(여자_16세_키))

  • 상대 빈도(relative frequency)는 비율(ratio)과 동일하다.
  • 상대 빈도 즉, 비율을 사용하면, 서로 다른 집단에 대하여, 각 범주별 차지하는 비중을 알 수 있게 되므로, 서로 다른 집단을 비교하는데 매우 유용하다.
  • 위 표를 하나로 합쳐보자.
# 성별에 따른 변수명 구분을 위해 column 앞에 특정 문자를 붙여줌
M_DF = Freq_table(cat_height(남자_16세_키))
M_DF = M_DF.add_prefix("M_")
F_DF = Freq_table(cat_height(여자_16세_키))
F_DF = F_DF.add_prefix("F_")

# 병합 및, 결측값은 0으로 채운다.
T_DF = pd.concat([M_DF, F_DF], axis=1).sort_index()
T_DF.fillna(0, inplace=True)

T_DF

  • 위 표를 보면, 남성(M)과 여성(F)의 도수분포표를 비율(ratio)을 이용해서, 두 집단의 규모 차이를 무시하고 비교할 수 있다.
  • 만 16세 남성의 59%는 170~180cm에 존재하며, 만 16세 여성의 50%는 160~170cm에 존재한다는 것을 알 수 있다.

 

 

 

 지금까지 기술통계학의 가장 기초가 되는 빈도 분석(Frequency analysis)의 도수분포표(Frequency distribution table)에 대해 알아보았다.

 빈도 분석은 모든 데이터 분석의 기반이 되며, 데이터의 분포를 파악하고, 연구자의 의도를 대상에게 전달하는 데 있어, 작성하기도 쉽고, 이해하기도 쉬우므로, 강력한 영향을 미친다.

 변수의 수가 무수히 많고, 하나하나의 변수를 구성하는 집단 역시 매우 많기 때문에, 모든 빈도를 보여주는 것보다, 연구자의 의도가 가장 잘 담겨 있는 대상에 대한 도수분포표를 보여주는 것이 매우 중요하며, 도수분포표를 생성하기 전에 자신이 전달하고자 하는 바가 무엇인지? 자신이 전달하고자 하는 바에서 대상 집단이 어떻게 되는지를 명확히 하도록 하자.

728x90
반응형

'Python으로 하는 기초통계학 > 기본 개념' 카테고리의 다른 글

중심경향치(1) - 최빈값, 중앙값  (0) 2021.03.03
도수분포표와 시각화  (0) 2021.03.02
통계 분석을 위한 데이터 준비  (0) 2021.03.01
변수(Variable)  (0) 2021.03.01
통계학이란?  (0) 2021.02.26
728x90
반응형

 데이터 분석을 하기 위해선, 제일 먼저 데이터가 필요하다. 난수 데이터를 생성하거나, 직접 데이터를 만드는 것도 나쁜 방법은 아니지만, 해당 포스팅을 보는 사람이 실제 데이터를 이용해서 논문을 쓰거나, 특정 결과물을 만들고 싶을 가능성이 높다고 생각하고, 실제 데이터 속에 숨어 있는 진짜 정보를 찾아내는 것이 지식이 되므로, 원시 자료(Rawdata)를 구하고, 학습에 필요한 변수들만 일부 추출하여, 기초 통계분석을 위한 학습용 데이터를 만들어보겠다.

 

 

0. 분석 환경 설명

분석 환경 구축은 상황에 따라 그 수준이 크게 차이가 나지만, 본 포스트에서는 윈도우 환경에서 아나콘다를 설치하고, 이를 통해 Python을 사용할 예정이다.

  • conda 4.9.2 버전, Python 3.7 버전을 사용하겠다.
  • 자세한 설치 방법은 이전 포스트인 "Python 파이썬과 아나콘다"를 참고하기 바란다.
  • Python을 사용할 방법은 아나콘다를 설치하면서 함께 설치 된 주피터 노트북을 사용할 것이나, 보다 편한 툴을 원하는 사람은 파이참("Python 파이썬과 파이참")을 사용하길 바란다.
  • 분석에는 기본적으로 pandas, numpy, scipy, matplotlib를 사용할 것이며, pandas는 1.1.3 버전, numpy는 1.19.2 버전, scipy는 1.5.2 버전, matplotlib는 3.3.2 버전을 사용할 것이다.
  • pandas, numpy, scipy, matplotlib는 아나콘다 설치 시, 기본적으로 설치되므로, 굳이 다른 라이브러리를 다운로드할 필요는 없으나, 이에 관심 있는 경우, "Python 필요한 모듈들을 설치해보자"를 참고하기 바란다.

 

 

 

 

1. 필요한 원시 자료 구하기

  • 원시 자료(Rawdata)는 말 그대로, 아무런 가공도 하지 않은, 가장 기본적인 데이터 상태를 가리킨다.
  • 정부 4.0 이후로, 수많은 공공 기관, 의료 기관 등에서 자체 관리하던 데이터를 통합 관리하려는 시도가 있었고, 그덕에 질 높은 원시 자료 구하기가 꽤 쉬워졌다.
  • 추천하는 원시자료 홈페이지는 다음과 같다.
  • 공공데이터포털 - 행정안전부에서 운영하는 각종 공공 데이터가 모여있는 곳
  • 보건복지데이터포털 - 건강위험인지조사, 노인실태조사 등
  • 고용조사 분석시스템 - 청년패널조사, 대졸자직업이동경로조사, 고령화연구패널조사
  • 보건의료빅데이터개방시스템 - 전국 병의원 및 약국 현황, 의료장비 상세 현황, 요양기관 개설 현황 등
  • 청소년건강행태조사 - 질병관리청
  • 게임이용자 패널 연구 - 한국콘텐츠진흥원
  • 한국복지패널 - 한국보건사회연구원
  • 해당 포스트에서는 비교적 접근하기 쉽고, 데이터의 질도 상당히 좋은 한국복지패널을 사용하도록 하겠다.

 

 

 

 

2. 청소년건강행태조사 원시자료 구하기

  • 위 참조를 통해 질병관리청의 청소년건강행태조사를 다운로드하자.
  • 청소년건강행태조사는 흡연, 음주, 신체활동과 같은 변수를 통해 청소년층의 건강행태를 파악하는 것을 목적으로, 2005년부터 매년마다 중·고등학교 재학생을 대상으로 꾸준히 실시해온 국가승인통계조사로, 직관적인 데이터와 높은 참여율로, 다루기 매우 편한 데이터다.
  • Python을 사용하여 분석할 것이기 때문에, SAS나 SPSS 무엇을 다운로드 받든 지 큰 상관은 없으나, 본 포스트에서는 SPSS를 사용하여 분석을 진행하도록 하겠다.
  • 본 포스트에서 다운로드할 데이터는 "제15차(2019) 청소년건강행태조사"다.

  • 해당 원시자료에 관심이 있거나, 이를 이용하여 연구를 해보고 싶다면, 지침서나 통계집 등의 산출물을 자세히 읽어보는 것을 추천한다.

 

 

 

 

3. 데이터 가져오기

  • spss, sas 데이터는 pandas를 이용해서 쉽게 가지고 올 수 있다.
import pandas as pd
>>> Rawdata = pd.read_spss("kyrbs2019.sav")

---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-2-a3be1637ac91> in <module>
----> 1 Rawdata = pd.read_spss("kyrbs2019.sav")

~\anaconda3\lib\site-packages\pandas\io\spss.py in read_spss(path, usecols, convert_categoricals)
     34     DataFrame
     35     """
---> 36     pyreadstat = import_optional_dependency("pyreadstat")
     37 
     38     if usecols is not None:

~\anaconda3\lib\site-packages\pandas\compat\_optional.py in import_optional_dependency(name, extra, raise_on_missing, on_version)
    108     except ImportError:
    109         if raise_on_missing:
--> 110             raise ImportError(msg) from None
    111         else:
    112             return None

ImportError: Missing optional dependency 'pyreadstat'.  Use pip or conda to install pyreadstat.
  • 처음 spss 데이터를 가지고 오는 경우, 위와 같은 오류가 뜰 수 있는데, ImportError에서 요청한 데로, pyreadstat 패키지를 깔면 해결된다.
>>> !pip install pyreadstat
Collecting pyreadstat
  Downloading pyreadstat-1.0.9-cp38-cp38-win_amd64.whl (1.2 MB)
Requirement already satisfied: pandas>0.24.0 in c:\users\gooop\anaconda3\lib\site-packages (from pyreadstat) (1.1.3)
Requirement already satisfied: python-dateutil>=2.7.3 in c:\users\gooop\anaconda3\lib\site-packages (from pandas>0.24.0->pyreadstat) (2.8.1)
Requirement already satisfied: pytz>=2017.2 in c:\users\gooop\anaconda3\lib\site-packages (from pandas>0.24.0->pyreadstat) (2020.1)
Requirement already satisfied: numpy>=1.15.4 in c:\users\gooop\anaconda3\lib\site-packages (from pandas>0.24.0->pyreadstat) (1.19.2)
Requirement already satisfied: six>=1.5 in c:\users\gooop\anaconda3\lib\site-packages (from python-dateutil>=2.7.3->pandas>0.24.0->pyreadstat) (1.15.0)
Installing collected packages: pyreadstat
Successfully installed pyreadstat-1.0.9
WARNING: You are using pip version 20.3.3; however, version 21.0.1 is available.
You should consider upgrading via the 'c:\users\gooop\anaconda3\python.exe -m pip install --upgrade pip' command.
>>> Rawdata = pd.read_spss("kyrbs2019.sav")
>>> Rawdata

  • 총 57303명의 객체(Rows)와 183개의 변수(Columns)가 있는 데이터가 가져와졌다.
  • 위 표를 DataFrame이라고 하며, 이에 대한 보다 자세한 정보를 알고자 한다면, 해당 포스팅("Pandas-데이터 프레임의 구조와 용어 정리")을 참고하길 바란다.

 

 

 

 

4. 필요 변수(Columns)만 추출하기

  • 이번에는 "제15차(2019) 청소년건강행태조사 원시자료(지침서)"를 보고, 가져온 원시자료를 파악해보자.
  • 변수의 이름은 겹치지 않는 것이 매우 중요하며, 그 내용이 길면 다루기 어려워지므로, 보통 영어 알파벳과 숫자로 구성한다.
  • 때문에, 원시자료 지침서를 참고하지 않는다면, 자신이 다룰 데이터의 변수들을 파악하기 쉽지 않으므로, 원시자료를 다루고자 한다면, 반드시 변수들이 정리되어 있는, 지침서나 코딩 북 등을 참고하기 바란다.
  • 모든 변수를 보는 것은 매우 비효율적이므로, 앞으로 분석에 필요할 청소년건강행태조사의 대표적인 변수 16개만 골라, 앞으로 학습할 데이터를 만들도록 하겠다.
  • 선택 변수 설명
변수명 변수 설명 변수명 변수 설명
SEX 성별 PA_TOT 하루 60분 이상 신체활동 일수
AGE 만나이 M_STR 평상시 스트레스 인지
HT 신장 M_SLP_EN 잠으로 피로 회복 정도
WT 체중 AC_LT 평생 음주 경험
E_S_RCRD 학업성적 TC_LT 평생 흡연 경험
E_SES 가정의 경제상태 INT_WD_MM 주중 학습 목적 이외 평균 인터넷 이용 시간
PR_HT 주관적 건강인지 INT_WK_MM 주말 학습 목적 이외 평균 인터넷 이용 시간
Target_DF = Rawdata[["SEX", "AGE", "HT", "WT", "E_S_RCRD", "E_SES",
                     "PR_HT", "PA_TOT", "M_STR", "M_SLP_EN", "AC_LT",
                     "TC_LT", "INT_WD_MM", "INT_WK_MM"]]
  • 변수의 수를 줄였으므로, 변수 이름을 아예 한글로 바꿔서 알아보기 쉽게 만들자.
Target_DF.columns = ["성별", "연령", "키", "몸무게", "학업성적",
                     "경제상태", "건강인지", "운동일수", "스트레스인지",
                     "피로회복정도", "음주경험", "흡연경험",
                     "주중_인터넷이용시간","주말_인터넷이용시간"]
                     
Target_DF

  • Python은 UTF-8을 기반으로 만들어졌기 때문에, 한글을 사용했을 때, 문제가 발생하기 쉬운 다른 언어들과 달리 한글을 Column의 이름으로 사용하거나, 객체를 담는 변수의 이름으로 사용할 수도 있다.
  • 물론, 가능은 하지만 실전에서는 가능한 한글을 사용하지 않기를 바란다.

 

 

 

 

5. 결측값 처리하기

  • 출력된 표를 보니 NaN이라는 이상한 문자가 있는 것을 볼 수 있다.
  • 이는 결측값(Missing Value)로 어떠한 연유에 의해 값이 누락된 것이다.
  • 결측값을 해결하는 문제는 데이터 분석에서 매우 민감하고 중요한 부분이지만, 이는 현 포스트에서 다루고자 하는 기초 통계학 부분과는 거리가 있으므로, 이를 아주 단순하게 처리하고 가도록 하겠다.
  • 결측값이 존재하는 변수는 다음과 같다.
>>> Target_DF.isna().sum()
성별                0
연령              234
키              1555
몸무게            1555
학업성적              0
경제상태              0
건강인지              0
운동일수              0
스트레스인지            0
피로회복정도            0
음주경험              0
흡연경험              0
주중_인터넷이용시간    14297
주말_인터넷이용시간    12488
dtype: int64
  • 결측값이 발생한 이유에 대한 분석은 보고서를 읽는 것이 가장 정확하지만, 매우 단순하게 해당 부분을 해결하겠다.
  • "주중_인터넷이용시간", "주말_인터넷이용시간"은 결측값 제외 시, 최솟값이 10.0인 것을 볼 때, 인터넷을 사용하지 않은 사람을 결측값으로 처리한 것으로 판단하고, 모두 0으로 바꾸도록 하겠다.
  • 연령, 키, 몸무게가 누락된 대상은 전체 데이터 셋 57,303명에서 차지하는 비중이 매우 작으므로, 제거하여 무시하도록 하겠다.
Target_DF["주중_인터넷이용시간"] = Target_DF.주중_인터넷이용시간.fillna(0)
Target_DF["주말_인터넷이용시간"] = Target_DF.주말_인터넷이용시간.fillna(0)
Target_DF.dropna(inplace = True)
Target_DF.reset_index(drop = True, inplace = True)
Target_DF

  • 위 코드 실행 시, 붉은색으로 SettingWithCopyWarning이 뜨는데, 이는 pandas의 메모리를 절약하기 위한 방법인, 데이터 종속성으로 인해 발생하는 문제로, 기존의 Rawdata와 Target_DF가 별개로 존재하게 되므로, 발생하는 문제다. 이를 해결하기 위해선, copy함수를 이용해서 Target_DF를 생성할 때, Rawdata와 별개로 흐르게 만들면 되지만, 딱히 결과에 영향을 주진 않으므로, 그냥 넘어가도록 하자.
  • 추출된 데이터 셋을 추후 쉽게 사용하기 위해 저장해놓도록 하겠다.
Target_DF.to_csv("Data_for_study.csv", index=False)

Data_for_study.csv
3.39MB

 

 Python으로 하는 기초통계학은 해당 데이터를 사용하여 진행할 것이다. 해당 데이터를 참고 파일로 올려놓을 테니, 위 부분을 생략하고 진행하고자 하는 경우, 바로 데이터만 다운로드하고 사용해도 괜찮다.

728x90
반응형

'Python으로 하는 기초통계학 > 기본 개념' 카테고리의 다른 글

중심경향치(1) - 최빈값, 중앙값  (0) 2021.03.03
도수분포표와 시각화  (0) 2021.03.02
도수분포표  (0) 2021.03.02
변수(Variable)  (0) 2021.03.01
통계학이란?  (0) 2021.02.26
728x90
반응형

 처음 통계학을 접하게 되었을 때, 변수(Variable)가 무엇인지 헷갈리는 경우가 꽤 많다. 학교에서 데이터 분석 강의를 하거나, 주변인들이 데이터 분석에 대해 질문을 해올 때, "변수라는 용어를 많이 사용하는데 대체 그 변수가 구체적으로 무엇이냐?"라는 질문을 종종 해온다.

 해당 파트에서 학습할 변수는 분포와 함께 데이터 분석에 있어, 상식으로 사용되는 개념이므로, 꼭 숙지하고 넘어가도록 하자.

 

 

변수(Variable)


 "변수(Variable)"는 우리가 관심 있는 대상이 가지고 있는 속성(Attribute)이다.


 위 한 줄이 가장 쉽게 변수를 설명할 수 있는 말인데, 막연하게 느껴질 수 있으므로, 좀 더 자세히 설명해보도록 하겠다. 이전 포스트("통계학이란? - 1.모집단과 표본집단")에서 "연구자가 관심 있는 대상""모집단"이라고 했다.

 이 모집단이 가지고 있는 속성(Attribute)이 바로 변수다. "속성(Attribute)"은 연구자가 정의할 수 있는 대상이 가지고 있는 성질이며, 동시에 대상을 특정 혹은 정의할 수 있는 개념이다.

 

예시를 통한 설명

  • 당신이 "사람"이라는 대상에 대해 관심이 있다고 가정해보자. 그럼 사람은, 성별, 연령, 국적, 키, 몸무게, 거주지, 최종 학력 등의 속성을 가지고 있다고 할 수 있다.
  • 만약 당신이 특정 속성을 갖는 사람을 찾고 싶다고 해보자. 성별이 여성, 연령이 24살이며 한국 국적을 갖고 있는 사람을 찾는다고 하면, 꽤 광범위하므로 찾기 쉽지 않을 것이다.
  • 여기에 변수를 하나하나 추가해보자. 키가 153cm, 몸무게 54kg, 대전광역시 거주, 대학교 졸업, 원무과에서 근무함.
  • 이런 식으로 변수가 하나하나 추가될수록 특정 개체를 쉽게 설명할 수 있게 된다. 우리는 우리가 관심 있는 대상을 이러한 변수들을 통해, 그들의 속성을 구체화시키고, 그들 개개인을 구분할 수 있다.
  • 모집단이 연구자의 관심 집단이 바뀌면, 바뀌듯이 변수 역시 모집단에 종속되어 변한다.

 

변수와 개인정보

  • 변수를 통해 개인을 특정할 수 있다고 하였는데, 변수에 따라 그 정도가 바뀌게 된다.
  • 단 하나로 대상을 특정할 수 있는 변수가 있으며, 3개 이상의 변수가 동시에 사용되어 대상을 특정할 수 있는 변수도 있다.
  • 예를 들어, 주민등록번호, 휴대폰 번호와 같은 변수는 아주 강력하게 특정 객체를 지목하게 한다.
  • 이름, 거주지, 연령과 같이 함께 사용되어, 강력하게 특정 객체를 지목하는 변수도 있다.
  • 이는 RDB의 Primary key, Super key와 유사한 개념이다.
  • 때문에 주민등록번호, 휴대폰 번호, 이름 같이 강력한 변수는 아예 데이터에 넣지 않거나, 만약 데이터에 들어가 있는 경우, 그 관리 수준이 상당히 엄중하다. 

 

 

 

 

 

 

1. 변수의 종류

  • 변수는 크게 "질적 변수(Qualitative variable)", "양적 변수(Quantitative variable)" 둘로 나뉜다.
  • 변수의 종류에 따라 접근하는 통계 분석 방법이 바뀌므로, 정확히 변수의 종류를 인식하는 것은 필수다.

 

 

 

 

2. 변수와 척도

  • 척도는 어떠한 현상을 측정하기 위해 만든 도구를 의미한다.
  • 연구자가 관심 있는 대상인 모집단이 존재하고, 그 모집단이 가지고 있는 속성인 변수가 존재하는데, 이 변수를 어떠한 도구를 이용해서 측정할 것인가를 이야기한다.
  • 척도는 위 변수를 측정하는 방법으로써 1:1로 대응되어 존재한다.
    (이산형 변수는 등간 변수라고도 부르며, 연속형 변수는 비율 변수라고도 부르므로, 변수의 이름과 척도의 이름은 동일하다.)

  • 변수와 헷갈리기 꽤 쉬운 개념으로, 척도는 종종 변수와 혼용되어 지칭되기도 한다.
  • 일반적으로, 명목 변수는 명목 척도로, 서열 변수는 서열 척도로, 등간 변수는 등간 척도로, 비율 변수는 비율 척도로 측정하기 때문에 이를 혼용하여 지칭해도 정보가 잘못 전달되거나 하는 문제가 발생할 가능성은 크지 않다.
  • 그러나, 변수와 척도는 동일한 개념은 아니기 때문에 주의할 필요는 있는데, 위 사진에서 보듯, 상위 척도를 이용하여, 보다 하위 레벨의 변수를 측정할 수도 있기 때문이다.
  • 예를 들어, 길이와 같은 연속형 변수를 1m보다 작다, 크다와 같이 이분화시켜 명목형 척도로 측정할 수 있으며, 10 cm 이하, 10cm ~ 1m 사이, 1m 이상 과 같은 서열 척도로 측정할 수도 있다.
  • 그러나, 하위 수준의 변수를 보다 높은 수준의 척도로는 측정할 수 없다(성별과 같은 명목 척도를 연속형 척도로 측정할 수는 없다).

 

 

 

 

3. 질적 변수(Quantitative Variable)

  • 범주형 변수(Categorical Variable)이라고도 하며, 말 그대로 변수 안에 있는 데이터들이 범주화되어 있다는 뜻이다.
  • 즉, 변수 안에 N개의 집단이 존재하며, 그 집단을 숫자로 나타낸다 할지라도 그 숫자는 데이터의 양을 줄이기 위한 목적이지, 그 숫자엔 숫자로써의 의미가 존재하지 않는다.
  • 즉, 문자로 나타낼 수 있는 변수를 의미한다.

 

3.1. 명목 변수(Nominal Variable)

  • 완전히 서로 관련 없는 문자들로 이루어진 변수를 의미한다.
  • 이를 쉽게 데이터화 하기 위해 숫자로 나타낸다 할지라도, 그 숫자에 대해선, 서로 구분하는 의미만 존재하지, 숫자가 가진 그 어떠한 정보도 존재하지 않는다.
  • 예를 들어, 성별의 남자를 1로, 여자를 2로 표기한다 할지라도, 여기에는 "남자는 1등 여자는 2등이다.", "여자가 남자보다 2배 더 우월하다."와 같은 의미는 전혀 존재하지 않는다.
  • 토트넘 축구 선수인 "손흥민의 등번호 7번"과 주장인 "위고 요리스의 등번호 1번"에는 그 어떠한 숫자적 의미가 존재하지 않는다.
  • 야구의 4번 타자와 같은 특정 숫자에 상징성이 있을 수는 있으나, 등번호를 늘여놓았을 때, 그 순서에 일정한 방향을 가진 서열이 존재하지 않기 때문에, 부여된 숫자는 단순히 객체를 구분하는 역할만 한다.

 

3.2. 서열 변수(Ordinal Variable)

  • 명목 척도 중에 순서의 개념이 존재하는 변수를 의미한다.
  • 예를 들어, 최종 학력은 "무학", "초등학교", "중학교", "고등학교", "대학교", "대학원"과 같이 명목형 척도로 측정되지만, 순서가 존재하기 때문에, 이를 숫자로 만들었을 때, 그 숫자에 아무런 의미가 존재하지 않는다고 할 수는 없다.
  • 즉, 숫자의 개념 중 순서의 개념이 존재하는 변수이다.
  • "무학 = 0, 초등학교 = 1, 중학교 = 2, 고등학교 = 3, 대학교 = 4, 대학원 = 5"로 나타낸다고 할 때, 이를 늘어놓으면, 5가 2보다 뒤에 있다라고는 할 수 있지만, "대학원(5) - 고등학교(3) = 중학교(2)"라고 할 수는 없다.

 

 

 

 

4. 양적 변수(Qualitative Variable)

  • 단순하게 연속형 변수(Continuous)라고 지칭하는 경우도 종종 있는데, 이 경우, 양적 변수를 구성하는 이산형 데이터와 연속형 데이터를 구분하기 어려워질 수 있으므로, 그냥 양적 변수라고 하길 추천한다.
  • 데이터를 숫자로 나타냈을 때, 숫자 그 자체인 경우다.
  • 그 숫자를 이산형으로만 나타낼 수 있는가, 연속형으로도 나타낼 수 있는가에 따라 이산형 변수(등간 변수)와 연속형 변수(비율 변수)가 나뉜다.

 

4.1. 등간 변수(Interval Variable)

  • 이산형으로만 나타낼 수 있는 숫자로, 각 숫자 사이가 일정하며, 그 사이에 그 어떠한 값도 존재하지 않는 데이터를 의미한다.
  • 이산형이라는 말은 숫자가 연속되지 않고 일정한 거리로 떨어져 있다는 소리로, 이해하기 쉽게 이야기하면 소수점이 존재하지 않는 경우라고 할 수 있다(물론 이는 이해하기 쉬운 예시이지 온도와 같이 소수점을 갖는 등간 변수 역시 존재하므로, 등간 변수에는 절대 소수점이 등장하지 않는다고 생각해서는 안된다).
  • 예를 들어, 남극에 있는 펭귄 수는 이산 되어 있는 숫자다. 펭귄이 반마리만 있는 경우, 이미 죽은 펭귄이므로, 이를 펭귄 0.5마리라고 할 수는 없다. 
  • 이글루 안에 펭귄 5마리가 있을 때, 이 이글루 안에 펭귄을 2마리 더 넣어 7마리로 만들 수 있고, 펭귄 3마리를 다시 빼서 4마리로 만들 수도 있으므로, 가감(더하기 빼기)이 가능하다.
  • 그러나, 펭귄 5마리에게 펭귄 2마리를 곱하거나 나눈다는 것은 불가능하며, 펭귄 1마리를 2로 나누겠다는 소리는 애초에 단위가 다르기 때문에 시도해서도 안되며, 펭귄을 죽이겠다는 소리이므로, 이런 상상은 하지도 말자.
  • 등간 변수와 비율 변수는 꽤 구분하기 어려운 개념인데, 대표적인 등간 변수인 온도는 36.2˚와 같이 소수점이 있는 경우도 존재하기 때문이다.
  • 온도는 절대적인 기준을 갖는 것이 아닌, 일정한 간격을 가지고 상대적으로 존재하는 것이기 때문에 36˚보다 18˚가 두 배 더 춥다고 할 수는 없다.

 

4.2. 비율 변수(Ratio Variable)

  • 연속형 숫자로 나타낼 수 있는 데이터로, 정수 사이에 수많은 값이 존재한다.
  • 170 cm와 171cm 사이에는 170.5, 170.05, 170.005와 같이 소수점으로 나타낼 수 있는 수많은 숫자가 존재한다.
  • 이를 비율 변수라고 부르는 이유는 측정된 데이터를 비율로 계산이 가능하기 때문으로, 이러한 비율 변수는 절대적인 기준이 존재하기 때문에 곱하고 나눌 수 있다.
  • 이 절대적인 기준이라는 것은 이 속성이 존재하지 않을 수 있는 절대 영점(Absolute Zero Point)이 존재한다는 소리로, 길이나 무게는 0이 되는 순간, 대상에게 있어 그 속성의 값이 "없다"가 될 수 있으나, 온도는 존재하지 않는다는 개념이 존재하지 않는다.
  • 비율 변수는 절대적인 기준이 존재하기 때문에 더하기, 빼기, 곱하기, 나누기가 모두 가능하다.
  • 비율 변수는 통계 분석에 있어 가장 편리한 대상으로, 모든 척도로 측정할 수 있기 때문에, 하위 변수로 쉽게 변환할 수 있다. 그 덕에 모든 통계 분석 기법의 대상이 될 수 있다.

 

 

 

 

5. 정리

  • 변수는 대상 집단(모집단)이 가지고 있는 속성이며, 이 변수는 크게 질적 변수(명목 변수, 서열 변수), 양적 변수(이산형 변수, 연속형 변수)로 나뉜다.
  • 변수를 측정하는 도구는 척도이며, 상위 변수는 하위 변수로 그 수준을 바꿀 수 있으며, 그로 인해 하위 척도로도 상위 변수를 측정할 수 있다.
  • 변수를 나누는 기준을 정리한 표는 다음과 같다.
변수 순서 더하기, 빼기 곱하기, 나누기 절대영점
명목 변수 X X X X
서열 변수 O X X X
등간 변수 O O X X
비율 변수 O O O O
  • 위 기준만으로 나누기 애매한 경우도 종종 존재하기 때문에, 이를 보다 단순화시킨 질적 변수, 양적 변수로 이분화시켜 변수를 구분하는 경우가 많다.
  • 변수의 종류에 따라 사용할 수 있는 통계 분석 기법이 크게 달라지기 때문에 변수의 종류가 무엇인지 판단하는 능력은 통계 분석을 위해 필수로 가지고 있어야 하는 기술이다.
728x90
반응형

'Python으로 하는 기초통계학 > 기본 개념' 카테고리의 다른 글

중심경향치(1) - 최빈값, 중앙값  (0) 2021.03.03
도수분포표와 시각화  (0) 2021.03.02
도수분포표  (0) 2021.03.02
통계 분석을 위한 데이터 준비  (0) 2021.03.01
통계학이란?  (0) 2021.02.26

+ Recent posts