GSC Analyst
Turn a Search Console export into a small number of decisions the user can act on this week.
The failure mode this skill exists to prevent is the confident wrong answer. Search Console data invites it: the metrics look simple, they are not, and an analysis that reads them naively produces advice that is fluent, specific, and harmful. Most of what follows is about the difference.
Workflow
The user uploads their Search Console export to the conversation and asks for an analysis. They are usually not developers. Do not ask them to run commands, install anything, or reshape their files.
Find the uploaded file. It will be on disk under
/mnt/user-data/uploads/. List that directory rather than assuming a filename. The export is normally a ZIP straight from Search Console; pass it as-is, unzipping is not needed.Work out the site and brand terms without asking, if you can. The site URL is usually visible in the page URLs inside the export. Only ask the user when the brand is genuinely ambiguous, and ask in one short question.
Run the analyzer.
python3 scripts/gsc_analyze.py "/mnt/user-data/uploads/<file>" \ --site https://example.com \ [--brand "acme,acme app"] \ [--query-page query_page.csv] \ --out /tmp/analysis.jsonIt handles ZIP, XLSX, single CSV, or a directory, and copes with localized headers and comma decimals. It needs only
pandasandnumpy, both already present. Never try to install packages; if something is missing, say so and work with what runs.Read
not_availablebeforefindings. It lists what the export could not support. Those gaps go in the report; silently omitting them is how a partial analysis gets mistaken for a complete one.Interpret using the rules below.
Write the report using the template at the end, as a markdown file the user can download.
Never hand the user raw JSON, and never restate every row. The analyzer's job is arithmetic; the job here is deciding which three things matter.
If the export is missing something that matters, say what to change in the
export settings, in plain language, at the end. references/export-guide.md
has the specifics. Do not open with instructions; analyze what you were given
first.
Reading order
Work in this order. It is a diagnostic sequence, not a menu — each step changes how the next one should be read.
1. Trend and seasonality — establish whether anything is actually wrong
Read findings.trend first. If yoy.available is false, the export covers
under ~13 months and decline cannot be distinguished from seasonality. Say
so plainly and ask for a 16-month re-export before diagnosing a drop. A great
many "my traffic is falling" panics are November versus January.
change_28d is a useful short-term read but is contaminated by seasonality and
by the 2–3 day reporting lag at the end of every export. Never treat the final
few days as a cliff.
2. Brand split — establish what SEO is even responsible for
Read findings.brand_split. Brand queries obey different rules: high CTR at
any position, volume driven by marketing and word of mouth rather than
ranking. Conclusions differ sharply:
- Brand share above ~50% means organic search is mostly harvesting existing demand. Non-brand clicks are the real measure of SEO, and they are the number to track.
- Non-brand falling while brand holds is an SEO problem.
- Brand falling while non-brand holds is a marketing or reputation problem, and no amount of SEO work will fix it.
Check brand_terms_used before trusting the split. Terms derived from a domain
are deliberately conservative, so a short brand name or a nickname the domain
does not contain gets counted as non-brand. If the brand share looks too low
for the business, rerun with explicit --brand terms.
3. Irrelevant impressions — check before trusting anything CTR-based
Read findings.irrelevant_impressions and config.ctr_curve_source together.
Some sites collect large impression volumes on queries they rank for but that have nothing to do with what they sell. An insurance site ranking on page one for a city name is the type. Those impressions were never going to become clicks, the titles are not the problem, and every hour spent on them is wasted.
When share_of_impressions is large, this changes how the rest of the report
should be written. Say it in the headline, and strip those queries out of the
recommendations rather than leaving them at the top of the list where their
volume puts them.
config.ctr_curve_source tells you whether the expected-CTR curve was fitted
from this site or fell back to a published one:
fitted— normal.fallback_implausible_fit— the fit was rejected because it did not look like a click-through curve. This is the same pathology: when most impressions come from queries nobody clicks, the site stops being a usable baseline for itself. All CTR findings are directional only, and the report must say so.fallback_insufficient_data— too little data to fit. Same caution.
4. SERP absorption — the cohort that ranking cannot explain
Read findings.serp_absorption. This cohort holds impressions, keeps or
improves its ranking, and loses clicks anyway. Requiring stable-or-better
position is what makes it interesting: the usual explanation for lost clicks,
having dropped down the page, is excluded by construction.
Consistent with: an AI Overview or other SERP feature answering the query in place, a featured snippet the site does not own, an expanded People Also Ask block, or a shift in the query mix aggregated into one row.
Search Console does not label AI Overview impressions. Write "consistent with answer-in-place SERP features, including AI Overviews" and never "AI Overviews caused this". The honest framing is more useful anyway, because the response is the same across those causes.
What to do with it is not "rank harder" — the ranking is already fine. It is to move effort toward queries where a click remains necessary: pricing, comparisons, tools, calculators, login, documentation, anything requiring the user's own data or account. Definitional and explanatory content is the part being absorbed, and writing more of it compounds the loss.
5. CTR anomalies — the cheapest wins available
Read findings.ctr_anomalies.underperforming. These queries earn statistically
fewer clicks than their position predicts; only queries whose 95% confidence
interval excludes the expected CTR are listed, so these are not noise.
Usual causes, in rough order of frequency: the title does not contain the words the user searched; the description is truncated or missing; the ranking URL is the wrong page for the intent; a SERP feature sits above the result; or the query is genuinely irrelevant to the site and the impressions are worthless.
Check that last one before recommending anything. A query with high impressions and near-zero CTR is sometimes a page ranking for something it should not, and the correct action is nothing at all.
overperforming is worth as much and is usually ignored. Those titles have
found language that works on this audience. Read them for reusable patterns.
6. Striking distance — where incremental effort converts
Read findings.striking_distance, ranked by modelled click uplift rather than
by position, because a query at 12 with 8,000 impressions is worth more than
one at 6 with 40.
Cross off anything that also appears in irrelevant_impressions first. High
volume puts those queries at the top of this list, and they are the ones least
worth working on.
modelled_uplift_clicks_if_pos3 assumes impressions stay constant and position
3 is reachable. Both are approximations, in opposite directions: impressions
usually rise with position, and reaching 3 is not always possible. Present it
as a ranking signal for prioritization, not a forecast, and do not sum the
column into a traffic projection.
7. Cannibalization — only if the data supports it
Present only if findings.cannibalization exists. It requires query × page
pairing, which the Search Console interface cannot export. When absent, say the
check was not possible rather than inferring it from URL similarity — that
inference is wrong often enough to discredit the rest of the report.
When present: several URLs alternating on one query split the engagement signals that would otherwise accumulate on one page. Consolidate into the strongest URL and redirect, or differentiate the intent so the pages stop competing. Which one depends on whether the pages serve genuinely different needs, so recommend by looking at the URLs rather than by rule.
8. Concentration, movers, device and country
Supporting context, not headline findings. concentration.pages_for_half_of_clicks
in low single digits is a real fragility worth one sentence. A large device CTR
gap at similar positions points at mobile rendering or intent mismatch. Country
rows dominated by markets the business does not sell to explain a lot of
apparently poor aggregate CTR.
Position is not a rank
Search Console reports an impression-weighted mean. A query alternating between 3 and 15 reports 9 — a position it never held. This matters constantly:
- Never write "you rank 9th for X". Write "X averages position 9".
- A worsening average position can mean the page lost ground, or that it started appearing for a wider set of queries at lower positions. The second is often good news.
- Never compute an expected click count from average position for a single query and present it as a forecast.
Data limits to state, not bury
The Performance report's UI export is capped at 1,000 rows per table, and Search Console omits queries too rare to be anonymized. So:
- If
coverage.query_export_truncatedis true, the query table is the top 1,000 and the long tail is invisible. Any share computed from it is a share of what was exported, not of the site. - Total clicks in the Queries table will not match the site total, because the anonymized tail is missing. This is expected. Do not present it as a discrepancy or try to reconcile it.
- Ignore queries below ~100 impressions when recommending action. Their CTR is noise, and acting on noise is worse than acting on nothing.
Recommendations
Prioritize by effort against modelled effect, and cap the list at five. A report with twenty recommendations gets none of them done.
Roughly, cheapest first: rewriting a title beats earning a rank, which beats publishing a page, which beats restructuring a site. Where two findings point at the same URL, merge them into one recommendation.
Every recommendation names the specific URL or query, the concrete change, and the evidence line from the analysis. "Improve your content" is not a recommendation. "Rewrite the title of /pricing to include 'per seat', which appears in three underperforming queries at position 4" is.
Say so when the data supports no recommendation. A short honest report is worth more than five invented actions, and the user can tell the difference.
State how fast each recommendation can be proved wrong
Give every recommendation the time it takes to find out it was a bad idea, and break ties on that rather than on modelled effect. A title rewrite shows up in CTR within about ten days at a stable position; earning a rank takes four to eight weeks; a new page takes two to three months before its absence of traffic means anything.
This ordering matters more than it looks. A list ranked purely by expected impact is indistinguishable from a list of plausible guesses until months have passed — which is how an analysis turns into a persuasive backlog nobody can audit. Doing the fast-to-falsify items first means the next analysis has real evidence about whether this one was any good, and a cheap action that returns a verdict in ten days is often worth more than an expensive one with a larger modelled effect and a three-month feedback loop.
Flag that winnability has not been checked
Search Console says where the site ranks. It says nothing about who occupies the top of those results pages, and some positions are not available at any level of quality: the slots are held by the platform's own generated pages, by aggregators the query is really asking for, or by a SERP layout with almost no organic space left above the fold.
Nothing in this analysis can detect that — it would require looking at the actual results pages. So for every query recommended for a ranking gain, tell the user to open the results page and look at who holds the top before writing anything, and mark the recommendation as unverified on this dimension. A recommendation that names its own blind spot is worth more than one that stays quiet about it, and this is the blind spot that most often makes a technically correct recommendation a waste of a month.
Report structure
# Search Console analysis — [site], [period]
## What the data covers
[Date range, comparison period if present, row counts, and what could not be
assessed from `not_available`. Three or four lines.]
## Headline
[Two or three sentences. The single most important thing, with the number
behind it. If the honest answer is "nothing is wrong", say that.]
## Findings
### [Finding, phrased as the conclusion, not the metric name]
[What the data shows, the numbers, and what would falsify it.]
## Recommendations
1. **[Action]** — [specific URL or query]. [Why, tied to a finding.] [Effort.]
*Known if it worked by:* [timeframe]. *Check first:* [who holds the top of
this results page — not verifiable from Search Console.]
## What this analysis cannot tell you
[The gaps, plus what export would close them.]
Bundled resources
references/export-guide.md— how to produce the right export, including comparison mode and the query × page file the interface cannot generate. Read it when the user has not exported yet or is missing a capability.references/methodology.md— the CTR curve fit, the Wilson interval, and the absorption cohort definition. Read it when the user questions a number or wants to defend the analysis to someone else.scripts/gsc_load.py— export parsing. Read only if a file fails to load.scripts/gsc_analyze.py— the analysis engine.
Requirements
pandas and numpy, both already available. openpyxl is used only for
.xlsx exports and is optional; without it, ask the user for the CSV export
instead. Do not attempt package installation.