Skip to content

Commit 6740d30

Browse files
✨ Add bid strength to impression-share correlate
Fetches keyword performance reports when building keyword index to calculate bid strength based on impressions and TTR. - STRONG: High impressions (1000+) with good TTR (5%+) - MODERATE: Medium impressions (100+) with decent TTR (2%+) - WEAK: Low performance metrics - UNKNOWN: No data in period Added Strength column to output table and CSV export includes impressions, taps, TTR, and bid_strength fields.
1 parent 722807a commit 6740d30

File tree

1 file changed

+100
-10
lines changed

1 file changed

+100
-10
lines changed

asa_api_cli/impression_share.py

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,10 @@ class CorrelatedSearchTerm:
538538
keyword_text: str | None = None
539539
current_bid: Decimal | None = None
540540
currency: str | None = None
541+
# Performance data for bid strength
542+
impressions: int = 0
543+
taps: int = 0
544+
ttr: float | None = None
541545

542546
@property
543547
def share_range(self) -> str:
@@ -562,6 +566,18 @@ def is_matched(self) -> bool:
562566
"""Whether this search term was matched to a keyword."""
563567
return self.keyword_id is not None
564568

569+
@property
570+
def bid_strength(self) -> str:
571+
"""Estimate bid strength based on impressions and TTR."""
572+
if self.impressions == 0:
573+
return "UNKNOWN"
574+
ttr = self.ttr or 0.0
575+
if self.impressions >= 1000 and ttr >= 0.05:
576+
return "STRONG"
577+
elif self.impressions >= 100 and ttr >= 0.02:
578+
return "MODERATE"
579+
return "WEAK"
580+
565581

566582
@dataclass
567583
class KeywordInfo:
@@ -575,16 +591,25 @@ class KeywordInfo:
575591
ad_group_name: str
576592
bid_amount: Decimal
577593
currency: str
594+
# Performance data
595+
impressions: int = 0
596+
taps: int = 0
597+
ttr: float | None = None
578598

579599

580600
def _build_keyword_index(
581601
client: "AppleSearchAdsClient",
582602
country: str,
603+
start_date: date | None = None,
604+
end_date: date | None = None,
583605
) -> dict[str, list[KeywordInfo]]:
584606
"""Build an index of keywords by text for a given country.
585607
586608
Returns a dict mapping lowercase keyword text -> list of KeywordInfo
587609
(multiple campaigns may have the same keyword).
610+
611+
If start_date and end_date are provided, also fetches performance data
612+
for bid strength calculation.
588613
"""
589614
keyword_index: dict[str, list[KeywordInfo]] = {}
590615

@@ -596,6 +621,25 @@ def _build_keyword_index(
596621
if country.upper() not in [c.upper() for c in campaign.countries_or_regions]:
597622
continue
598623

624+
# If date range provided, fetch keyword performance report
625+
keyword_performance: dict[int, tuple[int, int, float | None]] = {} # keyword_id -> (impr, taps, ttr)
626+
if start_date and end_date:
627+
try:
628+
report = client.reports.keywords(
629+
campaign_id=campaign.id,
630+
start_date=start_date,
631+
end_date=end_date,
632+
granularity=GranularityType.DAILY,
633+
)
634+
for row in report.row:
635+
if row.metadata.keyword_id and row.total:
636+
impr = row.total.impressions or 0
637+
taps = row.total.taps or 0
638+
ttr = taps / impr if impr > 0 else None
639+
keyword_performance[row.metadata.keyword_id] = (impr, taps, ttr)
640+
except AppleSearchAdsError:
641+
pass
642+
599643
# Get ad groups for this campaign
600644
try:
601645
ad_groups = list(client.campaigns(campaign.id).ad_groups.find(Selector().where("status", "==", "ENABLED")))
@@ -611,6 +655,10 @@ def _build_keyword_index(
611655

612656
for keyword in keywords:
613657
kw_text = keyword.text.lower()
658+
659+
# Get performance data if available
660+
perf = keyword_performance.get(keyword.id, (0, 0, None))
661+
614662
info = KeywordInfo(
615663
keyword_id=keyword.id,
616664
keyword_text=keyword.text,
@@ -620,6 +668,9 @@ def _build_keyword_index(
620668
ad_group_name=ad_group.name,
621669
bid_amount=Decimal(keyword.bid_amount.amount) if keyword.bid_amount else Decimal("0"),
622670
currency=keyword.bid_amount.currency if keyword.bid_amount else "USD",
671+
impressions=perf[0],
672+
taps=perf[1],
673+
ttr=perf[2],
623674
)
624675

625676
if kw_text not in keyword_index:
@@ -717,13 +768,13 @@ def correlate_impression_share(
717768
countries_in_data = {item.country.upper() for item in aggregated.values() if item.country}
718769
print_info(f"Found data for {len(countries_in_data)} countries: {', '.join(sorted(countries_in_data))}")
719770

720-
# Step 2: Build keyword index for each country
771+
# Step 2: Build keyword index for each country (with performance data)
721772
keyword_indices: dict[str, dict[str, list[KeywordInfo]]] = {}
722773
total_keywords = 0
723774

724775
for ctry in countries_in_data:
725-
with spinner(f"Building keyword index for {ctry}..."):
726-
keyword_indices[ctry] = _build_keyword_index(client, ctry)
776+
with spinner(f"Building keyword index for {ctry} (with performance data)..."):
777+
keyword_indices[ctry] = _build_keyword_index(client, ctry, start_date, end_date)
727778
kw_count = sum(len(v) for v in keyword_indices[ctry].values())
728779
total_keywords += kw_count
729780

@@ -757,6 +808,9 @@ def correlate_impression_share(
757808
keyword_text=match.keyword_text,
758809
current_bid=match.bid_amount,
759810
currency=match.currency,
811+
impressions=match.impressions,
812+
taps=match.taps,
813+
ttr=match.ttr,
760814
)
761815
)
762816
else:
@@ -815,6 +869,10 @@ def correlate_impression_share(
815869
"keyword_text",
816870
"current_bid",
817871
"currency",
872+
"impressions",
873+
"taps",
874+
"ttr",
875+
"bid_strength",
818876
]
819877
)
820878
for row in correlated:
@@ -832,6 +890,10 @@ def correlate_impression_share(
832890
row.keyword_text or "",
833891
row.current_bid or "",
834892
row.currency or "",
893+
row.impressions if row.is_matched else "",
894+
row.taps if row.is_matched else "",
895+
f"{row.ttr:.4f}" if row.ttr else "",
896+
row.bid_strength if row.is_matched else "",
835897
]
836898
)
837899
print_success(f"Exported {len(correlated)} rows to {output}")
@@ -851,9 +913,9 @@ def correlate_impression_share(
851913
table.add_column("Search Term", style="cyan", max_width=30)
852914
table.add_column("Ctry", style="dim", width=4)
853915
table.add_column("Share", justify="right", width=8)
854-
table.add_column("Campaign", style="magenta", max_width=25)
855-
table.add_column("Keyword", style="dim", max_width=20)
856-
table.add_column("Bid", justify="right", width=10)
916+
table.add_column("Campaign", style="magenta", max_width=20)
917+
table.add_column("Bid", justify="right", width=8)
918+
table.add_column("Strength", justify="center", width=8)
857919
table.add_column("Pop", justify="center", width=3)
858920

859921
for row in correlated[:display_limit]:
@@ -864,17 +926,30 @@ def correlate_impression_share(
864926
elif row.high_share < 0.5:
865927
share_style = "yellow"
866928

867-
bid_display = f"{row.current_bid:.2f} {row.currency}" if row.current_bid else "[dim]—[/dim]"
868-
campaign_display = row.campaign_name[:25] if row.campaign_name else "[dim]Not matched[/dim]"
869-
keyword_display = row.keyword_text[:20] if row.keyword_text else ""
929+
bid_display = f"{row.current_bid:.2f}" if row.current_bid else "[dim]—[/dim]"
930+
campaign_display = row.campaign_name[:20] if row.campaign_name else "[dim]Not matched[/dim]"
931+
932+
# Bid strength display
933+
if row.is_matched:
934+
strength = row.bid_strength
935+
if strength == "STRONG":
936+
strength_display = "[green]STRONG[/green]"
937+
elif strength == "MODERATE":
938+
strength_display = "[yellow]MOD[/yellow]"
939+
elif strength == "WEAK":
940+
strength_display = "[red]WEAK[/red]"
941+
else:
942+
strength_display = "[dim]—[/dim]"
943+
else:
944+
strength_display = "[dim]—[/dim]"
870945

871946
table.add_row(
872947
row.search_term[:30],
873948
row.country,
874949
f"[{share_style}]{row.share_range}[/{share_style}]",
875950
campaign_display,
876-
keyword_display,
877951
bid_display,
952+
strength_display,
878953
str(row.search_popularity) if row.search_popularity else "—",
879954
)
880955

@@ -888,6 +963,21 @@ def correlate_impression_share(
888963
console.print(f" [green]Matched to keywords:[/green] {matched_count}")
889964
console.print(f" [yellow]Unmatched (opportunities):[/yellow] {unmatched_count}")
890965

966+
# Bid strength summary for matched keywords
967+
matched_items = [c for c in correlated if c.is_matched]
968+
if matched_items:
969+
strong = sum(1 for c in matched_items if c.bid_strength == "STRONG")
970+
moderate = sum(1 for c in matched_items if c.bid_strength == "MODERATE")
971+
weak = sum(1 for c in matched_items if c.bid_strength == "WEAK")
972+
unknown = sum(1 for c in matched_items if c.bid_strength == "UNKNOWN")
973+
974+
console.print("\n[dim]Bid Strength (matched keywords):[/dim]")
975+
console.print(f" [green]Strong:[/green] {strong}")
976+
console.print(f" [yellow]Moderate:[/yellow] {moderate}")
977+
console.print(f" [red]Weak:[/red] {weak}")
978+
if unknown > 0:
979+
console.print(f" [dim]No data:[/dim] {unknown}")
980+
891981
low_share_matched = sum(1 for c in correlated if c.is_matched and c.high_share and c.high_share < 0.3)
892982
if low_share_matched > 0:
893983
console.print(

0 commit comments

Comments
 (0)