AO1: Google Ads Demand Quality

CRM/TRM Non-Brand Demand, Match-Type Quality, and Negative Keyword Controls

Angela Qian

AO1

Assess CRM/TRM non-brand demand and match-type quality to design an intent-based Ad Grants structure with strong negative keyword controls.

AO1 Business Problem

Is TRI using Google Ad Grants to reach new high-intent searchers, or mostly capturing users who already know TRI?

Analysis Focus

  • CRM vs. TRM campaign performance

  • Brand vs. non-brand search demand

  • Match type quality

  • Search term intent

  • Negative keyword opportunities

:::

AO1 Goal

Evaluate whether CRM/TRM campaigns are reaching: - Existing brand-aware users - New high-intent training audiences

AO1 Data Sources

Code
library(tidyverse)
library(janitor)
library(scales)
library(stringr)
library(forcats)

campaign_files <- c(
  CRM = "data/Campaign report (CRM).csv",
  TRM = "data/Campaign report (TRM).csv"
)

terms_files <- c(
  CRM = "data/Search terms report (CRM).csv",
  TRM = "data/Search terms report (TRM).csv"
)

keyword_files <- c(
  CRM = "data/Search keyword report (CRM).csv",
  TRM = "data/Search keyword report (TRM).csv"
)

read_ads_csv_skip2 <- function(path) {

  txt <- readLines(path,
                   encoding = "UTF-16LE",
                   warn = FALSE)

  tmp <- tempfile(fileext = ".csv")

  writeLines(txt, tmp, useBytes = TRUE)

  read_csv(
    tmp,
    skip = 2,
    show_col_types = FALSE
  ) |>
    clean_names()
}

to_num <- function(x) {

  x |>
    as.character() |>
    str_replace_all("%", "") |>
    str_replace_all("\\$", "") |>
    str_replace_all(",", "") |>
    as.numeric()
}

Reports Used

  • Campaign reports
  • Search term reports
  • Search keyword reports
  • CRM campaigns
  • TRM campaigns

Metrics Evaluated

  • Clicks
  • Cost
  • CPC
  • Impressions
  • Match type
  • Search intent

AO1 Match Type Analysis

Code
crm_keywords <- read_ads_csv_skip2(keyword_files["CRM"])
trm_keywords <- read_ads_csv_skip2(keyword_files["TRM"])

keyword_df <- bind_rows(
  crm_keywords |> mutate(program = "CRM"),
  trm_keywords |> mutate(program = "TRM")
)

match_col <- intersect(
  c("match_type",
    "keyword_match_type",
    "search_term_match_type",
    "match_type_with_variant"),
  names(keyword_df)
)[1]

if (is.na(match_col)) {

  keyword_df <- keyword_df |>
    mutate(match_type_clean = "Match Type Not Available")

} else {

  keyword_df <- keyword_df |>
    mutate(
      match_type_clean =
        str_to_title(as.character(.data[[match_col]]))
    )
}

keyword_summary <- keyword_df |>

  mutate(clicks = to_num(clicks)) |>

  group_by(program, match_type_clean) |>

  summarise(
    clicks = sum(clicks, na.rm = TRUE),
    .groups = "drop"
  ) |>

  group_by(program) |>

  mutate(
    click_share = clicks / sum(clicks, na.rm = TRUE)
  ) |>

  ungroup()

ggplot(keyword_summary,
       aes(x = click_share,
           y = fct_reorder(match_type_clean,
                           click_share),
           fill = program)) +

  geom_col(show.legend = FALSE) +

  facet_wrap(~program) +

  scale_x_continuous(labels = percent) +

  labs(
    title = "Match Type Click Share by Program",
    subtitle = "Broad match dominates CRM and TRM traffic",
    x = "Share of Clicks",
    y = NULL
  ) +

  theme_minimal(base_size = 12) +

  theme(
    plot.title = element_text(face = "bold")
  )

Match Type Takeaways

  • Broad match generated the majority of clicks
  • CRM and TRM campaigns rely heavily on discovery traffic
  • Broad match increases visibility but also increases irrelevant query risk
  • Match type separation is needed for cleaner optimization

AO1 Brand vs Non-Brand Search Demand

Code
crm_terms <- read_ads_csv_skip2(terms_files["CRM"])
trm_terms <- read_ads_csv_skip2(terms_files["TRM"])

terms_df <- bind_rows(
  crm_terms |> mutate(program = "CRM"),
  trm_terms |> mutate(program = "TRM")
)

brand_summary <- terms_df |>

  mutate(
    clicks = to_num(clicks),

    brand_flag = if_else(
      str_detect(
        str_to_lower(search_term),
        "trauma resource institute|\\btri\\b"
      ),
      "Brand / Navigational",
      "Non-Brand / Discovery"
    )
  ) |>

  group_by(program, brand_flag) |>

  summarise(
    clicks = sum(clicks, na.rm = TRUE),
    .groups = "drop"
  ) |>

  group_by(program) |>

  mutate(
    click_share = clicks / sum(clicks)
  )

ggplot(
  brand_summary,
  aes(x = brand_flag,
      y = click_share,
      fill = brand_flag)
) +

  geom_col(show.legend = FALSE) +

  facet_wrap(~program) +

  scale_y_continuous(labels = percent) +

  labs(
    title = "Brand vs Non-Brand Search Demand",
    subtitle = "Measures TRI's ability to reach new audiences",
    x = NULL,
    y = "Share of Clicks"
  ) +

  theme_minimal(base_size = 12) +

  theme(
    plot.title = element_text(face = "bold")
  )

Brand vs Non-Brand Takeaways

  • Branded searches reflect existing awareness
  • Non-brand searches represent scalable acquisition opportunity
  • TRM showed stronger non-brand search themes
  • CRM appeared more dependent on branded discovery

AO1 Search Term Analysis

Code
top_terms <- terms_df |>

  mutate(
    clicks = to_num(clicks)
  ) |>

  group_by(program, search_term) |>

  summarise(
    clicks = sum(clicks, na.rm = TRUE),
    .groups = "drop"
  ) |>

  arrange(desc(clicks)) |>

  group_by(program) |>

  slice_head(n = 8) |>

  ungroup() |>

  mutate(
    search_term = str_wrap(search_term,
                           width = 28),

    search_term =
      fct_reorder(search_term,
                  clicks)
  )

ggplot(
  top_terms,
  aes(x = clicks,
      y = search_term,
      fill = program)
) +

  geom_col(show.legend = FALSE) +

  facet_wrap(~program,
             scales = "free_y") +

  scale_x_continuous(labels = comma) +

  labs(
    title = "Top Search Terms by Clicks",
    subtitle = "Search terms reveal user intent quality",
    x = "Clicks",
    y = NULL
  ) +

  theme_minimal(base_size = 12) +

  theme(
    plot.title = element_text(face = "bold")
  )

Search Term Takeaways

  • Search terms provide the clearest signal of user intent
  • Non-brand trauma-training searches represent growth opportunities
  • Search term reviews should guide future keyword expansion
  • Low-intent queries should become negative keywords

AO1 Recommendations

Campaign Type Purpose
Brand Campaign Capture existing TRI awareness
CRM Non-Brand Expand community resiliency discovery
TRM Non-Brand Expand trauma-training acquisition
Donation Campaign Support fundraising intent
Training Campaign Capture high-intent booking traffic

AO1 Optimization Recommendations

  • Separate broad, phrase, and exact match ad groups
  • Build a recurring negative keyword review process
  • Promote strong search terms into phrase/exact match
  • Align landing pages with search intent
  • Re-evaluate campaigns after conversion validation

Final AO1 Takeaway

TRI should use Google Ad Grants as a controlled demand-harvesting system focused on search intent quality rather than raw traffic volume alone.