---
title: "nDCG@10은 상위 열 개의 순서를 어떻게 점수로 바꾸는가"
slug: "ndcg-at-10"
category: "RAG"
topic: "rag"
subtopic: "rag-evaluation"
tags: ["nDCG","Information Retrieval","Ranking","Graded Relevance","trec_eval"]
status: "published"
created: "2026-07-21"
updated: "2026-07-21"
summary: "nDCG@10의 gain과 discount, ideal ranking, 정규화를 손으로 계산하고 trec_eval의 실제 구현 차이와 검색 평가에서 놓치기 쉬운 조건을 확인한다."
kind: "Study Note"
evidence: "Järvelin·Kekäläinen 원문과 NIST trec_eval 소스 확인, 손계산과 trec_eval 실행"
---

검색 논문을 읽으면 `nDCG@10`이 자주 등장한다. 처음에는 상위 열 개 중 정답 비율이라고 생각했다. 그 설명은 precision에 더 가깝다.

`nDCG@10`은 세 가지를 함께 본다.

- 관련성이 높은 문서를 더 가치 있게 본다.
- 같은 관련 문서라도 앞에서 찾으면 더 높은 점수를 준다.
- 현재 query에서 가능한 이상적인 순서와 비교해 0에서 1 사이로 정규화한다.

이 글에서는 [Recall@100](/posts/recall-at-100)과 무엇이 다른지, [Pyserini](/posts/pyserini-reproducible-retrieval)에서 어떤 명령으로 계산하는지까지 연결한다.

## 이름부터 네 조각으로 나눈다

`nDCG@10`은 `n`, `D`, `C`, `G`와 cutoff 10으로 나눠 읽을 수 있다.

| 조각 | 뜻 | 하는 일 |
|---|---|---|
| G | Gain | relevance label을 문서의 가치로 바꾼다. |
| C | Cumulative | 1위부터 현재 rank까지 gain을 더한다. |
| D | Discounted | 뒤에 나온 문서의 gain을 줄인다. |
| n | normalized | 실제 DCG를 이상적인 DCG로 나눈다. |
| @10 | cutoff | 상위 열 개만 계산한다. |

먼저 query $q$에서 rank $i$ 문서의 relevance를 $rel_i$라고 둔다. gain 함수를 $g$라고 하면 cutoff $k$의 DCG는 다음처럼 쓸 수 있다.

$$
\operatorname{DCG}@k
=
\sum_{i=1}^{k}\frac{g(rel_i)}{\log_2(i+1)}
$$

rank 1의 denominator는 $\log_2 2=1$이므로 gain을 그대로 받는다. 뒤로 갈수록 denominator가 커져 같은 문서도 덜 기여한다.

| rank $i$ | discount $1/\log_2(i+1)$ |
|---:|---:|
| 1 | 1.0000 |
| 2 | 0.6309 |
| 3 | 0.5000 |
| 4 | 0.4307 |
| 10 | 0.2891 |

Järvelin과 Kekäläinen의 원식은 rank 1을 따로 두고 rank 2부터 $rel_i/\log_2 i$를 사용했다. 원식에서는 rank 2도 discount가 1이다. 위에서 사용한 현재 `trec_eval` 식은 denominator를 $\log_2(i+1)$로 옮겨 rank 2부터 약 0.6309를 적용한다. 두 식은 표기만 다른 동치식이 아니라 실제 점수가 달라지는 variant다.

## 정규화에는 query별 이상적인 순서가 필요하다

DCG만 비교하면 query마다 relevant document 수와 relevance 분포가 다른 문제가 남는다. grade 3 문서가 열 개 있는 query와 grade 1 문서가 한 개뿐인 query의 가능한 총 gain은 같지 않다.

그래서 qrels의 문서를 relevance 내림차순으로 놓은 ideal ranking을 만든다. 그 순서의 DCG가 IDCG다.

$$
\operatorname{nDCG}@k
=
\frac{\operatorname{DCG}@k}{\operatorname{IDCG}@k}
$$

실제 top-$k$가 ideal top-$k$와 같으면 1이다. 관련 문서가 뒤로 밀리거나 낮은 grade 문서가 먼저 나오면 1보다 작아진다.

정규화는 model score를 0에서 1로 바꾸는 작업이 아니다. BM25 score나 cosine similarity의 scale은 계산에 들어가지 않는다. run file의 score는 순서를 정하는 데 사용되고, 순서가 정해진 뒤에는 qrels의 relevance가 gain을 만든다.

## 한 query를 손으로 계산했다

다음 qrels에는 관련 문서가 다섯 개 있다. relevance는 1에서 3까지다.

```text
q1 0 D1 3
q1 0 D2 2
q1 0 D3 1
q1 0 D4 1
q1 0 D5 1
```

검색기는 다음 순서로 여섯 개를 반환했다. `X`와 `Y`는 qrels에 없는 문서다.

```text
q1 Q0 D2 1 10.0 demo
q1 Q0 X  2  9.0 demo
q1 Q0 D1 3  8.0 demo
q1 Q0 D3 4  7.0 demo
q1 Q0 Y  5  6.0 demo
q1 Q0 D4 6  5.0 demo
```

NIST `trec_eval`의 `ndcg_cut`과 같은 선형 gain $g(rel)=rel$을 사용했다.

| rank | 문서 | relevance | discounted gain |
|---:|---|---:|---:|
| 1 | D2 | 2 | 2.0000 |
| 2 | X | 0 | 0.0000 |
| 3 | D1 | 3 | 1.5000 |
| 4 | D3 | 1 | 0.4307 |
| 5 | Y | 0 | 0.0000 |
| 6 | D4 | 1 | 0.3562 |

상위 열 개 중 반환하지 않은 rank 7부터 10까지는 gain 0으로 본다.

$$
\operatorname{DCG}@10
=2+\frac{3}{\log_2 4}+\frac{1}{\log_2 5}+\frac{1}{\log_2 7}
=4.2869
$$

ideal ranking의 relevance 순서는 $[3,2,1,1,1]$이다.

$$
\operatorname{IDCG}@10
=3+\frac{2}{\log_2 3}+\frac{1}{\log_2 4}
+\frac{1}{\log_2 5}+\frac{1}{\log_2 6}
=5.5794
$$

따라서 최종 값은 다음과 같다.

$$
\operatorname{nDCG}@10
=\frac{4.2869}{5.5794}
=0.7683
$$

같은 파일을 현재 NIST `trec_eval` 소스에서 직접 build해 실행했다.

```bash
trec_eval -m ndcg_cut.10 -m recall.3,100 qrels.txt run.txt
```

```text
recall_3               all    0.4000
recall_100             all    0.8000
ndcg_cut_10            all    0.7683
```

손계산과 실행 결과가 소수 넷째 자리까지 같았다.

## nDCG라는 이름만으로 gain 함수를 알 수 없다

가장 중요하게 바로잡은 부분이다. nDCG 식은 하나로 고정돼 있지 않다.

자주 만나는 다른 형태는 relevance 차이를 지수로 벌린다.

$$
g(rel)=2^{rel}-1
$$

같은 예제에 이 gain을 적용하면 DCG는 7.2869, IDCG는 10.2103, nDCG는 0.7137이 된다. `trec_eval` 결과 0.7683과 다르다.

Järvelin과 Kekäläinen의 원문도 relevance label을 그대로 쓰는 방법만 고정하지 않았다. 실험에서 `0, 1, 2, 3`뿐 아니라 `0, 1, 4, 10`과 `0, 1, 10, 100` 같은 gain weight를 비교했다. 높은 relevance를 몇 배 더 가치 있게 볼지는 평가 설계의 일부였다.

NIST `trec_eval`의 `m_ndcg_cut.c`는 gain이 qrels의 relevance 값이라고 명시하고, 실제 합에서도 `gain / log2(rank + 1)`을 사용한다. 따라서 결과를 기록할 때 `nDCG@10`만 남기면 부족하다.

```text
metric implementation
gain function
discount function
cutoff
qrels relevance scale
```

논문 표의 `nDCG@10`을 다른 library로 다시 계산할 때 gain 정의를 확인하지 않으면 model이 아니라 evaluator 차이를 성능 차이로 오해할 수 있다.

## Recall@100과 보는 실패가 다르다

관련 문서가 top-100 안에 들어오기만 하면 `Recall@100`의 numerator에 포함된다. rank 1에 있든 rank 100에 있든 한 건이다.

반면 nDCG는 앞쪽 순서를 구분한다. relevant document가 하나뿐인 binary qrels를 생각하면 그 문서가 rank 1에 있을 때 nDCG는 1이고 rank 10에 있을 때는 약 0.289다. 두 경우의 `Recall@100`은 모두 1이다.

이 차이 때문에 two-stage retrieval에서는 두 지표의 역할을 나눠 볼 수 있다.

```text
first-stage retriever  → Recall@100으로 후보 누락 확인
reranker output        → nDCG@10으로 상위 순서 확인
```

reranker가 nDCG@10을 높였는데 Recall@100이 그대로인 것은 이상하지 않다. 후보 문서를 새로 찾지 않고 기존 후보의 순서만 바꿨기 때문이다.

반대로 `nDCG@10=1`도 관련 문서를 모두 찾았다는 뜻은 아니다. relevant document가 100개여도 가장 좋은 열 개만 정확히 위에 놓으면 nDCG@10은 1이 될 수 있다. 이때 top-10만 반환했다면 recall은 0.1이다.

## 평가 결과를 왜곡하는 조건

### Unjudged document를 0으로 보는 문제

`trec_eval`의 표준 경로는 qrels에 없는 문서를 대부분의 지표에서 nonrelevant처럼 처리한다. 새 retriever가 기존 pooling system이 찾지 못한 관련 문서를 가져와도 판정 전에는 gain 0이 될 수 있다.

따라서 dense나 hybrid system이 lexical baseline과 다른 문서를 많이 찾는다면 `nDCG@10`과 함께 top-$k$의 judged 비율을 확인해야 한다. 점수가 낮은 이유가 ranking 실패인지 judgment coverage 부족인지 분리하기 위해서다.

### Query 평균이 실패 유형을 숨기는 문제

보통 최종 `all` 값은 query별 nDCG를 평균낸 macro average다. 쉬운 query가 많으면 특정 업무 유형의 실패가 가려질 수 있다.

```text
identifier query
procedure query
symptom query
long-form question
```

전체 평균을 유지하되 query type별 분포와 낮은 점수의 실제 결과도 같이 봐야 한다.

### Relevance label이 모호한 문제

grade 3과 grade 2의 차이를 annotator가 일관되게 판단하지 못하면 지수 gain은 그 작은 불일치를 크게 증폭할 수 있다. label 설명, 중복 판정, disagreement 처리 없이 graded metric만 정교하게 계산해도 평가 자체가 정교해지지는 않는다.

## Pyserini에서 계산하는 명령

qrels와 TREC run file이 있으면 다음처럼 계산한다.

```bash
python -m pyserini.eval.trec_eval \
  -q -c \
  -m ndcg_cut.10 \
  qrels.txt run.txt
```

`-q`는 query별 값도 출력한다. `-c`는 qrels에 있지만 run에 빠진 query를 0으로 포함한다. 이 옵션이 없으면 결과를 하나도 반환하지 못한 query가 평균에서 빠질 수 있다.

비교 실험에서는 다음 항목을 함께 고정한다.

- 동일한 qrels와 query 집합
- 동일한 cutoff 10
- 동일한 evaluator version
- 동일한 relevance scale과 gain 정의
- 동일한 unjudged document 처리
- query별 결과와 macro average

## 내가 기억할 한 문장

`nDCG@10`은 **관련 문서를 얼마나 많이 찾았는가**보다 **가치가 높은 문서를 상위 열 개 안에서 얼마나 이상적으로 정렬했는가**에 답한다.

숫자를 읽기 전에는 gain 함수와 qrels 생성 과정을 먼저 확인한다. 첫 단계 후보가 정답을 놓쳤는지는 [Recall@100](/posts/recall-at-100)으로 따로 본다.

## References

1. Kalervo Järvelin, Jaana Kekäläinen. [Cumulated gain-based evaluation of IR techniques](https://doi.org/10.1145/582415.582418). ACM TOIS, 2002.
2. Tampere University. [저자 공개 원문 PDF](https://trepo.tuni.fi/bitstream/handle/10024/65718/cumulated_gain_based_indicators_2002.pdf?isAllowed=y&sequence=1).
3. NIST. [`trec_eval`의 `ndcg_cut` 구현](https://github.com/usnistgov/trec_eval/blob/ba38899cbd4de0fb699b47f39b64ef1c107e4a5c/m_ndcg_cut.c).
4. NIST. [`trec_eval`의 qrels와 run 결합 구현](https://github.com/usnistgov/trec_eval/blob/ba38899cbd4de0fb699b47f39b64ef1c107e4a5c/form_res_rels.c).
5. Castorini. [Pyserini `trec_eval` wrapper](https://github.com/castorini/pyserini/blob/pyserini-2.3.0/pyserini/eval/trec_eval.py).

원문과 소스는 2026-07-21에 확인했다.
