Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 7 additions & 7 deletions examples/experiments/create_search_custom_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -105,99 +105,137 @@ 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(
client: GoogleAdsClient, customer_id: str, experiment_resource_name: str
) -> 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.
Expand All @@ -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
)
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -285,20 +331,22 @@ 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"
" 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"
)
Expand All @@ -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.
Expand Down