├── .gitignore ├── LICENSE ├── README.md ├── docs ├── code-of-conduct.md └── images │ ├── download-json.png │ ├── googleairways_campaign.png │ ├── googlestore │ ├── chromecastultra.png │ ├── googlehome.png │ ├── googlehomemini.png │ ├── googlewifi.jpg │ ├── pixel2.png │ ├── pixelbook.png │ └── pixelbuds.png │ ├── googlestore_campaign.png │ ├── oauth.png │ └── vision │ ├── bradpitt.jpg │ ├── chair.jpg │ ├── dachshund.jpg │ ├── googlenyc.jpg │ ├── hotdog.jpg │ ├── kingscrossstation.jpg │ ├── oldtrafford.jpg │ ├── rp.jpg │ ├── teslamodels.jpg │ └── timessquarenyc.jpg └── dv360-automation-notebook.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.pyc 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # Mac hidden files 120 | .DS_Store 121 | 122 | .github/ 123 | /.github -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *This is not an officially supported Google product.* 2 | 3 | # DV360 Automation 4 | 5 | ## Overview 6 | 7 | The following notebook includes accompanying code snippets for the Display & Video 360 Automation workshop, an interactive workshop for DV360 advertisers to explore functionality of the [Display & Video 360 API](https://developers.google.com/display-video/api/reference/rest) and [DoubleClick Bid Manager API](https://developers.google.com/bid-manager/v1.1). 8 | 9 | It is intended to provide a snapshot of the available functionality and is by no means an exhaustive look at all features of the API. For additional support using the DV360 API, refer to the [official developer documentation](https://developers.google.com/display-video/api/guides/getting-started/overview). 10 | 11 | ## Notebook 12 | 13 | This .ipynb notebook is intended for use within [Google Colab](https://colab.research.google.com/), but should be fully compatible with other Jupyter notebook environments with small adjustments. 14 | 15 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google/dv360-automation/blob/master/dv360-automation-notebook.ipynb) 16 | 17 | ## Contents 18 | 19 | * Setup and authentication 20 | * Structured Data Files (SDF) 21 | * Manually create SDF 22 | * Editing SDF programmatically 23 | * SDF + Entity Read Files 24 | * SDF + Cloud Vision API 25 | * Optimisation using Reports 26 | * Challenge 27 | * Display & Video 360 API 28 | * Campaign builds 29 | * Individual targeting 30 | * Bulk targeting 31 | * Optimisation (external trigger) 32 | * Optimisation (reporting data) 33 | * Creative upload 34 | * Challenge 35 | * Resources 36 | 37 | ## License 38 | 39 | Apache 2.0 -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Google Open Source Community Guidelines 2 | 3 | At Google, we recognize and celebrate the creativity and collaboration of open 4 | source contributors and the diversity of skills, experiences, cultures, and 5 | opinions they bring to the projects and communities they participate in. 6 | 7 | Every one of Google's open source projects and communities are inclusive 8 | environments, based on treating all individuals respectfully, regardless of 9 | gender identity and expression, sexual orientation, disabilities, 10 | neurodiversity, physical appearance, body size, ethnicity, nationality, race, 11 | age, religion, or similar personal characteristic. 12 | 13 | We value diverse opinions, but we value respectful behavior more. 14 | 15 | Respectful behavior includes: 16 | 17 | * Being considerate, kind, constructive, and helpful. 18 | * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or 19 | physically threatening behavior, speech, and imagery. 20 | * Not engaging in unwanted physical contact. 21 | 22 | Some Google open source projects [may adopt][] an explicit project code of 23 | conduct, which may have additional detailed expectations for participants. Most 24 | of those projects will use our [modified Contributor Covenant][]. 25 | 26 | [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct 27 | [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ 28 | 29 | ## Resolve peacefully 30 | 31 | We do not believe that all conflict is necessarily bad; healthy debate and 32 | disagreement often yields positive results. However, it is never okay to be 33 | disrespectful. 34 | 35 | If you see someone behaving disrespectfully, you are encouraged to address the 36 | behavior directly with those involved. Many issues can be resolved quickly and 37 | easily, and this gives people more control over the outcome of their dispute. 38 | If you are unable to resolve the matter for any reason, or if the behavior is 39 | threatening or harassing, report it. We are dedicated to providing an 40 | environment where participants feel welcome and safe. 41 | 42 | ## Reporting problems 43 | 44 | Some Google open source projects may adopt a project-specific code of conduct. 45 | In those cases, a Google employee will be identified as the Project Steward, 46 | who will receive and handle reports of code of conduct violations. In the event 47 | that a project hasn’t identified a Project Steward, you can report problems by 48 | emailing opensource@google.com. 49 | 50 | We will investigate every complaint, but you may not receive a direct response. 51 | We will use our discretion in determining when and how to follow up on reported 52 | incidents, which may range from not taking action to permanent expulsion from 53 | the project and project-sponsored spaces. We will notify the accused of the 54 | report and provide them an opportunity to discuss it before any action is 55 | taken. The identity of the reporter will be omitted from the details of the 56 | report supplied to the accused. In potentially harmful situations, such as 57 | ongoing harassment or threats to anyone's safety, we may take action without 58 | notice. 59 | 60 | *This document was adapted from the [IndieWeb Code of Conduct][] and can also 61 | be found at .* 62 | 63 | [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct 64 | -------------------------------------------------------------------------------- /docs/images/download-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/download-json.png -------------------------------------------------------------------------------- /docs/images/googleairways_campaign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googleairways_campaign.png -------------------------------------------------------------------------------- /docs/images/googlestore/chromecastultra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore/chromecastultra.png -------------------------------------------------------------------------------- /docs/images/googlestore/googlehome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore/googlehome.png -------------------------------------------------------------------------------- /docs/images/googlestore/googlehomemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore/googlehomemini.png -------------------------------------------------------------------------------- /docs/images/googlestore/googlewifi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore/googlewifi.jpg -------------------------------------------------------------------------------- /docs/images/googlestore/pixel2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore/pixel2.png -------------------------------------------------------------------------------- /docs/images/googlestore/pixelbook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore/pixelbook.png -------------------------------------------------------------------------------- /docs/images/googlestore/pixelbuds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore/pixelbuds.png -------------------------------------------------------------------------------- /docs/images/googlestore_campaign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/googlestore_campaign.png -------------------------------------------------------------------------------- /docs/images/oauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/oauth.png -------------------------------------------------------------------------------- /docs/images/vision/bradpitt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/bradpitt.jpg -------------------------------------------------------------------------------- /docs/images/vision/chair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/chair.jpg -------------------------------------------------------------------------------- /docs/images/vision/dachshund.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/dachshund.jpg -------------------------------------------------------------------------------- /docs/images/vision/googlenyc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/googlenyc.jpg -------------------------------------------------------------------------------- /docs/images/vision/hotdog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/hotdog.jpg -------------------------------------------------------------------------------- /docs/images/vision/kingscrossstation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/kingscrossstation.jpg -------------------------------------------------------------------------------- /docs/images/vision/oldtrafford.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/oldtrafford.jpg -------------------------------------------------------------------------------- /docs/images/vision/rp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/rp.jpg -------------------------------------------------------------------------------- /docs/images/vision/teslamodels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/teslamodels.jpg -------------------------------------------------------------------------------- /docs/images/vision/timessquarenyc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/dv360-automation/12cb3565fe647d872b0878dd5f2ca0a656055bb8/docs/images/vision/timessquarenyc.jpg -------------------------------------------------------------------------------- /dv360-automation-notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "dv360-automation-notebook.ipynb", 7 | "provenance": [], 8 | "collapsed_sections": [ 9 | "rvbhSbEqIkvz" 10 | ], 11 | "toc_visible": true 12 | }, 13 | "kernelspec": { 14 | "name": "python3", 15 | "display_name": "Python 3" 16 | } 17 | }, 18 | "cells": [ 19 | { 20 | "cell_type": "code", 21 | "metadata": { 22 | "id": "daZgRYTNfKjd" 23 | }, 24 | "source": [ 25 | "# Copyright 2020 Google LLC\n", 26 | "\n", 27 | "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", 28 | "# you may not use this file except in compliance with the License.\n", 29 | "# You may obtain a copy of the License at\n", 30 | "\n", 31 | "# https://www.apache.org/licenses/LICENSE-2.0\n", 32 | "\n", 33 | "# Unless required by applicable law or agreed to in writing, software\n", 34 | "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", 35 | "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", 36 | "# See the License for the specific language governing permissions and\n", 37 | "# limitations under the License." 38 | ], 39 | "execution_count": null, 40 | "outputs": [] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": { 45 | "id": "j9oYcd0LsZaG" 46 | }, 47 | "source": [ 48 | "# DV360 Automation: codelab\n", 49 | "Author: Matt Lynam\n", 50 | "\n", 51 | "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google/dv360-automation/blob/master/dv360-automation-notebook.ipynb)" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": { 57 | "id": "jdTyEks_9XJD" 58 | }, 59 | "source": [ 60 | "# Objective\n", 61 | "\n", 62 | "Enable Display & Video 360 (DV360) advertisers to increase workflow efficiency by utilising the right automation solution according to their needs, resources and technical capability.\n", 63 | "\n", 64 | "**Goals**\n", 65 | "* Provide an overview of the current automation suite available in DV360\n", 66 | "* Demonstrate the capabilities and limitations of DV360's UI and APIs\n", 67 | "* Explore common advertiser use cases and pitfalls\n", 68 | "* Acquire hands-on experience by applying key concepts using a fictional case study" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": { 74 | "id": "5mXddxxCyopl" 75 | }, 76 | "source": [ 77 | "# 0) Setup and authentication" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": { 83 | "id": "uuo6MXz7KXpM" 84 | }, 85 | "source": [ 86 | "**Google Colab primer**\n", 87 | "\n", 88 | "Google Colaboratory, or \"Colab\" for short, allows you to write and execute Python in your browser, with:\n", 89 | "- Zero configuration required\n", 90 | "- Free access to GPUs\n", 91 | "- Easy sharing & colaboration \n", 92 | "\n", 93 | "A notebook is a list of cells, containing either **explanatory text** or **executable code** and its output. This is a **text cell**. \n", 94 | "\n", 95 | "Useful Colab tips\n", 96 | "* Double-click within the cell to edit\n", 97 | "* Code cells can be executed by clicking the **Play icon** in the left gutter of the cell; or with **Cmd/Ctrl + Enter** to run the cell in place;\n", 98 | "* Use **Cmd/Ctrl + /** to comment out a line of code" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": { 104 | "id": "tNwwCLFNyhEP" 105 | }, 106 | "source": [ 107 | "## 0.1 Install Python client libraries" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": { 113 | "id": "qrAZKywqxzbf" 114 | }, 115 | "source": [ 116 | "Run the following block to install the latest Google Python Client Library and import additional libraries used for this workshop." 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "metadata": { 122 | "id": "4RnbX0JsMKNG" 123 | }, 124 | "source": [ 125 | "!pip install google-api-python-client\n", 126 | "!pip install google-cloud-vision\n", 127 | "\n", 128 | "import csv\n", 129 | "import datetime\n", 130 | "import io\n", 131 | "import json\n", 132 | "import pprint\n", 133 | "\n", 134 | "from google.api_core import retry\n", 135 | "from google.cloud import vision\n", 136 | "from google.colab import files\n", 137 | "from google_auth_oauthlib.flow import InstalledAppFlow\n", 138 | "from googleapiclient import discovery\n", 139 | "from googleapiclient import http\n", 140 | "import pandas as pd\n", 141 | "import requests\n", 142 | "\n", 143 | "print('Successfully imported Python libraries!')" 144 | ], 145 | "execution_count": null, 146 | "outputs": [] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": { 151 | "id": "9fOhUBvXzfKh" 152 | }, 153 | "source": [ 154 | "## 0.2 Setup your GCP project\n", 155 | "\n", 156 | "To utilise the DV360 API, you need a Google Cloud project. For the purpose of this workshop, we've done this for you, but normally you'd have to complete the following steps, before you can make requests using the DV360 API:\n", 157 | "\n", 158 | "1. Select or create a [Google Cloud Platform project.](https://console.cloud.google.com/cloud-resource-manager)\n", 159 | "\n", 160 | "2. Enable [billing](https://cloud.google.com/billing/docs/how-to/modify-project) on your project.\n", 161 | "\n", 162 | "3. Enable the 'Display & Video 360' and 'DoubleClick Bid Manager' API from the [API library](https://console.cloud.google.com/apis/library)\n", 163 | "\n", 164 | "**Create GCP credentials**\n", 165 | "\n", 166 | "We've also generated credentials for you, but if you needed to generate new credentials, this would be the process:\n", 167 | "\n", 168 | "1. Go to the [API credentials page](https://console.cloud.google.com/apis/credentials) in the Cloud Platform Console.\n", 169 | "1. Fill out the required fields on the [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent).\n", 170 | "1. On the [credentials page](https://console.cloud.google.com/apis/credentials), click Create credentials >> OAuth client ID.\n", 171 | ">![Download JSON](https://github.com/google/dv360-automation/blob/master/docs/images/oauth.png?raw=true)\n", 172 | "1. Select **Other** as the application type, and then click **Create**.\n", 173 | "1. Download the credentials by clicking the **Download JSON** button \n", 174 | "> ![Download JSON](https://github.com/google/dv360-automation/blob/master/docs/images/download-json.png?raw=true)\n", 175 | "\n", 176 | "Reference: https://developers.google.com/display-video/api/guides/how-tos/authorizing" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": { 182 | "id": "3NK281NQpdIo" 183 | }, 184 | "source": [ 185 | "## 0.3 Authentication\n", 186 | "\n", 187 | "Next, we'll permission the application to submit authorised API requests on our behalf using OAuth authentication.\n", 188 | "\n", 189 | "The following scopes are specified in an array:\n", 190 | "* [DBM API](https://developers.google.com/bid-manager/how-tos/authorizing)\n", 191 | "* [Display Video API](https://developers.google.com/display-video/api/guides/how-tos/authorizing)\n", 192 | "* [GCP Storage Read](https://cloud.google.com/storage/docs/authentication)\n", 193 | "* [Cloud Vision API](https://cloud.google.com/vision/docs/auth)\n", 194 | "\n", 195 | "Reference:\n", 196 | "* [Example OAuth2 Python Library](https://developers.google.com/api-client-library/python/auth/web-app#example)\n", 197 | "* [Google scopes](https://developers.google.com/identity/protocols/googlescopes)\n", 198 | "\n" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "metadata": { 204 | "id": "9Rw3r-ijKBs_" 205 | }, 206 | "source": [ 207 | "API_SCOPES = ['https://www.googleapis.com/auth/doubleclickbidmanager',\n", 208 | " 'https://www.googleapis.com/auth/display-video',\n", 209 | " 'https://www.googleapis.com/auth/devstorage.read_only',\n", 210 | " 'https://www.googleapis.com/auth/cloud-vision']\n", 211 | "\n", 212 | "# Authenticate using user credentials stored in client_secrets.json\n", 213 | "client_secrets_file = files.upload()\n", 214 | "client_secrets_json = json.loads(next(iter(client_secrets_file.values())))\n", 215 | "\n", 216 | "flow = InstalledAppFlow.from_client_config(client_secrets_json, API_SCOPES)\n", 217 | "credentials = flow.run_console()\n", 218 | "print('Success!')\n", 219 | "\n", 220 | "# Build DBM Read API service object\n", 221 | "dbm_service = discovery.build(\n", 222 | " 'doubleclickbidmanager', 'v1.1', credentials=credentials)\n", 223 | "print('DBM API service object created')\n", 224 | "\n", 225 | "# Build Google Cloud Storage Read API service object\n", 226 | "gcs_service = discovery.build('storage', 'v1', credentials=credentials)\n", 227 | "print('GCS service object created')\n", 228 | "\n", 229 | "# Create Display Video API service object\n", 230 | "display_video_service = discovery.build(\n", 231 | " 'displayvideo', 'v1', credentials=credentials)\n", 232 | "\n", 233 | "print('Display Video API service object created')" 234 | ], 235 | "execution_count": null, 236 | "outputs": [] 237 | }, 238 | { 239 | "cell_type": "markdown", 240 | "metadata": { 241 | "id": "cWj6pqFUpt0w" 242 | }, 243 | "source": [ 244 | "## 0.4 Set DV360 account settings" 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "metadata": { 250 | "id": "k0K6yTfovoAY" 251 | }, 252 | "source": [ 253 | "Next, we need to set our DV360 parameters, and generate a sandbox (test) campaign.\n", 254 | "\n", 255 | "Note, if you'd prefer to use an existing campaign, update CAMPAIGN_ID below." 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "metadata": { 261 | "id": "N5Inh0JrwLuX", 262 | "cellView": "form" 263 | }, 264 | "source": [ 265 | "PARTNER_ID = '234340' #@param {type:\"string\"}\n", 266 | "ADVERTISER_ID = '2436036' #@param {type:\"string\"}\n", 267 | "CAMPAIGN_ID = '4258803' #@param {type:\"string\"}\n", 268 | "\n", 269 | "# For use with legacy DBM API\n", 270 | "SDF_VERSION = '5.3' #@param {type:\"string\"}\n", 271 | "\n", 272 | "# For use with DV360 API\n", 273 | "SDF_VERSION_DV360 = 'SDF_VERSION_5_3' #@param {type:\"string\"}\n", 274 | "\n", 275 | "print('DV360 settings saved!')" 276 | ], 277 | "execution_count": null, 278 | "outputs": [] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": { 283 | "id": "9ajx6SBWweh3" 284 | }, 285 | "source": [ 286 | "**Create a new 'sandbox' campaign to use with the rest of the exercises**\n", 287 | "\n", 288 | "Executing the following code block will overwrite any CAMPAIGN_ID used above." 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "metadata": { 294 | "id": "S3zwMWIQv7pl", 295 | "cellView": "form" 296 | }, 297 | "source": [ 298 | "YOUR_NAME = 'Matt' #@param {type:\"string\"}\n", 299 | "\n", 300 | "# Set dates for new campaign\n", 301 | "month = datetime.datetime.today().strftime('%m')\n", 302 | "day = datetime.datetime.today().strftime('%d')\n", 303 | "year = datetime.datetime.today().strftime('%Y')\n", 304 | "month_plus30 = (datetime.datetime.today() +\n", 305 | " datetime.timedelta(days=30)).strftime('%m')\n", 306 | "day_plus30 = (datetime.datetime.today() +\n", 307 | " datetime.timedelta(days=30)).strftime('%d')\n", 308 | "year_plus30 = (datetime.datetime.today() +\n", 309 | " datetime.timedelta(days=30)).strftime('%Y')\n", 310 | "\n", 311 | "\n", 312 | "def create_campaign(YOUR_NAME):\n", 313 | " \"\"\"Creates a new DV360 Campaign object.\"\"\"\n", 314 | "\n", 315 | " campaign_name = f'{year}-{month}-{day} | {YOUR_NAME}'\n", 316 | " campaign_obj = {\n", 317 | " 'displayName': campaign_name,\n", 318 | " 'entityStatus': 'ENTITY_STATUS_ACTIVE',\n", 319 | " 'campaignGoal': {\n", 320 | " 'campaignGoalType': 'CAMPAIGN_GOAL_TYPE_ONLINE_ACTION',\n", 321 | " 'performanceGoal': {\n", 322 | " 'performanceGoalType': 'PERFORMANCE_GOAL_TYPE_CPC',\n", 323 | " 'performanceGoalAmountMicros': 1000000\n", 324 | " }\n", 325 | " },\n", 326 | " 'campaignFlight': {\n", 327 | " 'plannedSpendAmountMicros': 1000000,\n", 328 | " 'plannedDates': {\n", 329 | " 'startDate': {\n", 330 | " 'year': year,\n", 331 | " 'month': month,\n", 332 | " 'day': day\n", 333 | " },\n", 334 | " 'endDate': {\n", 335 | " 'year': year_plus30,\n", 336 | " 'month': month_plus30,\n", 337 | " 'day': day_plus30\n", 338 | " }\n", 339 | " }\n", 340 | " },\n", 341 | " 'frequencyCap': {\n", 342 | " 'maxImpressions': 10,\n", 343 | " 'timeUnit': 'TIME_UNIT_DAYS',\n", 344 | " 'timeUnitCount': 1\n", 345 | " }\n", 346 | " }\n", 347 | "\n", 348 | " # Create the campaign.\n", 349 | " campaign = display_video_service.advertisers().campaigns().create(\n", 350 | " advertiserId=ADVERTISER_ID,\n", 351 | " body=campaign_obj\n", 352 | " ).execute()\n", 353 | "\n", 354 | " return campaign\n", 355 | "\n", 356 | "new_campaign = create_campaign(YOUR_NAME)\n", 357 | "\n", 358 | "# Display the new campaign.\n", 359 | "CAMPAIGN_ID = new_campaign['campaignId']\n", 360 | "print(f\"\\nCampaign '{new_campaign['name']}' was created.\"\n", 361 | " f\"\\nCampaign id: '{new_campaign['campaignId']}'\"\n", 362 | " f\"\\nCampaign name: '{new_campaign['displayName']}'\"\n", 363 | " f\"\\nCampaign status: '{new_campaign['entityStatus']}'\")" 364 | ], 365 | "execution_count": null, 366 | "outputs": [] 367 | }, 368 | { 369 | "cell_type": "markdown", 370 | "metadata": { 371 | "id": "rvbhSbEqIkvz" 372 | }, 373 | "source": [ 374 | "# 1**A**) SDF using DBM API (sunset)" 375 | ] 376 | }, 377 | { 378 | "cell_type": "markdown", 379 | "metadata": { 380 | "id": "gRBmSMnPBB0p" 381 | }, 382 | "source": [ 383 | "**Important**: the SDF resource (sdf.download) for the DBM API has migrated to a new endpoint (displayvideo.googleapis.com). SDF methods using this (doubleclickbidmanager.googleapis.com) endpoint have been [sunset](https://developers.google.com/bid-manager/release-notes), and will not be updated moving forward.\n", 384 | "\n", 385 | "**Please follow track 1B, for code samples using the DV360 API.**" 386 | ] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "metadata": { 391 | "id": "KqkQ32KWmH8T" 392 | }, 393 | "source": [ 394 | "Reference: https://developers.google.com/bid-manager/v1.1/sdf\n", 395 | "\n", 396 | "Structured Data Files (SDF) are a way of using spreadsheets to make bulk changes to DV360 entities, including Campaigns, Insertion Orders, Line Items, TrueView Ad Groups, TrueView Ads and deals. SDF are the first step on the path to full automation in DV360, but only allow you to automate so far, as we'll explore now..." 397 | ] 398 | }, 399 | { 400 | "cell_type": "markdown", 401 | "metadata": { 402 | "id": "k5XefbfWnZ_e" 403 | }, 404 | "source": [ 405 | "## 1.1 Manually create SDF" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "metadata": { 411 | "id": "B36X1Stqp_AR" 412 | }, 413 | "source": [ 414 | "1. Create a copy of the [Google Store product feed](https://docs.google.com/spreadsheets/d/1Z-hL3KY9ynmajME6kW3QYbiFqmlg9BFvdRqEYy3Rvs4/copy)\n", 415 | "2. Update the highlighted cells (B2:B3) on the tab called \"sdf_insertionorders\"\n", 416 | "3. Save the updated \"**sdf_insertionorders**\" tab and \"**sdf_lineitems**\" tab to .CSV (File >> Download >> CSV)\n", 417 | "3. Upload the two .CSV files together in the [DV360 UI](https://displayvideo.google.com/)\n", 418 | "\n", 419 | "This will create a very basic campaign, with 2 insertion orders, and 10 lineitems per insertion order." 420 | ] 421 | }, 422 | { 423 | "cell_type": "markdown", 424 | "metadata": { 425 | "id": "i7uq5m32nMAU" 426 | }, 427 | "source": [ 428 | "## 1.2 Editing SDF programmatically" 429 | ] 430 | }, 431 | { 432 | "cell_type": "markdown", 433 | "metadata": { 434 | "id": "I7rzAYIncWRx" 435 | }, 436 | "source": [ 437 | "Our new LineItems are missing some important targeting and inventory controls:\n", 438 | "* Channels (e.g. groups of publisher URLs)\n", 439 | "* Inventory source\n", 440 | "* Brand safety\n", 441 | "* Geo targeting\n", 442 | "\n", 443 | "Let’s use software to make these changes for us..." 444 | ] 445 | }, 446 | { 447 | "cell_type": "code", 448 | "metadata": { 449 | "id": "HSuqlMTNIYxs" 450 | }, 451 | "source": [ 452 | "# Configure the sdf.download request\n", 453 | "request_body = {\n", 454 | " 'fileTypes': ['LINE_ITEM'],\n", 455 | " 'filterType': 'CAMPAIGN_ID',\n", 456 | " 'filterIds': [CAMPAIGN_ID],\n", 457 | " 'version': SDF_VERSION\n", 458 | "}\n", 459 | "\n", 460 | "# Make the request to download all SDF LineItems for your new campaign\n", 461 | "request = dbm_service.sdf().download(body=request_body)\n", 462 | "response = request.execute()\n", 463 | "\n", 464 | "# Load SDF response to Pandas DataFrame\n", 465 | "sdf_df = pd.read_csv(io.StringIO(response['lineItems']))\n", 466 | "\n", 467 | "# Show sample (5 rows) of DataFrame\n", 468 | "sdf_df.head()" 469 | ], 470 | "execution_count": null, 471 | "outputs": [] 472 | }, 473 | { 474 | "cell_type": "markdown", 475 | "metadata": { 476 | "id": "JU7gy0kxceCH" 477 | }, 478 | "source": [ 479 | "Define a boilerplate targeting template that all Line Items should adhere too" 480 | ] 481 | }, 482 | { 483 | "cell_type": "code", 484 | "metadata": { 485 | "id": "KWzYviqecdGo" 486 | }, 487 | "source": [ 488 | "targeting_template = {\n", 489 | " 'Channel Targeting - Include':\n", 490 | " '2580510;',\n", 491 | " 'Channel Targeting - Exclude':\n", 492 | " '2580509;',\n", 493 | " 'Inventory Source Targeting - Include':\n", 494 | " '1;',\n", 495 | " 'Inventory Source Targeting - Exclude':\n", 496 | " '6; 8; 9; 10; 2; 11; 12; 13; 16; 20; 23; 27; 29; 30; 31; 34; 35; 36; '\n", 497 | " '38; 43; 46; 50; 51; 56; 60; 63; 67; 74;',\n", 498 | " 'Digital Content Labels - Exclude':\n", 499 | " 'G; PG; T;',\n", 500 | " 'Brand Safety Sensitivity Setting':\n", 501 | " 'Use custom',\n", 502 | " 'Brand Safety Custom Settings':\n", 503 | " 'Adult; Alcohol; Derogatory; Downloads & Sharing; Drugs; Gambling; '\n", 504 | " 'Politics; Profanity; Religion; Sensitive social issues; Suggestive; '\n", 505 | " 'Tobacco; Tragedy; Transportation Accidents; Violence; Weapons;'\n", 506 | "}" 507 | ], 508 | "execution_count": null, 509 | "outputs": [] 510 | }, 511 | { 512 | "cell_type": "markdown", 513 | "metadata": { 514 | "id": "BBnq6nyic0I0" 515 | }, 516 | "source": [ 517 | "Modify latest SDF LineItems file and update the columns according to the targeting template" 518 | ] 519 | }, 520 | { 521 | "cell_type": "code", 522 | "metadata": { 523 | "id": "VEcwGXl3aglI" 524 | }, 525 | "source": [ 526 | "# Overwrite targeting columns using 'targeting_template'\n", 527 | "sdf_df['Channel Targeting - Include'] = targeting_template[\n", 528 | " 'Channel Targeting - Include']\n", 529 | "sdf_df['Channel Targeting - Exclude'] = targeting_template[\n", 530 | " 'Channel Targeting - Exclude']\n", 531 | "sdf_df['Inventory Source Targeting - Include'] = targeting_template[\n", 532 | " 'Inventory Source Targeting - Include']\n", 533 | "sdf_df['Inventory Source Targeting - Exclude'] = targeting_template[\n", 534 | " 'Inventory Source Targeting - Exclude']\n", 535 | "sdf_df['Digital Content Labels - Exclude'] = targeting_template[\n", 536 | " 'Digital Content Labels - Exclude']\n", 537 | "sdf_df['Brand Safety Sensitivity Setting'] = targeting_template[\n", 538 | " 'Brand Safety Sensitivity Setting']\n", 539 | "sdf_df['Brand Safety Custom Settings'] = targeting_template[\n", 540 | " 'Brand Safety Custom Settings']\n", 541 | "\n", 542 | "# Save modified dataframe to remote storage in Colab\n", 543 | "sdf_df.to_csv('sdf_update1_controls.csv', index=False)\n", 544 | "\n", 545 | "# Show sample (5 rows) of DataFrame\n", 546 | "sdf_df.head()" 547 | ], 548 | "execution_count": null, 549 | "outputs": [] 550 | }, 551 | { 552 | "cell_type": "code", 553 | "metadata": { 554 | "id": "zkAUGWXBC9RD" 555 | }, 556 | "source": [ 557 | "# Download modified csv to local storage\n", 558 | "files.download('sdf_update1_controls.csv')\n", 559 | "\n", 560 | "print(\n", 561 | " \"Success, check your downloads for a file called 'sdf_update1_controls.csv'\"\n", 562 | ")" 563 | ], 564 | "execution_count": null, 565 | "outputs": [] 566 | }, 567 | { 568 | "cell_type": "markdown", 569 | "metadata": { 570 | "id": "Ppq8TCiUj3Re" 571 | }, 572 | "source": [ 573 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 574 | "\n", 575 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 576 | ] 577 | }, 578 | { 579 | "cell_type": "markdown", 580 | "metadata": { 581 | "id": "V6AoRF2qXZDX" 582 | }, 583 | "source": [ 584 | "## 1.3 SDF + Entity Read Files\n", 585 | "\n", 586 | "**What are Entity Read Files (ERFs)?** ERFs are flat files (.JSON) in Google Cloud Storage that contain lookup values for DV360 entities like geographies, creatives, etc. Each DV360 entity (Advertiser, Campaign, LineItem, etc) has a corresponding .JSON file in Cloud Storage retained free-of-charge for 60 days from their processing date.\n", 587 | "\n", 588 | "ERFs consist of 1 file per entity type, written x1 per day to two seperate Cloud buckets: \n", 589 | "\n", 590 | "1. **[Public](https://developers.google.com/bid-manager/guides/entity-read/overview#public-tables)** (10 .JSON files) - contain common public data such as GeoLocation and Language which are stored in the gdbm-public bucket (the same bucket for every DV360 user).\n", 591 | "2. **[Private](https://developers.google.com/bid-manager/guides/entity-read/overview#private-tables)** (13 .JSON files) - contain information about the DV360 Partner's campaigns, creatives, budgets and other private data and are stored in Partner-specific buckets (restricted to specific users)\n", 592 | "\n", 593 | "Reference: https://developers.google.com/bid-manager/guides/entity-read/overview\n", 594 | "\n", 595 | "ERFs can be used to speed up, and automate, the creation of SDF files.\n", 596 | "\n", 597 | "Let's explore this now..." 598 | ] 599 | }, 600 | { 601 | "cell_type": "markdown", 602 | "metadata": { 603 | "id": "2FU9srN57G1a" 604 | }, 605 | "source": [ 606 | "Download yesterday's GeoLocation.json from [public ERF bucket](https://console.cloud.google.com/storage/browser/gdbm-public/entity/) using Google Cloud Storage API" 607 | ] 608 | }, 609 | { 610 | "cell_type": "code", 611 | "metadata": { 612 | "id": "5JUHJBajXT_E" 613 | }, 614 | "source": [ 615 | "# Actually today-7 to avoid issues with collection\n", 616 | "yesterday = datetime.date.today() - datetime.timedelta(7)\n", 617 | "\n", 618 | "# Download public ERF for geolocation info\n", 619 | "request = gcs_service.objects().get_media(\n", 620 | " bucket='gdbm-public',\n", 621 | " object='entity/' + yesterday.strftime('%Y%m%d') + '.0.GeoLocation.json')\n", 622 | "\n", 623 | "response = request.execute()\n", 624 | "geolocations = json.loads(response)\n", 625 | "\n", 626 | "print('GeoLocation.json successfully downloaded \\n')\n", 627 | "print(\"Here's a random sample of 5 entries:\\n\")\n", 628 | "pprint.pprint(geolocations[0:5])" 629 | ], 630 | "execution_count": null, 631 | "outputs": [] 632 | }, 633 | { 634 | "cell_type": "markdown", 635 | "metadata": { 636 | "id": "_-eQF4OWmjFi" 637 | }, 638 | "source": [ 639 | "Retrieve a list of country codes / IDs from GeoLocation.json for each of our store locations" 640 | ] 641 | }, 642 | { 643 | "cell_type": "code", 644 | "metadata": { 645 | "id": "Q-83_ZFZZE36" 646 | }, 647 | "source": [ 648 | "# Provide a list of store locations\n", 649 | "store_locations = ['United Kingdom', 'France', 'Spain', 'Germany', 'Portugal']\n", 650 | "\n", 651 | "# Create a new dictionary to save the country code and ID later on\n", 652 | "geo_targeting_ids = {}\n", 653 | "\n", 654 | "# Note: GeoLocation.json is over 800,000 lines\n", 655 | "for location in geolocations:\n", 656 | " if location['canonical_name'] in store_locations:\n", 657 | " geo_targeting_ids[location['country_code']] = location['id']\n", 658 | " print(location)\n", 659 | "\n", 660 | "print(geo_targeting_ids)" 661 | ], 662 | "execution_count": null, 663 | "outputs": [] 664 | }, 665 | { 666 | "cell_type": "markdown", 667 | "metadata": { 668 | "id": "RpwP14QNfLZJ" 669 | }, 670 | "source": [ 671 | "Download the latest SDF LineItems (because we've made changes since our last download)" 672 | ] 673 | }, 674 | { 675 | "cell_type": "code", 676 | "metadata": { 677 | "id": "KveWvKKbmAxC" 678 | }, 679 | "source": [ 680 | "# Configure the sdf.download request\n", 681 | "request_body = {\n", 682 | " 'fileTypes': ['LINE_ITEM'],\n", 683 | " 'filterType': 'CAMPAIGN_ID',\n", 684 | " 'filterIds': [CAMPAIGN_ID],\n", 685 | " 'version': SDF_VERSION\n", 686 | "}\n", 687 | "\n", 688 | "# Make the request to download all SDF LineItems for your new campaign\n", 689 | "request = dbm_service.sdf().download(body=request_body)\n", 690 | "response = request.execute()\n", 691 | "\n", 692 | "# Load SDF response to Pandas DataFrame\n", 693 | "sdf_df = pd.read_csv(io.StringIO(response['lineItems']))\n", 694 | "\n", 695 | "# Show sample (5 rows) of DataFrame\n", 696 | "sdf_df.head()" 697 | ], 698 | "execution_count": null, 699 | "outputs": [] 700 | }, 701 | { 702 | "cell_type": "markdown", 703 | "metadata": { 704 | "id": "nYRZX4sv9XX3" 705 | }, 706 | "source": [ 707 | "Modify the contents of the latest SDF output, then save a new CSV with updated Geo Targeting IDs" 708 | ] 709 | }, 710 | { 711 | "cell_type": "code", 712 | "metadata": { 713 | "id": "Kv18CgWm79WH" 714 | }, 715 | "source": [ 716 | "for country in geo_targeting_ids:\n", 717 | " target_country = geo_targeting_ids[country]\n", 718 | " sdf_df.loc[sdf_df.Name.str.contains(country),\n", 719 | " 'Geography Targeting - Include'] = f'{target_country};'\n", 720 | "\n", 721 | "# Save modified dataframe to remote storage in Colab\n", 722 | "sdf_df.to_csv('sdf_update2_geo.csv', index=False)\n", 723 | "\n", 724 | "# Display updated DataFrame\n", 725 | "sdf_df.head()" 726 | ], 727 | "execution_count": null, 728 | "outputs": [] 729 | }, 730 | { 731 | "cell_type": "code", 732 | "metadata": { 733 | "id": "VIyiy_8yDpML" 734 | }, 735 | "source": [ 736 | "# Download modified csv to local storage\n", 737 | "files.download('sdf_update2_geo.csv')\n", 738 | "\n", 739 | "print(\"Success, look for a file called 'sdf_update2_geo.csv' in your downloads folder\")" 740 | ], 741 | "execution_count": null, 742 | "outputs": [] 743 | }, 744 | { 745 | "cell_type": "markdown", 746 | "metadata": { 747 | "id": "QPPaW8pw7vjU" 748 | }, 749 | "source": [ 750 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 751 | "\n", 752 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 753 | ] 754 | }, 755 | { 756 | "cell_type": "markdown", 757 | "metadata": { 758 | "id": "hwgK-0aOoRBt" 759 | }, 760 | "source": [ 761 | "## 1.4 SDF + Cloud Vision API" 762 | ] 763 | }, 764 | { 765 | "cell_type": "markdown", 766 | "metadata": { 767 | "id": "e2LE4Ovu8O4N" 768 | }, 769 | "source": [ 770 | "Next, let's look at how we you can utilise external APIs.\n", 771 | "\n", 772 | "1. Download the '[product_feed](https://docs.google.com/spreadsheets/d/1Z-hL3KY9ynmajME6kW3QYbiFqmlg9BFvdRqEYy3Rvs4/edit#gid=0)' tab from Google Store as CSV (File >> Download >> CSV)\n", 773 | "2. Execute the following code block and upload '**product_feed.csv**'\n", 774 | "3. This will create a new Python dictionary (key:value pairing), mapping SKUs with their image link\n", 775 | "\n", 776 | "**Warning: Cloud Vision API is paid product, utilising the following example in your own Cloud project will incur costs.**\n", 777 | "\n", 778 | "Try out the Cloud Vision API for free at [cloud.google.com/vision](https://cloud.google.com/vision/)" 779 | ] 780 | }, 781 | { 782 | "cell_type": "code", 783 | "metadata": { 784 | "id": "ldOZQ_ZqXqRO" 785 | }, 786 | "source": [ 787 | "# Upload product feed using Colab's upload utility\n", 788 | "product_feed_csv = files.upload()\n", 789 | "\n", 790 | "contents = next(iter(product_feed_csv.values())).decode('utf-8')\n", 791 | "products = csv.DictReader(io.StringIO(contents))\n", 792 | "\n", 793 | "image_url_list = {}\n", 794 | "\n", 795 | "# Iterate through each row and update dict() with sku:link\n", 796 | "for row in products:\n", 797 | " image_url_list[row['sku']] = row['image_link']\n", 798 | "\n", 799 | "pprint.pprint(image_url_list)" 800 | ], 801 | "execution_count": null, 802 | "outputs": [] 803 | }, 804 | { 805 | "cell_type": "markdown", 806 | "metadata": { 807 | "id": "mNjm7kU4W6oI" 808 | }, 809 | "source": [ 810 | "Define a function to send images to the Cloud Vision API" 811 | ] 812 | }, 813 | { 814 | "cell_type": "code", 815 | "metadata": { 816 | "id": "KNkhcjpOW5lD" 817 | }, 818 | "source": [ 819 | "def vision_analysis(image_url):\n", 820 | " \"\"\"Process images using the Cloud Vision API.\"\"\"\n", 821 | "\n", 822 | " # Assign image URL\n", 823 | " image = vision.Image()\n", 824 | " image.source.image_uri = image_url\n", 825 | "\n", 826 | " # Instantiates a Vision client\n", 827 | " client = vision.ImageAnnotatorClient(credentials=credentials)\n", 828 | "\n", 829 | " # Performs label detection on the image file\n", 830 | " vision_response = client.label_detection(image=image)\n", 831 | "\n", 832 | " dv360_targeting_keywords = []\n", 833 | " labels = []\n", 834 | "\n", 835 | " for label in vision_response.label_annotations:\n", 836 | " dv360_targeting_keywords.append(label.description)\n", 837 | " label = f'{label.description} ({label.score:.2%})'\n", 838 | " labels.append(label)\n", 839 | "\n", 840 | " return dv360_targeting_keywords, labels" 841 | ], 842 | "execution_count": null, 843 | "outputs": [] 844 | }, 845 | { 846 | "cell_type": "markdown", 847 | "metadata": { 848 | "id": "2UsWhANfXCRj" 849 | }, 850 | "source": [ 851 | "Run our images through the function, and return a lookup table" 852 | ] 853 | }, 854 | { 855 | "cell_type": "code", 856 | "metadata": { 857 | "id": "bgTm5NZINzmU" 858 | }, 859 | "source": [ 860 | "imageslookup = {}\n", 861 | "\n", 862 | "for sku, url in image_url_list.items():\n", 863 | " imageslookup[sku], vision_labels = vision_analysis(url)\n", 864 | " print(f'Analysis completed for: {url}')\n", 865 | " print('Labels (confidence score):')\n", 866 | " pprint.pprint(vision_labels, indent=4)\n", 867 | " print('=' * 30)\n", 868 | "\n", 869 | "print('\\n\\nLookup table:')\n", 870 | "pprint.pprint(imageslookup, indent=4)" 871 | ], 872 | "execution_count": null, 873 | "outputs": [] 874 | }, 875 | { 876 | "cell_type": "markdown", 877 | "metadata": { 878 | "id": "ykTWQbBmvcep" 879 | }, 880 | "source": [ 881 | "Now we have our new labels from the Vision API, we need to write these into the keywords targeting field" 882 | ] 883 | }, 884 | { 885 | "cell_type": "code", 886 | "metadata": { 887 | "id": "C7IhSHjoTtjG" 888 | }, 889 | "source": [ 890 | "# Configure the sdf.download request\n", 891 | "request_body = {\n", 892 | " 'fileTypes': ['LINE_ITEM'],\n", 893 | " 'filterType': 'CAMPAIGN_ID',\n", 894 | " 'filterIds': [CAMPAIGN_ID],\n", 895 | " 'version': SDF_VERSION\n", 896 | "}\n", 897 | "\n", 898 | "request = dbm_service.sdf().download(body=request_body)\n", 899 | "response = request.execute()\n", 900 | "\n", 901 | "# Load SDF response to Pandas DataFrame\n", 902 | "sdf_df = pd.read_csv(io.StringIO(response['lineItems']))\n", 903 | "\n", 904 | "for product in imageslookup:\n", 905 | " sdf_df.loc[sdf_df.Name.str.contains(product),\n", 906 | " 'Keyword Targeting - Include'] = ';'.join(\n", 907 | " imageslookup[product]).lower()\n", 908 | "\n", 909 | "# Save modified dataframe to remote storage in Colab\n", 910 | "sdf_df.to_csv('sdf_update3_keywords.csv', index=False)\n", 911 | "\n", 912 | "# Show sample (5 rows) of DataFrame\n", 913 | "sdf_df.head()" 914 | ], 915 | "execution_count": null, 916 | "outputs": [] 917 | }, 918 | { 919 | "cell_type": "code", 920 | "metadata": { 921 | "id": "zRGmf195EX6S" 922 | }, 923 | "source": [ 924 | "# Download modified csv to local storage\n", 925 | "files.download('sdf_update3_keywords.csv')\n", 926 | "\n", 927 | "print(\"Success, look for the file called 'sdf_update3_keywords.csv' in your downloads folder\")" 928 | ], 929 | "execution_count": null, 930 | "outputs": [] 931 | }, 932 | { 933 | "cell_type": "markdown", 934 | "metadata": { 935 | "id": "jcEp8PQxjfqL" 936 | }, 937 | "source": [ 938 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 939 | "\n", 940 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 941 | ] 942 | }, 943 | { 944 | "cell_type": "markdown", 945 | "metadata": { 946 | "id": "te8f1dfnguwP" 947 | }, 948 | "source": [ 949 | "## 1.5 Optimisation using Reports" 950 | ] 951 | }, 952 | { 953 | "cell_type": "markdown", 954 | "metadata": { 955 | "id": "_ZgDBsFrpR5e" 956 | }, 957 | "source": [ 958 | "Next, we'll look at how you could combine reporting data, with operations such as optimising bid multipliers or deactivating activity. **Note: your new campaign has no performance history, so we'll use an existing campaign for this exercise.**" 959 | ] 960 | }, 961 | { 962 | "cell_type": "code", 963 | "metadata": { 964 | "id": "PI88YAdY4X0L" 965 | }, 966 | "source": [ 967 | "# Define DV360 report definition (i.e. metrics and filters)\n", 968 | "report_definition = {\n", 969 | " 'params': {\n", 970 | " 'type': 'TYPE_GENERAL',\n", 971 | " 'metrics': [\n", 972 | " 'METRIC_IMPRESSIONS', 'METRIC_CLICKS', 'METRIC_CTR',\n", 973 | " 'METRIC_REVENUE_ADVERTISER'\n", 974 | " ],\n", 975 | " 'groupBys': [\n", 976 | " 'FILTER_ADVERTISER', 'FILTER_INSERTION_ORDER', 'FILTER_LINE_ITEM',\n", 977 | " 'FILTER_ADVERTISER_CURRENCY'\n", 978 | " ],\n", 979 | " 'filters': [{\n", 980 | " 'type': 'FILTER_ADVERTISER',\n", 981 | " 'value': ADVERTISER_ID\n", 982 | " }],\n", 983 | " },\n", 984 | " 'metadata': {\n", 985 | " 'title': 'DV360 Automation API-generated report',\n", 986 | " 'dataRange': 'LAST_90_DAYS',\n", 987 | " 'format': 'csv'\n", 988 | " },\n", 989 | " 'schedule': {\n", 990 | " 'frequency': 'ONE_TIME'\n", 991 | " }\n", 992 | "}\n", 993 | "\n", 994 | "# Create new query using report definition\n", 995 | "operation = dbm_service.queries().createquery(body=report_definition).execute()\n", 996 | "pprint.pprint(operation)\n", 997 | "\n", 998 | "\n", 999 | "# Runs the given Queries.getquery request, retrying with an exponential\n", 1000 | "# backoff. Returns completed operation. Will raise an exception if the\n", 1001 | "# operation takes more than five hours to complete.\n", 1002 | "@retry.Retry(\n", 1003 | " predicate=retry.if_exception_type(Exception),\n", 1004 | " initial=5,\n", 1005 | " maximum=60,\n", 1006 | " deadline=18000)\n", 1007 | "def check_get_query_completion(getquery_request):\n", 1008 | " \"\"\"Queries metadata to check for completion.\"\"\"\n", 1009 | " completion_response = getquery_request.execute()\n", 1010 | " pprint.pprint(completion_response)\n", 1011 | " if completion_response['metadata']['running']:\n", 1012 | " raise Exception('The operation has not completed.')\n", 1013 | " return completion_response\n", 1014 | "\n", 1015 | "getquery_request = dbm_service.queries().getquery(queryId=operation['queryId'])\n", 1016 | "getquery_response = check_get_query_completion(getquery_request)" 1017 | ], 1018 | "execution_count": null, 1019 | "outputs": [] 1020 | }, 1021 | { 1022 | "cell_type": "code", 1023 | "metadata": { 1024 | "id": "pWo5y2q5oL2O" 1025 | }, 1026 | "source": [ 1027 | "report_url = getquery_response['metadata'][\n", 1028 | " 'googleCloudStoragePathForLatestReport']\n", 1029 | "\n", 1030 | "# Use skipfooter to remove report footer from data\n", 1031 | "report_df = pd.read_csv(report_url, skipfooter=16, engine='python')\n", 1032 | "report_df.head(10)" 1033 | ], 1034 | "execution_count": null, 1035 | "outputs": [] 1036 | }, 1037 | { 1038 | "cell_type": "code", 1039 | "metadata": { 1040 | "id": "iNhekXWm_fI1" 1041 | }, 1042 | "source": [ 1043 | "# Define our 'KPIs'\n", 1044 | "ctr_target = 0.15\n", 1045 | "imp_threshold = 10000\n", 1046 | "\n", 1047 | "# Convert IDs to remove decimal point, then string\n", 1048 | "report_df['Line Item ID'] = report_df['Line Item ID'].apply(int)\n", 1049 | "poor_performers = report_df.query(\n", 1050 | " 'Impressions > @imp_threshold & (Clicks / Impressions)*100 < @ctr_target')\n", 1051 | "\n", 1052 | "# Convert results to Python list\n", 1053 | "poor_performers = list(poor_performers['Line Item ID'])\n", 1054 | "\n", 1055 | "print(f'There are {len(poor_performers)} LineItems with a CTR'\n", 1056 | " f' < {ctr_target}% and over {imp_threshold} impressions:'\n", 1057 | " f'\\n{poor_performers}')" 1058 | ], 1059 | "execution_count": null, 1060 | "outputs": [] 1061 | }, 1062 | { 1063 | "cell_type": "markdown", 1064 | "metadata": { 1065 | "id": "O7yB9RGqwh7G" 1066 | }, 1067 | "source": [ 1068 | "Download an updated SDF LineItems file, and if the LineItem ID is in the poor performers list, add a **Geo bid multiplier to half the bids (0.5)**" 1069 | ] 1070 | }, 1071 | { 1072 | "cell_type": "code", 1073 | "metadata": { 1074 | "id": "Q27InRnPg0Ux" 1075 | }, 1076 | "source": [ 1077 | "# Configure the sdf.download request\n", 1078 | "request_body = {\n", 1079 | " 'fileTypes': ['LINE_ITEM'],\n", 1080 | " 'filterType': 'CAMPAIGN_ID',\n", 1081 | " 'filterIds': ['1914007'],\n", 1082 | " 'version': SDF_VERSION\n", 1083 | "}\n", 1084 | "\n", 1085 | "request = dbm_service.sdf().download(body=request_body)\n", 1086 | "response = request.execute()\n", 1087 | "\n", 1088 | "# Load SDF response to Pandas DataFrame\n", 1089 | "sdf_df = pd.read_csv(io.StringIO(response['lineItems']))\n", 1090 | "\n", 1091 | "for li in poor_performers:\n", 1092 | " geo = sdf_df.loc[sdf_df['Line Item Id'] == li,\n", 1093 | " 'Geography Targeting - Include'].iloc[0]\n", 1094 | " sdf_df.loc[sdf_df['Line Item Id'] == li,\n", 1095 | " 'Bid Multipliers'] = f'(geo; {geo} 0.5;);'\n", 1096 | "\n", 1097 | "# Save modified dataframe to remote storage in Colab\n", 1098 | "sdf_df.to_csv('sdf_update4_bidmultipliers.csv', index=False)\n", 1099 | "\n", 1100 | "# Display updated DataFrame\n", 1101 | "sdf_df.head()" 1102 | ], 1103 | "execution_count": null, 1104 | "outputs": [] 1105 | }, 1106 | { 1107 | "cell_type": "code", 1108 | "metadata": { 1109 | "id": "3CtCZFnclCjF" 1110 | }, 1111 | "source": [ 1112 | "files.download('sdf_update4_bidmultipliers.csv')\n", 1113 | "\n", 1114 | "print('Success, your new SDF file has been downloaded')" 1115 | ], 1116 | "execution_count": null, 1117 | "outputs": [] 1118 | }, 1119 | { 1120 | "cell_type": "markdown", 1121 | "metadata": { 1122 | "id": "yCedNSYNxBmo" 1123 | }, 1124 | "source": [ 1125 | "Note the only rows included in the output, are those that we want to modify.\n", 1126 | "\n", 1127 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 1128 | "\n", 1129 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 1130 | ] 1131 | }, 1132 | { 1133 | "cell_type": "markdown", 1134 | "metadata": { 1135 | "id": "ZKxO-essKw2X" 1136 | }, 1137 | "source": [ 1138 | "## 1.6 Challenge" 1139 | ] 1140 | }, 1141 | { 1142 | "cell_type": "markdown", 1143 | "metadata": { 1144 | "id": "IdqS3dt5bEkG" 1145 | }, 1146 | "source": [ 1147 | "**Challenge: update your campaign with both language and audience targeting.**\n", 1148 | "\n", 1149 | "* All Lineitems should target the following Google audiences\n", 1150 | "\n", 1151 | " * Affinity Categories » Technology » Mobile Enthusiasts\n", 1152 | " * Affinity Categories » Technology » Technophiles » High-End Computer Aficionado\n", 1153 | " * In-Market Categories » Consumer Electronics\n", 1154 | "\n", 1155 | "* LineItems for France, should be targeted at French speakers\n", 1156 | "* LineItems for Great Britain, should be targeted at English speakers\n", 1157 | "\n", 1158 | "**Tips**\n", 1159 | "\n", 1160 | "* Google Audience IDs can be found in the DV360 UI or by downloading an SDF with an existing audience applied\n", 1161 | "* Language IDs can be found in the Language.json ERF file or by downloading an SDF with the language already applied" 1162 | ] 1163 | }, 1164 | { 1165 | "cell_type": "code", 1166 | "metadata": { 1167 | "id": "sojF69yu_lep" 1168 | }, 1169 | "source": [ 1170 | "#TODO" 1171 | ], 1172 | "execution_count": null, 1173 | "outputs": [] 1174 | }, 1175 | { 1176 | "cell_type": "markdown", 1177 | "metadata": { 1178 | "id": "6pKHsIyz_I0h" 1179 | }, 1180 | "source": [ 1181 | "**Solution**" 1182 | ] 1183 | }, 1184 | { 1185 | "cell_type": "code", 1186 | "metadata": { 1187 | "id": "Etn7jthhcZbd" 1188 | }, 1189 | "source": [ 1190 | "# Format today-2 in required date format\n", 1191 | "yesterday = (datetime.date.today() - datetime.timedelta(2)).strftime('%Y%m%d')\n", 1192 | "\n", 1193 | "# Download ERF for Language.json from public GCS bucket\n", 1194 | "request = gcs_service.objects().get_media(\n", 1195 | " bucket='gdbm-public', object='entity/' + yesterday + '.0.Language.json')\n", 1196 | "\n", 1197 | "response = request.execute()\n", 1198 | "languages = json.loads(response)\n", 1199 | "\n", 1200 | "language_targets = ['en', 'fr']\n", 1201 | "lang_targeting_ids = {}\n", 1202 | "\n", 1203 | "# Search language.json for language targets 'en' and 'fr'\n", 1204 | "for lang in languages:\n", 1205 | " if lang['code'] in language_targets:\n", 1206 | " lang_targeting_ids[lang['code']] = lang['id']\n", 1207 | " print(lang)\n", 1208 | "\n", 1209 | "print(lang_targeting_ids)\n", 1210 | "\n", 1211 | "# Define targeting template\n", 1212 | "targeting_template = {\n", 1213 | " 'Affinity & In Market Targeting - Include': '4569529;4586809;4497529;',\n", 1214 | "}\n", 1215 | "\n", 1216 | "# Configure the sdf.download request\n", 1217 | "request_body = {\n", 1218 | " 'fileTypes': ['LINE_ITEM'],\n", 1219 | " 'filterType': 'CAMPAIGN_ID',\n", 1220 | " 'filterIds': [CAMPAIGN_ID],\n", 1221 | " 'version': SDF_VERSION\n", 1222 | "}\n", 1223 | "\n", 1224 | "request = dbm_service.sdf().download(body=request_body)\n", 1225 | "response = request.execute()\n", 1226 | "\n", 1227 | "# Load SDF response to Pandas DataFrame\n", 1228 | "sdf_df = pd.read_csv(io.StringIO(response['lineItems']))\n", 1229 | "\n", 1230 | "# Update DataFrame with Language and Audience targeting\n", 1231 | "sdf_df.loc[sdf_df.Name.str.contains('GB'),\n", 1232 | " 'Language Targeting - Include'] = f\"{lang_targeting_ids['en']};\"\n", 1233 | "sdf_df.loc[sdf_df.Name.str.contains('FR'),\n", 1234 | " 'Language Targeting - Include'] = f\"{lang_targeting_ids['fr']};\"\n", 1235 | "sdf_df['Affinity & In Market Targeting - Include'] = targeting_template[\n", 1236 | " 'Affinity & In Market Targeting - Include']\n", 1237 | "\n", 1238 | "# Save modified dataframe to remote storage in Colab\n", 1239 | "sdf_df.to_csv('sdf_update5_challenge.csv', index=False)\n", 1240 | "\n", 1241 | "# Display updated DataFrame\n", 1242 | "sdf_df.head()" 1243 | ], 1244 | "execution_count": null, 1245 | "outputs": [] 1246 | }, 1247 | { 1248 | "cell_type": "code", 1249 | "metadata": { 1250 | "id": "DCDckj4GEkUa" 1251 | }, 1252 | "source": [ 1253 | "# Download file to disk using Colab syntax\n", 1254 | "files.download('sdf_update5_challenge.csv')\n", 1255 | "\n", 1256 | "print(\"Success, check your downloads for a file called 'sdf_update5_challenge.csv'\")" 1257 | ], 1258 | "execution_count": null, 1259 | "outputs": [] 1260 | }, 1261 | { 1262 | "cell_type": "markdown", 1263 | "metadata": { 1264 | "id": "uPUEEyfTv4k8" 1265 | }, 1266 | "source": [ 1267 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)" 1268 | ] 1269 | }, 1270 | { 1271 | "cell_type": "markdown", 1272 | "metadata": { 1273 | "id": "gtB1gFw57vVj" 1274 | }, 1275 | "source": [ 1276 | "# 1**B**) SDF using DV360 API" 1277 | ] 1278 | }, 1279 | { 1280 | "cell_type": "markdown", 1281 | "metadata": { 1282 | "id": "ObFT1NV8nNci" 1283 | }, 1284 | "source": [ 1285 | "Reference: https://developers.google.com/display-video/api/reference/rest/v1/sdfdownloadtasks/create\n", 1286 | "\n", 1287 | "Structured Data Files (SDF) are a way of using spreadsheets to make bulk changes to DV360 entities, including Campaigns, Insertion Orders, Line Items, TrueView Ad Groups, TrueView Ads and deals. SDF are the first step on the path to full automation in DV360, but only allow you to automate so far, as we'll explore now..." 1288 | ] 1289 | }, 1290 | { 1291 | "cell_type": "markdown", 1292 | "metadata": { 1293 | "id": "CBS6enmb7vVk" 1294 | }, 1295 | "source": [ 1296 | "## 1.1 Manually create SDF" 1297 | ] 1298 | }, 1299 | { 1300 | "cell_type": "markdown", 1301 | "metadata": { 1302 | "id": "lXAQWcG-7vVk" 1303 | }, 1304 | "source": [ 1305 | "1. Create a copy of the [Google Store product feed](https://docs.google.com/spreadsheets/d/1Z-hL3KY9ynmajME6kW3QYbiFqmlg9BFvdRqEYy3Rvs4/copy)\n", 1306 | "2. Update the highlighted cells (B2:B3) on the tab called \"sdf_insertionorders\"\n", 1307 | "3. Save the updated \"**sdf_insertionorders**\" tab and \"**sdf_lineitems**\" tab to .CSV (File >> Download >> CSV)\n", 1308 | "3. Upload the two .CSV files together in the [DV360 UI](https://displayvideo.google.com/)\n", 1309 | "\n", 1310 | "This will create a very basic campaign, with 2 insertion orders, and 10 lineitems per insertion order." 1311 | ] 1312 | }, 1313 | { 1314 | "cell_type": "markdown", 1315 | "metadata": { 1316 | "id": "JggrDagX7vVl" 1317 | }, 1318 | "source": [ 1319 | "## 1.2 Editing SDF programmatically" 1320 | ] 1321 | }, 1322 | { 1323 | "cell_type": "markdown", 1324 | "metadata": { 1325 | "id": "WEGDvl4W7vVl" 1326 | }, 1327 | "source": [ 1328 | "Our new LineItems are missing some important targeting and inventory controls:\n", 1329 | "* Channels (e.g. groups of publisher URLs)\n", 1330 | "* Inventory source\n", 1331 | "* Brand safety\n", 1332 | "* Geo targeting\n", 1333 | "\n", 1334 | "Let’s use software to make these changes for us..." 1335 | ] 1336 | }, 1337 | { 1338 | "cell_type": "markdown", 1339 | "metadata": { 1340 | "id": "HiYmlpZADIa_" 1341 | }, 1342 | "source": [ 1343 | "**Create a function to download SDFs**\n", 1344 | "\n", 1345 | "As we'll be downloading multiple SDF files in the next exercises, we've created a function to handle to the download process for us." 1346 | ] 1347 | }, 1348 | { 1349 | "cell_type": "code", 1350 | "metadata": { 1351 | "id": "RNmABxen7vVn" 1352 | }, 1353 | "source": [ 1354 | "def download_sdf(request_body):\n", 1355 | " \"\"\"Download sdf .zip, extract .csv files, load 'SDF-LineItems.csv' to Pandas DataFrame.\"\"\"\n", 1356 | "\n", 1357 | " # Create the sdfdownloadtask\n", 1358 | " sdf_operation = display_video_service.sdfdownloadtasks().create(\n", 1359 | " body=sdf_body).execute()\n", 1360 | "\n", 1361 | " print(f'Operation {sdf_operation[\"name\"]} was created.')\n", 1362 | "\n", 1363 | " # Configure the operations.get request\n", 1364 | " get_request = display_video_service.sdfdownloadtasks().operations().get(\n", 1365 | " name=sdf_operation['name'])\n", 1366 | "\n", 1367 | " # Runs the given operations.get request, retrying with an exponential\n", 1368 | " # backoff. Returns completed operation. Will raise an exception if the\n", 1369 | " # operation takes more than five hours to complete.\n", 1370 | " @retry.Retry(predicate=retry.if_exception_type(Exception),\n", 1371 | " initial=5, maximum=60, deadline=18000)\n", 1372 | " def check_sdf_downloadtask_completion(get_request):\n", 1373 | " operation = get_request.execute()\n", 1374 | " if 'done' not in operation:\n", 1375 | " raise Exception('The operation has not completed.')\n", 1376 | " return operation\n", 1377 | "\n", 1378 | " # Get current status of operation with exponential backoff retry logic\n", 1379 | " operation = check_sdf_downloadtask_completion(get_request)\n", 1380 | "\n", 1381 | " # Check if the operation finished with an error and return\n", 1382 | " if 'error' in operation:\n", 1383 | " raise Exception(f'The operation finished in error with code {operation[\"error\"][\"code\"]} {operation[\"error\"][\"message\"]}')\n", 1384 | "\n", 1385 | " print('The operation completed successfully.')\n", 1386 | " print('Resource {operation[\"response\"][\"resourceName\"]} was created.')\n", 1387 | "\n", 1388 | " # Extract download file resource name to use in download request\n", 1389 | " resource_name = operation['response']['resourceName']\n", 1390 | "\n", 1391 | " # Configure the Media.download request\n", 1392 | " dowload_request = display_video_service.media().download_media(\n", 1393 | " resourceName=resource_name)\n", 1394 | "\n", 1395 | " output_file = f\"{resource_name.replace('/','-')}.zip\"\n", 1396 | "\n", 1397 | " # Create output stream for downloaded file\n", 1398 | " outstream = io.FileIO(output_file, mode='wb')\n", 1399 | "\n", 1400 | " # Make downloader object\n", 1401 | " downloader = http.MediaIoBaseDownload(outstream, dowload_request)\n", 1402 | "\n", 1403 | " # Download media file in chunks until finished\n", 1404 | " download_finished = False\n", 1405 | " while download_finished is False:\n", 1406 | " _, download_finished = downloader.next_chunk()\n", 1407 | "\n", 1408 | " print(f'File downloaded to {output_file}')\n", 1409 | "\n", 1410 | " # Load output into a Pandas dataframe\n", 1411 | " df = pd.read_csv(output_file, compression='zip')\n", 1412 | " return df\n", 1413 | "\n", 1414 | "print('Download SDF function created')" 1415 | ], 1416 | "execution_count": null, 1417 | "outputs": [] 1418 | }, 1419 | { 1420 | "cell_type": "markdown", 1421 | "metadata": { 1422 | "id": "jOZVpzPv7vVs" 1423 | }, 1424 | "source": [ 1425 | "Define a boilerplate targeting template that all Line Items should adhere too" 1426 | ] 1427 | }, 1428 | { 1429 | "cell_type": "code", 1430 | "metadata": { 1431 | "id": "EyHwwkOl7vVs" 1432 | }, 1433 | "source": [ 1434 | "targeting_template = {\n", 1435 | " 'Channel Targeting - Include':\n", 1436 | " '2580510;',\n", 1437 | " 'Channel Targeting - Exclude':\n", 1438 | " '2580509;',\n", 1439 | " 'Inventory Source Targeting - Include':\n", 1440 | " '1;',\n", 1441 | " 'Inventory Source Targeting - Exclude':\n", 1442 | " '6; 8; 9; 10; 2; 11; 12; 13; 16; 20; 23; 27; 29; 30; 31; 34; 35; 36; '\n", 1443 | " '38; 43; 46; 50; 51; 56; 60; 63; 67; 74;',\n", 1444 | " 'Digital Content Labels - Exclude':\n", 1445 | " 'G; PG; T;',\n", 1446 | " 'Brand Safety Sensitivity Setting':\n", 1447 | " 'Use custom',\n", 1448 | " 'Brand Safety Custom Settings':\n", 1449 | " 'Adult; Alcohol; Derogatory; Downloads & Sharing; Drugs; Gambling; '\n", 1450 | " 'Politics; Profanity; Religion; Sensitive social issues; Suggestive; '\n", 1451 | " 'Tobacco; Tragedy; Transportation Accidents; Violence; Weapons;'\n", 1452 | "}" 1453 | ], 1454 | "execution_count": null, 1455 | "outputs": [] 1456 | }, 1457 | { 1458 | "cell_type": "markdown", 1459 | "metadata": { 1460 | "id": "h7qeNTjF7vVu" 1461 | }, 1462 | "source": [ 1463 | "Modify latest SDF LineItems file and update the columns according to the targeting template" 1464 | ] 1465 | }, 1466 | { 1467 | "cell_type": "code", 1468 | "metadata": { 1469 | "id": "xqLYVrr37vVu" 1470 | }, 1471 | "source": [ 1472 | "# Configure the sdfdownloadtasks.create request\n", 1473 | "sdf_body = {\n", 1474 | " 'version': SDF_VERSION_DV360,\n", 1475 | " 'advertiserId': ADVERTISER_ID,\n", 1476 | " 'parentEntityFilter': {\n", 1477 | " 'fileType': ['FILE_TYPE_LINE_ITEM'],\n", 1478 | " 'filterType': 'FILTER_TYPE_CAMPAIGN_ID',\n", 1479 | " 'filterIds': [CAMPAIGN_ID]\n", 1480 | " }\n", 1481 | "}\n", 1482 | "\n", 1483 | "# Fetch updated SDF lineitem\n", 1484 | "sdf_df = download_sdf(sdf_body)\n", 1485 | "\n", 1486 | "# Overwrite targeting columns using 'targeting_template'\n", 1487 | "sdf_df['Channel Targeting - Include'] = targeting_template[\n", 1488 | " 'Channel Targeting - Include']\n", 1489 | "sdf_df['Channel Targeting - Exclude'] = targeting_template[\n", 1490 | " 'Channel Targeting - Exclude']\n", 1491 | "sdf_df['Inventory Source Targeting - Include'] = targeting_template[\n", 1492 | " 'Inventory Source Targeting - Include']\n", 1493 | "sdf_df['Inventory Source Targeting - Exclude'] = targeting_template[\n", 1494 | " 'Inventory Source Targeting - Exclude']\n", 1495 | "sdf_df['Digital Content Labels - Exclude'] = targeting_template[\n", 1496 | " 'Digital Content Labels - Exclude']\n", 1497 | "sdf_df['Brand Safety Sensitivity Setting'] = targeting_template[\n", 1498 | " 'Brand Safety Sensitivity Setting']\n", 1499 | "sdf_df['Brand Safety Custom Settings'] = targeting_template[\n", 1500 | " 'Brand Safety Custom Settings']\n", 1501 | "\n", 1502 | "# Save modified dataframe to remote storage in Colab\n", 1503 | "sdf_df.to_csv('sdf_update1_controls.csv', index=False)\n", 1504 | "\n", 1505 | "# Show sample (5 rows) of DataFrame\n", 1506 | "sdf_df.head()" 1507 | ], 1508 | "execution_count": null, 1509 | "outputs": [] 1510 | }, 1511 | { 1512 | "cell_type": "code", 1513 | "metadata": { 1514 | "id": "zVnETmI4rSBh" 1515 | }, 1516 | "source": [ 1517 | "# Download modified csv to local storage\n", 1518 | "files.download('sdf_update1_controls.csv')\n", 1519 | "\n", 1520 | "print(\n", 1521 | " \"Success, check your downloads for a file called 'sdf_update1_controls.csv'\"\n", 1522 | ")" 1523 | ], 1524 | "execution_count": null, 1525 | "outputs": [] 1526 | }, 1527 | { 1528 | "cell_type": "markdown", 1529 | "metadata": { 1530 | "id": "uLPlzjQp7vVz" 1531 | }, 1532 | "source": [ 1533 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 1534 | "\n", 1535 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 1536 | ] 1537 | }, 1538 | { 1539 | "cell_type": "markdown", 1540 | "metadata": { 1541 | "id": "i07wvH_17vVz" 1542 | }, 1543 | "source": [ 1544 | "## 1.3 SDF + Entity Read Files\n", 1545 | "\n", 1546 | "**What are Entity Read Files (ERFs)?** ERFs are flat files (.JSON) in Google Cloud Storage that contain lookup values for DV360 entities like geographies, creatives, etc. Each DV360 entity (Advertiser, Campaign, LineItem, etc) has a corresponding .JSON file in Cloud Storage retained free-of-charge for 60 days from their processing date.\n", 1547 | "\n", 1548 | "ERFs consist of 1 file per entity type, written x1 per day to two seperate Cloud buckets: \n", 1549 | "\n", 1550 | "1. **[Public](https://developers.google.com/bid-manager/guides/entity-read/overview#public-tables)** (10 .JSON files) - contain common public data such as GeoLocation and Language which are stored in the gdbm-public bucket (the same bucket for every DV360 user).\n", 1551 | "2. **[Private](https://developers.google.com/bid-manager/guides/entity-read/overview#private-tables)** (13 .JSON files) - contain information about the DV360 Partner's campaigns, creatives, budgets and other private data and are stored in Partner-specific buckets (restricted to specific users)\n", 1552 | "\n", 1553 | "Reference: https://developers.google.com/bid-manager/guides/entity-read/overview\n", 1554 | "\n", 1555 | "ERFs can be used to speed up, and automate, the creation of SDF files.\n", 1556 | "\n", 1557 | "Let's explore this now..." 1558 | ] 1559 | }, 1560 | { 1561 | "cell_type": "markdown", 1562 | "metadata": { 1563 | "id": "2qNRwveW7vVz" 1564 | }, 1565 | "source": [ 1566 | "Download yesterday's GeoLocation.json from [public ERF bucket](https://console.cloud.google.com/storage/browser/gdbm-public/entity/) using Google Cloud Storage API" 1567 | ] 1568 | }, 1569 | { 1570 | "cell_type": "code", 1571 | "metadata": { 1572 | "id": "q3MTss5w7vVz" 1573 | }, 1574 | "source": [ 1575 | "# Actually today-7 to avoid issues with collection\n", 1576 | "yesterday = datetime.date.today() - datetime.timedelta(7) \n", 1577 | "\n", 1578 | "# Download public ERF for geolocation info\n", 1579 | "request = gcs_service.objects().get_media(\n", 1580 | " bucket='gdbm-public',\n", 1581 | " object='entity/' + yesterday.strftime('%Y%m%d') + '.0.GeoLocation.json')\n", 1582 | "\n", 1583 | "response = request.execute()\n", 1584 | "geolocations = json.loads(response)\n", 1585 | "\n", 1586 | "print('GeoLocation.json successfully downloaded \\n')\n", 1587 | "print(\"Here's a random sample of 5 entries:\\n\")\n", 1588 | "pprint.pprint(geolocations[0:5])" 1589 | ], 1590 | "execution_count": null, 1591 | "outputs": [] 1592 | }, 1593 | { 1594 | "cell_type": "markdown", 1595 | "metadata": { 1596 | "id": "oTQIVXMN7vV1" 1597 | }, 1598 | "source": [ 1599 | "Retrieve a list of country codes / IDs from GeoLocation.json for each of our store locations" 1600 | ] 1601 | }, 1602 | { 1603 | "cell_type": "code", 1604 | "metadata": { 1605 | "id": "uqLxW8TD7vV1" 1606 | }, 1607 | "source": [ 1608 | "# Provide a list of store locations\n", 1609 | "store_locations = ['United Kingdom', 'France', 'Spain', 'Germany', 'Portugal']\n", 1610 | "\n", 1611 | "# Create a new dictionary to save the country code and ID later on\n", 1612 | "geo_targeting_ids = {}\n", 1613 | "\n", 1614 | "# Note: GeoLocation.json is over 800,000 lines\n", 1615 | "for location in geolocations:\n", 1616 | " if location['canonical_name'] in store_locations:\n", 1617 | " geo_targeting_ids[location['country_code']] = location['id']\n", 1618 | " print(location)\n", 1619 | "\n", 1620 | "print(geo_targeting_ids)" 1621 | ], 1622 | "execution_count": null, 1623 | "outputs": [] 1624 | }, 1625 | { 1626 | "cell_type": "markdown", 1627 | "metadata": { 1628 | "id": "1kYTjGut7vV3" 1629 | }, 1630 | "source": [ 1631 | "Download the latest SDF LineItems (because we've made changes since our last download)" 1632 | ] 1633 | }, 1634 | { 1635 | "cell_type": "code", 1636 | "metadata": { 1637 | "id": "Wrk890JJ7vV8" 1638 | }, 1639 | "source": [ 1640 | "# Configure the sdfdownloadtasks.create request\n", 1641 | "sdf_body = {\n", 1642 | " 'version': SDF_VERSION_DV360,\n", 1643 | " 'advertiserId': ADVERTISER_ID,\n", 1644 | " 'parentEntityFilter': {\n", 1645 | " 'fileType': ['FILE_TYPE_LINE_ITEM'],\n", 1646 | " 'filterType': 'FILTER_TYPE_CAMPAIGN_ID',\n", 1647 | " 'filterIds': [CAMPAIGN_ID]\n", 1648 | " }\n", 1649 | "}\n", 1650 | "\n", 1651 | "sdf_df = download_sdf(sdf_body)\n", 1652 | "sdf_df.head()" 1653 | ], 1654 | "execution_count": null, 1655 | "outputs": [] 1656 | }, 1657 | { 1658 | "cell_type": "markdown", 1659 | "metadata": { 1660 | "id": "dR4WpdKS7vV5" 1661 | }, 1662 | "source": [ 1663 | "Modify the contents of the latest SDF output, then save a new CSV with updated Geo Targeting IDs" 1664 | ] 1665 | }, 1666 | { 1667 | "cell_type": "code", 1668 | "metadata": { 1669 | "id": "MPjjXbh97vWA" 1670 | }, 1671 | "source": [ 1672 | "for country in geo_targeting_ids:\n", 1673 | " target_country = geo_targeting_ids[country]\n", 1674 | " sdf_df.loc[sdf_df.Name.str.contains(country),\n", 1675 | " 'Geography Targeting - Include'] = f'{target_country};'\n", 1676 | "\n", 1677 | "# Save modified dataframe to remote storage in Colab\n", 1678 | "sdf_df.to_csv('sdf_update2_geo.csv', index=False)\n", 1679 | "\n", 1680 | "# Display updated DataFrame\n", 1681 | "sdf_df.head()" 1682 | ], 1683 | "execution_count": null, 1684 | "outputs": [] 1685 | }, 1686 | { 1687 | "cell_type": "code", 1688 | "metadata": { 1689 | "id": "8Ag3TWRixCCb" 1690 | }, 1691 | "source": [ 1692 | "# Download modified csv to local storage\n", 1693 | "files.download('sdf_update2_geo.csv')\n", 1694 | "\n", 1695 | "print(\"Success, see file 'sdf_update2_geo.csv' in your downloads folder\")" 1696 | ], 1697 | "execution_count": null, 1698 | "outputs": [] 1699 | }, 1700 | { 1701 | "cell_type": "markdown", 1702 | "metadata": { 1703 | "id": "XuRnqwo27vWD" 1704 | }, 1705 | "source": [ 1706 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 1707 | "\n", 1708 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 1709 | ] 1710 | }, 1711 | { 1712 | "cell_type": "markdown", 1713 | "metadata": { 1714 | "id": "MGewvDOC7vWD" 1715 | }, 1716 | "source": [ 1717 | "## 1.4 SDF + Cloud Vision API" 1718 | ] 1719 | }, 1720 | { 1721 | "cell_type": "markdown", 1722 | "metadata": { 1723 | "id": "ZfD34CJv7vWD" 1724 | }, 1725 | "source": [ 1726 | "Next, let's look at how we you can utilise external APIs.\n", 1727 | "\n", 1728 | "1. Download the '[product_feed](https://docs.google.com/spreadsheets/d/1Z-hL3KY9ynmajME6kW3QYbiFqmlg9BFvdRqEYy3Rvs4/edit#gid=0)' tab from Google Store as CSV (File >> Download >> CSV)\n", 1729 | "2. Execute the following code block and upload '**product_feed.csv**'\n", 1730 | "3. This will create a new Python dictionary (key:value pairing), mapping SKUs with their image link\n", 1731 | "\n", 1732 | "**Warning: Cloud Vision API is paid product, utilising the following example in your own Cloud project will incur costs.**\n", 1733 | "\n", 1734 | "Try out the Cloud Vision API for free at [cloud.google.com/vision](https://cloud.google.com/vision/)" 1735 | ] 1736 | }, 1737 | { 1738 | "cell_type": "code", 1739 | "metadata": { 1740 | "id": "UpfksP0E7vWE" 1741 | }, 1742 | "source": [ 1743 | "# Upload product feed using Colab's upload utility\n", 1744 | "product_feed_csv = files.upload()\n", 1745 | "\n", 1746 | "contents = next(iter(product_feed_csv.values())).decode('utf-8')\n", 1747 | "products = csv.DictReader(io.StringIO(contents))\n", 1748 | "\n", 1749 | "image_url_list = {}\n", 1750 | "\n", 1751 | "# Iterate through each row and update dict() with sku:link\n", 1752 | "for row in products:\n", 1753 | " image_url_list[row['sku']] = row['image_link']\n", 1754 | "\n", 1755 | "pprint.pprint(image_url_list)" 1756 | ], 1757 | "execution_count": null, 1758 | "outputs": [] 1759 | }, 1760 | { 1761 | "cell_type": "markdown", 1762 | "metadata": { 1763 | "id": "VTxKA00E7vWF" 1764 | }, 1765 | "source": [ 1766 | "Define a function to send images to the Cloud Vision API" 1767 | ] 1768 | }, 1769 | { 1770 | "cell_type": "code", 1771 | "metadata": { 1772 | "id": "Dqcf6sD97vWG" 1773 | }, 1774 | "source": [ 1775 | "def vision_analysis(image_url):\n", 1776 | " \"\"\"Process images using the Cloud Vision API.\"\"\"\n", 1777 | "\n", 1778 | " # Assign image URL\n", 1779 | " image = vision.Image()\n", 1780 | " image.source.image_uri = image_url\n", 1781 | "\n", 1782 | " # Instantiates a Vision client\n", 1783 | " client = vision.ImageAnnotatorClient(credentials=credentials)\n", 1784 | "\n", 1785 | " # Performs label detection on the image file\n", 1786 | " response = client.label_detection(image=image)\n", 1787 | "\n", 1788 | " dv360_targeting_keywords = []\n", 1789 | " vision_labels = []\n", 1790 | "\n", 1791 | " for label in response.label_annotations:\n", 1792 | " dv360_targeting_keywords.append(label.description)\n", 1793 | " label = f'{label.description} ({label.score:.2%})'\n", 1794 | " vision_labels.append(label)\n", 1795 | "\n", 1796 | " return dv360_targeting_keywords, vision_labels\n", 1797 | "\n", 1798 | "print(\"Vision function created\")" 1799 | ], 1800 | "execution_count": null, 1801 | "outputs": [] 1802 | }, 1803 | { 1804 | "cell_type": "markdown", 1805 | "metadata": { 1806 | "id": "_stGr-0B7vWH" 1807 | }, 1808 | "source": [ 1809 | "Run our images through the function, and return a lookup table (reference)" 1810 | ] 1811 | }, 1812 | { 1813 | "cell_type": "code", 1814 | "metadata": { 1815 | "id": "RQbFwf-V7vWH" 1816 | }, 1817 | "source": [ 1818 | "imageslookup = {}\n", 1819 | "\n", 1820 | "for sku, url in image_url_list.items():\n", 1821 | " imageslookup[sku], vision_labels = vision_analysis(url)\n", 1822 | " print(f'Analysis completed for: {url}')\n", 1823 | " print('Labels (confidence score):')\n", 1824 | " pprint.pprint(vision_labels, indent=4)\n", 1825 | " print('=' * 30)" 1826 | ], 1827 | "execution_count": null, 1828 | "outputs": [] 1829 | }, 1830 | { 1831 | "cell_type": "markdown", 1832 | "metadata": { 1833 | "id": "bg2lGXt2MHHp" 1834 | }, 1835 | "source": [ 1836 | "View the results of our Vision analysis:" 1837 | ] 1838 | }, 1839 | { 1840 | "cell_type": "code", 1841 | "metadata": { 1842 | "id": "he1UeRHkMDMM" 1843 | }, 1844 | "source": [ 1845 | "print('\\n\\nLookup table:')\n", 1846 | "pprint.pprint(imageslookup, indent=4)" 1847 | ], 1848 | "execution_count": null, 1849 | "outputs": [] 1850 | }, 1851 | { 1852 | "cell_type": "markdown", 1853 | "metadata": { 1854 | "id": "UKI6tslROroj" 1855 | }, 1856 | "source": [ 1857 | "Download the latest SDF LineItems (because we've made changes since our last download)" 1858 | ] 1859 | }, 1860 | { 1861 | "cell_type": "code", 1862 | "metadata": { 1863 | "id": "rlV5EFl9OiEe" 1864 | }, 1865 | "source": [ 1866 | "# Configure the sdfdownloadtasks.create request\n", 1867 | "sdf_body = {\n", 1868 | " 'version': SDF_VERSION_DV360,\n", 1869 | " 'advertiserId': ADVERTISER_ID,\n", 1870 | " 'parentEntityFilter': {\n", 1871 | " 'fileType': ['FILE_TYPE_LINE_ITEM'],\n", 1872 | " 'filterType': 'FILTER_TYPE_CAMPAIGN_ID',\n", 1873 | " 'filterIds': [CAMPAIGN_ID]\n", 1874 | " }\n", 1875 | "}\n", 1876 | "\n", 1877 | "sdf_df = download_sdf(sdf_body)\n", 1878 | "sdf_df.head()" 1879 | ], 1880 | "execution_count": null, 1881 | "outputs": [] 1882 | }, 1883 | { 1884 | "cell_type": "markdown", 1885 | "metadata": { 1886 | "id": "mgm1xQYd7vWJ" 1887 | }, 1888 | "source": [ 1889 | "Now we have our new labels from the Vision API, we need to write these into the keywords targeting field" 1890 | ] 1891 | }, 1892 | { 1893 | "cell_type": "code", 1894 | "metadata": { 1895 | "id": "scDCPwlOpZIN" 1896 | }, 1897 | "source": [ 1898 | "for product in imageslookup:\n", 1899 | " sdf_df.loc[sdf_df.Name.str.contains(product),\n", 1900 | " 'Keyword Targeting - Include'] = ';'.join(\n", 1901 | " imageslookup[product]).lower()\n", 1902 | "\n", 1903 | "# Save modified dataframe to remote storage in Colab\n", 1904 | "sdf_df.to_csv('sdf_update3_keywords.csv', index=False)\n", 1905 | "\n", 1906 | "sdf_df.head()" 1907 | ], 1908 | "execution_count": null, 1909 | "outputs": [] 1910 | }, 1911 | { 1912 | "cell_type": "code", 1913 | "metadata": { 1914 | "id": "zpGmzMtM5sxY" 1915 | }, 1916 | "source": [ 1917 | "# Download modified csv to local storage\n", 1918 | "files.download('sdf_update3_keywords.csv')\n", 1919 | "\n", 1920 | "print(\"Success, see 'sdf_update3_keywords.csv' in your downloads folder\")" 1921 | ], 1922 | "execution_count": null, 1923 | "outputs": [] 1924 | }, 1925 | { 1926 | "cell_type": "markdown", 1927 | "metadata": { 1928 | "id": "NwfbmKuX7vWM" 1929 | }, 1930 | "source": [ 1931 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 1932 | "\n", 1933 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 1934 | ] 1935 | }, 1936 | { 1937 | "cell_type": "markdown", 1938 | "metadata": { 1939 | "id": "J0EItGkg7vWP" 1940 | }, 1941 | "source": [ 1942 | "## 1.5 Optimisation using Reports" 1943 | ] 1944 | }, 1945 | { 1946 | "cell_type": "markdown", 1947 | "metadata": { 1948 | "id": "gL7zXvOE7vWQ" 1949 | }, 1950 | "source": [ 1951 | "Next, we'll look at how you could combine reporting data, with operations such as optimising bid multipliers or deactivating activity. **Note: your new campaign has no performance history, so we'll use an existing campaign for this exercise.**" 1952 | ] 1953 | }, 1954 | { 1955 | "cell_type": "code", 1956 | "metadata": { 1957 | "id": "XxNcOozFxmMG" 1958 | }, 1959 | "source": [ 1960 | "# Define DV360 report definition (i.e. metrics and filters)\n", 1961 | "report_definition = {\n", 1962 | " 'params': {\n", 1963 | " 'type': 'TYPE_GENERAL',\n", 1964 | " 'metrics': [\n", 1965 | " 'METRIC_IMPRESSIONS', 'METRIC_CLICKS', 'METRIC_CTR',\n", 1966 | " 'METRIC_REVENUE_ADVERTISER'\n", 1967 | " ],\n", 1968 | " 'groupBys': [\n", 1969 | " 'FILTER_ADVERTISER', 'FILTER_INSERTION_ORDER', 'FILTER_LINE_ITEM',\n", 1970 | " 'FILTER_ADVERTISER_CURRENCY'\n", 1971 | " ],\n", 1972 | " 'filters': [{\n", 1973 | " 'type': 'FILTER_ADVERTISER',\n", 1974 | " 'value': ADVERTISER_ID\n", 1975 | " }],\n", 1976 | " },\n", 1977 | " 'metadata': {\n", 1978 | " 'title': 'DV360 Automation API-generated report',\n", 1979 | " 'dataRange': 'LAST_90_DAYS',\n", 1980 | " 'format': 'csv'\n", 1981 | " },\n", 1982 | " 'schedule': {\n", 1983 | " 'frequency': 'ONE_TIME'\n", 1984 | " }\n", 1985 | "}\n", 1986 | "\n", 1987 | "# Create new query using report definition\n", 1988 | "operation = dbm_service.queries().createquery(body=report_definition).execute()\n", 1989 | "pprint.pprint(operation)\n", 1990 | "\n", 1991 | "\n", 1992 | "# Runs the given Queries.getquery request, retrying with an exponential\n", 1993 | "# backoff. Returns completed operation. Will raise an exception if the\n", 1994 | "# operation takes more than five hours to complete.\n", 1995 | "@retry.Retry(predicate=retry.if_exception_type(Exception),\n", 1996 | " initial=5, maximum=60, deadline=18000)\n", 1997 | "def check_get_query_completion(getquery_request):\n", 1998 | " response = getquery_request.execute()\n", 1999 | " pprint.pprint(response)\n", 2000 | " if response['metadata']['running']:\n", 2001 | " raise Exception('The operation has not completed.')\n", 2002 | " return response\n", 2003 | "\n", 2004 | "getquery_request = dbm_service.queries().getquery(queryId=operation['queryId'])\n", 2005 | "response = check_get_query_completion(getquery_request)" 2006 | ], 2007 | "execution_count": null, 2008 | "outputs": [] 2009 | }, 2010 | { 2011 | "cell_type": "code", 2012 | "metadata": { 2013 | "id": "pjuBG9M67vWT" 2014 | }, 2015 | "source": [ 2016 | "report_url = response['metadata']['googleCloudStoragePathForLatestReport']\n", 2017 | "\n", 2018 | "# Use skipfooter to remove report footer from data\n", 2019 | "report_df = pd.read_csv(report_url, skipfooter=16, engine='python')\n", 2020 | "report_df.head(10)" 2021 | ], 2022 | "execution_count": null, 2023 | "outputs": [] 2024 | }, 2025 | { 2026 | "cell_type": "code", 2027 | "metadata": { 2028 | "id": "sZ9mrKopnWYq" 2029 | }, 2030 | "source": [ 2031 | "# Define our 'KPIs'\n", 2032 | "ctr_target = 0.15\n", 2033 | "imp_threshold = 1000\n", 2034 | "\n", 2035 | "# Convert IDs to remove decimal point, then string\n", 2036 | "report_df['Line Item ID'] = report_df['Line Item ID'].apply(int)\n", 2037 | "poor_performers = report_df.query(\n", 2038 | " 'Impressions > @imp_threshold & (Clicks / Impressions)*100 < @ctr_target')\n", 2039 | "\n", 2040 | "# Convert results to Python list\n", 2041 | "poor_performers = list(poor_performers['Line Item ID'])\n", 2042 | "\n", 2043 | "print(f'There are {len(poor_performers)} LineItems with a CTR'\n", 2044 | " f' < {ctr_target}% and over {imp_threshold} impressions:'\n", 2045 | " f'\\n{poor_performers}')" 2046 | ], 2047 | "execution_count": null, 2048 | "outputs": [] 2049 | }, 2050 | { 2051 | "cell_type": "markdown", 2052 | "metadata": { 2053 | "id": "tXUOHDTp7vWW" 2054 | }, 2055 | "source": [ 2056 | "Download an updated SDF LineItems file, and if the LineItem ID is in the poor performers list, add a **Geo bid multiplier to half the bids (0.5)**" 2057 | ] 2058 | }, 2059 | { 2060 | "cell_type": "code", 2061 | "metadata": { 2062 | "id": "BToWuOg8lX_m" 2063 | }, 2064 | "source": [ 2065 | "# Configure the sdfdownloadtasks.create request\n", 2066 | "sdf_body = {\n", 2067 | " 'version': SDF_VERSION_DV360,\n", 2068 | " 'advertiserId': ADVERTISER_ID,\n", 2069 | " 'parentEntityFilter': {\n", 2070 | " 'fileType': ['FILE_TYPE_LINE_ITEM'],\n", 2071 | " 'filterType': 'FILTER_TYPE_CAMPAIGN_ID',\n", 2072 | " 'filterIds': ['1914007']\n", 2073 | " }\n", 2074 | "}\n", 2075 | "\n", 2076 | "sdf_df = download_sdf(sdf_body)\n", 2077 | "sdf_df.head()\n", 2078 | "\n", 2079 | "for li in poor_performers:\n", 2080 | " geo = sdf_df.loc[sdf_df['Line Item Id'] == li,\n", 2081 | " 'Geography Targeting - Include'].iloc[0]\n", 2082 | " sdf_df.loc[sdf_df['Line Item Id'] == li,\n", 2083 | " 'Bid Multipliers'] = f'(geo; {geo} 0.5;);'\n", 2084 | "\n", 2085 | "# Save modified dataframe to remote storage in Colab\n", 2086 | "sdf_df.to_csv('sdf_update4_bidmultipliers.csv', index=False)\n", 2087 | "\n", 2088 | "# Display updated DataFrame\n", 2089 | "sdf_df.head()" 2090 | ], 2091 | "execution_count": null, 2092 | "outputs": [] 2093 | }, 2094 | { 2095 | "cell_type": "code", 2096 | "metadata": { 2097 | "id": "JD3Jn1EDl2Ni" 2098 | }, 2099 | "source": [ 2100 | "files.download('sdf_update4_bidmultipliers.csv')\n", 2101 | "\n", 2102 | "print('Success, your new SDF file has been downloaded')" 2103 | ], 2104 | "execution_count": null, 2105 | "outputs": [] 2106 | }, 2107 | { 2108 | "cell_type": "markdown", 2109 | "metadata": { 2110 | "id": "2FLhQCPm7vWa" 2111 | }, 2112 | "source": [ 2113 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)\n", 2114 | "\n", 2115 | "Once the changes have been applied successfully, check the 'Targeting' controls within 'Line Item details'" 2116 | ] 2117 | }, 2118 | { 2119 | "cell_type": "markdown", 2120 | "metadata": { 2121 | "id": "gud5WGnAEHII" 2122 | }, 2123 | "source": [ 2124 | "## 1.6 Challenge" 2125 | ] 2126 | }, 2127 | { 2128 | "cell_type": "markdown", 2129 | "metadata": { 2130 | "id": "hW-x35-HEHIM" 2131 | }, 2132 | "source": [ 2133 | "**Challenge: update your campaign with both language and audience targeting.**\n", 2134 | "\n", 2135 | "* All Lineitems should target the following Google audiences\n", 2136 | "\n", 2137 | " * Affinity Categories » Technology » Mobile Enthusiasts\n", 2138 | " * Affinity Categories » Technology » Technophiles » High-End Computer Aficionado\n", 2139 | " * In-Market Categories » Consumer Electronics\n", 2140 | "\n", 2141 | "* LineItems for France, should be targeted at French speakers\n", 2142 | "* LineItems for Great Britain, should be targeted at English speakers\n", 2143 | "\n", 2144 | "**Tips**\n", 2145 | "\n", 2146 | "* Google Audience IDs can be found in the DV360 UI or by downloading an SDF with an existing audience applied\n", 2147 | "* Language IDs can be found in the Language.json ERF file or by downloading an SDF with the language already applied" 2148 | ] 2149 | }, 2150 | { 2151 | "cell_type": "code", 2152 | "metadata": { 2153 | "id": "fYVahrWS_j13" 2154 | }, 2155 | "source": [ 2156 | "#TODO" 2157 | ], 2158 | "execution_count": null, 2159 | "outputs": [] 2160 | }, 2161 | { 2162 | "cell_type": "markdown", 2163 | "metadata": { 2164 | "id": "0CAwyEM2-3pf" 2165 | }, 2166 | "source": [ 2167 | "**Solution**" 2168 | ] 2169 | }, 2170 | { 2171 | "cell_type": "code", 2172 | "metadata": { 2173 | "id": "p57U2_hvEHIV" 2174 | }, 2175 | "source": [ 2176 | "# Format today-7 in required date format\n", 2177 | "yesterday = (datetime.date.today() - datetime.timedelta(7)).strftime('%Y%m%d')\n", 2178 | "\n", 2179 | "# Download ERF for Language.json from public GCS bucket\n", 2180 | "request = gcs_service.objects().get_media(\n", 2181 | " bucket='gdbm-public', object='entity/' + yesterday + '.0.Language.json')\n", 2182 | "\n", 2183 | "response = request.execute()\n", 2184 | "languages = json.loads(response)\n", 2185 | "\n", 2186 | "language_targets = ['en', 'fr']\n", 2187 | "lang_targeting_ids = {}\n", 2188 | "\n", 2189 | "# Search language.json for language targets 'en' and 'fr'\n", 2190 | "for lang in languages:\n", 2191 | " if lang['code'] in language_targets:\n", 2192 | " lang_targeting_ids[lang['code']] = lang['id']\n", 2193 | " print(lang)\n", 2194 | "\n", 2195 | "print(lang_targeting_ids)\n", 2196 | "\n", 2197 | "# Define targeting template\n", 2198 | "targeting_template = {\n", 2199 | " 'Affinity & In Market Targeting - Include': '4569529;4586809;4497529;',\n", 2200 | "}\n", 2201 | "\n", 2202 | "# Configure the sdfdownloadtasks.create request\n", 2203 | "sdf_body = {\n", 2204 | " 'version': SDF_VERSION_DV360,\n", 2205 | " 'advertiserId': ADVERTISER_ID,\n", 2206 | " 'parentEntityFilter': {\n", 2207 | " 'fileType': ['FILE_TYPE_LINE_ITEM'],\n", 2208 | " 'filterType': 'FILTER_TYPE_CAMPAIGN_ID',\n", 2209 | " 'filterIds': [CAMPAIGN_ID]\n", 2210 | " }\n", 2211 | "}\n", 2212 | "\n", 2213 | "sdf_df = download_sdf(sdf_body)\n", 2214 | "\n", 2215 | "# Update DataFrame with Language and Audience targeting\n", 2216 | "sdf_df.loc[sdf_df.Name.str.contains('GB'),\n", 2217 | " 'Language Targeting - Include'] = f\"{lang_targeting_ids['en']};\"\n", 2218 | "sdf_df.loc[sdf_df.Name.str.contains('FR'),\n", 2219 | " 'Language Targeting - Include'] = f\"{lang_targeting_ids['fr']};\"\n", 2220 | "sdf_df['Affinity & In Market Targeting - Include'] = targeting_template[\n", 2221 | " 'Affinity & In Market Targeting - Include']\n", 2222 | "\n", 2223 | "# Save modified dataframe to remote storage in Colab\n", 2224 | "sdf_df.to_csv('sdf_update5_challenge.csv', index=False)\n", 2225 | "\n", 2226 | "# Display updated DataFrame\n", 2227 | "sdf_df.head()" 2228 | ], 2229 | "execution_count": null, 2230 | "outputs": [] 2231 | }, 2232 | { 2233 | "cell_type": "code", 2234 | "metadata": { 2235 | "id": "xVr4Tx0WGu_U" 2236 | }, 2237 | "source": [ 2238 | "# Download file to disk using Colab syntax\n", 2239 | "files.download('sdf_update5_challenge.csv')\n", 2240 | "\n", 2241 | "print(\"Success, see downloads folder for file 'sdf_update5_challenge.csv'\")" 2242 | ], 2243 | "execution_count": null, 2244 | "outputs": [] 2245 | }, 2246 | { 2247 | "cell_type": "markdown", 2248 | "metadata": { 2249 | "id": "Y-5rO2qCEHIW" 2250 | }, 2251 | "source": [ 2252 | "Upload the output .csv file in the [DV360 UI](https://displayvideo.google.com/)" 2253 | ] 2254 | }, 2255 | { 2256 | "cell_type": "markdown", 2257 | "metadata": { 2258 | "id": "dffZuobP-Ytw" 2259 | }, 2260 | "source": [ 2261 | "# 2) Display & Video 360 API" 2262 | ] 2263 | }, 2264 | { 2265 | "cell_type": "markdown", 2266 | "metadata": { 2267 | "id": "IZi5XIkN-Yt0" 2268 | }, 2269 | "source": [ 2270 | "**What is the Display & Video 360 API?** The Display & Video 360 API (formly known as the DV360 Write API) is the programmatic interface for the Display & Video 360 platform. It allows developers to easily and efficiently automate complex Display & Video 360 workflows, such as creating insertion orders and setting targeting options for individual line items.\n", 2271 | "\n", 2272 | "We'll use it now to build upon the campaign we created earlier using SDF.\n", 2273 | "\n", 2274 | "Reference: https://developers.google.com/display-video/api/reference/rest\n" 2275 | ] 2276 | }, 2277 | { 2278 | "cell_type": "markdown", 2279 | "metadata": { 2280 | "id": "ntpwiPG--Yt3" 2281 | }, 2282 | "source": [ 2283 | "## 2.1 Campaign builds" 2284 | ] 2285 | }, 2286 | { 2287 | "cell_type": "markdown", 2288 | "metadata": { 2289 | "id": "ZDAePeL284Fe" 2290 | }, 2291 | "source": [ 2292 | "**Check Advertiser (ADVERTISER_ID) has active Lineitems**" 2293 | ] 2294 | }, 2295 | { 2296 | "cell_type": "code", 2297 | "metadata": { 2298 | "id": "NtrAMvidPLQA" 2299 | }, 2300 | "source": [ 2301 | "request = display_video_service.advertisers().lineItems().list(\n", 2302 | " advertiserId=ADVERTISER_ID,\n", 2303 | " filter='entityStatus=\"ENTITY_STATUS_ACTIVE\"',\n", 2304 | " pageSize=1\n", 2305 | " )\n", 2306 | "\n", 2307 | "response = request.execute()\n", 2308 | "\n", 2309 | "# Check if response is empty.\n", 2310 | "if not response:\n", 2311 | " print('Advertiser has no active Line Items')\n", 2312 | "else:\n", 2313 | " pprint.pprint(response['lineItems'])" 2314 | ], 2315 | "execution_count": null, 2316 | "outputs": [] 2317 | }, 2318 | { 2319 | "cell_type": "code", 2320 | "metadata": { 2321 | "id": "3E3S1ja1c6Jd" 2322 | }, 2323 | "source": [ 2324 | "def get_active_lineitems(ADVERTISER_ID, CAMPAIGN_ID):\n", 2325 | " \"\"\"Returns list of Lineitems with active status.\"\"\"\n", 2326 | " list_lineitems = display_video_service.advertisers().lineItems().list(\n", 2327 | " advertiserId=ADVERTISER_ID,\n", 2328 | " filter=f'entityStatus=\"ENTITY_STATUS_ACTIVE\" AND campaignId=\"{CAMPAIGN_ID}\"',\n", 2329 | " fields='lineItems(lineItemId,displayName)' # Return only two fields\n", 2330 | " ).execute()\n", 2331 | "\n", 2332 | " active_lineitems = [li['lineItemId'] for li in list_lineitems['lineItems']]\n", 2333 | " return active_lineitems" 2334 | ], 2335 | "execution_count": null, 2336 | "outputs": [] 2337 | }, 2338 | { 2339 | "cell_type": "markdown", 2340 | "metadata": { 2341 | "id": "k978_cI8__N_" 2342 | }, 2343 | "source": [ 2344 | "Upload the [extended feed](https://docs.google.com/spreadsheets/d/1CmP0q7QQa0GPJnLlSgqROw_oMXrrbNj8osITTR1-Wh8/edit#gid=472205656) for Google Store's new territories: Spain, Germany and Portugal." 2345 | ] 2346 | }, 2347 | { 2348 | "cell_type": "code", 2349 | "metadata": { 2350 | "id": "fv8eXlga-Yt5" 2351 | }, 2352 | "source": [ 2353 | "# Upload product feed using Colab's upload utility\n", 2354 | "product_feed_csv = files.upload()\n", 2355 | "\n", 2356 | "contents = next(iter(product_feed_csv.values())).decode('utf-8')\n", 2357 | "products = list(csv.DictReader(io.StringIO(contents)))\n", 2358 | "\n", 2359 | "# Create unique list of country-codes -- set() automatically de dupes\n", 2360 | "unique_country_codes = set([row['country code'] for row in products])\n", 2361 | "\n", 2362 | "print(unique_country_codes)" 2363 | ], 2364 | "execution_count": null, 2365 | "outputs": [] 2366 | }, 2367 | { 2368 | "cell_type": "markdown", 2369 | "metadata": { 2370 | "id": "N7M5N1SQ-Yt7" 2371 | }, 2372 | "source": [ 2373 | "**Create Insertion Order template**\n", 2374 | "\n", 2375 | "Here we're defining a new a function called 'create_insertion_order'. Note: **all new Insertion Orders and Line Items** created using the DV360 API are created in 'Draft' mode (as a safety mechanism), and must be activated with a second API call, or via the UI (e.g. manually by a trader)." 2376 | ] 2377 | }, 2378 | { 2379 | "cell_type": "code", 2380 | "metadata": { 2381 | "id": "KL6UCHj3-Yt8" 2382 | }, 2383 | "source": [ 2384 | "def create_insertion_order(parent_campaign_id, new_io_name):\n", 2385 | " \"\"\"Creates a new DV360 insertion order object.\"\"\"\n", 2386 | "\n", 2387 | " # Define our new Insertion Order boilerplate\n", 2388 | " new_insertion_order = {\n", 2389 | " 'campaignId': parent_campaign_id,\n", 2390 | " 'displayName': new_io_name, # Define naming convention\n", 2391 | " 'entityStatus': 'ENTITY_STATUS_DRAFT',\n", 2392 | " 'pacing': {\n", 2393 | " 'pacingPeriod': 'PACING_PERIOD_DAILY',\n", 2394 | " 'pacingType': 'PACING_TYPE_EVEN',\n", 2395 | " 'dailyMaxMicros': '1000000' # Equiv to $1 or local currency\n", 2396 | " },\n", 2397 | " 'frequencyCap': {\n", 2398 | " 'unlimited': False,\n", 2399 | " 'timeUnit': 'TIME_UNIT_MONTHS',\n", 2400 | " 'timeUnitCount': 1,\n", 2401 | " 'maxImpressions': 5\n", 2402 | " },\n", 2403 | " 'performanceGoal': {\n", 2404 | " 'performanceGoalType': 'PERFORMANCE_GOAL_TYPE_CPC',\n", 2405 | " 'performanceGoalAmountMicros': '1000000', # $1 CPM/CPC target\n", 2406 | " },\n", 2407 | " 'bidStrategy': {\n", 2408 | " 'fixedBid': {\n", 2409 | " 'bidAmountMicros': '0'\n", 2410 | " },\n", 2411 | " },\n", 2412 | " 'budget': {\n", 2413 | " 'automationType':\n", 2414 | " 'INSERTION_ORDER_AUTOMATION_TYPE_NONE',\n", 2415 | " 'budgetUnit':\n", 2416 | " 'BUDGET_UNIT_CURRENCY',\n", 2417 | " 'budgetSegments': [{\n", 2418 | " 'budgetAmountMicros':\n", 2419 | " '30000000', # Equiv to $30 or local currency\n", 2420 | " 'description': 'My first segment',\n", 2421 | " 'dateRange': {\n", 2422 | " 'startDate': {\n", 2423 | " 'year': year,\n", 2424 | " 'month': month,\n", 2425 | " 'day': day\n", 2426 | " },\n", 2427 | " 'endDate': {\n", 2428 | " 'year': year_plus30,\n", 2429 | " 'month': month_plus30,\n", 2430 | " 'day': day_plus30\n", 2431 | " }\n", 2432 | " }\n", 2433 | " }]\n", 2434 | " }\n", 2435 | " }\n", 2436 | "\n", 2437 | " # API create() request to generate new Insertion Order\n", 2438 | " newinsertionorder_request = display_video_service.advertisers(\n", 2439 | " ).insertionOrders().create(\n", 2440 | " advertiserId=ADVERTISER_ID, body=new_insertion_order).execute()\n", 2441 | "\n", 2442 | " # Define patch to activate new Insertion Order afer creation\n", 2443 | " patch = {\n", 2444 | " 'entityStatus': 'ENTITY_STATUS_ACTIVE',\n", 2445 | " }\n", 2446 | "\n", 2447 | " # API patch() request\n", 2448 | " display_video_service.advertisers().insertionOrders().patch(\n", 2449 | " advertiserId=ADVERTISER_ID,\n", 2450 | " insertionOrderId=newinsertionorder_request['insertionOrderId'],\n", 2451 | " updateMask='entityStatus',\n", 2452 | " body=patch).execute()\n", 2453 | "\n", 2454 | " print(newinsertionorder_request)\n", 2455 | " return newinsertionorder_request\n", 2456 | "\n", 2457 | "\n", 2458 | "print('Insertion Order function created')" 2459 | ], 2460 | "execution_count": null, 2461 | "outputs": [] 2462 | }, 2463 | { 2464 | "cell_type": "markdown", 2465 | "metadata": { 2466 | "id": "1cIrHfi5-Yt9" 2467 | }, 2468 | "source": [ 2469 | "**Create LineItem template**\n", 2470 | "\n", 2471 | "Here we define a new function called 'create_lineitem', based on a template we specified.\n", 2472 | "\n", 2473 | "Note: the following template does not include any targeting controls by default. Normally, we **strongly** encourage the addition of targeting before activating a line item." 2474 | ] 2475 | }, 2476 | { 2477 | "cell_type": "code", 2478 | "metadata": { 2479 | "id": "2ffby0-Z-Yt-" 2480 | }, 2481 | "source": [ 2482 | "def create_lineitem(parent_io_id, new_li_name):\n", 2483 | " \"\"\"Creates a new DV360 lineitem object.\"\"\"\n", 2484 | "\n", 2485 | " # Define our new LineItem boilerplate\n", 2486 | " new_lineitem = {\n", 2487 | " 'advertiserId': ADVERTISER_ID,\n", 2488 | " 'insertionOrderId': parent_io_id,\n", 2489 | " 'displayName': new_li_name, # Define naming convention\n", 2490 | " 'lineItemType': 'LINE_ITEM_TYPE_DISPLAY_DEFAULT',\n", 2491 | " 'entityStatus': 'ENTITY_STATUS_DRAFT',\n", 2492 | " 'flight': {\n", 2493 | " 'flightDateType': 'LINE_ITEM_FLIGHT_DATE_TYPE_INHERITED',\n", 2494 | " },\n", 2495 | " 'pacing': {\n", 2496 | " 'pacingPeriod': 'PACING_PERIOD_DAILY',\n", 2497 | " 'pacingType': 'PACING_TYPE_EVEN',\n", 2498 | " 'dailyMaxMicros': '1000000'\n", 2499 | " },\n", 2500 | " 'frequencyCap': {\n", 2501 | " 'timeUnit': 'TIME_UNIT_MONTHS',\n", 2502 | " 'timeUnitCount': 1,\n", 2503 | " 'maxImpressions': 5\n", 2504 | " },\n", 2505 | " 'partnerRevenueModel': {\n", 2506 | " 'markupType': 'PARTNER_REVENUE_MODEL_MARKUP_TYPE_TOTAL_MEDIA_COST_MARKUP'\n", 2507 | " },\n", 2508 | " 'budget': {\n", 2509 | " 'budgetAllocationType': 'LINE_ITEM_BUDGET_ALLOCATION_TYPE_UNLIMITED',\n", 2510 | " 'budgetUnit': 'BUDGET_UNIT_CURRENCY'\n", 2511 | " },\n", 2512 | " 'bidStrategy': {\n", 2513 | " 'fixedBid': {\n", 2514 | " 'bidAmountMicros': '1000000'\n", 2515 | " }\n", 2516 | " }\n", 2517 | " }\n", 2518 | "\n", 2519 | " # API create() request to generate new Lineitem\n", 2520 | " newlineitem_request = display_video_service.advertisers().lineItems().create(\n", 2521 | " advertiserId=ADVERTISER_ID, body=new_lineitem).execute()\n", 2522 | "\n", 2523 | " # Define patch to activate new Line Item afer creation\n", 2524 | " patch = {\n", 2525 | " 'entityStatus': 'ENTITY_STATUS_ACTIVE',\n", 2526 | " }\n", 2527 | "\n", 2528 | " # API patch() request\n", 2529 | " display_video_service.advertisers().lineItems().patch(\n", 2530 | " advertiserId=ADVERTISER_ID,\n", 2531 | " lineItemId=newlineitem_request['lineItemId'],\n", 2532 | " updateMask='entityStatus',\n", 2533 | " body=patch).execute()\n", 2534 | "\n", 2535 | " print(newlineitem_request)\n", 2536 | " return newlineitem_request\n", 2537 | "\n", 2538 | "print('LineItem function created')" 2539 | ], 2540 | "execution_count": null, 2541 | "outputs": [] 2542 | }, 2543 | { 2544 | "cell_type": "markdown", 2545 | "metadata": { 2546 | "id": "BA6fttC7-Yt_" 2547 | }, 2548 | "source": [ 2549 | "**Build our new campaign**\n", 2550 | "\n", 2551 | "First, we'll loop through the list of countries generated at the beginning, and for each country, create a new Insertion Order by calling our function 'create_insertion_order'. Within that loop, we find every product that is sold in the corresponding country-code, and create a new Line Item for every matching product using our function 'create_lineitem'.\n", 2552 | "\n", 2553 | "Sit tight, this one can take a while (~10 mins)...\n", 2554 | "\n", 2555 | "[Link to DV360 UI](https://displayvideo.google.com/)" 2556 | ] 2557 | }, 2558 | { 2559 | "cell_type": "code", 2560 | "metadata": { 2561 | "id": "TBBdj0Zi-YuA" 2562 | }, 2563 | "source": [ 2564 | "%%time\n", 2565 | "for country_code in unique_country_codes:\n", 2566 | " # Create() and patch() new Insertion Order\n", 2567 | " io_name = f'Google Store | {country_code} | Display | Prospecting'\n", 2568 | " insertionorder = create_insertion_order(CAMPAIGN_ID, io_name)\n", 2569 | " for row in products:\n", 2570 | " if country_code in row['country code']:\n", 2571 | " # Create() and patch() new LineItem\n", 2572 | " li_name = f\"{row['country code']} | {row['title']} | {row['sku']}\"\n", 2573 | " lineitem = create_lineitem(insertionorder['insertionOrderId'], li_name)\n", 2574 | "\n", 2575 | "print('Process completed')" 2576 | ], 2577 | "execution_count": null, 2578 | "outputs": [] 2579 | }, 2580 | { 2581 | "cell_type": "markdown", 2582 | "metadata": { 2583 | "id": "gtbmAfRP-5tE" 2584 | }, 2585 | "source": [ 2586 | "If successful, the result should look similar to the below in DV360:\n", 2587 | "\n", 2588 | "![Finished campaign structure](https://github.com/google/dv360-automation/blob/master/docs/images/googlestore_campaign.png?raw=true)" 2589 | ] 2590 | }, 2591 | { 2592 | "cell_type": "markdown", 2593 | "metadata": { 2594 | "id": "fYwoPT-STy7Y" 2595 | }, 2596 | "source": [ 2597 | "## 2.2 Individual targeting\n", 2598 | "\n", 2599 | "Reference: https://developers.google.com/display-video/api/guides/managing-line-items/targeting" 2600 | ] 2601 | }, 2602 | { 2603 | "cell_type": "markdown", 2604 | "metadata": { 2605 | "id": "h_ANWETmUFHj" 2606 | }, 2607 | "source": [ 2608 | "Retrieve a list of available targeting options using [targetingTypes().targetingOptions()](https://developers.google.com/display-video/api/reference/rest/v1/targetingTypes.targetingOptions/list)\n", 2609 | "\n", 2610 | "The following example demonstrates retrieving of Browser targeting options only.\n", 2611 | "\n", 2612 | "The \"BrowserDetails\" field is only applicable with \"TARGETING_TYPE_BROWSER\"." 2613 | ] 2614 | }, 2615 | { 2616 | "cell_type": "code", 2617 | "metadata": { 2618 | "id": "T8_oXsJ46cFN" 2619 | }, 2620 | "source": [ 2621 | "# Create the page token variable.\n", 2622 | "next_page_token = ''\n", 2623 | "\n", 2624 | "while True:\n", 2625 | " # Request the targeting options list.\n", 2626 | " response = display_video_service.targetingTypes().targetingOptions().list(\n", 2627 | " advertiserId=ADVERTISER_ID,\n", 2628 | " targetingType='TARGETING_TYPE_BROWSER',\n", 2629 | " pageToken=next_page_token).execute()\n", 2630 | "\n", 2631 | " # Check if response is empty.\n", 2632 | " if not response:\n", 2633 | " print('List request returned no Targeting Options')\n", 2634 | " break\n", 2635 | "\n", 2636 | " # Iterate over retrieved targeting options.\n", 2637 | " options_dict = {}\n", 2638 | " for option in response['targetingOptions']:\n", 2639 | " options_dict[\n", 2640 | " option['targetingOptionId']] = option['browserDetails']['displayName']\n", 2641 | "\n", 2642 | " # Break out of loop if there is no next page.\n", 2643 | " if 'nextPageToken' not in response:\n", 2644 | " break\n", 2645 | "\n", 2646 | " # Update the next page token.\n", 2647 | " next_page_token = response['nextPageToken']\n", 2648 | "\n", 2649 | "pprint.pprint(options_dict)" 2650 | ], 2651 | "execution_count": null, 2652 | "outputs": [] 2653 | }, 2654 | { 2655 | "cell_type": "markdown", 2656 | "metadata": { 2657 | "id": "SHtjFi7mI3m5" 2658 | }, 2659 | "source": [ 2660 | "**Apply individual targeting criteria to single entity**" 2661 | ] 2662 | }, 2663 | { 2664 | "cell_type": "code", 2665 | "metadata": { 2666 | "id": "0gLSgH1sI3Br" 2667 | }, 2668 | "source": [ 2669 | "# Return list of Lineitems with active status\n", 2670 | "active_lineitems = get_active_lineitems(ADVERTISER_ID, CAMPAIGN_ID)\n", 2671 | "\n", 2672 | "# Fetch first Lineitem ID\n", 2673 | "lineitem_id = active_lineitems[0]\n", 2674 | "\n", 2675 | "# Create a assigned targeting option object.\n", 2676 | "assigned_targeting_option_obj = {\n", 2677 | " 'browserDetails': {\n", 2678 | " 'targetingOptionId': '500072'\n", 2679 | " }\n", 2680 | "}\n", 2681 | "\n", 2682 | "# Create the assigned targeting option.\n", 2683 | "assigned_targeting_option = display_video_service.advertisers().lineItems(\n", 2684 | ").targetingTypes().assignedTargetingOptions().create(\n", 2685 | " advertiserId=ADVERTISER_ID,\n", 2686 | " lineItemId=f'{lineitem_id}',\n", 2687 | " targetingType='TARGETING_TYPE_BROWSER',\n", 2688 | " body=assigned_targeting_option_obj\n", 2689 | ").execute()\n", 2690 | "\n", 2691 | "# Display the new assigned targeting option.\n", 2692 | "print(f\"Assigned Targeting Option {assigned_targeting_option['name']} created.\")" 2693 | ], 2694 | "execution_count": null, 2695 | "outputs": [] 2696 | }, 2697 | { 2698 | "cell_type": "markdown", 2699 | "metadata": { 2700 | "id": "NmMRfajo1jgB" 2701 | }, 2702 | "source": [ 2703 | "**Applying individual targeting criteria to multiple entities**" 2704 | ] 2705 | }, 2706 | { 2707 | "cell_type": "code", 2708 | "metadata": { 2709 | "id": "fM63xGck1i0g" 2710 | }, 2711 | "source": [ 2712 | "# Create the page token variable.\n", 2713 | "next_page_token = ''\n", 2714 | "\n", 2715 | "while True:\n", 2716 | "# Request the targeting options list.\n", 2717 | " response = display_video_service.googleAudiences().list(\n", 2718 | " advertiserId=ADVERTISER_ID,\n", 2719 | " filter='displayName : \"Technology\"',\n", 2720 | " pageToken=next_page_token).execute()\n", 2721 | "\n", 2722 | " # Check if response is empty.\n", 2723 | " if not response:\n", 2724 | " print('List request returned no Targeting Options')\n", 2725 | " break\n", 2726 | "\n", 2727 | " # Iterate over retrieved targeting options.\n", 2728 | " options_dict = {}\n", 2729 | " for option in response['googleAudiences']:\n", 2730 | " options_dict[option['googleAudienceId']] = [\n", 2731 | " option['displayName'], option['googleAudienceType']\n", 2732 | " ]\n", 2733 | "\n", 2734 | " # Break out of loop if there is no next page.\n", 2735 | " if 'nextPageToken' not in response:\n", 2736 | " break\n", 2737 | "\n", 2738 | " # Update the next page token.\n", 2739 | " next_page_token = response['nextPageToken']\n", 2740 | "\n", 2741 | "pprint.pprint(response)" 2742 | ], 2743 | "execution_count": null, 2744 | "outputs": [] 2745 | }, 2746 | { 2747 | "cell_type": "code", 2748 | "metadata": { 2749 | "id": "hvRPd12X4eMF" 2750 | }, 2751 | "source": [ 2752 | "google_audience_id = '92948'\n", 2753 | "\n", 2754 | "# Return list of Lineitems with active status\n", 2755 | "active_lineitems = get_active_lineitems(ADVERTISER_ID, CAMPAIGN_ID)\n", 2756 | "\n", 2757 | "# Create a assigned targeting option object.\n", 2758 | "assigned_targeting_option_obj = {\n", 2759 | " 'audienceGroupDetails': {\n", 2760 | " 'includedGoogleAudienceGroup': {\n", 2761 | " 'settings': [{\n", 2762 | " 'googleAudienceId': f'{google_audience_id}'\n", 2763 | " }]\n", 2764 | " }\n", 2765 | " }\n", 2766 | "}\n", 2767 | "\n", 2768 | "pprint.pprint(assigned_targeting_option_obj)\n", 2769 | "\n", 2770 | "# Update bulk targeting\n", 2771 | "for li in active_lineitems:\n", 2772 | " # Create the assigned targeting option.\n", 2773 | " assigned_targeting_option = display_video_service.advertisers().lineItems(\n", 2774 | " ).targetingTypes().assignedTargetingOptions().create(\n", 2775 | " advertiserId=ADVERTISER_ID,\n", 2776 | " lineItemId=f'{li}',\n", 2777 | " targetingType='TARGETING_TYPE_AUDIENCE_GROUP',\n", 2778 | " body=assigned_targeting_option_obj).execute()\n", 2779 | " # Display the new assigned targeting option.\n", 2780 | " print(f\"Targeting Option {assigned_targeting_option['name']} created.\")" 2781 | ], 2782 | "execution_count": null, 2783 | "outputs": [] 2784 | }, 2785 | { 2786 | "cell_type": "markdown", 2787 | "metadata": { 2788 | "id": "qiWKuw7uydMo" 2789 | }, 2790 | "source": [ 2791 | "## 2.3 Bulk targeting" 2792 | ] 2793 | }, 2794 | { 2795 | "cell_type": "markdown", 2796 | "metadata": { 2797 | "id": "7_ZQZnMJoXaM" 2798 | }, 2799 | "source": [ 2800 | "Bulk updates using templated targeting controls" 2801 | ] 2802 | }, 2803 | { 2804 | "cell_type": "code", 2805 | "metadata": { 2806 | "id": "gYTXfZR6ejvt" 2807 | }, 2808 | "source": [ 2809 | "def set_default_li_targeting(lineitem_id):\n", 2810 | " \"\"\"Sets default LineItem targeting according to standard template.\"\"\"\n", 2811 | "\n", 2812 | " # Define 'Channels'\n", 2813 | " create_channel_assigned_targetingoptions = []\n", 2814 | " for targeting_id in ['1777746835', '1778039430']:\n", 2815 | " create_channel_assigned_targetingoptions.append(\n", 2816 | " {'channelDetails': {\n", 2817 | " 'channelId': targeting_id,\n", 2818 | " 'negative': False\n", 2819 | " }})\n", 2820 | "\n", 2821 | " # Define 'Inventory'\n", 2822 | " create_inventory_assigned_targetingoptions = []\n", 2823 | " for targeting_id in ['1']:\n", 2824 | " create_inventory_assigned_targetingoptions.append(\n", 2825 | " {'inventorySourceDetails': {'inventorySourceId': targeting_id}}\n", 2826 | " )\n", 2827 | "\n", 2828 | " # Define 'Sensitive categories'\n", 2829 | " create_sensitive_cat_assigned_targetingoptions = []\n", 2830 | " sensitive_category = [\n", 2831 | " '1163177997', '1163178297', '118521027123', '118521027843',\n", 2832 | " '118521028083', '118521028563', '118521028803', '1596254697'\n", 2833 | " ]\n", 2834 | " for targeting_id in sensitive_category:\n", 2835 | " create_sensitive_cat_assigned_targetingoptions.append({\n", 2836 | " 'sensitiveCategoryExclusionDetails': {\n", 2837 | " 'excludedTargetingOptionId': targeting_id\n", 2838 | " }\n", 2839 | " })\n", 2840 | "\n", 2841 | " # Define 'Digital content labels'\n", 2842 | " create_digital_content_assigned_targetingoptions = []\n", 2843 | " content_rating_tier = ['19875634320', '19875634200', '19875634080']\n", 2844 | " for targeting_id in content_rating_tier:\n", 2845 | " create_digital_content_assigned_targetingoptions.append({\n", 2846 | " 'digitalContentLabelExclusionDetails': {\n", 2847 | " 'excludedTargetingOptionId': targeting_id\n", 2848 | " }\n", 2849 | " })\n", 2850 | "\n", 2851 | " # Contruct request\n", 2852 | " bulk_edit_line_item_request = {\n", 2853 | " 'createRequests': [\n", 2854 | " {\n", 2855 | " 'targetingType':\n", 2856 | " 'TARGETING_TYPE_CHANNEL',\n", 2857 | " 'assignedTargetingOptions': [\n", 2858 | " create_channel_assigned_targetingoptions\n", 2859 | " ]\n", 2860 | " },\n", 2861 | " {\n", 2862 | " 'targetingType':\n", 2863 | " 'TARGETING_TYPE_INVENTORY_SOURCE',\n", 2864 | " 'assignedTargetingOptions': [\n", 2865 | " create_inventory_assigned_targetingoptions\n", 2866 | " ]\n", 2867 | " },\n", 2868 | " {\n", 2869 | " 'targetingType':\n", 2870 | " 'TARGETING_TYPE_SENSITIVE_CATEGORY_EXCLUSION',\n", 2871 | " 'assignedTargetingOptions': [\n", 2872 | " create_sensitive_cat_assigned_targetingoptions\n", 2873 | " ]\n", 2874 | " },\n", 2875 | " {\n", 2876 | " 'targetingType':\n", 2877 | " 'TARGETING_TYPE_DIGITAL_CONTENT_LABEL_EXCLUSION',\n", 2878 | " 'assignedTargetingOptions': [\n", 2879 | " create_digital_content_assigned_targetingoptions\n", 2880 | " ]\n", 2881 | " },\n", 2882 | " ]\n", 2883 | " }\n", 2884 | "\n", 2885 | " # Edit the line item targeting.\n", 2886 | " bulk_request = display_video_service.advertisers().lineItems(\n", 2887 | " ).bulkEditLineItemAssignedTargetingOptions(\n", 2888 | " advertiserId=ADVERTISER_ID,\n", 2889 | " lineItemId=lineitem_id,\n", 2890 | " body=bulk_edit_line_item_request\n", 2891 | " )\n", 2892 | "\n", 2893 | " bulk_response = bulk_request.execute()\n", 2894 | "\n", 2895 | " # Check if response is empty.\n", 2896 | " # If not, iterate over and display new assigned targeting options.\n", 2897 | " if not bulk_response:\n", 2898 | " print('Bulk edit request created no new AssignedTargetingOptions')\n", 2899 | " else:\n", 2900 | " for assigned_targeting_option in bulk_response[\n", 2901 | " 'createdAssignedTargetingOptions']:\n", 2902 | " print(f\"Targeting Option {assigned_targeting_option['name']} created.\")\n", 2903 | "\n", 2904 | "\n", 2905 | "print('Lineitem targeting function created')" 2906 | ], 2907 | "execution_count": null, 2908 | "outputs": [] 2909 | }, 2910 | { 2911 | "cell_type": "markdown", 2912 | "metadata": { 2913 | "id": "5BhB61oWyu0B" 2914 | }, 2915 | "source": [ 2916 | "**Retrieve list of active LineItems, and Apply bulk targeting**" 2917 | ] 2918 | }, 2919 | { 2920 | "cell_type": "code", 2921 | "metadata": { 2922 | "id": "MLgcXk4InnlP" 2923 | }, 2924 | "source": [ 2925 | "# Return list of Lineitems with active status\n", 2926 | "active_lineitems = get_active_lineitems(ADVERTISER_ID, CAMPAIGN_ID)\n", 2927 | "\n", 2928 | "# Update bulk targeting\n", 2929 | "for li in active_lineitems:\n", 2930 | " set_default_li_targeting(li)" 2931 | ], 2932 | "execution_count": null, 2933 | "outputs": [] 2934 | }, 2935 | { 2936 | "cell_type": "markdown", 2937 | "metadata": { 2938 | "id": "bwZ0PLWM-YuC" 2939 | }, 2940 | "source": [ 2941 | "## 2.4 Optimisation (external trigger)" 2942 | ] 2943 | }, 2944 | { 2945 | "cell_type": "markdown", 2946 | "metadata": { 2947 | "id": "un90ah0D-YuG" 2948 | }, 2949 | "source": [ 2950 | "The following optimisations will be completed on your campaign, created earlier.\n", 2951 | "\n", 2952 | "**Create functions to 'deactivate' or 'optimise' Lineitems**" 2953 | ] 2954 | }, 2955 | { 2956 | "cell_type": "code", 2957 | "metadata": { 2958 | "id": "il-nDuIa-YuG" 2959 | }, 2960 | "source": [ 2961 | "def optimise_lineitem(lineitem_id, action):\n", 2962 | " \"\"\"Optimises lineitem according to given parameter.\"\"\"\n", 2963 | "\n", 2964 | " lineitem_object = display_video_service.advertisers().lineItems().get(\n", 2965 | " advertiserId=ADVERTISER_ID,\n", 2966 | " lineItemId=lineitem_id).execute()\n", 2967 | "\n", 2968 | " if lineitem_object['entityStatus'] == 'ENTITY_STATUS_ACTIVE':\n", 2969 | " if action == 'pause':\n", 2970 | " patch = {\n", 2971 | " 'entityStatus': 'ENTITY_STATUS_PAUSED',\n", 2972 | " }\n", 2973 | "\n", 2974 | " lineitem_patched = display_video_service.advertisers().lineItems().patch(\n", 2975 | " advertiserId=ADVERTISER_ID,\n", 2976 | " lineItemId=lineitem_id,\n", 2977 | " updateMask='entityStatus',\n", 2978 | " body=patch).execute()\n", 2979 | " print(f\"LineItemID {lineitem_patched['name']} was paused\")\n", 2980 | "\n", 2981 | " elif action == 'optimise':\n", 2982 | " patch = {'bidStrategy': {'fixedBid': {'bidAmountMicros': '500000'},}}\n", 2983 | "\n", 2984 | " lineitem_patched = display_video_service.advertisers().lineItems().patch(\n", 2985 | " advertiserId=ADVERTISER_ID,\n", 2986 | " lineItemId=lineitem_id,\n", 2987 | " updateMask='bidStrategy',\n", 2988 | " body=patch).execute()\n", 2989 | "\n", 2990 | " print(f\"{lineitem_patched['name']} was optimised\")\n", 2991 | " else:\n", 2992 | " print(\"Not a valid action, must be either 'pause' or 'optimise'\")\n", 2993 | " else:\n", 2994 | " print(\n", 2995 | " f\"{lineitem_object['name']} already paused/archived - no action taken\")\n", 2996 | "\n", 2997 | "print('Optimisation function created')" 2998 | ], 2999 | "execution_count": null, 3000 | "outputs": [] 3001 | }, 3002 | { 3003 | "cell_type": "markdown", 3004 | "metadata": { 3005 | "id": "8FC5rtARpFfO" 3006 | }, 3007 | "source": [ 3008 | "**Creat list of out of stock products**" 3009 | ] 3010 | }, 3011 | { 3012 | "cell_type": "code", 3013 | "metadata": { 3014 | "id": "U0VexcT53rl5" 3015 | }, 3016 | "source": [ 3017 | "out_of_stock_list = []\n", 3018 | "products = csv.DictReader(io.StringIO(contents))\n", 3019 | "\n", 3020 | "# Iterate through each row, checking for products where availability = 0\n", 3021 | "for row in products:\n", 3022 | " if row['availability'] == '0':\n", 3023 | " out_of_stock_list.append(row['sku'])\n", 3024 | "\n", 3025 | "# This should generate a list of 9 SKUs that are no-longer in stock\n", 3026 | "print(\n", 3027 | " f'Found {len(out_of_stock_list)} out-of-stock products {out_of_stock_list}')" 3028 | ], 3029 | "execution_count": null, 3030 | "outputs": [] 3031 | }, 3032 | { 3033 | "cell_type": "markdown", 3034 | "metadata": { 3035 | "id": "9ud9o6TOpnTK" 3036 | }, 3037 | "source": [ 3038 | "**Process optimisation**" 3039 | ] 3040 | }, 3041 | { 3042 | "cell_type": "code", 3043 | "metadata": { 3044 | "id": "EchVLYGO39Jr" 3045 | }, 3046 | "source": [ 3047 | "# Return list of Lineitems with active status\n", 3048 | "active_lineitems = get_active_lineitems(ADVERTISER_ID, CAMPAIGN_ID)\n", 3049 | "\n", 3050 | "# Iterate through out-of-stock list. If sku is found in lineitem's name, perform optimisation.\n", 3051 | "for product in out_of_stock_list:\n", 3052 | " for key, value in active_lineitems.items():\n", 3053 | " if product in key:\n", 3054 | " optimise_lineitem(value, 'pause')" 3055 | ], 3056 | "execution_count": null, 3057 | "outputs": [] 3058 | }, 3059 | { 3060 | "cell_type": "markdown", 3061 | "metadata": { 3062 | "id": "9d415mAw5rwQ" 3063 | }, 3064 | "source": [ 3065 | "## 2.5 Optimisation (reporting data)" 3066 | ] 3067 | }, 3068 | { 3069 | "cell_type": "markdown", 3070 | "metadata": { 3071 | "id": "lO-c-kdd27XV" 3072 | }, 3073 | "source": [ 3074 | "As your new campaign has no performance data, the following optimisations will be completed on an existing campaign with historical data.\n", 3075 | "\n", 3076 | "**Create new performance report and fetch results**" 3077 | ] 3078 | }, 3079 | { 3080 | "cell_type": "code", 3081 | "metadata": { 3082 | "id": "_kuwxEO4qcaY" 3083 | }, 3084 | "source": [ 3085 | "# Define DV360 report definition (i.e. metrics and filters)\n", 3086 | "report_definition = {\n", 3087 | " 'params': {\n", 3088 | " 'type': 'TYPE_GENERAL',\n", 3089 | " 'metrics': [\n", 3090 | " 'METRIC_IMPRESSIONS', 'METRIC_CLICKS', 'METRIC_CTR',\n", 3091 | " 'METRIC_REVENUE_ADVERTISER'\n", 3092 | " ],\n", 3093 | " 'groupBys': [\n", 3094 | " 'FILTER_ADVERTISER', 'FILTER_INSERTION_ORDER', 'FILTER_LINE_ITEM',\n", 3095 | " 'FILTER_ADVERTISER_CURRENCY'\n", 3096 | " ],\n", 3097 | " 'filters': [{\n", 3098 | " 'type': 'FILTER_ADVERTISER',\n", 3099 | " 'value': ADVERTISER_ID\n", 3100 | " }],\n", 3101 | " },\n", 3102 | " 'metadata': {\n", 3103 | " 'title': 'DV360 Automation API-generated report',\n", 3104 | " 'dataRange': 'LAST_90_DAYS',\n", 3105 | " 'format': 'csv'\n", 3106 | " },\n", 3107 | " 'schedule': {\n", 3108 | " 'frequency': 'ONE_TIME'\n", 3109 | " }\n", 3110 | "}\n", 3111 | "\n", 3112 | "# Create new query using report definition\n", 3113 | "operation = dbm_service.queries().createquery(body=report_definition).execute()\n", 3114 | "pprint.pprint(operation)\n", 3115 | "\n", 3116 | "\n", 3117 | "# Runs the given Queries.getquery request, retrying with an exponential\n", 3118 | "# backoff. Returns completed operation. Will raise an exception if the\n", 3119 | "# operation takes more than five hours to complete.\n", 3120 | "@retry.Retry(\n", 3121 | " predicate=retry.if_exception_type(Exception),\n", 3122 | " initial=5,\n", 3123 | " maximum=60,\n", 3124 | " deadline=18000)\n", 3125 | "def check_get_query_completion(getquery_request):\n", 3126 | " response = getquery_request.execute()\n", 3127 | " pprint.pprint(response)\n", 3128 | " if response['metadata']['running']:\n", 3129 | " raise Exception('The operation has not completed.')\n", 3130 | " return response\n", 3131 | "\n", 3132 | "getquery_request = dbm_service.queries().getquery(queryId=operation['queryId'])\n", 3133 | "response = check_get_query_completion(getquery_request)" 3134 | ], 3135 | "execution_count": null, 3136 | "outputs": [] 3137 | }, 3138 | { 3139 | "cell_type": "markdown", 3140 | "metadata": { 3141 | "id": "wgVYtoeC3Nh8" 3142 | }, 3143 | "source": [ 3144 | "**Load report to Pandas DataFrame**" 3145 | ] 3146 | }, 3147 | { 3148 | "cell_type": "code", 3149 | "metadata": { 3150 | "id": "kpHMihk1rACk" 3151 | }, 3152 | "source": [ 3153 | "# Capture report URL from response\n", 3154 | "report_url = response['metadata']['googleCloudStoragePathForLatestReport']\n", 3155 | "\n", 3156 | "# Use skipfooter to remove report footer from data\n", 3157 | "report_df = pd.read_csv(report_url, skipfooter=16, engine='python')\n", 3158 | "report_df.head(10)" 3159 | ], 3160 | "execution_count": null, 3161 | "outputs": [] 3162 | }, 3163 | { 3164 | "cell_type": "markdown", 3165 | "metadata": { 3166 | "id": "bv8FTwDI-YuE" 3167 | }, 3168 | "source": [ 3169 | "**Create two lists of poorly performing LineItems**\n", 3170 | "1. LineItems that should be paused\n", 3171 | "2. Lineitems to reduce bids" 3172 | ] 3173 | }, 3174 | { 3175 | "cell_type": "code", 3176 | "metadata": { 3177 | "id": "Lo7Dmcc8-YuE" 3178 | }, 3179 | "source": [ 3180 | "# Define our 'KPIs'\n", 3181 | "ctr_to_pause = 0.1\n", 3182 | "ctr_to_optimise = 0.3\n", 3183 | "imp_threshold = 5000\n", 3184 | "\n", 3185 | "# Convert IDs to remove decimal point, then string\n", 3186 | "report_df['Line Item ID'] = report_df['Line Item ID'].apply(int)\n", 3187 | "lineitems_to_pause = report_df.query('Impressions > @imp_threshold and (Clicks / Impressions)*100 < @ctr_to_pause')\n", 3188 | "lineitems_to_reducebid = report_df.query('Impressions > @imp_threshold and (Clicks / Impressions)*100 > @ctr_to_pause < @ctr_to_optimise')\n", 3189 | "\n", 3190 | "# Convert results to Python list\n", 3191 | "lineitems_to_pause = list(lineitems_to_pause['Line Item ID'])\n", 3192 | "lineitems_to_reducebid = list(lineitems_to_reducebid['Line Item ID'])\n", 3193 | "\n", 3194 | "print(f'Found {len(lineitems_to_pause)} LineItems with a CTR'\n", 3195 | " f'< {ctr_to_pause}% and > {imp_threshold} impressions:'\n", 3196 | " f'{lineitems_to_pause}')\n", 3197 | "\n", 3198 | "print(f'Found {len(lineitems_to_reducebid)} LineItems with a CTR'\n", 3199 | " f' between {ctr_to_pause}%-{ctr_to_optimise}%, and > {imp_threshold}'\n", 3200 | " f'\\n impressions: {lineitems_to_reducebid}')" 3201 | ], 3202 | "execution_count": null, 3203 | "outputs": [] 3204 | }, 3205 | { 3206 | "cell_type": "markdown", 3207 | "metadata": { 3208 | "id": "i5OdhLB2-YuI" 3209 | }, 3210 | "source": [ 3211 | "**Process optimisation**" 3212 | ] 3213 | }, 3214 | { 3215 | "cell_type": "code", 3216 | "metadata": { 3217 | "id": "4srm9okw-YuJ" 3218 | }, 3219 | "source": [ 3220 | "%%time\n", 3221 | "if lineitems_to_pause:\n", 3222 | " for lineitem in lineitems_to_pause:\n", 3223 | " optimise_lineitem(str(lineitem), 'pause')\n", 3224 | "\n", 3225 | "if lineitems_to_reducebid:\n", 3226 | " for lineitem in lineitems_to_reducebid:\n", 3227 | " optimise_lineitem(str(lineitem), 'optimise')\n", 3228 | " \n", 3229 | "print('Optimisation completed')" 3230 | ], 3231 | "execution_count": null, 3232 | "outputs": [] 3233 | }, 3234 | { 3235 | "cell_type": "markdown", 3236 | "metadata": { 3237 | "id": "mbm9y7SJC0vO" 3238 | }, 3239 | "source": [ 3240 | "## 2.6 Creative upload" 3241 | ] 3242 | }, 3243 | { 3244 | "cell_type": "markdown", 3245 | "metadata": { 3246 | "id": "eAx3HvokmU8s" 3247 | }, 3248 | "source": [ 3249 | "**Uploading Display creatives from remote storage (http)**\n", 3250 | "\n", 3251 | "The following demonstrates how to upload image assets from remote storage, but it's also possible to upload from local storage.\n", 3252 | "\n", 3253 | "Reference: https://developers.google.com/display-video/api/guides/creating-creatives/overview" 3254 | ] 3255 | }, 3256 | { 3257 | "cell_type": "code", 3258 | "metadata": { 3259 | "id": "bDqB-2GzEKIj" 3260 | }, 3261 | "source": [ 3262 | "def upload_creative_image_asset(asset_url, click_url):\n", 3263 | " \"\"\"Creates a new DV360 creative object.\"\"\"\n", 3264 | "\n", 3265 | " # Fetch asset from cloud storage using requests library\n", 3266 | " asset = requests.get(asset_url)\n", 3267 | "\n", 3268 | " # Create upload object from http image url\n", 3269 | " fh = io.BytesIO(asset.content)\n", 3270 | " media_body = http.MediaIoBaseUpload(fh, mimetype='image/png',\n", 3271 | " chunksize=1024*1024, resumable=True)\n", 3272 | "\n", 3273 | " # Extract filename from url path\n", 3274 | " filename = str(asset_url.rsplit(sep='/', maxsplit=1)[1])\n", 3275 | "\n", 3276 | " # Create the request body\n", 3277 | " body = {'filename': filename}\n", 3278 | "\n", 3279 | " # Upload the asset\n", 3280 | " asset_request = display_video_service.advertisers().assets().upload(\n", 3281 | " advertiserId=ADVERTISER_ID, body=body, media_body=media_body).execute()\n", 3282 | "\n", 3283 | " # Display the new asset media ID\n", 3284 | " print(f\"Asset was created with media ID {asset_request['asset']['mediaId']}\")\n", 3285 | "\n", 3286 | " display_name = f'{filename}'.split(sep='.')[0].lower() + ' 300x250'\n", 3287 | "\n", 3288 | " # Create a creative object.\n", 3289 | " creative_obj = {\n", 3290 | " 'displayName':\n", 3291 | " f'{display_name}',\n", 3292 | " 'entityStatus':\n", 3293 | " 'ENTITY_STATUS_ACTIVE',\n", 3294 | " 'creativeType':\n", 3295 | " 'CREATIVE_TYPE_STANDARD',\n", 3296 | " 'hostingSource':\n", 3297 | " 'HOSTING_SOURCE_HOSTED',\n", 3298 | " 'dimensions': {\n", 3299 | " 'widthPixels': 300,\n", 3300 | " 'heightPixels': 250\n", 3301 | " },\n", 3302 | " 'assets': [{\n", 3303 | " 'asset': {\n", 3304 | " 'mediaId': asset_request['asset']['mediaId']\n", 3305 | " },\n", 3306 | " 'role': 'ASSET_ROLE_MAIN'\n", 3307 | " }],\n", 3308 | " 'exitEvents': [{\n", 3309 | " 'type': 'EXIT_EVENT_TYPE_DEFAULT',\n", 3310 | " 'url': f'{click_url}',\n", 3311 | " }]\n", 3312 | " }\n", 3313 | "\n", 3314 | " creative_request = display_video_service.advertisers().creatives().create(\n", 3315 | " advertiserId=ADVERTISER_ID,\n", 3316 | " body=creative_obj\n", 3317 | " ).execute()\n", 3318 | "\n", 3319 | " # Display the new creative ID\n", 3320 | " print(f\"Creative was created with ID {creative_request['creativeId']}\"\n", 3321 | " f\" and DisplayName '{creative_request['displayName']}'\")\n", 3322 | "\n", 3323 | " pprint.pprint(creative_request)\n", 3324 | "\n", 3325 | "print('Creative upload function defined')" 3326 | ], 3327 | "execution_count": null, 3328 | "outputs": [] 3329 | }, 3330 | { 3331 | "cell_type": "markdown", 3332 | "metadata": { 3333 | "id": "UTRn3IlxjWbY" 3334 | }, 3335 | "source": [ 3336 | "**Upload image creatives**\n", 3337 | "\n", 3338 | "Note, all of the following assets are the same dimension (300x250) and type 'CREATIVE_TYPE_STANDARD'. \n", 3339 | "\n", 3340 | "When uploading assets of multiple sizes, the creatives.create body must reflect this." 3341 | ] 3342 | }, 3343 | { 3344 | "cell_type": "code", 3345 | "metadata": { 3346 | "id": "yhjRUJtGN86X" 3347 | }, 3348 | "source": [ 3349 | "image_assets = {\n", 3350 | " 'https://github.com/google/dv360-automation/blob/master/docs/images/googlestore/pixelbook.png?raw=true':\n", 3351 | " 'https://store.google.com/product/google_pixelbook',\n", 3352 | " 'https://github.com/google/dv360-automation/blob/master/docs/images/googlestore/googlehome.png?raw=true':\n", 3353 | " 'https://store.google.com/product/google_home_hub',\n", 3354 | " 'https://github.com/google/dv360-automation/blob/master/docs/images/googlestore/googlehomemini.png?raw=true':\n", 3355 | " 'https://store.google.com/product/google_home_mini',\n", 3356 | " 'https://github.com/google/dv360-automation/blob/master/docs/images/googlestore/pixel2.png?raw=true':\n", 3357 | " 'https://store.google.com/product/pixel_2',\n", 3358 | " 'https://github.com/google/dv360-automation/blob/master/docs/images/googlestore/chromecastultra.png?raw=true':\n", 3359 | " 'https://store.google.com/product/chromecast_ultra'\n", 3360 | "}\n", 3361 | "\n", 3362 | "for asset, click_url in image_assets.items():\n", 3363 | " upload_creative_image_asset(asset, click_url)" 3364 | ], 3365 | "execution_count": null, 3366 | "outputs": [] 3367 | }, 3368 | { 3369 | "cell_type": "markdown", 3370 | "metadata": { 3371 | "id": "KzSBf65OnjWH" 3372 | }, 3373 | "source": [ 3374 | "## 2.7 Challenge" 3375 | ] 3376 | }, 3377 | { 3378 | "cell_type": "markdown", 3379 | "metadata": { 3380 | "id": "zhcY4KyOvZWR" 3381 | }, 3382 | "source": [ 3383 | "Challenge: build a new campaign for 'Google Airways' using the flights feed provided [here](https://docs.google.com/spreadsheets/d/1CmP0q7QQa0GPJnLlSgqROw_oMXrrbNj8osITTR1-Wh8/edit#gid=1820110788).\n", 3384 | "\n", 3385 | "**Tips**\n", 3386 | "\n", 3387 | "* You don't need to rewrite any functions, reuse the existing ones\n", 3388 | "* Don't forget to use print() statements to see progress within a for loop\n", 3389 | "\n", 3390 | "Your final campaign should look similar to the below:\n", 3391 | "\n", 3392 | "![Example campaign structure](https://github.com/google/dv360-automation/blob/master/docs/images/googleairways_campaign.png?raw=true)\n" 3393 | ] 3394 | }, 3395 | { 3396 | "cell_type": "code", 3397 | "metadata": { 3398 | "id": "H2WDUjRQ_bkA" 3399 | }, 3400 | "source": [ 3401 | "#TODO" 3402 | ], 3403 | "execution_count": null, 3404 | "outputs": [] 3405 | }, 3406 | { 3407 | "cell_type": "markdown", 3408 | "metadata": { 3409 | "id": "-tFjU6q1_MSX" 3410 | }, 3411 | "source": [ 3412 | "**Solution**" 3413 | ] 3414 | }, 3415 | { 3416 | "cell_type": "code", 3417 | "metadata": { 3418 | "id": "2KaNBkNxK3Zl" 3419 | }, 3420 | "source": [ 3421 | "%%time\n", 3422 | "# Load flight information from CSV file\n", 3423 | "googleairways_routes = files.upload()\n", 3424 | "contents = next(iter(googleairways_routes.values())).decode('utf-8')\n", 3425 | "routes = list(csv.DictReader(io.StringIO(contents)))\n", 3426 | "\n", 3427 | "# Create a unique set (de-duped) of cities from the routes provided\n", 3428 | "unique_cities = set()\n", 3429 | "for row in routes:\n", 3430 | " unique_cities.add(row['airport-city'])\n", 3431 | "print(unique_cities)\n", 3432 | "\n", 3433 | "# Create Campaign and Patch()\n", 3434 | "new_campaign = create_campaign('Google Airways')\n", 3435 | "print(new_campaign)\n", 3436 | "\n", 3437 | "# Step through each city within our unique set of cities\n", 3438 | "for city in unique_cities:\n", 3439 | " # Create Insertion Order and Patch()\n", 3440 | " io_name = f'Flights | {city}'\n", 3441 | " create_io = create_insertion_order(new_campaign['campaignId'], io_name)\n", 3442 | "# Step through each route(row) of the CSV upload\n", 3443 | " for row in routes:\n", 3444 | " if city == row['airport-city']:\n", 3445 | " # Create LineItems and Patch()\n", 3446 | " li_name = f\"Flight {row['flightno']} | {row['depairport-city']} to {row['arrairport-city']}\"\n", 3447 | " create_lis = create_lineitem(create_io['insertionOrderId'], li_name)\n", 3448 | "\n", 3449 | "print('Process completed')" 3450 | ], 3451 | "execution_count": null, 3452 | "outputs": [] 3453 | }, 3454 | { 3455 | "cell_type": "markdown", 3456 | "metadata": { 3457 | "id": "DNlljNZGxkFB" 3458 | }, 3459 | "source": [ 3460 | "[Link to DV360 UI](https://displayvideo.google.com/)" 3461 | ] 3462 | }, 3463 | { 3464 | "cell_type": "markdown", 3465 | "metadata": { 3466 | "id": "A9AsAKyL_8Nw" 3467 | }, 3468 | "source": [ 3469 | "# Resources" 3470 | ] 3471 | }, 3472 | { 3473 | "cell_type": "markdown", 3474 | "metadata": { 3475 | "id": "vGwAmM9TgyZf" 3476 | }, 3477 | "source": [ 3478 | "* [Getting started with SDF](https://support.google.com/displayvideo/answer/6301070?hl=en) in DV360 guide\n", 3479 | "* [Structured Data Files (SDF)](https://developers.google.com/bid-manager/guides/structured-data-file/format) developer guide\n", 3480 | "* Getting started with the [Display & Video 360 API](https://developers.google.com/display-video/api/guides/getting-started/overview) developer guide\n", 3481 | "* Getting started with the [DoubleClick Bid Manager API](https://developers.google.com/bid-manager/guides/getting-started-api) developer guide\n", 3482 | "* How to access [Entity Read Files](https://developers.google.com/bid-manager/guides/entity-read/overview)\n", 3483 | "* Quickstart: Setup the [Vision API](https://cloud.google.com/vision/docs/setup)\n", 3484 | "\n", 3485 | "---\n", 3486 | "\n", 3487 | "Please help us improve this workshop by completing the [satisfaction survey](https://docs.google.com/forms/d/e/1FAIpQLScohvcSHWuUHdoVB--Q6YSpHRMFwmnl3BjCcM23X6RZa79kkw/viewform)\n", 3488 | "\n", 3489 | "Thank you!" 3490 | ] 3491 | }, 3492 | { 3493 | "cell_type": "markdown", 3494 | "metadata": { 3495 | "id": "NNXYZjPw0RK2" 3496 | }, 3497 | "source": [ 3498 | "# Clean up\n" 3499 | ] 3500 | }, 3501 | { 3502 | "cell_type": "markdown", 3503 | "metadata": { 3504 | "id": "lbc_BHYPZE5g" 3505 | }, 3506 | "source": [ 3507 | "To clean up all of the DV360 resources used during these exercises, you can run the following script. **Warning**: this will remove all Campaigns from the DV360 advertiser specified in ADVERTISER_ID, unless they are explicitly defined as a 'protected_campaign'" 3508 | ] 3509 | }, 3510 | { 3511 | "cell_type": "code", 3512 | "metadata": { 3513 | "id": "tjnTinGYj8Xl" 3514 | }, 3515 | "source": [ 3516 | "# Exclude following campaigns in the reset process\n", 3517 | "protected_campaigns = ['1914007','985747']\n", 3518 | "\n", 3519 | "def reset_demo_account():\n", 3520 | " \"\"\"Reset DV360 account to earlier state.\"\"\"\n", 3521 | "\n", 3522 | " print('Resetting DV360 account...')\n", 3523 | "\n", 3524 | " # Reactivate Campaigns\n", 3525 | " list_campaigns = display_video_service.advertisers().campaigns().list(\n", 3526 | " advertiserId=ADVERTISER_ID,\n", 3527 | " filter='entityStatus=\"ENTITY_STATUS_ACTIVE\"').execute()\n", 3528 | "\n", 3529 | " results = list_campaigns['campaigns']\n", 3530 | " print(f'Found {len(results)} active campaigns')\n", 3531 | "\n", 3532 | " for index, campaign in enumerate(results, start=1):\n", 3533 | " print(f'Campaign {index} of {len(results)}')\n", 3534 | " pause_campaign(campaign['campaignId'])\n", 3535 | "\n", 3536 | " # Reactivate LineItems\n", 3537 | " list_lineitems = display_video_service.advertisers().lineItems().list(\n", 3538 | " advertiserId=ADVERTISER_ID,\n", 3539 | " filter='entityStatus=\"ENTITY_STATUS_PAUSED\" AND campaignId=\"1914007\"'\n", 3540 | " ).execute()\n", 3541 | "\n", 3542 | " if not list_lineitems:\n", 3543 | " print('No paused lineitems found')\n", 3544 | " else:\n", 3545 | " for index, li in enumerate(list_lineitems['lineItems'], start=1):\n", 3546 | " print(f\"Lineitem {index} of {len(list_lineitems['lineItems'])}\")\n", 3547 | " lineitem_id = li['lineItemId']\n", 3548 | " activate_lineitem(lineitem_id)\n", 3549 | "\n", 3550 | " print('Account reset completed')\n", 3551 | "\n", 3552 | "\n", 3553 | "def delete_campaign(campaign_id):\n", 3554 | " \"\"\"Updates DV360 campaign object status to deleted.\"\"\"\n", 3555 | "\n", 3556 | " if campaign_id in protected_campaigns:\n", 3557 | " print(f'Campaign ID {campaign_id} not deleted (protected campaign)')\n", 3558 | " else:\n", 3559 | " try:\n", 3560 | " display_video_service.advertisers().campaigns().delete(\n", 3561 | " advertiserId=ADVERTISER_ID, campaignId=campaign_id).execute()\n", 3562 | " print(f'{campaign_id} successfully deleted')\n", 3563 | " except Exception:\n", 3564 | " print('Could not delete campaign')\n", 3565 | "\n", 3566 | "\n", 3567 | "def archive_campaign(campaign_id):\n", 3568 | " \"\"\"Updates DV360 campaign object status to archived.\"\"\"\n", 3569 | "\n", 3570 | " patch = {'entityStatus': 'ENTITY_STATUS_ARCHIVED'}\n", 3571 | "\n", 3572 | " if campaign_id in protected_campaigns:\n", 3573 | " print(f'Campaign ID {campaign_id} not archived (protected campaign)')\n", 3574 | " else:\n", 3575 | " archive_campaign = display_video_service.advertisers().campaigns().patch(\n", 3576 | " advertiserId=ADVERTISER_ID,\n", 3577 | " campaignId=campaign_id,\n", 3578 | " updateMask='entityStatus',\n", 3579 | " body=patch).execute()\n", 3580 | " print(f'Campaign ID {campaign_id} successfully archived')\n", 3581 | "\n", 3582 | "\n", 3583 | "def pause_campaign(campaign_id):\n", 3584 | " \"\"\"Updates DV360 campaign object status to paused.\"\"\"\n", 3585 | "\n", 3586 | " patch = {'entityStatus': 'ENTITY_STATUS_PAUSED'}\n", 3587 | "\n", 3588 | " if campaign_id in protected_campaigns:\n", 3589 | " print(f'Campaign ID {campaign_id} not paused (protected campaign)')\n", 3590 | " else:\n", 3591 | " display_video_service.advertisers().campaigns().patch(\n", 3592 | " advertiserId=ADVERTISER_ID,\n", 3593 | " campaignId=campaign_id,\n", 3594 | " updateMask='entityStatus',\n", 3595 | " body=patch).execute()\n", 3596 | " print(f'Campaign ID {campaign_id} successfully paused')\n", 3597 | "\n", 3598 | "\n", 3599 | "def activate_lineitem(lineitem_id):\n", 3600 | " \"\"\"Updates DV360 lineitem object status to active.\"\"\"\n", 3601 | "\n", 3602 | " patch = {'entityStatus': 'ENTITY_STATUS_ACTIVE'}\n", 3603 | "\n", 3604 | " display_video_service.advertisers().lineItems().patch(\n", 3605 | " lineItemId=lineitem_id,\n", 3606 | " advertiserId=ADVERTISER_ID,\n", 3607 | " updateMask='entityStatus',\n", 3608 | " body=patch).execute()\n", 3609 | " print(f'Lineitem ID {lineitem_id} reactivated')" 3610 | ], 3611 | "execution_count": null, 3612 | "outputs": [] 3613 | }, 3614 | { 3615 | "cell_type": "code", 3616 | "metadata": { 3617 | "id": "VXGrLBA7FWqk" 3618 | }, 3619 | "source": [ 3620 | "# @title { display-mode: \"form\" }\n", 3621 | "#@markdown Reset DV360 account\n", 3622 | "\n", 3623 | "# Call main function to intialise reset procedure\n", 3624 | "reset_demo_account()" 3625 | ], 3626 | "execution_count": null, 3627 | "outputs": [] 3628 | }, 3629 | { 3630 | "cell_type": "markdown", 3631 | "metadata": { 3632 | "id": "ftw74L9-lK7T" 3633 | }, 3634 | "source": [ 3635 | "Copyright 2020 Google Inc. Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, 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" 3636 | ] 3637 | } 3638 | ] 3639 | } --------------------------------------------------------------------------------