Skip to content

Commit 4b4f160

Browse files
Update experiments examples for clarity and accuracy (#1078)
1 parent 65e1574 commit 4b4f160

4 files changed

Lines changed: 134 additions & 83 deletions

File tree

examples/experiments/create_asset_optimization_experiment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
"""This example shows how to create an OPTIMIZE_ASSETS experiment.
14+
"""Creates an OPTIMIZE_ASSETS experiment.
1515
1616
Asset optimization experiments are used to test different asset combinations
1717
within Performance Max campaigns.

examples/experiments/create_search_adopt_ai_max_experiment.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
"""This example shows how to create an ADOPT_AI_MAX intra-campaign experiment for a Search campaign.
14+
"""Creates an ADOPT_AI_MAX intra-campaign experiment for a Search campaign.
1515
1616
Intra-campaign experiments split traffic *within* the campaign, based on whether
1717
the feature (in this case, AI Max) is enabled or not.
@@ -75,6 +75,8 @@ def main(client: GoogleAdsClient, customer_id: str, campaign_id: str) -> None:
7575

7676
# Create a campaign operation with an update mask to enable AI Max and
7777
# configure asset automation settings.
78+
# Note: For intra-campaign experiments, these settings are applied to the
79+
# base campaign but are only active for the treatment traffic split.
7880
campaign_operation = client.get_type("MutateOperation")
7981
campaign = campaign_operation.campaign_operation.update
8082
campaign.resource_name = googleads_service.campaign_path(

examples/experiments/create_search_custom_experiment.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16-
"""This example creates a standard, system-managed campaign experiment.
16+
"""Creates a standard, system-managed campaign experiment of type SEARCH_CUSTOM.
1717
18-
It demonstrates how to create an experiment, configure its control and treatment
19-
arms (where the treatment arm automatically generates a draft campaign), modify
20-
the system-generated draft campaign, and schedule the experiment.
18+
Sets up the experiment, configures control and treatment arms (where the
19+
treatment arm automatically generates a draft campaign), modifies the
20+
system-generated draft campaign, and schedules the experiment.
2121
22-
Note: This standard draft-based workflow does not apply to all experiment types
23-
(e.g., intra-campaign or asset optimization experiments) that do not use system-generated
24-
treatment campaign copies.
22+
Note: This standard draft-based workflow applies only to experiment types
23+
that use system-generated treatment campaign copies, and excludes
24+
intra-campaign or asset optimization experiments.
2525
"""
2626

2727
import argparse

examples/experiments/get_experiment_performance.py renamed to examples/experiments/evaluate_and_update_experiment.py

Lines changed: 123 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15-
"""This example illustrates how to retrieve performance metrics for an experiment.
15+
"""Retrieves performance metrics for an experiment, evaluates the performance,
16+
and takes action on the experiment accordingly.
1617
1718
It shows how to query statistical significance metrics for the experiment,
1819
and how to execute actions such as promoting, ending, or graduating an experiment.
@@ -21,7 +22,6 @@
2122
import argparse
2223
import sys
2324
import uuid
24-
from typing import Iterator, List
2525

2626
from google.ads.googleads.client import GoogleAdsClient
2727
from google.ads.googleads.errors import GoogleAdsException
@@ -54,7 +54,8 @@
5454

5555

5656
def main(client: GoogleAdsClient, customer_id: str, experiment_id: str) -> None:
57-
"""The main method that queries the experiment performance and evaluates it.
57+
"""Queries experiment performance, evaluates the performance metrics, and updates
58+
the experiment accordingly (graduates, promotes, ends, or allows to continue running).
5859
5960
Args:
6061
client: an initialized GoogleAdsClient instance.
@@ -64,13 +65,12 @@ def main(client: GoogleAdsClient, customer_id: str, experiment_id: str) -> None:
6465
ga_service: GoogleAdsServiceClient = client.get_service("GoogleAdsService")
6566

6667
# Query to retrieve the experiment.
67-
# Notice that we request the statistical metrics (e.g., p-value, point estimate,
68+
# Notice that we request the statistical metrics (for example, p-value, point estimate,
6869
# margin of error) which are populated based on the treatment arm.
6970
query = f"""
7071
SELECT
7172
experiment.resource_name,
7273
experiment.name,
73-
experiment.resource_name,
7474
experiment.experiment_id,
7575
experiment.type,
7676
metrics.conversions_absolute_change_p_value,
@@ -105,99 +105,137 @@ def main(client: GoogleAdsClient, customer_id: str, experiment_id: str) -> None:
105105
print(f"No experiment found for experiment ID: {experiment_id}")
106106

107107

108-
# [START get_experiment_performance_1]
108+
# [START evaluate_and_update_experiment_1]
109109
def evaluate_experiment(
110110
client: GoogleAdsClient, customer_id: str, row: GoogleAdsRow
111111
) -> None:
112-
"""Evaluates the performance of the experiment.
112+
"""Evaluates the performance of the experiment and updates it accordingly
113+
(for example, promotes, ends, or graduates).
114+
115+
Checks conversion and click metrics against statistical significance thresholds
116+
to determine the appropriate action to take on the experiment.
113117
114118
Args:
115119
client: an initialized GoogleAdsClient instance.
116120
customer_id: a client customer ID.
117-
row: a GoogleAdsRow containing the experiment arm and metrics.
121+
row: a GoogleAdsRow containing the experiment and metrics.
118122
"""
123+
# This function evaluates performance metrics and immediately takes action
124+
# to update the experiment's status (promote, end, or graduate) if
125+
# statistical significance thresholds are met.
119126
metrics = row.metrics
120127
experiment_resource_name = row.experiment.resource_name
121128

122-
# 1. Evaluate conversion success as a primary success signal.
129+
has_conv_metrics = (
130+
"conversions_absolute_change_p_value" in metrics
131+
and "conversions_absolute_change_point_estimate" in metrics
132+
and "conversions_absolute_change_margin_of_error" in metrics
133+
)
134+
has_click_metrics = (
135+
"clicks_p_value" in metrics
136+
and "clicks_point_estimate" in metrics
137+
and "clicks_margin_of_error" in metrics
138+
)
139+
140+
# 1. Evaluate conversion success as a primary success signal if available.
123141
# - Point Estimate: Represents the estimated average lift or difference in conversions.
124142
# - Margin of Error: Outlines the confidence interval bounds. Note that the margin_of_error provided by the API is calculated for a preset confidence level which is set based on the experiment type.
125143
# - Lower Bound: (Point Estimate - Margin of Error). If this value is above 0,
126144
# we have statistical significance that performance has improved.
127-
conv_p_value = metrics.conversions_absolute_change_p_value
128-
conv_lift = metrics.conversions_absolute_change_point_estimate
129-
conv_error = metrics.conversions_absolute_change_margin_of_error
130-
conv_lower_bound = conv_lift - conv_error
131-
132-
if conv_p_value <= P_VALUE_THRESHOLD:
133-
if conv_lower_bound > 0:
134-
print(
135-
"Significant Success: Conversions increased. Even at the lower"
136-
f" bound, the lift is {conv_lower_bound:.2f}. Promoting"
137-
" changes."
138-
)
139-
promote_experiment(client, customer_id, experiment_resource_name)
140-
return
141-
elif (conv_lift + conv_error) < 0:
142-
print(
143-
"Significant Decline: Even the upper bound"
144-
f" ({conv_lift + conv_error:.2f}) is below zero. Ending"
145-
" experiment."
146-
)
147-
end_experiment(client, customer_id, experiment_resource_name)
145+
if has_conv_metrics:
146+
conv_p_value = metrics.conversions_absolute_change_p_value
147+
conv_lift = metrics.conversions_absolute_change_point_estimate
148+
conv_error = metrics.conversions_absolute_change_margin_of_error
149+
conv_lower_bound = conv_lift - conv_error
150+
151+
if conv_p_value <= P_VALUE_THRESHOLD:
152+
if conv_lower_bound > 0:
153+
print(
154+
"Significant Success: Conversions increased. Even at the lower"
155+
f" bound, the lift is {conv_lower_bound:.2f}. Promoting"
156+
" changes."
157+
)
158+
promote_experiment(
159+
client, customer_id, experiment_resource_name
160+
)
161+
return
162+
elif (conv_lift + conv_error) < 0:
163+
print(
164+
"Significant Decline: Even the upper bound"
165+
f" ({conv_lift + conv_error:.2f}) is below zero. Ending"
166+
" experiment."
167+
)
168+
end_experiment(client, customer_id, experiment_resource_name)
169+
return
170+
171+
# 2. Evaluate click volume as a secondary signal.
172+
# This is helpful as an early indicator or for lower-volume accounts.
173+
click_p_value = metrics.clicks_p_value
174+
click_lift = metrics.clicks_point_estimate
175+
click_error = metrics.clicks_margin_of_error
176+
click_lower_bound = click_lift - click_error
177+
178+
if click_p_value <= P_VALUE_THRESHOLD and click_lower_bound > 0:
179+
# We have a directional winner: high confidence in more traffic,
180+
# but not enough data to confirm conversion impact yet.
181+
print(f"Click volume is significantly up (+{click_lift*100:.1f}%).")
182+
183+
# Graduation is only supported for separate campaign experiments, not
184+
# intra-campaign experiments where there is no separate treatment campaign.
185+
experiment_type_name = row.experiment.type_.name
186+
if (
187+
experiment_type_name != "ADOPT_BROAD_MATCH_KEYWORDS"
188+
and experiment_type_name != "ADOPT_AI_MAX"
189+
):
190+
print(
191+
"Graduating treatment campaign for further manual analysis."
192+
)
193+
graduate_experiment(
194+
client, customer_id, experiment_resource_name
195+
)
196+
else:
197+
print(
198+
"Intra-campaign trial detected: graduation is not supported. "
199+
"Continuing to run the experiment to gather more conversion data."
200+
)
148201
return
149202

150-
# 2. Evaluate click volume as a secondary signal.
151-
# This is helpful as an early indicator or for lower-volume accounts.
152-
click_p_value = metrics.clicks_p_value
153-
click_lift = metrics.clicks_point_estimate
154-
click_error = metrics.clicks_margin_of_error
155-
click_lower_bound = click_lift - click_error
156-
157-
if click_p_value <= P_VALUE_THRESHOLD and click_lower_bound > 0:
158-
# We have a directional winner: high confidence in more traffic,
159-
# but not enough data to confirm conversion impact yet.
203+
# 3. Print status if no action was taken.
204+
if has_conv_metrics or has_click_metrics:
205+
conv_status = (
206+
f"Conversions (p={metrics.conversions_absolute_change_p_value:.2f}, "
207+
f"lift={metrics.conversions_absolute_change_point_estimate:.2f} +/- "
208+
f"{metrics.conversions_absolute_change_margin_of_error:.2f})"
209+
if has_conv_metrics
210+
else "Conversions (not populated)"
211+
)
212+
click_status = (
213+
f"Clicks (p={metrics.clicks_p_value:.2f}, "
214+
f"lift={metrics.clicks_point_estimate:.2f} +/- "
215+
f"{metrics.clicks_margin_of_error:.2f})"
216+
if has_click_metrics
217+
else "Clicks (not populated)"
218+
)
160219
print(
161-
f"Click volume is significantly up (+{click_lift*100:.1f}%). "
162-
"Graduating treatment for further manual analysis."
220+
f"Inconclusive: No significant action taken. {conv_status}, {click_status}."
221+
" Allowing the experiment to continue running."
163222
)
164-
165-
# Graduate if it's a separate campaign test.
166-
# This keeps the high-volume treatment running independently.
167-
# Intra-campaign experiments (like ADOPT_BROAD_MATCH_KEYWORDS and
168-
# ADOPT_AI_MAX) run directly within the base campaign, meaning there is only
169-
# a single campaign involved and no separate treatment campaign to graduate.
170-
# Therefore, graduation is not supported for intra-campaign experiments.
171-
experiment_type_name = row.experiment.type_.name
172-
if (
173-
experiment_type_name != "ADOPT_BROAD_MATCH_KEYWORDS"
174-
and experiment_type_name != "ADOPT_AI_MAX"
175-
):
176-
graduate_experiment(client, customer_id, experiment_resource_name)
177-
else:
178-
print(
179-
"Intra-campaign trial detected: Graduation is not supported"
180-
" because there is only one campaign. Continuing to run to"
181-
" gather more conversion data."
182-
)
183223
else:
184-
# Both conversions and clicks are noisy.
185224
print(
186-
"Inconclusive: No significant lift in Conversions"
187-
f" (p={conv_p_value:.2f}) or Clicks (p={click_p_value:.2f})."
188-
f" Current estimated lift: {conv_lift:.2f} +/- {conv_error:.2f}."
189-
" Continue running."
225+
"Conversion and click performance metrics are not yet populated. "
226+
"Allowing the experiment to continue running."
190227
)
191-
# [END get_experiment_performance_1]
228+
# [END evaluate_and_update_experiment_1]
192229

193230

194231
def promote_experiment(
195232
client: GoogleAdsClient, customer_id: str, experiment_resource_name: str
196233
) -> None:
197234
"""Promotes the experiment trial campaign to the base campaign.
198235
199-
Promotion is an asynchronous long-running process that copies the trial campaign's
200-
settings and creatives back to the base campaign and subsequently ends the experiment.
236+
Promotion is an asynchronous long-running process that copies the trial
237+
campaign's settings and creatives back to the base campaign and subsequently
238+
ends the experiment.
201239
202240
Args:
203241
client: an initialized GoogleAdsClient instance.
@@ -208,6 +246,12 @@ def promote_experiment(
208246
"ExperimentService"
209247
)
210248
# This method returns a long running operation (LRO).
249+
# - To block until the operation is complete: call operation.result()
250+
# - For non-blocking status checks: use operation.done()
251+
# - For manual polling or persistent tracking: store operation.operation.name
252+
#
253+
# For more information on handling LROs, see:
254+
# https://developers.google.com/google-ads/api/docs/concepts/long-running-operations
211255
operation = experiment_service.promote_experiment(
212256
resource_name=experiment_resource_name
213257
)
@@ -227,8 +271,7 @@ def end_experiment(
227271
) -> None:
228272
"""Immediately ends the experiment.
229273
230-
This sets the scheduled end date of the experiment to the current date and time,
231-
terminating further traffic split serving without waiting for the end of the day.
274+
Terminates the traffic split and sets the end date to the current time.
232275
233276
Args:
234277
client: an initialized GoogleAdsClient instance.
@@ -245,7 +288,9 @@ def end_experiment(
245288
def graduate_experiment(
246289
client: GoogleAdsClient, customer_id: str, experiment_resource_name: str
247290
) -> None:
248-
"""Graduates the experiment to a full campaign.
291+
"""Graduates the experiment to a full standalone campaign.
292+
293+
This process involves creating a new budget and mapping the treatment campaign to it.
249294
250295
Args:
251296
client: an initialized GoogleAdsClient instance.
@@ -276,6 +321,7 @@ def graduate_experiment(
276321
# 2. Query the experiment_arm to retrieve the treatment campaign's resource name.
277322
# The treatment arm has control set to False.
278323
ga_service: GoogleAdsServiceClient = client.get_service("GoogleAdsService")
324+
# Query for the campaigns associated with the treatment arm of the experiment.
279325
query = f"""
280326
SELECT
281327
experiment_arm.campaigns
@@ -285,20 +331,22 @@ def graduate_experiment(
285331
"""
286332
search_response = ga_service.search(customer_id=customer_id, query=query)
287333

334+
# Find the resource name of the treatment campaign.
288335
treatment_campaign_resource_name = None
289336
for row in search_response:
290337
if row.experiment_arm.campaigns:
291338
treatment_campaign_resource_name = row.experiment_arm.campaigns[0]
292339
break
293340

341+
# Verify that a treatment campaign was found.
294342
if not treatment_campaign_resource_name:
295343
print(
296344
"Could not find the treatment campaign associated with this"
297345
" experiment."
298346
)
299347
return
300348

301-
# 3. Build the Graduation Mapping and execute.
349+
# 3. Build the budget mapping and execute the graduation request.
302350
experiment_service: ExperimentServiceClient = client.get_service(
303351
"ExperimentService"
304352
)
@@ -322,7 +370,8 @@ def graduate_experiment(
322370
if __name__ == "__main__":
323371
parser = argparse.ArgumentParser(
324372
description=(
325-
"Lists and evaluates performance metrics for a campaign experiment."
373+
"Retrieves performance metrics for an experiment, evaluates the"
374+
" performance and takes action on the experiment accordingly."
326375
)
327376
)
328377
# The following argument(s) should be provided to run the example.

0 commit comments

Comments
 (0)