Skip to content

Instantly share code, notes, and snippets.

@phiture
Last active February 23, 2020 00:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phiture/a29d073308c7e3b5e2da90467ee09a27 to your computer and use it in GitHub Desktop.
Save phiture/a29d073308c7e3b5e2da90467ee09a27 to your computer and use it in GitHub Desktop.
Apple Search Ads API Keyword Management Demo
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Apple Search Ads API Keyword Management Demo"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"First, install the ```searchads_api``` [package in your command line with:](https://pypi.org/project/searchads-api/) \n",
"\n",
"```pip install searchads_api```\n",
"\n",
"Then import:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"from datetime import date, timedelta\n",
"from searchads_api import SearchAdsAPI"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Before you start, go into the Apple Search Ads console and download the Search Ads certificates (Settings > API > Create API Certificate), unzip them and place them in your working directory in a new folder named \"certs\". Make sure to never share these certificates and never upload them to a repository. These are the two certificates:\n",
"\n",
"- ```Api-certificate-name.pem```\n",
"- ```Api-certificate-name.key```\n",
"\n",
"These will be used to establish API access. While you are still in the ASA interface make sure to also copy the account ID.\n",
"You are now ready to establish the API connection:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"account_id = #Place account ID here\n",
"api = SearchAdsAPI(account_id, \"Api-certificate_name.pem\", \"Api-certificate_name.key\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Possible campaign structure:\n",
"\n",
"<img src=\"https://miro.medium.com/max/1500/1*tzeaKmxI1xwwGWlKRwc3xw.jpeg\" alt=\"Phiture\">\n",
"\n",
"\n",
"1. Script: Add all targeting Exact Match terms as negative Exact Match keywords in Discovery Campaign \n",
"2. Script: Adding Exact Match keywords as Broad Match keywords to the Discovery Campaign \n",
"3. Script: Add Search Terms from Discovery as Exact Match keywords to the Probing \n",
"4. Manual: on a weekly basis badly performing keywords are paused and keywords with enough volume that are performing well are redistributed to the other Campaigns"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you have a similar Campaign structure as we have at Phiture [(read an article about this here)](https://asostack.com/how-to-set-up-a-systematic-apple-search-ads-campaign-structure-with-5-campaign-types-64a0fa0b21d4) and you want to test search terms found in the Discovery Campaign from the last 30 days for value by moving them to the Probing Campaign (step 3.), this can be done in the following way:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"discovery_campaign_id = #Place Discovery Campaign ID here\n",
"\n",
"start = (date.today() - timedelta(days=30)).isoformat() # for example for the last 30 days\n",
"end = date.today().isoformat()\n",
"\n",
"try:\n",
" row, grandTotals = api.get_searcherms_report_by_date(discovery_campaign_id, start, end, limit=0)\n",
"except Exception as e:\n",
" print(\"get_searcherms_report_by_date\", str(e))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You might only want to keep certain information such as the search term text and the average Cost-per-Tap (CPT) which could be useful when deciding on the bid for each keyword:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"search_terms = []\n",
"\n",
"for value in row:\n",
" search_terms.append([ value['metadata']['searchTermText'], value['total']['avgCPT']['amount'], value['total']['avgCPT']['currency'] ])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As a Data Analyst/Scientist you might prefer analysing the data in a dataframe:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"df = pd.DataFrame(search_terms, columns=['text', 'amount', 'currency'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It makes sense to use the average CPT and set the bid 25% higher than the CPT before moving keywors to the Probing Campaign. This makes most sense when a high default bid has been set in the Discovery Campaign (possibly in combination with a CPA goal). Any further analysis you would like to do, now is the time. \n",
"\n",
"Some examples:\n",
"\n",
"- Selecting search terms based on metrics such as Taps, Downloads, Cost-per-Tap (CPT), Cost-per-Acquisition (CPA)\n",
"- Only selecting search terms that have reached a certain volume and CPA goal.\n",
"\n",
"For now we will only change the bid to 125% of the CPT since we haven't kept the other variables:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"df['amount'] = (pd.to_numeric(df[\"amount\"]) * 1.25).round(2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Lets also set the bids for keywords with a CPT of 0.00 to USD 1.00:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"df['amount'] = np.where(df.amount == 0, 1, df.amount)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Don't forget to also set match type and the status:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>text</th>\n",
" <th>amount</th>\n",
" <th>currency</th>\n",
" <th>matchType</th>\n",
" <th>status</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>sat nav</td>\n",
" <td>1.64</td>\n",
" <td>USD</td>\n",
" <td>EXACT</td>\n",
" <td>ACTIVE</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>ncp</td>\n",
" <td>0.98</td>\n",
" <td>USD</td>\n",
" <td>EXACT</td>\n",
" <td>ACTIVE</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" text amount currency matchType status\n",
"2 sat nav 1.64 USD EXACT ACTIVE\n",
"3 ncp 0.98 USD EXACT ACTIVE"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df['matchType'] = 'EXACT' \n",
"df['status'] = 'ACTIVE'\n",
"\n",
"df[2:4]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The data has to be prepared in the right structure before adding the new keywords to the Probing Campaign:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Data structure:\n",
"\n",
" {'text': 'sat nav', 'matchType': 'EXACT', 'status': 'ACTIVE', 'bidAmount': {'amount': '1.31', 'currency': 'USD'}} \n",
"\n"
]
}
],
"source": [
"keywords = []\n",
"\n",
"for index, row in df.iterrows():\n",
" keywords.append({\n",
" 'text': row['text'],\n",
" 'matchType': row['matchType'],\n",
" 'status': row['status'],\n",
" 'bidAmount': {\n",
" 'amount': str(row['amount']) ,\n",
" 'currency': row['currency']\n",
" }\n",
" })\n",
"\n",
"print('Data structure:\\n\\n',keywords[2],'\\n')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, add the new exact match keywords to the Probing Campaign:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"probing_campaign_id = #Place Probing Campaign ID here\n",
"probing_adgroup_id = #Place Probing Ad group ID here\n",
"\n",
"try:\n",
" res = api.add_targeting_keywords(probing_campaign_id, probing_adgroup_id, keywords)\n",
" #print(res['error']) # In case you want to print errors uncomment this line\n",
"except Exception as e:\n",
" print(\"add_targeting_keywords\", str(e))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once this is working you might want to set up the script on a server to run weekly or even daily. You can use a [crontab scheduler](https://en.wikipedia.org/wiki/Cron) for this. For example letting the script run [once a day at 11am](https://crontab.guru/#0_11_*_*_*) would look something like this:\n",
"\n",
"```0 11 * * * python3 searchadsapi.py```\n",
"\n",
"If you decide to run the script manually you don't have to convert your code into executable script (```.py```) but can just run your Jupyter Notebook (```.ipynb```)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment