반응형

원본 링크



3 Best (Often Better) Alternatives To Histograms, Avoid Binning Bias


Photo by Vladislav Vasnetov on Pexels



Binning Bias, The Biggest Flaw of Histograms

히스토그램은 직관적이고 분포의 모양을 이해하기 우해 쉽게 그릴 수 있다.

그러나, 작업을 진행함에 따라 여러분은 히스토그램이 그렇게 좋지 않다는 것을 알 것이다. 히스토그램은 값을 bins라라는 구간에 그룹짓고 히스토그램내 각 bin의 높이는 그 bin에 포인스 숫자를 나타낸다. 다음 예제를 보자.


위 히스토그램에서 대부분의 점수가 60과 80사이에 있다는 것을 즉시 알 수 있다. bin을 10부터 20까지의 수로 변경하면 어떻게 되는지 보자.


여전히 분명하게 앞의 형태가 있다. 계속 변경해 보자. 이번에는 20에서 40으로 바꾼다.


이제, 분포가 보이는 것처럼 매끄럽지 않다. 40, 62, 68 및 80 부근의 작은 피크가 40개의 빈에서 확인된다. 따라서 bin의 수는 실제로 분포에 대한 중요한 통찰력을 모호하게 할 수 있다.

그러나 bin의 수를 너무 많이 변경하는 것은 단지 무작위 노이즈만 생길 수 있고 중요성을 찾은 것처럼 만들 수 있다. 이는 히스토그램의 가장 큰 결함(flaw)인 binning bias를 수반한다.

Binning bias는 도표를 그리기위한 bin의 수를 변경하면 동일 데이터에 대한 다른 표현을 얻게 되는 히스토그램의 위험(pitfall)이다.

앞으로 binning bias를 피하고 분포 비교에 더 나은 결과를 제공하는 3가지 히스토그램 대안을 알아본다.



이산(discrete)과 연속(continuous) 데이터

대안을 알아보기 전에 데이터 타입에 대한 정보를 알아보자.

두가지 수치형 데이터 형태가 있다

  • 이산(discrete) 데이터 : 나이, 시험점수, 년, 요일 또는 날짜 등과 같은 개별 시간 단위 같이 수를 세어(count) 기록된 데이터
  • 연속(continuous) 데이터 : 키, 몸무게, 거리 등과 같이 측정되어(measure) 기록된 데이터. 시간 자체도 또한 연속 데이터로써 간주된다. 연속 데이터에 대한 한가지 정의는 동일 데이터는 다른 측정단위로 표현될 수 있다는 것이다. 예를 들면, 거리는 마일, 킬로미터, 미터, 센티미터, 밀리미터로 측정될 수 있고 목록은 연속(continue)이다. 얼마나 작은지는 중요하지 않고 연속 데이터에 대해 더 작은 측정 단위를 찾을 수 있다.

돈과 가격에 대한 참고, 통계학자들은 돈이 연속인지 이산인지에 대해 토론했다. 따라서 여기서는 너무 많이 들어가지 않는다. 그러나 금융업과 세금 시스템은 연속 데이터로써 돈을 간주한다는 것이 중요하다.



Probability Mass Function(확률질량함수) — PMF Plots

히스토그램의 첫번째 대안은 확률질량함수의 결과를 도표로 그리는 것이다.

확률질량함수는 이산(discrete) 값의 분포(시퀀스)를 취하여 각 고유값의 빈도를 반환한다. 다음 분포를 보자.


x = [4, 6, 5, 6, 4, 3, 2]

이 분포의 PMF를 계산하려면 empiricaldist 라이브러리의 Pmf 함수를 사용한다.


# import the function
from empiricaldist import Pmf  # pip install empiricaldist

# Compute PMF
pmf_dist = Pmf.from_seq(x, normalize=False)
pmf_dist


분포를 생성하려면 인자로써 시퀀스를 취하는 Pmf 함수의 from_seq 메소드를 사용한다.

결과는 전달된 분포의 고유값을 갖는 Pmf 객체(내부적으로 pandas series)로 정렬된 인덱스와 probs 컬럼에 빈도가 제공된다.

만약 normalizetrue로 하면 probs은 전체를 더하면 1이 되는 각 값의 분수 빈도를 포함한다.


pmf_dist_norm = Pmf.from_seq(x, normalize=True)
>>> pmf_dist_norm


어떤 값의 빈도를 구하려면 대괄호(blacket) 연산자를 사용한다.


# Get the frequency of 4
>>> pmf_dist_norm[4]

0.2857142857142857

위 예제는 확률질량함수의 아이디어를 제공하는 간단한 예제였다. 다음으로 student_perfomance 데이터셋을 사용하여 진행해 본다.


marks = pd.read_csv('data/student_performance.csv')
>>> marks.head()




>>> marks.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 8 columns):
 #   Column                       Non-Null Count  Dtype 
---  ------                       --------------  ----- 
 0   gender                       1000 non-null   object
 1   race/ethnicity               1000 non-null   object
 2   parental level of education  1000 non-null   object
 3   lunch                        1000 non-null   object
 4   test preparation course      1000 non-null   object
 5   math score                   1000 non-null   int64 
 6   reading score                1000 non-null   int64 
 7   writing score                1000 non-null   int64 
dtypes: int64(3), object(5)
memory usage: 62.6+ KB

우선, 수학점수에 대한 PMF 분포를 구한다.


math_pmf = Pmf.from_seq(marks['math score'])
>>> math_pmf.head()

0     0.001
8     0.001
18    0.001
19    0.001
22    0.001
Name: math score, dtype: float64

이전과 같이 math_pmf는 수학 점수의 고유값을 분리하고 정규화(normalize)한다. Pmf 객체는 꺽은 선 그래프를 그리는 기본 plot 메소드를 포함한다.(막대 그래프는 .bar 함수를 사용할 수 있다.)


fig, ax = plt.subplots(figsize=(12,6))

math_pmf.plot()

ax.set(xlabel='Math Score',
       ylabel='Frequency',
       title='The Probabilty of Each Score Occuring Among Students');


꺽은 선에서 각 데이터 포인트는 분포내 고유점수와 빈도 백분위와 일치한다.

한층 더 쉬운 해석을 위해 중간값과 25번째 백분위를 표시한다.


# Find median
median_score = marks['math score'].median()
# Extract its prob
median_prob = math_pmf[median_score]
# Find 25th percentile
percentile_25th = marks['math score'].describe()['25%']
# Extract its prob
percentile_prob = math_pmf[percentile_25th]

# Recreate the plot with annotations
fig, ax = plt.subplots(figsize=(12,6))
# Plot the PMF
math_pmf.plot()
# Labelling
ax.set(xlabel='Math Score',
       ylabel='Frequency',
       title='The Probabilty of Each Score Occuring Among Students')

# Annotate the median score
ax.annotate(text=f'Median Score: {int(median_score)}',
            xy=(median_score, median_prob),
            xycoords='data',
            fontsize=15,
            xytext=(-350, 50),
            textcoords='offset points', 
            arrowprops={'color': 'red'})

# Annotate the 25th percentile
ax.annotate(text=f'25th percentile: {int(percentile_25th)}',
            xy=(percentile_25th, percentile_prob),
            xycoords='data',
            fontsize=15,
            xytext=(-300, -75),
            textcoords='offset points', 
            arrowprops={'color': 'red'});


거의 학생의 25%가 66점이고 약 17%가 57점이다.

도표에서 대다수의 학생들이 55 ~ 70점인 것을 알 수 있다. 이 도표의 이점은 40에서의 피크와 거의 55에서의 피크같이 데이터내 몇몇 돌출점(spike)를 알 수 있다는 것이다. 이는 히스토그램을 사용한다면 모호하게 되고 좋아 보이지 않을 것이다.


# Create axes
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 10),)
# Set a padding
fig.tight_layout(pad=4)
# PLot a histogram of marks
sns.histplot(marks['math score'], bins=25, ax=ax1)  # Binning bias, takes a while to get the right bin numbers
ax1.set(xlabel='Math score',
        ylabel='Frequency',
        title='Histogram of Math Scores')
ax2.plot(math_pmf)
ax2.set(xlabel='Math score',
        ylabel='Frequency',
        title='PMF Plot of Math Scores');


마지막으로 PMF 결과를 해석하는 또다른 방법이 있다. 앞서 우리는 거의 25%의 학생이 66의 중간 점수를 받은 것을 보았다. 이것은 또한 분포에서 무작위로 학생의 점수를 뽑으면 66점을 얻을 확률이 25%라는 것을 의미한다. 무엇인가를 보는 이러한 방식은 이후에 도움이 될 것이다.

PMF 함수는 이산값에서 가장 잘 동작한다는 것을 기억하자.



Cumulative Distribution Function (CDF, 누적분포함수) Plot

PMF 도표의 문제점을 깨달은 사람이 있는가? PMF 도표는 너무 많은 고유값을 갖는 분포에서는 동작하지 않는다. 예를 들면, 이산 분포를 시뮬레이션하기 위해 2,000개의 정수를 만들어 PMF로 그래프를 그리면,


# Create 2000 random integers
numbers = np.random.randint(5000, 10000, size=2000)

# Plot the numbers
fig, ax = plt.subplots(figsize=(16, 8))

ax.plot(Pmf.from_seq(numbers))

plt.show();


PMF 도표를 사용하면 어떤 의미있는 통찰력을 유도할 수 없는 너무 많은 노이즈가 있다. 또한 분포를 가장 잘 나타내는 binning을 선택하기 힘들기 때문에 히스토그램을 사용하는 것은 아주 위험할 수 있다.

binning : bin의 구간(범위, 크기)을 정하는 것

이런한 분포를 위해 덜 알려져있지만 PMF와 다른 매우 유익한 함수인 누적분포함수가 있다. 누적분포함수는 어떤 형태의 값(이산, 연속, 혼합)을 취하고 분포의 경향을 보여준다.

더 잘 이해하기 위해 간단한 예제를 살펴보자.


# Simple dist
dist = [1, 2, 3, 4, 5, 6, 7]

7개의 연이은 더미 변수를 생성하였다. CDF를 생성하기 위해 empiricaldist라이브러리의 Cdf 함수를 사용한다.


from empiricaldist import Cdf

# Create the distribution
cdf_dist = Cdf.from_seq(dist)
cdf_dist


Cdffrom_seq메소드와 유사하다. Cdf 또한 고유값을 취하여 인덱스로 정렬한다. 차이점은 확률이다.

분포에서 무작위 값 x를 선택하면 누적분포함수(Cumulative Distribution Function)는 선택한 값보다 작거나 같은 값을 얻을 확률을 나타낸다.


1에서 7까지 값의 소규모 분포에 대해 5를 선택했다고 해 보자. 5보다 작거나 같은 5개의 값이 있다. 따라서 CDF(5)는 거의 72%일 것이다. 유사하게 CDF(1)은 14%이고 분포에서 쵀대값은 모든 값이 최고값보다 작거나 같기 때문에 항상 100%의 확률을 갖는다.

이를 표현하는 또다른 방법은 우선 각 고유값의 소수 빈도(fractional frequency)가 계산되어 정렬되면 분포로부터 무작위 값 x의 CDF값은 x보다 작거나 같은 모든 고유값의 개별 빈도의 합과 같을 것이다라는 것이다.

다음으로 seaborn의 내장 데이터셋인 다이어몬드 데이터셋을 사용하여 다이어몬드 가격에 대한 CDF 도표를 살펴본다.



# load data
diamonds = sns.load_dataset('diamonds')

>>> diamonds.info()


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 53940 entries, 0 to 53939
Data columns (total 10 columns):
 #   Column   Non-Null Count  Dtype   
---  ------   --------------  -----   
 0   carat    53940 non-null  float64 
 1   cut      53940 non-null  category
 2   color    53940 non-null  category
 3   clarity  53940 non-null  category
 4   depth    53940 non-null  float64 
 5   table    53940 non-null  float64 
 6   price    53940 non-null  int64   
 7   x        53940 non-null  float64 
 8   y        53940 non-null  float64 
 9   z        53940 non-null  float64 
dtypes: category(3), float64(6), int64(1)
memory usage: 3.0 MB


# Create the CDF
cdf_prices = Cdf.from_seq(diamonds['price'])

# Plot the CDF
fig, ax = plt.subplots(figsize=(16, 8))
ax.plot(cdf_prices)
plt.show();


누적 빈도를 그래프로 그리는 것은 임의성을 없애고 노이즈로 인해 방해받지 않을 수 있도록 한다. 이전과 마찬가지로 중앙값과 25번째 백분위를 나타내보자.


# Calculate the median price
median_price = diamonds['price'].median()

# Get the frequency for median
median_prob = cdf_prices[median_price]

# 25th percentile
percentile_25th = diamonds['price'].describe()['25%']

# Probability of 25th percentile
percentile_prob = cdf_prices[percentile_25th]


fig, ax = plt.subplots(figsize=(16,8))

# PLot the CDF
ax.plot(cdf_prices)

# Annotate median price
ax.annotate(text='Median price',
            xy=(median_price, median_prob),
            xycoords='data',
            textcoords='offset points',
            xytext=(70, 200),
            fontsize=15,
            arrowprops={'color': 'red'})

# Annotate 25th percentile
ax.annotate(text='25th percentile',
            xy=(percentile_25th, percentile_prob),
            xycoords='data',
            textcoords='offset points',
            xytext=(200, 0),
            fontsize=15,
            arrowprops={'color': 'red'})

ax.set(title='CDF of Diamond Prices',
       xlabel='Prices ($)',
       ylabel='Cumulative Frequency')
plt.show()


근본적으로 CDF는 단지 백분위의 또다른 형태(frame)일 뿐이다. 결과에서 찾응 어떤 값, 17이라고 하면 CDF는 이것의 백분위 또는 분포에서 17보다 작거나 같은 값의 백분율이 무엇인지를 알려준다.

CDFs의 뚜련한 이점중 하나는 동일 도표에 다른 분포를 그릴때 알 수 있다.

3가지 다른 다이어몬드 형태의 가격을 도표로 그려보자. ideal, premium, verry good 다이어몬드 형태에 대한 CDFs를 만든다.


# Ideal cuts
ideal = diamonds['cut'] == 'Ideal'
ideal_cdf = Cdf.from_seq(diamonds[ideal]['price'])

# Premium cuts
premium = diamonds['cut'] == 'Premium'
premium_cdf = Cdf.from_seq(diamonds[premium]['price'])

# Very good cuts
very_good = diamonds['cut'] == 'Very Good'
very_good_cdf = Cdf.from_seq(diamonds[very_good]['price'])

fig, ax = plt.subplots(figsize=(16,8))
# Plot the ideal diamonds
ax.plot(ideal_cdf)
# Plot the premium diamonds
ax.plot(premium_cdf)
# Plot the very good diamonds
ax.plot(very_good_cdf)

ax.set(title='Comparing the Price of Differently-cut Diamonds',
       xlabel='Price ($)',
       ylabel='Cumulative Frequency')
plt.legend(['Ideal', 'Premium', 'Very Good'])
plt.show()


이 그래프를 올바르게 해석하면 중요한것을 발견할 것이다. 기대화는 다르게 매우 낮은 가격에서 premiumvery good 보다 ideal이 훨씬 더 많이 있다.

0에서 2500사이 가격의 다이어몬드를 보자. 이 구간에서 가장 높은 선은 ideal의 파란선이다. 사실 60% 또는 그보다 적은 ideal 다이어몬드가 0~2500사이 가격이다. 비교에서 다른 선의 높이는 very good에서는 거의 50%, premium에서는 ~45%이다.

따라서 CDF 그래프로 분포를 비교할 때 몇몇 구간간 선의 높이(가파름)가 그 구간에서 포인트의 수를 나타낸다. 선이 더 높을 수록 더 많은 값이 있다.

구간이 수평선에 더 가까운 기울기인 선을 포함하면 이는 그 구간에 더 적은 포인트가 있디만 그것들의 값은 더 크가는 것을 나타낸다. 예를 들면, 위 그래프에서 모든 3개의 선은 12500달러이후 거의 수평선이 된다. 이는 그 금액 이상으로 가격이 매겨진 다이어몬드가 아주 적다는 것을 나타낸다.

보통 다른 그룹의 분포 비교에 CDFs를 사용하는 것이 더 좋다. CDFs는 다른 그래프와 비교된 분포의 더 나은 관점을 제공한다.



Kernel Density Estimate (KDE, 커널밀도추정) Plot, Interpretation

분포의 모양을 보는 또다른 그랲프는 KDE 그래프(Kernel Density Estimate)이다. KDE 그래프는 확률질량함수(PMF)의 대안이지만 모든 유형의 분포에 적용되는 확률밀도함수(PDF, Probability Density Function)을 사용한다.

이산분포에 너무 많은 고유값이 있다면 PMFs는 어떠한 통찰력을 주는 것보다 오히려 그래프에 무작위 노이즈를 발생시킬 뿐이다. 그러나, 데이터를 모호하게하거나 과다하게 표현하는 것같은 binning bias가 되는 문제점에 빠지지 않기 때문에 히스토그램을 사용하는 것보다 훨씬 낫다.

연속 데이터의 분포로 작업할 때, 각 값이 소수점을 갖을 수 있기 때문에 거의 모든 데이터 포인트가 고유할 수 있다. 이런 형태의 데이터로는 "정확히 2인치 강수량의 정확한 확률은 무엇인가?"같은 질문을 할 수 없다. 이렇게 질문하는 것은 하나의 물 분자가 더 많거나 더 적은 것이 아닌 그리고 2.01이 아닌 그리고 1.9999가 아닌 정확하게 2인치 강수량의 확률은 무엇인가를 묻는 것과 같다. 대답은 물론 2의 무한한 소수점이 있을 수 있기 때문에 0이다.

그러나, 거의 2인치 강수량의 확률이 무엇인가에 대한 질문은 쉽게 답할 수 있다. 확률밀도함수(PDF)는 이러한 형태의 질문에 대답할 수 있도록 한다.

통계에서 PDFs의 결과는 커널밀도추정(KDE)라는 메소드를 사용하여 추정된다. 통계적 세부내용으로 들어가지 않고 여기서는 KDE 그래프를 그리는 법을 알아본다.

시작하려면 KDE 그래프를 seaborn의 kdeplot을 사용하여 그릴 수 있다. 다이어몬드 데이터셋을 사용해 그려보자.


fig, ax = plt.subplots(figsize=(16, 8))

sns.kdeplot(diamonds['price'])

ax.set(title='Kernel Density Estimation of Diamond Prices',
       xlabel='Price ($)');


통계에 익숙하지 않다면 KDE 그래프의 y축은 무시하자. 이를 설명하는 것은 이 글의 범위 밖이다.

CDF와 다르게, 우리는 매끄러운 곡선을 얻을 수 있는 곳에서 KDE 그래프는 분포의 중심 경향(central tendency), 이원성(bimodality), 왜도(skew)를 쉽게 찾아내기에 가장 좋다.

kdeplot은 결과를 그룹짓기 위해 두번째 변수를 지정하는 hue인자가 있다.


fig, ax = plt.subplots(figsize=(16, 8))
sns.kdeplot(x='price', hue='cut', data=diamonds)
ax.set(title='KDE Plot of Different Cut Diamonds')
plt.show();


KDE 그래프는 다른 분포 비교와 동시에 분포의 개별적인 양을 알아차리기에 매우 좋다. 예를 들면,위의 위 그래프는 어떤 분포가 더 많은 값을 갖고 있고 분포에서 군집 된 분포뿐만 아니라 왜도(skewness) 및 양식(modality)도 보여준다.

분포에 아주 많은 값이 있고 3개이상 그룹을 비교할 때는 KDE를 사용하는 것이 좋다.

반응형

+ Recent posts