# Mining search terms into negatives

## Get a report you can trust

- Date range: at least 30 days, 90 for accounts with fewer than about 30 conversions a month. End the range before the conversion lag, the days a conversion usually takes to be recorded. If the lag is unknown, leave out the most recent 7 days. A term from last week with no conversions may simply not have converted yet.
- Use the conversion column that bidding optimises to (Conversions, the primary actions). "All conv." mixes in secondary actions and makes junk look productive.
- Remove every filter before downloading. The totals rows at the bottom show what the visible rows carry: compare the search terms total with the account or campaign total. Google omits terms without enough query activity, so part of the spend can never be negated by name. In one audited account a filter left on showed a single keyword's terms; with it removed, hidden low-volume terms still carried about a third of spend.
- API results have no totals rows. Run the campaign cost query in [reading-the-account.md](reading-the-account.md) for the same dates and pass the sum as `--total-cost`.
- Layouts seen in practice: two lines (report name, date range) above the header, "Total: ..." rows at the end, sometimes comma CSV in UTF-8 and sometimes tab-separated UTF-16. `scripts/negatives.mjs` reads both. It treats a row as a total only when it is labelled "Total:" or starts with "Total" with blank campaign and ad group cells, so a search term such as "total invoice software" stays a search term.
- Number formats: the script decides the decimal mark once per file from cells that show it clearly (`7.206,25` or `0,00` means a decimal comma; `1,350.00` or `0.00` means a decimal point) and reads every number column the same way. When no cell decides it, it warns; compare the clicks and cost it prints against the report before trusting the run.
- The Added/Excluded column: Added means the term is already a keyword; Excluded means a negative blocks it (its clicks came before the negative or from a scope the negative does not cover); None means neither; Added / Excluded means a keyword and a negative clash, which is a conflict to fix. The API equivalent is `search_term_view.status` with ADDED, EXCLUDED, ADDED_EXCLUDED and NONE.
- The Match type column shows how the search matched, including close variants. Rows labelled AI Max (seen in practice) are AI Max search term matching; the API exposes this as `segments.search_term_match_source`.
- Performance Max terms come from their own report and their own API resource. Analyse them as a separate run.

## N-gram analysis

A wasteful word is usually spread over many small search terms, each too small to act on. N-grams add them up.

- The script splits each term into words and builds every run of 1, 2 and 3 words. Each n-gram counts once per search term, then impressions, clicks, cost, conversions and value are summed across terms. `terms` is the number of distinct search terms containing it; `converting_terms` is how many of those converted.
- Single stop words (to, for, the, in) are dropped as unigrams but kept inside longer n-grams, so `what is`, `how to` and `near me` survive.

### Splitting brand from non-brand

- Brand conversion rates are several times higher and would make every non-brand n-gram look bad, so brand and non-brand run separately.
- Run `node scripts/negatives.mjs campaigns search-terms.csv` first. It lists every campaign in the file with rows, clicks, cost and conversions.
- `--campaign NAME` keeps and `--not-campaign NAME` drops one whole campaign name, case ignored. Repeat the flag for each campaign. Quote names with spaces.
- `--campaign-match` and `--not-campaign-match` take a case-insensitive regular expression for accounts with many brand campaigns. Test the pattern against the `campaigns` list first: `brand` and even `\bbrand` match "Non-Brand" too. Anchor it, for example `^Brand` when every brand campaign name starts that way.
- Each run prints "campaigns in this run" and "campaigns left out". It stops with an error when a `--campaign` or `--not-campaign` name matches no campaign in the file, or when no rows are left. Check both runs against the split in `martech-context/stack.md`: every campaign in exactly one run.

### Thresholds the flags use

- Baseline conversion rate `p` = conversions divided by clicks for the rows in scope.
- `no-conv-significant`: zero conversions and at least `ln(0.05) / ln(1 - p)` clicks, roughly `3 / p`. At that point there is about a 5% chance of seeing no conversions if the n-gram converted at the baseline rate. Examples: `p` of 1% needs 299 clicks, 3.85% needs 77, 10% needs 29. Change the confidence with `--confidence`.
- The test runs once per n-gram. With a few hundred n-grams above the click threshold, about 1 in 20 of those that convert at baseline is still flagged by chance, and nested n-grams (`template`, `invoice template`, `free invoice template`) repeat the same evidence. A flag is a prompt to read the terms, the same as `no-conv-over-target-cpa`, not proof.
- Added terms (already keywords, often exact match) usually convert best and raise `p`, which lowers the click threshold for generic words. `--baseline-exclude-added` computes `p` without rows whose Added/Excluded value starts with Added; the target CPA still comes from every row in scope.
- `no-conv-over-target-cpa`: zero conversions and cost at or above the target CPA (`--target-cpa`, or the file's own CPA). This is a prompt to read the terms, not proof.
- `cpa-over-2x-target`: it converts, with enough clicks, at twice the target CPA or more. Do not negate the n-gram. Look at its terms and negate only the ones that never convert, or move the converting ones to their own ad group.
- `converting_terms` above zero: a broad or phrase negative on this n-gram blocks a search that converted. Narrow it (longer phrase, exact on the bad terms) or leave it.
- For value-based bidding, compare `conv_value / cost` per n-gram with the target ROAS instead of CPA, and treat ROAS under half the target with enough clicks the same way as `cpa-over-2x-target`.

### Reading order

1. Sort by cost (the script does) and read every flagged row.
2. Read the unflagged unigrams in the top 30 by cost for meaning: a theme word can be cheap per term but large in total.
3. Read the 50 most expensive single search terms. Some waste is one term, not a pattern.
4. Read the terms with the most impressions and no clicks only if impression share or CTR matters to the client; they cost nothing on CPC bidding.

### Picking the n-gram length

- Start from the shortest flagged or theme n-gram with `converting_terms` of 0, usually the unigram.
- If the unigram has converting terms, try the bigram that carries the waste as a phrase negative, then the trigram.
- If no length is clean, add exact negatives on the individual bad search terms.
- Never add a longer n-gram that a shorter one already in the sheet covers: `free invoice template` adds nothing once `template` is in.

## Themes and seed terms

Meaning comes first. A term in a theme that can never buy is negated at any volume. A term that might buy needs a flag from the numbers and `converting_terms` of zero. Seeds are starting points; check each one against the business.

- Job seeker: jobs, job, career, careers, hiring, salary, salaries, internship, intern, vacancy, vacancies, recruiter, resume, cv, "interview questions", "work from home".
  - Trap: businesses that sell recruiting or HR products, and staffing agencies.
- Free or DIY: free, template, templates, diy, sample, samples, example, examples, printable, crack, torrent, "open source".
  - Trap: "free trial", "free demo", "free quote" and "free consultation" convert for SaaS and services. When they do, negate "free template" as a phrase or the specific words, not broad `free`. "download" is a buying word for desktop software; "template" is the product for a template seller.
- Wrong intent (learning and research): "what is", definition, meaning, "how to", tutorial, course, courses, certification, training, exam, ppt, wiki.
  - Trap: "how to automate X" can be a buyer describing the problem. Training providers sell courses and certifications. Use phrase negatives for multi-word themes so they stay narrow.
- Irrelevant: another meaning of the same word, a different product, consumer rather than business use (personal, for kids, phone app), another industry.
  - Trap: an acronym the business sells under two different meanings. Route each meaning to its own ad group and landing page, with exact or phrase routing negatives, instead of negating either.
- Competitor: other companies' brand and product names.
  - Not a negative by default. Options: a separate competitor campaign with competitor names as negatives everywhere else; brand exclusions in Performance Max or AI Max; or leave them. The business owner decides and the decision goes in `martech-context/decisions.md`.
- Wrong location: places outside the service area.
  - First check the campaign's location option: "Presence or Interest" (the default and recommended option) or "Presence". Someone inside the target area searching for a distant city still matches either way, so place-name negatives are still needed. Use broad for one-word places, phrase for multi-word ones ("new york").
  - Trap: place names that are also product words, or neighbourhoods inside the served area.
- Existing customer (mostly brand campaigns): login, "sign in", support, "phone number", cancel, refund, billing, status, outage.
  - A business decision: negate them in the brand campaign when its goal is new customers and organic results serve support well; keep them when competitors bid on the brand.

### Conversions that are not buyers

In lead generation the Conversions column often counts every form fill. Job seekers, students and vendors submit demo and contact forms, so a waste theme can show `converting_terms` above zero and the CONVERTING check then protects it.

- When a theme term that cannot buy shows conversions, compare those conversions with qualified outcomes in the CRM or with offline conversion imports where they exist (lead status, opportunity, the email domain pattern).
- If none of them qualified, propose the negative and write the comparison as the owner decision on its CONVERTING line. Do not delete the line or the row silently.
- If the CRM cannot answer, keep the term, and raise the conversion definition as a measurement problem for the owner.

### Buying words to protect

Never propose these as negatives without the owner's approval, and expect `converting_terms` above zero on them: pricing, price, cost, quote, demo, trial, buy, vs, versus, alternative, alternatives, review, reviews, best, top, compare, software, tool, tools, platform, agency, service, services, company, "near me", "for business".

## The approval sheet

One row per negative, the format of `assets/example-negatives-approved.csv`:

- Theme, Level (account, shared list, campaign, ad group), List, Campaign, Ad Group, Keyword, Match type, Evidence, Approved by, Approved on.
- Evidence quotes the n-gram row or the search term with clicks, cost, conversions and flag, or says "theme seed term" for a negative added before any traffic.
- Approved by and Approved on are required for competitor, wrong location and existing-customer rows, and for any row kept despite a CONVERTING or CONFLICT line.
- A blank Campaign with a List name means the list is applied to every campaign. When a list is applied to only some campaigns, write one row per campaign so the conflict check scopes it correctly.
- Keep competitor, location and existing-customer rows in a separate block for the owner to decide.
