> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rankraft.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# How metrics are calculated

> The formula behind every report metric, a worked example, and how to reproduce the numbers from raw answers.

Every answer metric is computed over **all answers in the range, pooled**: each answer counts once, whichever engine gave it. For per-engine figures, add `platform` to `group_by` and each row is computed over that engine's answers alone. The API and the dashboard run the same code, so the same range and filters give the same number.

## Which answers count

* Answers collected successfully on a day inside the range (UTC).
* **Not** Google result pages that showed no AI Overview. They are dropped before anything is counted, from the numerators and from `answers`.
* When you pass a `filter`, only answers matching it.

## Formulas

### Mention rate

$$
\text{mention\_rate} = \frac{\text{answers naming the brand}}{\text{answers}}
$$

An answer names you when it uses your brand name or one of its aliases. 0–1.

### Citation rate

$$
\text{citation\_rate} = \frac{\text{answers linking to at least one page on the brand's site}}{\text{answers}}
$$

Ten links to your site in one answer count once. For a competitor, "its site" is the domains on its competitor entry. 0–1.

### Prompt coverage

$$
\text{prompt\_coverage} = \frac{\text{distinct prompts where the brand was named at least once}}{\text{distinct prompts asked}}
$$

A prompt named on any day, on any engine, in the range counts as covered. That is why coverage over a month is usually higher than coverage on any one day. 0–1.

### Visibility Score

$$
\text{visibility\_score} = 100 \times (0.45 \cdot \text{mention\_rate} + 0.35 \cdot \text{citation\_rate} + 0.20 \cdot \text{prompt\_coverage})
$$

The three rates are rounded to two decimals (whole percentage points) first, and the score is rounded to a whole number. 0–100.

### Share of voice

$$
\text{share\_of\_voice} = \frac{\text{answers naming the brand}}{\text{brand mentions across all tracked brands}}
$$

Each brand counts at most once per answer. Tracked brands are you and every competitor on the site's list. 0–1.

### Average position

$$
\text{average\_position} = \frac{\text{sum of the brand's positions}}{\text{answers naming the brand}}
$$

Position is the order in which an answer first names each tracked brand: the first is 1. Lower is better. Rounded to one decimal; `null` when the brand was never named.

### Rank

Order by Visibility Score among you and your **confirmed** competitors, within each group of rows. 1 is best.

### Citation share

$$
\text{citation\_share} = \frac{\text{citations of this domain (or page)}}{\text{all citations in the same group that pass the filter}}
$$

Counts every link, so a page linked twice in one answer counts twice. Rounded to four decimals. 0–1.

### Sentiment

$$
\text{positive} = \frac{\text{answers naming you in a positive tone}}{\text{answers naming you}}
$$

The same for `neutral` and `negative`. The three add up to 1 (give or take rounding). Answers not yet judged count as neutral.

## Worked example

A site with five prompts collects 24 answers in a week. Four are Google result pages with no AI Overview, so **20 answers** count.

| Input | Value |
| - | - |
| Answers naming you | 9 |
| Answers linking to your site | 5 |
| Prompts asked / prompts where you were named | 5 / 3 |
| Brand mentions in those 20 answers: you, rival A, rival B, rival C | 9, 12, 6, 3 (30 in all) |
| Sum of your positions in the 9 answers naming you | 18 |
| Citations of `example.com` / all citations | 8 / 50 |
| Tone of the 9 answers naming you: positive, neutral, negative | 6, 2, 1 |

| Metric | Calculation | Result |
| - | - | - |
| `mention_rate` | 9 / 20 | **0.45** |
| `citation_rate` | 5 / 20 | **0.25** |
| `prompt_coverage` | 3 / 5 | **0.60** |
| `visibility_score` | 100 × (0.45 × 0.45 + 0.35 × 0.25 + 0.20 × 0.60) = 100 × (0.2025 + 0.0875 + 0.12) | **41** |
| `share_of_voice` | 9 / 30 | **0.30** |
| `average_position` | 18 / 9 | **2.0** |
| `citation_share` (`example.com`) | 8 / 50 | **0.16** |
| `positive` / `neutral` / `negative` | 6 / 9, 2 / 9, 1 / 9 | **0.6667 / 0.2222 / 0.1111** |
| `answers` | | **20** |

> [!WARNING]
>
> Averaging seven daily Visibility Scores does not give this week's 41. Coverage in particular is 3 of 5 for the week even if no single day covered more than 2. Ask for the week in one call. See [Conventions & gotchas](/cookbook/conventions#dont-average-daily-rows-to-get-a-period-score).

## Reproducing the numbers from raw answers

[`POST /v1/answers`](/api-reference/endpoints/list-answers) returns one row per answer with everything the formulas need:

| Field | Contents |
| - | - |
| `id`, `date`, `platform`, `prompt` | Which answer, and its labels. |
| `brand_mentioned`, `brand_position` | Whether and where it named you. |
| `mentions` | Every tracked brand it named, in order of first appearance, each once. |
| `citations` | Each link, with `domain` and `own_site`. |
| `sentiment` | `positive`, `neutral`, `negative`, or `null` when not judged. |
| `text` | The answer itself. Empty on `ai_overviews` and `ai_mode` when the result page showed no AI Overview. |

This example computes the four headline figures for a range and checks them against the visibility report.

```js JavaScript theme={"system"}
import { RankRaft } from "rankraft-js";

const rr = new RankRaft();
const q = { site_id: process.env.RANKRAFT_SITE_ID, start_date: "2026-09-01", end_date: "2026-09-30" };

const all = await rr.answers.query({ ...q, include: ["prompt", "platform", "text", "brand_mentioned", "citations"], limit: 50 }).all();

// Google pages that showed no AI Overview are not answers.
const serp = new Set(["ai_overviews", "ai_mode"]);
const answers = all.filter((a) => !(serp.has(a.platform) && a.text === ""));

const r2 = (n) => Math.round(n * 100) / 100;
const named = answers.filter((a) => a.brand_mentioned);
const mention_rate = r2(named.length / answers.length);
const citation_rate = r2(answers.filter((a) => a.citations.some((c) => c.own_site)).length / answers.length);
const prompt_coverage = r2(new Set(named.map((a) => a.prompt.id)).size / new Set(answers.map((a) => a.prompt.id)).size);
const visibility_score = Math.round((0.45 * mention_rate + 0.35 * citation_rate + 0.2 * prompt_coverage) * 100);

const report = await rr.reports.visibility({ ...q, metrics: ["visibility_score", "mention_rate", "citation_rate", "prompt_coverage", "answers"] });
console.log({ answers: answers.length, mention_rate, citation_rate, prompt_coverage, visibility_score });
console.log(report.data[0]); // the same figures
```

```python Python theme={"system"}
from rankraft import RankRaft
import os

q = dict(site_id=os.environ["RANKRAFT_SITE_ID"], start_date="2026-09-01", end_date="2026-09-30")

with RankRaft() as rr:
    rows = rr.answers.query(**q, include=["prompt", "platform", "text", "brand_mentioned", "citations"], limit=50).all()
    report = rr.reports.visibility(**q, metrics=["visibility_score", "mention_rate", "citation_rate", "prompt_coverage", "answers"])

# Google pages that showed no AI Overview are not answers.
answers = [a for a in rows if not (a["platform"] in ("ai_overviews", "ai_mode") and a["text"] == "")]

named = [a for a in answers if a["brand_mentioned"]]
mention_rate = round(len(named) / len(answers), 2)
citation_rate = round(sum(any(c["own_site"] for c in a["citations"]) for a in answers) / len(answers), 2)
prompt_coverage = round(len({a["prompt"]["id"] for a in named}) / len({a["prompt"]["id"] for a in answers}), 2)
visibility_score = round((0.45 * mention_rate + 0.35 * citation_rate + 0.20 * prompt_coverage) * 100)

print(dict(answers=len(answers), mention_rate=mention_rate, citation_rate=citation_rate,
           prompt_coverage=prompt_coverage, visibility_score=visibility_score))
print(report.data[0])  # the same figures
```

> [!NOTE]
>
> Python's `round()` rounds halves to the nearest even number, so on an exact half (0.125) it can land one point away from the report. The report rounds halves up.

## Compared with Profound

If you are moving code across from Profound's API:

* Profound's `visibility_score` is a 0–1 share of answers that mention any brand. Ours is the 0–100 composite above.
* Profound computes each metric per engine, then averages across engines. Ours pools every answer; group by `platform` for per-engine figures.
* Profound counts days in US Eastern time. Ours are UTC.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.