The AI citation share-of-voice formula
AI citation share of voice is your cited domains divided by all cited domains across a fixed query set: SoV = your_citations / total_citations. That's the whole formula — but every term hides a decision that changes the number. Which denominator (cited domains, or answers that mention you)? How do you normalize a URL before it counts? Do you weight by the position a source held in its answer? And how many runs before the number is a measurement rather than a sample? This post pins down each of those so the figure you report is defensible. It's the reference companion to the runnable pipeline in measure AI share of voice without scraping — that post is the code, this one is the math.
Read this page with an AI
The shortest honest answer: pick a denominator and hold it. Citation share of voice is your brand's cited pages divided by every cited page across the query set; mention share of voice is answers that name your brand divided by answers delivered. Both are legitimate, both are percentages, and they are not the same number — the mistake that ruins these reports is computing one and labelling it the other. State which you used, next to the figure, every time.
Two denominators, two different questions
Share of voice is always a ratio of your presence to total presence. What varies is how you define "presence" — and AI answers give you two clean sources for it in the same record. sources[] supports citation share; answerText supports mention share. They answer different questions and can move in opposite directions in the same run, which is exactly why you can't blend them.
| Definition | Numerator | Denominator | The question it answers |
|---|---|---|---|
| Citation SoV | Your domain's cited pages | All cited pages across the query set | Do your pages earn the link? Hard to game — you either got cited or you didn't. |
| Mention SoV | Answers whose answerText names your brand | Answers delivered across the query set | Does the model say your name? Broader presence, but a name-check without a link counts. |
| Weighted citation SoV | Your cited pages, scaled by 1/position | All cited pages, scaled by 1/position | Are you cited high, not just cited? Rewards the first slot over the eighth. |
A brand can lead on mention share and trail on citation share — the model likes to name it but doesn't link its pages — or the reverse, where it's cited constantly but rarely named in prose. Both facts are useful; reported as one number they cancel into nonsense. The rule is one denominator per figure, chosen for the claim you're making, printed alongside it.
Normalization comes before counting
Before any division, the URLs have to be reduced to a canonical host, or the denominator is wrong. sources[] returns whatever URL the engine cited, and the same page shows up in incompatible forms across runs and engines. Count them raw and one page becomes three hosts — the citation table inflates for whoever gets linked with tracking parameters and deflates everyone linked cleanly.
from urllib.parse import urlparse, urlunparse
def normalize(url):
"""Reduce a cited URL to (host, clean_url) before it enters any count."""
parts = urlparse(url)
host = parts.netloc.lower().removeprefix("www.") # lowercase, drop www.
clean = urlunparse((
parts.scheme,
host,
parts.path.rstrip("/"), # drop trailing slash
"", # params
"", # query string — strip ?ref=, ?utm_ ...
"", # fragment
))
return host, clean
# These three cite the SAME page. Unnormalized, they count as three hosts.
for u in [
"https://www.acme.com/guide/",
"https://acme.com/guide",
"https://acme.com/guide?ref=pplx",
]:
print(normalize(u))
# -> ('acme.com', 'https://acme.com/guide') x3- Lowercase the host.
Acme.comandacme.comare one domain; hosts are case-insensitive, so fold them. - Drop the
www.prefix.www.acme.comandacme.comresolve to the same brand and must count as one. - Strip the query string.
?ref=pplx,?utm_source=...and friends are tracking noise that fractures a single page into many. - Strip the trailing slash.
/guide/and/guideare the same path — normalize the tail so they collapse. - Aggregate by host, diagnose by page. Count on the host, but keep the clean URL in a column: eight citations of one domain could be one guide cited eight times or eight scattered posts, and only the URL tells you which.
Normalization is the same for both denominators. For citation SoV you normalize each entry in sources[]; for mention SoV you normalize nothing about URLs but you do want a word-boundary match on the brand name — "Acme" should not match inside "Acmegraph". Different fields, but both need a canonicalization step before the count, or the number lies.
A worked example, end to end
Take one query — "best time tracking software" — fetched once. The engine returned an answer that named Acme and Toggl in prose, and cited six pages. Here is every source after normalization, with the position it held in the answer:
| Position | Normalized host | Brand | 1/position weight |
|---|---|---|---|
| 1 | acme.com | Acme | 1.00 |
| 2 | toggl.com | Toggl | 0.50 |
| 3 | acme.com | Acme | 0.33 |
| 4 | g2.com | other | 0.25 |
| 5 | northwind.io | Northwind | 0.20 |
| 6 | reddit.com | other | 0.17 |
Now run the three formulas over this single answer. Note that acme.com was cited twice — after normalization it's one host with two cited pages, which the count must reflect, not collapse to one.
total cited pages = 6
# Citation SoV — Acme's cited pages / all cited pages
Acme = 2 / 6 = 33.3%
Toggl = 1 / 6 = 16.7%
Northwind = 1 / 6 = 16.7%
other = 2 / 6 = 33.3%
# Mention SoV — this one answer names Acme and Toggl
# denominator is answers delivered (1 here), so per-brand it is 1/1 or 0/1
Acme = 1 / 1 = 100% (named)
Toggl = 1 / 1 = 100% (named)
Northwind = 0 / 1 = 0% (cited, but not named in prose)
# Weighted citation SoV — sum of 1/position for the brand / sum for all
total weight = 1.00 + 0.50 + 0.33 + 0.25 + 0.20 + 0.17 = 2.45
Acme = (1.00 + 0.33) / 2.45 = 54.3% # rewarded for holding slot 1
Toggl = 0.50 / 2.45 = 20.4%
Northwind = 0.20 / 2.45 = 8.2%
other = (0.25 + 0.17) / 2.45 = 17.1%Look at what changed. On plain citation share Acme and "other" tie at 33%. Weight by position and Acme jumps to 54% because it owns slot 1, while "other" — cited only in slots 4 and 6 — sinks to 17%. And Northwind, cited once but never named, is 17% on citation share yet 0% on mention share. Three defensible numbers, three different stories, from one answer. That's why the definition belongs next to the figure and why you never average across denominators.
Position-weighting is optional and it's a choice, not a correction. Use 1/position when being cited first genuinely matters more to you than being cited at all — a default recommendation versus an also-ran in a list. If every citation is worth the same to your argument, plain citation share is cleaner and easier to defend. Whatever you pick, apply it identically to every brand and every run.
Want the number computed for your brand before you write the code? Get a free AI-visibility audit → — no card, no account.
Get my free auditPosition is per-answer, not per-run
One subtlety the worked example glosses: position ranks within a single answer only, and it starts at 1. Being position: 1 of four sources is a different outcome from position: 1 of twelve — the first is a third of the citations, the second a twelfth. If you weight by position or trend it over time, you must record each answer's source count alongside it, or a shrinking answer will look like a rank gain that never happened.
- Store the per-answer source count. Position without the denominator it lived in is uninterpretable later. One column, recorded at fetch time, saves the whole series.
- Never compare positions across engines directly. A
position: 2on Perplexity (which cites nearly everything) and aposition: 2on ChatGPT (which often cites nothing) are not the same signal. Keep engines in separate series. - Empty
sources[]from ChatGPT is real data. When ChatGPT didn't browse, it returns no sources — that's the model answering from memory, not a fetch failure. For citation SoV that answer contributes zero to the denominator; for mention SoV it still counts if it named a brand.
Sampling and drift: why one run isn't the answer
The formula is exact; the input is not. A single run of a query set is one draw from a distribution the engines redraw on every ask. Perplexity re-searches each time and its citation set visibly drifts between runs; AI Overviews sometimes doesn't render at all. Treating one run's percentage as "the" share of voice is the statistical error underneath every other one.
- A query set plus repeated runs is the unit of measurement — not a single fetch. Freeze the query set, run it on a cadence, and average or track the distribution across runs rather than trusting any one.
- Append and diff; require movement across several runs. A brand moving from 15% to 18% in one run pair is noise until it holds across three or four. Demand consistency before you call a change a trend.
- Changing the query set starts a new series. Add three easy queries you rank well for and your share jumps with nothing improving. Hold the set for the comparison window; when you must change it, begin a new series rather than continuing the old one.
- Excluded records must leave both numerator and denominator. A
failedrecord — a missing AI Overview, a scrape that timed out — costs zero credits and contributes nothing. Divide by answers delivered, not queries sent, or you understate every brand at once.
Summary
The AI citation share-of-voice formula is your_citations / total_citations — trivial arithmetic wrapped around four decisions that decide whether it means anything: which denominator (citation or mention), how you normalize a URL before counting, whether you weight by position, and how many runs stand behind the figure. Pin those down, print the definition next to the number, and the metric survives a stakeholder's questions. When you're ready to compute it on a schedule, the runnable pipeline is in measure AI share of voice without scraping, and the endpoint that returns the raw answers is documented on the Python API page.
Get a free AI-visibility audit → · Read the docs → — No card, no account.
Get my free audit