Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

18 기술통계

Updated: 07 apr 2026

기술통계descriptive statistics는 수집된 데이터의 특성을 요약하고 설명하는 통계 기법이다. 데이터를 대표하는 값(평균값, 중앙값, 최빈값)과 데이터의 퍼짐 정도를 나타내는 값(분산, 표준편차, 범위, 사분범위) 등을 통해 데이터의 전반적인 특성을 파악한다.

18.1기본 설정

EDA에 필요한 핵심 라이브러리를 불러온다.

  • numpy: 수치 연산

  • pandas: 데이터프레임 기반 데이터 처리

  • matplotlib.pyplot: 데이터 시각화

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

데이터프레임 내 부동소수점을 소수점 이하 6자리까지만 출력하도록 지정한다.

pd.set_option('display.precision', 6)

실습 데이터가 저장된 원격 저장소의 URL을 지정한다.

data_url = 'https://raw.githubusercontent.com/codingalzi/code-workout-datasci/refs/heads/master/data/'

18.2데이터셋 불러오기

body_info = pd.read_csv(data_url + "body_info.csv", index_col='ID')
body_info
Loading...
body_info.info()
<class 'pandas.DataFrame'>
Index: 1000 entries, F001 to M500
Data columns (total 5 columns):
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   Sex                  1000 non-null   str    
 1   Age                  1000 non-null   float64
 2   Height               1000 non-null   float64
 3   Weight               1000 non-null   float64
 4   Body fat percentage  1000 non-null   float64
dtypes: float64(4), str(1)
memory usage: 51.8 KB
body_info['Sex'].value_counts()
Sex F 500 M 500 Name: count, dtype: int64

기술통계 분석을 위해 body_info 데이터프레임의 키(Height) 데이터를 사용한다. 먼저 처음 10명의 키 데이터를 넘파이 어레이와 데이터프레임으로 각각 추출한다.

heights = body_info['Height'].values[:10]
heights
array([161.4, 168.1, 163.4, 164.1, 152.9, 159.7, 162.2, 157.6, 161.1, 167.1])
heights_df = body_info[['Height']].head(10)
heights_df
Loading...
body_info.head()
Loading...
body_info.tail()
Loading...

18.3데이터 대표값

이전 챕터에서 describe() 메서드를 이용해 데이터의 기초 통계량을 빠르게 확인했다. 이번 챕터에서는 그때 확인했던 각 통계량의 의미와 계산 방법을 하나씩 살펴본다. 주어진 데이터셋을 대표하는 값으로 평균값, 중앙값, 최빈값이 가장 많이 사용된다.

평균값

평균값은 데이터를 모두 더한 뒤, 데이터의 개수로 나눈 값을 말한다. 데이터셋이 nn 개의 데이터 x1,x2,,xnx_1, x_2, \cdots, x_n로 이루어져 있다면 이 데이터셋의 평균값 xˉ\bar x는 다음과 같이 구한다.

xˉ=1ni=1nxi=1n(x1+x2++xn)\bar x = \frac 1 n \sum_{i=1}^{n} x_i = \frac 1 n (x_1 + x_2 + \cdots + x_n)

예를 들어, 처음 10명의 키 데이터의 평균값은 위에서 생성한 넘파이 어레이 heights 또는 데이터프레임 heights_df를 이용해 3가지 방식으로 계산할 수 있다.

  • 파이썬

mean = sum(heights) / len(heights)
print("평균값:", mean)
평균값: 161.75999999999996
  • 넘파이 어레이의 mean() 메서드 활용

np.mean(heights)
np.float64(161.76)
  • 데이터프레임의 mean() 메서드 활용

heights_df.mean()
Height 161.76 dtype: float64

중앙값

중앙값은 데이터를 크기 순서대로 나열할 때 정확히 중앙에 위치한 값이다. 따라서 중앙값을 구하려면 먼저 값들을 크기순으로 정렬해야 한다. 이때 넘파이 어레이의 sort() 메서드를 사용하면 편리하다.

sorted_heights = np.sort(heights)
sorted_heights
array([152.9, 157.6, 159.7, 161.1, 161.4, 162.2, 163.4, 164.1, 167.1, 168.1])

정렬한 후에 데이터 개수 n이 짝수인지 홀수인지에 따라 중앙값을 지정한다.

  • n이 홀수일 때: (n+1)/2 번째 데이터

  • n이 짝수일 때: n/2 번째 데이터와 (n/2) + 1 번째 데이터의 평균값

따라서 처음 10명의 키 데이터의 중앙값을 계산하는 방법은 다음과 같다.

  • 파이썬

n = len(sorted_heights)

if n % 2 == 0:
    m0 = sorted_heights[n//2 - 1]
    m1 = sorted_heights[n//2]
    median = (m0 + m1) / 2
else:
    median = sorted_heights[(n+1)//2 - 1]

print("중앙값:", median)
중앙값: 161.8
  • 넘파이 어레이의 median() 메서드 활용

np.median(heights)
np.float64(161.8)
  • 데이터프레임의 median() 메서드 활용

heights_df.median()
Height 161.8 dtype: float64

최빈값

최빈값은 데이터에서 가장 많이 나타나는 값이다. 키 데이터처럼 연속형 데이터의 경우, 동일한 값이 여러 번 나타나는 경우가 드물 수 있다. 데이터프레임의 mode() 메서드를 이용하여 최빈값을 확인할 수 있다.

heights_df.mode()
Loading...

18.4데이터 분포

편차

각 데이터가 평균으로부터 떨어져 있는 정도를 편차deviation라 한다. 즉, 편차는 데이터들이 평균값으로부터 떨어져 있는 정도를 나타내는 값이다.

앞에서 10명의 키 데이터의 평균값을 구했으므로, 각 사람의 키 편차는 다음과 같이 계산된다.

mean = np.mean(heights)
print('heights의 평균값:', mean)

deviation = heights - mean
deviation
heights의 평균값: 161.76
array([-0.36, 6.34, 1.64, 2.34, -8.86, -2.06, 0.44, -4.16, -0.66, 5.34])

평균값이 같은 데이터라 하더라도 편차는 많이 다를 수 있다. 예를 들어 다음 10명의 편차는 이전 10명의 편차보다 상대적으로 많이 작다.

another_heights = np.array([160.8, 162.8, 161.8, 161.0, 162.4, 162.0, 161.2, 162.6, 161.4, 161.6])

another_mean = np.mean(another_heights)
print('another_heights의 평균값:', another_mean)

another_deviation = another_heights - another_mean
another_deviation
another_heights의 평균값: 161.76
array([-0.96, 1.04, 0.04, -0.76, 0.64, 0.24, -0.56, 0.84, -0.36, -0.16])

another_heights에 포함된 데이터들의 편차가 훨씬 작다.

데이터프레임을 이용하여 동일한 결과를 확인할 수 있다.

아래 코드는 heights_df 데이터프레임을 복제해서 새로 생성한 데이터프레임에 편차 데이터를 추가하는 방법을 보여준다.

summary_df = heights_df.copy()
summary_df['deviation'] = deviation

summary_df
Loading...

mean() 메서드를 사용하면 각 열의 평균값을 출력하므로, 키 데이터의 평균값과 편차들의 평균값을 확인할 수 있다. 편차의 평균값은 항상 0 임에 주의한다.

summary_df.mean()
Height 1.617600e+02 deviation 5.684342e-15 dtype: float64

분산

데이터들의 편차는 항상 음수와 양수가 섞여 평균값이 0이 된다. 데이터들이 평균값을 기준으로 얼마나 떨어져 있는지를 나타내는 측정값으로는 적절치 않다. 이러한 용도로 쓰이는 값이 바로 지금부터 소개할 분산variance과 표준편차standard deviation이다.

분산variance는 편차의 제곱의 평균값이며, 계산식은 다음과 같다.

S2=1ni=1n(xixˉ)2=1n{(x1xˉ)2+(x2xˉ)2++(xnxˉ)2}S^2 = \frac 1 n \sum_{i=1}^{n} (x_i - \bar x)^2 = \frac 1 n \left\{(x_1 - \bar x)^2 + (x_2 - \bar x)^2 + \cdots + (x_n - \bar x)^2 \right \}

heights에 포함된 10명의 키 데이터의 분산은 편차의 제곱에 mean() 메서드를 적용해 구할 수 있다.

np.mean(deviation ** 2)
np.float64(17.768399999999986)

넘파이의 var()와 데이터프레임의 var() 메서드를 활용해 구해도 된다. 하지만 데이터프레임을 이용할 때는 ddof=0 키워드 인자를 사용해 (편향된) 표본분산sample variance을 계산토록 해야한다.

np.var(heights)
np.float64(17.768399999999986)
heights_df.var(ddof=0)
Height 17.7684 dtype: float64

ddof 키워드 인자를 생략하면 ddof=1로 지정된 기본 옵션이 실행되며 불편분산unbiased variance, 즉 편향되지 않은 표본분산을 계산한다. 편향분산biased variance과 불편분산의 구분은 잠시 후에 설명한다.

heights_df.var()
Height 19.742667 dtype: float64
heights_df.var(ddof=0)
Height 17.7684 dtype: float64

한편, np.var() 함수 또한 ddof 키워드 인자를 사용할 수 있는데, 기본값은 데이터프레임의 경우와는 달리 0으로 지정되어 있어서 기본적으로 편향분산을 계산한다.

np.var(heights, ddof=0)
np.float64(17.768399999999986)

ddof=1로 지정하면 불편분산을 계산한다.

np.var(heights, ddof=1)
np.float64(19.74266666666665)

아래 코드는 편차의 제곱을 새로운 변수 summary_df가 가리키는 데이터프레임의 새로운 특성(변인)으로 추가한다.

summary_df['square of deviation'] = np.square(deviation)
summary_df
Loading...

각 특성의 평균값을 계산하면 앞서 heights에 포함된 데이터들의 평균값, 편차 평균값, (편향)분산이 확인된다.

summary_df.mean()
Height 1.617600e+02 deviation 5.684342e-15 square of deviation 1.776840e+01 dtype: float64

표준편차

분산은 편차 제곱의 평균값이므로, 원래 데이터에 사용된 단위가 아닌 해당 단위의 제곱을 사용한다. 따라서 동일한 단위로 데이터의 분포도를 측정하는 용도로 분산의 제곱근인 표준편차standard deviation를 활용한다.

S=S2=1ni=1n(xixˉ)2S = \sqrt{S^2} = \sqrt{\frac 1 n \sum_{i=1}^{n} (x_i - \bar x)^2}

10명의 키 데이터의 표준편차는 넘파이의 제곱근 함수인 np.sqrt()를 이용해 직접 계산하거나, 넘파이의 np.std() 함수를 바로 이용해도 된다.

np.sqrt(np.var(heights))
np.float64(4.215257999221398)
np.std(heights)
np.float64(4.215257999221398)

단, 데이터프레임의 std() 메서드를 사용할 때는 앞에서처럼 편향분산을 위한 ddof=0 옵션을 함께 지정해야 한다.

heights_df.std(ddof=0)
Height 4.215258 dtype: float64

범위

범위range는 데이터의 최댓값과 최솟값의 차이를 가리킨다.

Rg=xmaxxminRg = x_{\max} - x_{\min}

heights에 포함된 데이터의 범위는 다음과 같이 구한다.

np.max(heights) - np.min(heights)
np.float64(15.199999999999989)

범위는 최댓값과 최소값이 변하면 그에 따라 값이 크게 요동칠 수 있다. 따라서 데이터 분포를 이해하는 데는 범위 대신 데이터의 변화에 덜 민감한 사분위수와 사분범위가 주로 이용된다.

사분위수와 사분범위

데이터의 하위 25%, 50%, 75%에 위치하는 값을 각각 제1사분위수(Q1), 제2사분위수(Q2), 제3사분위수(Q3)라 한다. Q2는 앞서 살펴본 중앙값에 해당한다.

사분범위interquartile range는 제3사분위수에서 제1사분위수를 뺀 값으로서, 평균값을 중심으로 50%의 데이터가 모여있는 구간의 길이를 나타낸다. 아래 코드는 heights의 사분범위 heights_IQR을 계산하는데, np.percentile() 함수는 지정된 %에 해당하는 값을 반환하는 함수이다.

heights_Q1 = np.percentile(heights, 25)
heights_Q3 = np.percentile(heights, 75)
heights_IQR = heights_Q3 - heights_Q1
heights_IQR
np.float64(3.8750000000000284)

상자 그림

상자 그림은 데이터의 범위, 사분위수, 사분범위를 동시에 보여준다. 상자 그림은 다음 값들을 점, 선분, 직사각형 등으로 표시하는데, 그림에 표시되는 값들을 위에서부터 나열하면 다음과 같다.

  • 너무 큰 이상치outlier에 해당하는 점들

  • (Q3 + 1.5 * IQR)에 해당하는 값의 수평 선분

  • Q3, Q2, Q1에 해당하는 값을 이용한 직사각형

  • (Q1 - 1.5 * IQR)에 해당하는 값의 수평 선분

  • 너무 작은 이상치outlier에 해당하는 점들

이제 데이터프레임 summary_dfboxplot() 메서드를 이용하여 Height 특성에 포함된 값들을 대상으로 상자 그림을 그린다.

summary_df.boxplot(column=['Height'], grid=False)
plt.show()
<Figure size 640x480 with 1 Axes>

18.5기술통계 종합

데이터프레임의 describe() 메서드는 데이터의 개수, 평균값, 표준편차, 최댓값, 최소값, 사분위수에 대한 정보를 담고 있는 데이터프레임을 생성한다. 지금까지 개별적으로 살펴본 통계량을 한번에 확인할 수 있어 매우 유용하다.

heights_df = summary_df[['Height']]
heights_df.describe()
Loading...

지금까지 10명의 키 데이터로 각 기술통계량의 의미와 계산 방법을 익혔다. 이제 body_info 데이터프레임 전체에 describe() 메서드를 적용하면 1000명의 모든 수치형 특성(나이, 키, 몸무게, 체지방률)에 대한 기술통계를 한번에 확인할 수 있다.

body_info.describe()
Loading...

18.5.1그룹별 기술통계

범주형 변수를 기준으로 그룹을 나누어 기술통계를 비교하면 데이터의 특성을 더 깊이 이해할 수 있다. body_info 데이터프레임은 성별(Sex) 변수를 포함하므로, groupby() 메서드를 이용하여 성별에 따른 기술통계를 비교할 수 있다.

먼저 성별에 따른 평균값을 비교한다.

body_info.groupby('Sex').mean(numeric_only=True)
Loading...

describe() 메서드를 함께 사용하면 각 그룹의 전체 기술통계를 한번에 비교할 수 있다.

body_info.groupby('Sex').describe()
Loading...

18.5.2히스토그램

상자 그림은 사분위수와 이상치를 잘 보여주지만, 데이터의 분포 형태를 직관적으로 파악하기 어렵다. 히스토그램histogram은 데이터를 구간별로 나누어 각 구간에 속하는 데이터의 개수를 막대로 표현한 그래프로, 데이터의 분포 형태를 한눈에 파악할 수 있다. 히스토그램의 구간(bins) 설정 등 상세 내용은 시각화 챕터에서 다룬다.

body_info['Height'].hist(bins=20, edgecolor='black')
plt.xlabel('Height')
plt.ylabel('Frequency')
plt.title('Height Distribution')
plt.show()
<Figure size 640x480 with 1 Axes>

18.6기술통계량 활용: 데이터 정규화

앞서 배운 기술통계량(평균, 표준편차, 최댓값, 최솟값)은 데이터를 요약하는 것 외에도 데이터를 변환하는 데 직접 활용된다. 데이터의 특성마다 단위와 범위가 다르면 직접 비교하거나 분석 모델에 함께 사용하기 어렵다. 예를 들어, 키는 150~190 범위이고 체지방률은 10~40 범위인데, 이 둘을 동일한 기준으로 비교하려면 정규화normalization가 필요하다. 대표적인 정규화 방법으로 min-max 스케일링표준화(z-score)가 있다.

min-max 스케일링

min-max 스케일링은 데이터를 0과 1 사이의 값으로 변환한다. 변환 공식은 다음과 같다.

xscaled=xxminxmaxxminx_{\text{scaled}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}}

body_info의 키 데이터에 min-max 스케일링을 적용한다.

height_col = body_info[['Height']]
height_minmax = (height_col - height_col.min()) / (height_col.max() - height_col.min())
height_minmax.describe()
Loading...

변환된 데이터의 최솟값은 0, 최댓값은 1이 된다.

표준화 (z-score)

표준화는 데이터를 평균이 0, 표준편차가 1이 되도록 변환한다. 변환 공식은 다음과 같다.

z=xxˉSz = \frac{x - \bar{x}}{S}
height_z = (height_col - height_col.mean()) / height_col.std(ddof=0)
height_z.describe()
Loading...

변환된 데이터의 평균은 0(에 매우 가까운 값), 표준편차는 1이 된다. 다음 챕터에서는 분산 개념을 두 변수 사이의 관계로 확장한 공분산과 상관계수를 살펴본다.

18.7연습문제

문제 1

기본 데이터 저장소에 있는 sc_weir.csv 파일은 광주광력시에서부터 전라남도 나주를 거쳐 서해까지 이어지는 영산강에 설치된 승촌보에서 측정한 두 종류의 데이터를 담고 있다.

  • Chl-a: 녹조 발생의 주요 요인인 클로로필-A의 수치 100개

  • Discharge: 보에서 방출되는 시간당 방류량 수치 100개

클로로필-A 농도가 높을 수록 수질(water quality)이 나빠지며, 방류량이 많을 수록 클로로필-A 농도는 일반적으로 떨어지며, 두 데이터 사이의 상관관계를 확인하기 위해 수집되었다.

sc_weir_df = pd.read_csv(data_url+'sc_weir.csv')
sc_weir_df
Loading...

데이터프레임 values 속성

데이터프레임의 values 속성은 행과 열의 정보를 제외한 항목들로만 구성된 동일한 모양의 넘파이 어레이를 가리킨다.

sc_weir_arr = sc_weir_df.values
sc_weir_arr.shape
(100, 2)

처음 10개의 항목은 다음과 같다.

sc_weir_arr[:10]
array([[51, 25], [51, 25], [53, 24], [53, 24], [54, 22], [55, 22], [57, 21], [57, 21], [59, 20], [60, 20]])

(1) 방류량의 평균값을 계산하라.

답:

  • 넘파이 활용

방류량은 1번 열의 데이터다.

discharge_arr = sc_weir_arr[:, 1]
np.mean(discharge_arr)
np.float64(14.93)
  • 데이터프레임 활용

sc_weir_df['Discharge'].mean()
np.float64(14.93)
desc = sc_weir_df.describe()
mean_desc = desc.loc['mean']
mean_desc['Discharge']
np.float64(14.93)

(2) 방류량의 중앙값을 계산하라.

  • 넘파이 활용

np.median(discharge_arr)
np.float64(13.0)
  • 데이터프레임 활용

sc_weir_df['Discharge'].median()
np.float64(13.0)
desc = sc_weir_df.describe()
median_desc = desc.loc['50%']
median_desc['Discharge']
np.float64(13.0)

(3) 방류량의 최빈값을 계산하라.

  • 넘파이 활용: 지원되지 않음.

  • 데이터프레임 활용

sc_weir_df['Discharge'].mode()
0 11 Name: Discharge, dtype: int64
mode_desc = sc_weir_df['Discharge'].mode()
type(mode_desc)
pandas.Series
mode_desc.index
RangeIndex(start=0, stop=1, step=1)
mode_desc[0]
np.int64(11)

(4) 방류량의 제1, 제3 사분위수를 계산하라.

  • 넘파이 활용

Q1 = np.percentile(discharge_arr, 25)
Q3 = np.percentile(discharge_arr, 75)
print(Q1, Q3, sep='\n')
11.0
16.0
  • 데이터프레임 활용

desc.loc[['25%', '75%']]
Loading...

(5) 클로로필-A 데이터의 범위를 계산하라.

  • 넘파이 활용

클로로필-A 데이터는 0번 열이다.

chl_a_arr = sc_weir_arr[:, 0]
print('min:', chl_a_arr.min())
print('max:', chl_a_arr.max())
print('범위:', chl_a_arr.max() - chl_a_arr.min())
min: 51
max: 125
범위: 74
  • 데이터프레임 활용

desc.loc[['min', 'max']]
Loading...
(desc.loc['max'] - desc.loc['min']).loc['Chl-a']
np.float64(74.0)

(6) 클로로필-A 데이터의 분산을 계산하라.

  • 넘파이 활용

np.var(chl_a_arr, ddof=0)
np.float64(506.8004000000001)
  • 데이터프레임 활용

sc_weir_df.var(ddof=0)
Chl-a 506.8004 Discharge 40.2851 dtype: float64
sc_weir_df.var(ddof=0).loc['Chl-a']
np.float64(506.8004000000001)

(7) 클로로필-A 데이터의 표준편차를 계산하라.

  • 넘파이 활용

np.std(chl_a_arr,ddof=0)
np.float64(22.51222778847087)
  • 데이터프레임 활용

sc_weir_df.std(ddof=0)
Chl-a 22.512228 Discharge 6.347054 dtype: float64
sc_weir_df.std(ddof=0).loc['Chl-a']
np.float64(22.51222778847087)

문제 2

기본 데이터 저장소에 있는 california_housing.csv 파일은 미국 캘리포니아 주의 주택 정보를 담고 있다.

(1) 위 파일의 내용을 데이터프레임으로 불러와서 housing_df 변수에 할당하라. 단, 인덱스는 별도로 지정하지 않는다.

housing_df = pd.read_csv(data_url+'california_housing.csv')
housing_df
Loading...

(2) total_rooms 특성값만으로 구성된 데이터프레임을 이용하여 평균값, 중앙값, 최빈값, 분산, 표준편차, 범위, 사분범위를 구하라. 또한 상자 그림을 그리고 정규화를 진행한다.

total_rooms 특성만 갖는 데이터프레임을 지정한다.

total_rooms = housing_df.loc[:, ['total_rooms']]
total_rooms
Loading...
  • 평균값과 중앙값은 describe() 메서드로 확인. 범위와 사분범위 정보도 추출 가능.

total_rooms.describe()
Loading...
  • 분산

total_rooms.var(ddof=0)
total_rooms 4.759215e+06 dtype: float64
  • 표준편차

total_rooms.std(ddof=0)
total_rooms 2181.562402 dtype: float64
  • 상자범위

total_rooms.boxplot(grid=False)
plt.show()
<Figure size 640x480 with 1 Axes>
  • 데이터프레임 정규화: min-max 스케일링

minmax = (total_rooms - total_rooms.min()) / (total_rooms.max() - total_rooms.min())
minmax
Loading...
minmax.describe()
Loading...
  • 데이터프레임 정규화: 표준화

z = (total_rooms - total_rooms.mean()) / total_rooms.std(ddof=0)
z
Loading...
z.describe()
Loading...

넘파이 어레이를 이용한 정규화도 물론 가능하다.

total_rooms_arr = total_rooms.values
total_rooms_arr.shape
(20640, 1)
  • 어레이 정규화: min-max 스케일링

minmax_arr = (total_rooms_arr - np.min(total_rooms_arr)) / (np.max(total_rooms_arr) - np.min(total_rooms_arr))
minmax_arr
array([[0.02233074], [0.18050257], [0.03726029], ..., [0.05727657], [0.04725571], [0.07078183]], shape=(20640, 1))
np.min(minmax_arr)
np.float64(0.0)
np.max(minmax_arr)
np.float64(1.0)
  • 어레이 정규화: 표준화

z_arr = (total_rooms_arr - np.mean(total_rooms_arr)) / np.std(total_rooms_arr)
z_arr
array([[-0.8048191 ], [ 2.0458901 ], [-0.53574589], ..., [-0.17499526], [-0.35559977], [ 0.06840827]], shape=(20640, 1))
np.mean(z_arr)
np.float64(2.2032332891786052e-17)
np.std(z_arr)
np.float64(1.0)