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
1718It shows how to query statistical significance metrics for the experiment,
1819and how to execute actions such as promoting, ending, or graduating an experiment.
2122import argparse
2223import sys
2324import uuid
24- from typing import Iterator , List
2525
2626from google .ads .googleads .client import GoogleAdsClient
2727from google .ads .googleads .errors import GoogleAdsException
5454
5555
5656def 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 ]
109109def 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
194231def 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(
245288def 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(
322370if __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