p값 파이썬 예제. 커피맛 데이터로 배우는 통계
논문을 읽다 보면 p<0.05라는 표현이 자주 나옵니다. p값은 영어로 p value 또는 p-value라고도 씁니다. 커피맛 논문에서도 어떤 물로 내린 커피가 더 달게 느껴졌는지, 신맛이 더 또렷했는지, 바디감이 더 강했는지를 판단할 때 p값이 등장합니다.
p값은 관찰된 차이가 우연히 나올 수도 있는 차이인지, 아니면 우연으로 보기에는 꽤 드문 차이인지를 보는 숫자입니다. 이번 포스팅에서는 커피맛 점수 raw data를 가지고 p값을 파이썬으로 계산해보겠습니다.
고등학교 수학에서 배우는 평균, 경우의 수, 자료의 비교 위에 파이썬 코드를 하나 얹어보겠습니다. 코드는 복사해서 바로 실행할 수 있도록 만들었습니다.
글의 순서
p값은 무엇을 말하는 숫자일까?
커피맛 raw data를 표로 먼저 본다
평균 차이를 계산한다
경우의 수 252가 나오는 이유
순열검정으로 p값을 계산한다
복사해서 바로 실행하는 전체 코드
결과를 그래프로 확인한다
p값을 해석할 때 주의할 점
p값은 무엇을 말하는 숫자일까?
두 종류의 커피가 있다고 해봅니다. 하나는 TDS 45 ppm 물로 내린 커피이고, 다른 하나는 TDS 95 ppm 물로 내린 커피입니다. 사람들이 두 커피의 단맛에 점수를 매겼습니다. 한쪽 평균이 더 높게 나왔습니다.
여기서 바로 “TDS 95 ppm 물이 더 달게 만든다”고 말하면 위험합니다. 사람마다 입맛이 다르고, 점수를 주는 기준도 조금씩 다릅니다. 작은 표본에서 우연히 차이가 커 보였을 수도 있습니다.
p값, p value, p-value는 다음 질문에 답하기 위한 숫자입니다.
p값이 작다는 건 “차이가 없다”는 가정 하에서는 이런 결과가 나오기 힘들다는 뜻입니다. 그러니까 “어, 이 가정이 틀린 것 같다 → 실제로 차이가 있는 것 같다”라고 판단하게 되는 겁니다. 이게 p<0.05를 "통계적으로 유의하다", 다른 말로 "통계적으로 의미가 있다"고 부르는 이유입니다.
커피맛 raw data를 표로 먼저 본다
아래 데이터는 p값 설명을 위한 가상의 커피맛 점수입니다. 실제 논문의 원자료가 아닙니다. TDS 45 ppm 물로 내린 커피에서 5개의 단맛 점수, TDS 95 ppm 물로 내린 커피에서 5개의 단맛 점수를 얻었다고 가정하겠습니다. 점수는 1점부터 7점까지 줄 수 있습니다.
| 데이터 번호 | 물 조건 | 단맛 점수 |
|---|---|---|
| 1 | TDS 45 ppm | 3 |
| 2 | TDS 45 ppm | 4 |
| 3 | TDS 45 ppm | 3 |
| 4 | TDS 45 ppm | 4 |
| 5 | TDS 45 ppm | 3 |
| 6 | TDS 95 ppm | 5 |
| 7 | TDS 95 ppm | 6 |
| 8 | TDS 95 ppm | 5 |
| 9 | TDS 95 ppm | 5 |
| 10 | TDS 95 ppm | 6 |
여기서 데이터 번호 1~5는 TDS 45 ppm 조건의 점수이고, 데이터 번호 6~10은 TDS 95 ppm 조건의 점수입니다. 같은 5명이 두 커피를 모두 마신 자료로 보는 것이 아니라, 두 조건에서 각각 5개씩 얻은 평가 데이터로 보는 예제입니다.
표를 보면 TDS 95 ppm 물로 내린 커피의 단맛 점수가 더 높아 보입니다. 하지만 통계는 눈으로 보이는 차이를 바로 결론으로 삼지 않습니다. 먼저 평균을 계산하고, 그 차이가 우연히 나올 수 있는지 확인합니다.
파이썬에서는 이 raw data를 아래처럼 저장할 수 있습니다. 한 줄이 한 번의 평가 데이터입니다. 데이터 번호, 물 조건, 단맛 점수가 한 묶음으로 들어 있습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
raw_data = [ {"sample_id": 1, "water": "TDS 45 ppm", "sweetness": 3}, {"sample_id": 2, "water": "TDS 45 ppm", "sweetness": 4}, {"sample_id": 3, "water": "TDS 45 ppm", "sweetness": 3}, {"sample_id": 4, "water": "TDS 45 ppm", "sweetness": 4}, {"sample_id": 5, "water": "TDS 45 ppm", "sweetness": 3}, {"sample_id": 6, "water": "TDS 95 ppm", "sweetness": 5}, {"sample_id": 7, "water": "TDS 95 ppm", "sweetness": 6}, {"sample_id": 8, "water": "TDS 95 ppm", "sweetness": 5}, {"sample_id": 9, "water": "TDS 95 ppm", "sweetness": 5}, {"sample_id": 10, "water": "TDS 95 ppm", "sweetness": 6}, ] print(raw_data) |
이 방식은 단순한 숫자 리스트보다 가독성이 좋습니다. 나중에 단맛뿐 아니라 신맛, 쓴맛, 뒷맛, 바디감을 함께 기록할 때도 확장하기 쉽습니다.
만약 같은 5명이 두 커피를 모두 마시고 점수를 준 자료라면, 그것은 짝지어진 자료입니다. 그때는 지금처럼 10개 중 5개를 고르는 방식이 아니라, 각 사람의 점수 차이를 기준으로 분석하는 방법을 쓰는 것이 더 알맞습니다. 이번 글에서는 p값의 기본 감각을 익히기 위해 두 조건에서 각각 5개씩 얻은 독립된 평가 데이터로 설명하겠습니다.
평균 차이를 계산한다
평균은 여러 점수를 하나의 숫자로 요약해줍니다. 수식으로 쓰면 다음과 같습니다.
TDS 45 ppm 커피의 단맛 점수는 3, 4, 3, 4, 3입니다. 평균은 3.4입니다.
TDS 95 ppm 커피의 단맛 점수는 5, 6, 5, 5, 6입니다. 평균은 5.4입니다.
따라서 관찰된 평균 차이는 2.0입니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def mean(data): return sum(data) / len(data) tds_45 = [3, 4, 3, 4, 3] tds_95 = [5, 6, 5, 5, 6] mean_45 = mean(tds_45) mean_95 = mean(tds_95) observed_diff = abs(mean_95 - mean_45) print("TDS 45 ppm 평균:", mean_45) print("TDS 95 ppm 평균:", mean_95) print("관찰된 평균 차이:", observed_diff) |
|
1 2 3 |
TDS 45 ppm 평균: 3.4 TDS 95 ppm 평균: 5.4 관찰된 평균 차이: 2.0 |
경우의 수 252가 나오는 이유
p-value를 구하려면 먼저 “두 커피에 실제 차이가 없다”고 가정합니다. 이 가정 아래에서는 TDS 45 ppm, TDS 95 ppm이라는 이름표가 중요하지 않습니다. 10개의 단맛 점수를 한데 모은 뒤, 아무렇게나 5개와 5개로 다시 나눠볼 수 있습니다.
10개의 점수를 10장의 카드라고 생각해보겠습니다. 우리는 이 10장 중에서 5장을 뽑아 첫 번째 그룹으로 만듭니다. 그러면 남은 5장은 자동으로 두 번째 그룹이 됩니다. 따라서 필요한 질문은 이것입니다.
이것을 수학 기호로 쓰면 다음과 같습니다.
여기서 C는 조합을 뜻합니다. 조합은 순서를 따지지 않고, 어떤 것을 골랐는지만 봅니다. 예를 들어 1번, 2번, 3번, 4번, 5번 카드를 고른 것과 5번, 4번, 3번, 2번, 1번 카드를 고른 것은 같은 선택입니다. 뽑힌 카드는 같고, 순서만 다르기 때문입니다.
조합의 일반 공식은 다음과 같습니다.
여기서 n은 전체 개수, r은 그중에서 고르는 개수입니다. 느낌표 !는 팩토리얼이라고 읽습니다. 예를 들어 5!는 5부터 1까지 모두 곱하라는 뜻입니다.
이번 예제에서는 전체 점수가 10개이고, 그중 5개를 고릅니다. 그래서 n은 10, r은 5입니다.
10에서 5를 빼면 5이므로 아래처럼 정리됩니다.
이 식을 펼쳐 쓰면 다음과 같습니다.
위아래에 같은 5!가 있으므로 하나를 약분할 수 있습니다. 그러면 계산이 훨씬 간단해집니다.
분자는 30240이고, 분모는 120입니다.
따라서 10개 중 5개를 고르는 방법은 252가지입니다.
파이썬은 이 252가지 나누는 방법을 모두 만들어봅니다. 그리고 각 경우마다 두 그룹의 평균 차이를 계산합니다. 그중 실제 관찰된 평균 차이 2.0 이상이 나온 경우가 몇 번인지 셉니다.
이번 예제에서는 252가지 중 2가지 경우에서만 평균 차이 2.0 이상이 나왔습니다. 따라서 p값은 다음과 같습니다.
이 말은 두 커피에 실제 차이가 없다고 가정했을 때, 지금처럼 평균 차이 2.0 이상이 우연히 나올 가능성이 약 0.8%라는 뜻입니다. 그래서 이 예제에서는 단맛 점수 차이를 단순한 우연으로 보기 어렵다고 판단합니다.
순열검정으로 p값을 계산한다
지금 사용하는 방법은 순열검정입니다. 순열검정은 데이터의 이름표를 바꿔 붙여보며, 우연히 어느 정도 차이가 나올 수 있는지 확인하는 방법입니다.
실제 관찰된 평균 차이는 2.0이었습니다. 이제 10개의 점수를 가능한 모든 방식으로 5개와 5개로 나누고, 각 경우의 평균 차이를 계산합니다. 그중 평균 차이가 2.0 이상인 경우를 세면 됩니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
from itertools import combinations def mean(data): return sum(data) / len(data) tds_45 = [3, 4, 3, 4, 3] tds_95 = [5, 6, 5, 5, 6] observed_diff = abs(mean(tds_95) - mean(tds_45)) all_scores = tds_45 + tds_95 n = len(tds_45) count_all = 0 count_extreme = 0 for group1_index in combinations(range(len(all_scores)), n): group1_index = set(group1_index) group1 = [] group2 = [] for i, score in enumerate(all_scores): if i in group1_index: group1.append(score) else: group2.append(score) diff = abs(mean(group2) - mean(group1)) count_all += 1 if diff >= observed_diff: count_extreme += 1 p_value = count_extreme / count_all print("관찰된 평균 차이:", observed_diff) print("전체 경우의 수:", count_all) print("관찰된 차이 이상이 나온 경우:", count_extreme) print("p값:", p_value) |
|
1 2 3 4 |
관찰된 평균 차이: 2.0 전체 경우의 수: 252 관찰된 차이 이상이 나온 경우: 2 p값: 0.007936507936507936 |
p값은 약 0.008입니다. 백분율로 바꾸면 약 0.8%입니다. 두 커피에 실제 차이가 없다고 가정했을 때, 지금처럼 평균 차이 2.0 이상이 우연히 나올 가능성이 약 0.8%라는 뜻입니다.
이 값은 0.05보다 작습니다. 그래서 보통의 통계 기준에서는 “우연으로 보기에는 차이가 꽤 뚜렷하다”고 판단합니다.
복사해서 바로 실행하는 전체 코드
아래 코드는 하나의 파일로 복사해서 바로 실행할 수 있는 전체 코드입니다. 파이썬 기본 라이브러리만 사용합니다. 별도의 통계 패키지는 필요 없습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
from itertools import combinations # ============================================================ # 1. raw data 입력 # ============================================================ # 설명용 가상 데이터입니다. # 실제 논문의 원자료가 아닙니다. # sweetness: 단맛 점수, 1점 = 약함, 7점 = 강함 raw_data = [ {"sample_id": 1, "water": "TDS 45 ppm", "sweetness": 3}, {"sample_id": 2, "water": "TDS 45 ppm", "sweetness": 4}, {"sample_id": 3, "water": "TDS 45 ppm", "sweetness": 3}, {"sample_id": 4, "water": "TDS 45 ppm", "sweetness": 4}, {"sample_id": 5, "water": "TDS 45 ppm", "sweetness": 3}, {"sample_id": 6, "water": "TDS 95 ppm", "sweetness": 5}, {"sample_id": 7, "water": "TDS 95 ppm", "sweetness": 6}, {"sample_id": 8, "water": "TDS 95 ppm", "sweetness": 5}, {"sample_id": 9, "water": "TDS 95 ppm", "sweetness": 5}, {"sample_id": 10, "water": "TDS 95 ppm", "sweetness": 6}, ] # ============================================================ # 2. 함수 정의 # ============================================================ def mean(data): """평균 계산 함수""" return sum(data) / len(data) def get_scores(raw_data, water_name): """특정 물 조건의 단맛 점수만 골라내는 함수""" scores = [] for row in raw_data: if row["water"] == water_name: scores.append(row["sweetness"]) return scores def print_raw_data_table(raw_data): """raw data를 보기 좋게 출력하는 함수""" print("Raw Data") print("-" * 42) print(f"{'sample_id':>9} | {'water':>10} | {'sweetness':>9}") print("-" * 42) for row in raw_data: print(f"{row['sample_id']:>9} | {row['water']:>10} | {row['sweetness']:>9}") print("-" * 42) def permutation_p_value(group_a, group_b): """ 순열검정으로 p값 계산 두 그룹에 차이가 없다고 가정하고, 전체 점수를 가능한 모든 방식으로 다시 나눈다. """ observed_diff = abs(mean(group_b) - mean(group_a)) all_scores = group_a + group_b n = len(group_a) count_all = 0 count_extreme = 0 for group1_index in combinations(range(len(all_scores)), n): group1_index = set(group1_index) group1 = [] group2 = [] for i, score in enumerate(all_scores): if i in group1_index: group1.append(score) else: group2.append(score) diff = abs(mean(group2) - mean(group1)) count_all += 1 if diff >= observed_diff: count_extreme += 1 p_value = count_extreme / count_all return observed_diff, count_all, count_extreme, p_value # ============================================================ # 3. 계산 실행 # ============================================================ tds_45 = get_scores(raw_data, "TDS 45 ppm") tds_95 = get_scores(raw_data, "TDS 95 ppm") mean_45 = mean(tds_45) mean_95 = mean(tds_95) observed_diff, count_all, count_extreme, p_value = permutation_p_value(tds_45, tds_95) # ============================================================ # 4. 결과 출력 # ============================================================ print_raw_data_table(raw_data) print() print("분석 결과") print("-" * 42) print("TDS 45 ppm 점수:", tds_45) print("TDS 95 ppm 점수:", tds_95) print("TDS 45 ppm 평균:", mean_45) print("TDS 95 ppm 평균:", mean_95) print("관찰된 평균 차이:", observed_diff) print("전체 경우의 수:", count_all) print("관찰된 차이 이상이 나온 경우:", count_extreme) print("p값:", p_value) print() if p_value < 0.05: print("해석: p값이 0.05보다 작습니다. 우연으로 보기에는 차이가 꽤 뚜렷합니다.") else: print("해석: p값이 0.05보다 큽니다. 이 데이터만으로는 차이가 뚜렷하다고 보기 어렵습니다.") |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Raw Data ------------------------------------------ sample_id | water | sweetness ------------------------------------------ 1 | TDS 45 ppm | 3 2 | TDS 45 ppm | 4 3 | TDS 45 ppm | 3 4 | TDS 45 ppm | 4 5 | TDS 45 ppm | 3 6 | TDS 95 ppm | 5 7 | TDS 95 ppm | 6 8 | TDS 95 ppm | 5 9 | TDS 95 ppm | 5 10 | TDS 95 ppm | 6 ------------------------------------------ 분석 결과 ------------------------------------------ TDS 45 ppm 점수: [3, 4, 3, 4, 3] TDS 95 ppm 점수: [5, 6, 5, 5, 6] TDS 45 ppm 평균: 3.4 TDS 95 ppm 평균: 5.4 관찰된 평균 차이: 2.0 전체 경우의 수: 252 관찰된 차이 이상이 나온 경우: 2 p값: 0.007936507936507936 해석: p값이 0.05보다 작습니다. 우연으로 보기에는 차이가 꽤 뚜렷합니다. |
결과를 그래프로 확인한다
숫자만 보는 것보다 그림으로 보는 것이 이해에 도움이 됩니다. 아래 코드는 두 물 조건의 단맛 점수를 점으로 표시하고, 평균을 가로선으로 함께 보여줍니다. 그래프 파일은 현재 파이썬 파일이 있는 폴더에 p_value_coffee_example.png라는 이름으로 저장됩니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import matplotlib.pyplot as plt from pathlib import Path # 가상의 커피 단맛 점수 tds_45 = [3, 4, 3, 4, 3] tds_95 = [5, 6, 5, 5, 6] def mean(data): return sum(data) / len(data) mean_45 = mean(tds_45) mean_95 = mean(tds_95) # x축 위치 x_45 = [1] * len(tds_45) x_95 = [2] * len(tds_95) plt.figure(figsize=(8, 5)) # raw data 점 표시 plt.scatter(x_45, tds_45, label="TDS 45 ppm raw data") plt.scatter(x_95, tds_95, label="TDS 95 ppm raw data") # 평균선 표시 plt.hlines(mean_45, 0.8, 1.2, linestyles="dashed", label="TDS 45 ppm mean") plt.hlines(mean_95, 1.8, 2.2, linestyles="dashed", label="TDS 95 ppm mean") plt.xticks([1, 2], ["TDS 45 ppm", "TDS 95 ppm"]) plt.ylabel("Sweetness Score") plt.title("Coffee Sweetness Scores and Mean Difference") plt.ylim(1, 7) plt.grid(True, axis="y") plt.legend() # 현재 폴더에 그림 저장 file_name = "p_value_coffee_example.png" save_path = Path.cwd() / file_name plt.savefig(save_path, dpi=200, bbox_inches="tight") plt.show() print("그래프 저장 위치:", save_path) |
그래프를 보면 raw data의 위치가 보입니다. TDS 45 ppm 점수는 3점과 4점 주변에 모여 있고, TDS 95 ppm 점수는 5점과 6점 주변에 모여 있습니다. 평균 차이도 눈으로 확인할 수 있습니다.
p값을 해석할 때 주의할 점
p값은 유용하지만 오해하기 쉬운 숫자입니다. p값이 0.008이라고 해서 “두 커피가 다를 확률이 99.2%”라는 뜻은 아닙니다. p값은 그런 식으로 읽는 숫자가 아닙니다.
p값은 먼저 “차이가 없다”고 가정합니다. 그 가정 아래에서 지금 관찰한 차이 이상이 나올 가능성을 계산합니다. 출발점이 다릅니다. 그래서 p값은 연구 결과를 판단하는 하나의 기준이지, 진실을 보증하는 도장이 아닙니다.
또한 p값이 작다고 해서 차이가 항상 중요하다는 뜻도 아닙니다. 표본 수가 아주 크면 작은 차이도 p값이 작게 나올 수 있습니다. 반대로 표본 수가 너무 작으면 실제 차이가 있어도 p값이 크게 나올 수 있습니다.
그래서 p값을 볼 때는 raw data를 함께 봐야 합니다. 평균이 얼마나 다른지, 점수들이 얼마나 흩어져 있는지, 표본 수가 충분한지 함께 봐야 합니다. 통계는 숫자 하나만 보는 일이 아니라, 자료의 모양을 함께 읽는 일입니다.
마치며 …
이번 포스팅에서는 p값의 뜻을 파이썬 코드로 살펴보았습니다. 두 커피의 단맛 점수를 비교하고, 평균 차이를 계산했습니다. 그다음 두 커피에 차이가 없다고 가정하고, 10개의 점수를 가능한 모든 방식으로 다시 나눠보았습니다.
그 결과 p값은 약 0.008로 나왔습니다. 차이가 없다는 가정 아래에서 지금처럼 큰 평균 차이가 우연히 나올 가능성이 약 0.8%라는 뜻입니다. 그래서 이 예제에서는 두 커피의 단맛 차이가 우연으로만 설명되기 어렵다고 볼 수 있습니다.
p값을 이해하면 논문을 읽는 눈이 달라집니다. p<0.05라는 표현을 볼 때, 그 뒤에 raw data, 평균 차이, 자료의 흩어짐, 표본 수가 있다는 것을 떠올릴 수 있습니다. 이것이 통계를 숫자 암기가 아니라 자료를 읽는 힘으로 만드는 첫걸음입니다.
함께 참고하면 좋은 글
▶ 피보나치 수열과 황금비율
▶ 파이썬 프로그래밍 시작 (8) 자료구조(Data Structure) : 리스트
▶ 파이썬 프로그래밍 시작 (11) 연습문제 : 자료구조
▶ 사용빈도 높은 파이썬 함수 : sorted, list 함수
▶ 물의 경도와 커피 맛. 커피추출에 알맞는 물의 경도는?
참고자료
임현승, 박우신, 박성호. (2026). “물의 TDS 조절이 커피의 관능적 품질 일관성에 미치는 영향: 국내 수원지별 미네랄 조성 차이와의 상대적 영향 비교를 중심으로.” Culinary Science & Hospitality Research, 32(1), 51-60.
확인한 내용 : 커피 관능평가에서 p값이 사용되는 맥락, 삼점검사, 특성차이검사, 분산분석, 관능 점수와 통계 해석의 관계.
파이썬 예제 데이터는 p값 개념 설명을 위해 만든 가상의 데이터입니다. 실제 논문의 원자료가 아닙니다.