diff --git a/examples/experiments/create_asset_optimization_experiment.py b/examples/experiments/create_asset_optimization_experiment.py index 1c6427836..fc37d96a5 100644 --- a/examples/experiments/create_asset_optimization_experiment.py +++ b/examples/experiments/create_asset_optimization_experiment.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""This example shows how to create an OPTIMIZE_ASSETS experiment. +"""Creates an OPTIMIZE_ASSETS experiment. Asset optimization experiments are used to test different asset combinations within Performance Max campaigns. diff --git a/examples/experiments/create_search_adopt_ai_max_experiment.py b/examples/experiments/create_search_adopt_ai_max_experiment.py index 2e842a447..24963d4e9 100644 --- a/examples/experiments/create_search_adopt_ai_max_experiment.py +++ b/examples/experiments/create_search_adopt_ai_max_experiment.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""This example shows how to create an ADOPT_AI_MAX intra-campaign experiment for a Search campaign. +"""Creates an ADOPT_AI_MAX intra-campaign experiment for a Search campaign. Intra-campaign experiments split traffic *within* the campaign, based on whether 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: # Create a campaign operation with an update mask to enable AI Max and # configure asset automation settings. + # Note: For intra-campaign experiments, these settings are applied to the + # base campaign but are only active for the treatment traffic split. campaign_operation = client.get_type("MutateOperation") campaign = campaign_operation.campaign_operation.update campaign.resource_name = googleads_service.campaign_path( diff --git a/examples/experiments/create_search_custom_experiment.py b/examples/experiments/create_search_custom_experiment.py index eaec7e9b5..a869ea57a 100644 --- a/examples/experiments/create_search_custom_experiment.py +++ b/examples/experiments/create_search_custom_experiment.py @@ -13,15 +13,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""This example creates a standard, system-managed campaign experiment. +"""Creates a standard, system-managed campaign experiment of type SEARCH_CUSTOM. -It demonstrates how to create an experiment, configure its control and treatment -arms (where the treatment arm automatically generates a draft campaign), modify -the system-generated draft campaign, and schedule the experiment. +Sets up the experiment, configures control and treatment arms (where the +treatment arm automatically generates a draft campaign), modifies the +system-generated draft campaign, and schedules the experiment. -Note: This standard draft-based workflow does not apply to all experiment types -(e.g., intra-campaign or asset optimization experiments) that do not use system-generated -treatment campaign copies. +Note: This standard draft-based workflow applies only to experiment types +that use system-generated treatment campaign copies, and excludes +intra-campaign or asset optimization experiments. """ import argparse diff --git a/examples/experiments/get_experiment_performance.py b/examples/experiments/evaluate_and_update_experiment.py similarity index 63% rename from examples/experiments/get_experiment_performance.py rename to examples/experiments/evaluate_and_update_experiment.py index 22ebd8487..ca0e43952 100644 --- a/examples/experiments/get_experiment_performance.py +++ b/examples/experiments/evaluate_and_update_experiment.py @@ -12,7 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""This example illustrates how to retrieve performance metrics for an experiment. +"""Retrieves performance metrics for an experiment, evaluates the performance, +and takes action on the experiment accordingly. It shows how to query statistical significance metrics for the experiment, and how to execute actions such as promoting, ending, or graduating an experiment. @@ -21,7 +22,6 @@ import argparse import sys import uuid -from typing import Iterator, List from google.ads.googleads.client import GoogleAdsClient from google.ads.googleads.errors import GoogleAdsException @@ -54,7 +54,8 @@ def main(client: GoogleAdsClient, customer_id: str, experiment_id: str) -> None: - """The main method that queries the experiment performance and evaluates it. + """Queries experiment performance, evaluates the performance metrics, and updates + the experiment accordingly (graduates, promotes, ends, or allows to continue running). Args: client: an initialized GoogleAdsClient instance. @@ -64,13 +65,12 @@ def main(client: GoogleAdsClient, customer_id: str, experiment_id: str) -> None: ga_service: GoogleAdsServiceClient = client.get_service("GoogleAdsService") # Query to retrieve the experiment. - # Notice that we request the statistical metrics (e.g., p-value, point estimate, + # Notice that we request the statistical metrics (for example, p-value, point estimate, # margin of error) which are populated based on the treatment arm. query = f""" SELECT experiment.resource_name, experiment.name, - experiment.resource_name, experiment.experiment_id, experiment.type, metrics.conversions_absolute_change_p_value, @@ -105,90 +105,127 @@ def main(client: GoogleAdsClient, customer_id: str, experiment_id: str) -> None: print(f"No experiment found for experiment ID: {experiment_id}") -# [START get_experiment_performance_1] +# [START evaluate_and_update_experiment_1] def evaluate_experiment( client: GoogleAdsClient, customer_id: str, row: GoogleAdsRow ) -> None: - """Evaluates the performance of the experiment. + """Evaluates the performance of the experiment and updates it accordingly + (for example, promotes, ends, or graduates). + + Checks conversion and click metrics against statistical significance thresholds + to determine the appropriate action to take on the experiment. Args: client: an initialized GoogleAdsClient instance. customer_id: a client customer ID. - row: a GoogleAdsRow containing the experiment arm and metrics. + row: a GoogleAdsRow containing the experiment and metrics. """ + # This function evaluates performance metrics and immediately takes action + # to update the experiment's status (promote, end, or graduate) if + # statistical significance thresholds are met. metrics = row.metrics experiment_resource_name = row.experiment.resource_name - # 1. Evaluate conversion success as a primary success signal. + has_conv_metrics = ( + "conversions_absolute_change_p_value" in metrics + and "conversions_absolute_change_point_estimate" in metrics + and "conversions_absolute_change_margin_of_error" in metrics + ) + has_click_metrics = ( + "clicks_p_value" in metrics + and "clicks_point_estimate" in metrics + and "clicks_margin_of_error" in metrics + ) + + # 1. Evaluate conversion success as a primary success signal if available. # - Point Estimate: Represents the estimated average lift or difference in conversions. # - 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. # - Lower Bound: (Point Estimate - Margin of Error). If this value is above 0, # we have statistical significance that performance has improved. - conv_p_value = metrics.conversions_absolute_change_p_value - conv_lift = metrics.conversions_absolute_change_point_estimate - conv_error = metrics.conversions_absolute_change_margin_of_error - conv_lower_bound = conv_lift - conv_error - - if conv_p_value <= P_VALUE_THRESHOLD: - if conv_lower_bound > 0: - print( - "Significant Success: Conversions increased. Even at the lower" - f" bound, the lift is {conv_lower_bound:.2f}. Promoting" - " changes." - ) - promote_experiment(client, customer_id, experiment_resource_name) - return - elif (conv_lift + conv_error) < 0: - print( - "Significant Decline: Even the upper bound" - f" ({conv_lift + conv_error:.2f}) is below zero. Ending" - " experiment." - ) - end_experiment(client, customer_id, experiment_resource_name) + if has_conv_metrics: + conv_p_value = metrics.conversions_absolute_change_p_value + conv_lift = metrics.conversions_absolute_change_point_estimate + conv_error = metrics.conversions_absolute_change_margin_of_error + conv_lower_bound = conv_lift - conv_error + + if conv_p_value <= P_VALUE_THRESHOLD: + if conv_lower_bound > 0: + print( + "Significant Success: Conversions increased. Even at the lower" + f" bound, the lift is {conv_lower_bound:.2f}. Promoting" + " changes." + ) + promote_experiment( + client, customer_id, experiment_resource_name + ) + return + elif (conv_lift + conv_error) < 0: + print( + "Significant Decline: Even the upper bound" + f" ({conv_lift + conv_error:.2f}) is below zero. Ending" + " experiment." + ) + end_experiment(client, customer_id, experiment_resource_name) + return + + # 2. Evaluate click volume as a secondary signal. + # This is helpful as an early indicator or for lower-volume accounts. + click_p_value = metrics.clicks_p_value + click_lift = metrics.clicks_point_estimate + click_error = metrics.clicks_margin_of_error + click_lower_bound = click_lift - click_error + + if click_p_value <= P_VALUE_THRESHOLD and click_lower_bound > 0: + # We have a directional winner: high confidence in more traffic, + # but not enough data to confirm conversion impact yet. + print(f"Click volume is significantly up (+{click_lift*100:.1f}%).") + + # Graduation is only supported for separate campaign experiments, not + # intra-campaign experiments where there is no separate treatment campaign. + experiment_type_name = row.experiment.type_.name + if ( + experiment_type_name != "ADOPT_BROAD_MATCH_KEYWORDS" + and experiment_type_name != "ADOPT_AI_MAX" + ): + print( + "Graduating treatment campaign for further manual analysis." + ) + graduate_experiment( + client, customer_id, experiment_resource_name + ) + else: + print( + "Intra-campaign trial detected: graduation is not supported. " + "Continuing to run the experiment to gather more conversion data." + ) return - # 2. Evaluate click volume as a secondary signal. - # This is helpful as an early indicator or for lower-volume accounts. - click_p_value = metrics.clicks_p_value - click_lift = metrics.clicks_point_estimate - click_error = metrics.clicks_margin_of_error - click_lower_bound = click_lift - click_error - - if click_p_value <= P_VALUE_THRESHOLD and click_lower_bound > 0: - # We have a directional winner: high confidence in more traffic, - # but not enough data to confirm conversion impact yet. + # 3. Print status if no action was taken. + if has_conv_metrics or has_click_metrics: + conv_status = ( + f"Conversions (p={metrics.conversions_absolute_change_p_value:.2f}, " + f"lift={metrics.conversions_absolute_change_point_estimate:.2f} +/- " + f"{metrics.conversions_absolute_change_margin_of_error:.2f})" + if has_conv_metrics + else "Conversions (not populated)" + ) + click_status = ( + f"Clicks (p={metrics.clicks_p_value:.2f}, " + f"lift={metrics.clicks_point_estimate:.2f} +/- " + f"{metrics.clicks_margin_of_error:.2f})" + if has_click_metrics + else "Clicks (not populated)" + ) print( - f"Click volume is significantly up (+{click_lift*100:.1f}%). " - "Graduating treatment for further manual analysis." + f"Inconclusive: No significant action taken. {conv_status}, {click_status}." + " Allowing the experiment to continue running." ) - - # Graduate if it's a separate campaign test. - # This keeps the high-volume treatment running independently. - # Intra-campaign experiments (like ADOPT_BROAD_MATCH_KEYWORDS and - # ADOPT_AI_MAX) run directly within the base campaign, meaning there is only - # a single campaign involved and no separate treatment campaign to graduate. - # Therefore, graduation is not supported for intra-campaign experiments. - experiment_type_name = row.experiment.type_.name - if ( - experiment_type_name != "ADOPT_BROAD_MATCH_KEYWORDS" - and experiment_type_name != "ADOPT_AI_MAX" - ): - graduate_experiment(client, customer_id, experiment_resource_name) - else: - print( - "Intra-campaign trial detected: Graduation is not supported" - " because there is only one campaign. Continuing to run to" - " gather more conversion data." - ) else: - # Both conversions and clicks are noisy. print( - "Inconclusive: No significant lift in Conversions" - f" (p={conv_p_value:.2f}) or Clicks (p={click_p_value:.2f})." - f" Current estimated lift: {conv_lift:.2f} +/- {conv_error:.2f}." - " Continue running." + "Conversion and click performance metrics are not yet populated. " + "Allowing the experiment to continue running." ) - # [END get_experiment_performance_1] + # [END evaluate_and_update_experiment_1] def promote_experiment( @@ -196,8 +233,9 @@ def promote_experiment( ) -> None: """Promotes the experiment trial campaign to the base campaign. - Promotion is an asynchronous long-running process that copies the trial campaign's - settings and creatives back to the base campaign and subsequently ends the experiment. + Promotion is an asynchronous long-running process that copies the trial + campaign's settings and creatives back to the base campaign and subsequently + ends the experiment. Args: client: an initialized GoogleAdsClient instance. @@ -208,6 +246,12 @@ def promote_experiment( "ExperimentService" ) # This method returns a long running operation (LRO). + # - To block until the operation is complete: call operation.result() + # - For non-blocking status checks: use operation.done() + # - For manual polling or persistent tracking: store operation.operation.name + # + # For more information on handling LROs, see: + # https://developers.google.com/google-ads/api/docs/concepts/long-running-operations operation = experiment_service.promote_experiment( resource_name=experiment_resource_name ) @@ -227,8 +271,7 @@ def end_experiment( ) -> None: """Immediately ends the experiment. - This sets the scheduled end date of the experiment to the current date and time, - terminating further traffic split serving without waiting for the end of the day. + Terminates the traffic split and sets the end date to the current time. Args: client: an initialized GoogleAdsClient instance. @@ -245,7 +288,9 @@ def end_experiment( def graduate_experiment( client: GoogleAdsClient, customer_id: str, experiment_resource_name: str ) -> None: - """Graduates the experiment to a full campaign. + """Graduates the experiment to a full standalone campaign. + + This process involves creating a new budget and mapping the treatment campaign to it. Args: client: an initialized GoogleAdsClient instance. @@ -276,6 +321,7 @@ def graduate_experiment( # 2. Query the experiment_arm to retrieve the treatment campaign's resource name. # The treatment arm has control set to False. ga_service: GoogleAdsServiceClient = client.get_service("GoogleAdsService") + # Query for the campaigns associated with the treatment arm of the experiment. query = f""" SELECT experiment_arm.campaigns @@ -285,12 +331,14 @@ def graduate_experiment( """ search_response = ga_service.search(customer_id=customer_id, query=query) + # Find the resource name of the treatment campaign. treatment_campaign_resource_name = None for row in search_response: if row.experiment_arm.campaigns: treatment_campaign_resource_name = row.experiment_arm.campaigns[0] break + # Verify that a treatment campaign was found. if not treatment_campaign_resource_name: print( "Could not find the treatment campaign associated with this" @@ -298,7 +346,7 @@ def graduate_experiment( ) return - # 3. Build the Graduation Mapping and execute. + # 3. Build the budget mapping and execute the graduation request. experiment_service: ExperimentServiceClient = client.get_service( "ExperimentService" ) @@ -322,7 +370,8 @@ def graduate_experiment( if __name__ == "__main__": parser = argparse.ArgumentParser( description=( - "Lists and evaluates performance metrics for a campaign experiment." + "Retrieves performance metrics for an experiment, evaluates the" + " performance and takes action on the experiment accordingly." ) ) # The following argument(s) should be provided to run the example.