Автоматизация «чистки» площадок в РСЯ

Используем API Директа и питон-скрипт. Понадобится access_token для работы с API Директа. Если его нет, смотрите как получить в конце прошлой статьи.

Готовим правило для исключения площадок

Есть разные подходы к тому, как принимать решения об отключении конкретной площадки. Я сделаю на примере слишком высокого CTR. Будем считать «плохими» все площадки CTR которых выше заданного значения. Скрипт легко адаптировать для поиска площадок со слишком высоким CPA по всем или по конкретной цели из Яндекс.Метрики.

Импорты

import requests
import json

import pandas as pd
import numpy as np

from datetime import datetime, timedelta
from time import sleep

import re

import warnings
warnings.filterwarnings('ignore')

Токен, логин и заголовки запроса

Если аккаунт агентский, добавьте «Use-Operator-Units»: «true», чтобы использовать баллы агентства.

access_token = '%ваш-токен%'
login = '%ваш-логин%'

headers = {
            "Authorization": f'Bearer {access_token}',
           "Client-Login": login,
           "Accept-Language": "ru",
           "processingMode": "auto",
           "returnMoneyInMicros": "false",
           "skipReportHeader": "true",
           "skipReportSummary": "true",
            "Use-Operator-Units": "true" # Использовать баллы API агентства
           }

Получаем данные о кампаниях

url = 'https://api.direct.yandex.com/json/v5/campaigns'
body = {
  "method": "get",
  "params": { 
    "SelectionCriteria": {  
      "Types": ["TEXT_CAMPAIGN"],
      "Statuses": ["ACCEPTED"],
    }, 
    "FieldNames": ["Id" , "Name", "State", "Status"], 
    }
  }
res = requests.post(url, headers=headers, json=body)

Формируем dataframe

camps = {}
for c in res.json()['result']['Campaigns']:
    for key in c:
        if key not in camps:
            camps[key] = []
            camps[key].append(c[key])
        else:
            camps[key].append(c[key])
    
df = pd.DataFrame(camps)
data = df.copy()
data
Получили таблицу с кампаниями, их ID, состоянием и статусом

Далее, надо оставить только РСЯ кампании — обычно специалисты по контекстной рекламе рекламе называют их особым образом — например пишут в разваниях _rsya, _net, _network, _context или как-то ещё — фильтруем кампании по названию и получаем лист с id сетевых кампаний.

network_campaigns_nametag_string = '_net_'
net_camps = data[data.Name.str.contains(network_campaigns_nametag_string)]['Id'].tolist()

Получаем статистику за 90 дней

Если указать значения в «Goals» : [XXXXX, YYYYY], получим кол-во конверсий именно по этим целям. Если не указывать, получим кол-во конверсий по всем целям (достижение любой цели).

Если нужен другой период, можно использовать шаблоны или пользовательские значения.

body = {
    "params": {
        "SelectionCriteria": {
            
"Filter": [{
        "Field": "CampaignId",
        "Operator": "IN",
        "Values": net_camps
      } ]            

        },
        "FieldNames": [
#             "Date",
            "CampaignName",
            "CampaignId",
            "ExternalNetworkName",
            "Placement",
#             "AdNetworkType",
#             "CampaignType",


            "Impressions",
            "Clicks",
            "Cost",
            "Bounces",
            "Conversions"
        ],
#           "Goals" : [66852307, 88594366], # Идентификаторы целей в Метрике, если не указывать — будет один столбец с общим кол-вом конверсий
        "ReportName": str(datetime.now()),
        "ReportType": "CAMPAIGN_PERFORMANCE_REPORT",
        "DateRangeType": "LAST_90_DAYS", # Период отчёта
        "Format": "TSV",
        "IncludeVAT": "YES", # Учитывать НДС
        "IncludeDiscount": "NO"
    }
}

url = 'https://api.direct.yandex.com/json/v5/reports'    

print(login)
status = None
while status in {201, 202, None}:
    res = requests.post(url, headers=headers, json=body)
    status = res.status_code
    retryIn = res.headers.get('retryIn', None)
    reportsInQueue = res.headers.get('reportsInQueue', None)
    print(f'status = {status} wait {retryIn}. queue {reportsInQueue}')
    if retryIn:
        sleep(int(retryIn))

Тут возможно придется немного подождать. Далее разбираем ответ и формируем dataframe:

lists = []
for line in res.text.split('\n'):
    if len(line.split('\t')) > 1:
        lists.append([login] + line.split('\t'))   

stat_dict = {}
n = 1
for line in lists:
    if n == 1:                  # Первую строку записываем ключами словаря   
        for col in line:
            stat_dict[col] = []
    else:                    #   Другие строки записываем как значения ключей словаря
        for y in range(0,len(line)):
            stat_dict[list(stat_dict.keys())[y]].append(line[y])        
    n += 1
        
yd = pd.DataFrame(stat_dict)
yd = yd.sort_values("Cost", ascending = 0)

yd['Impressions'] = yd['Impressions'].astype(int)
yd['Clicks'] = yd['Clicks'].astype(int)
yd['Cost'] = yd['Cost'].astype(float)

# Поправим значения в столбцах с конверсиями
for col in [x for x in yd.columns if 'Conversions' in x]:
    yd[col] = yd[col].map(lambda x: 0 if x == '--' else x).astype(int)

        
yd

Группируем статистику по площадкам:

find_bad = yd[yd['Placement'] != '--'].iloc[:,4:].groupby('Placement')\
.sum().reset_index().sort_values(by='Cost', ascending=0)


find_bad['CPC'] = find_bad['Cost']/find_bad['Clicks']
find_bad['CTR'] = find_bad['Clicks']/find_bad['Impressions']
find_bad = find_bad[find_bad['Clicks'] > 0]
find_bad

Получаем табличку:

Здесь можно рассчитать и другие важные для вас метрики — cr или cpa для нужных целей

Фильтруем

Получим площадки с CTR выше 1,5%. Можно задать комбинации фильтров, например CTR выше 2%, показов более 100 и т. п.

too_high_ctr = 0.015
find_bad_CTR = find_bad[find_bad['CTR'] > too_high_ctr]

#  Сортируем по убыванию расходов
bb = find_bad_CTR[['Placement', 'Cost']].sort_values(by='Cost', ascending = 0)
BADS = bb['Placement'].tolist()
print(BADS)
Получили блеклист площадок для исключения

Добавляем к текущим кампаниям

Функция принимает четыре аргумента: логин, токен, метка РСЯ-кампаний и блеклист.

Если площадки в новом блеклисте уже исключены — они не добавятся.

Если существующий блеклист и новый в сумме дают более 1000 площадок — текущие остаются, из нового добавится такое кол-во площадок, чтобы в сумме было 1000 (приоритет самым затратным).

def addNewExcludedSites(login, access_token, network_campaigns_nametag_string, new_excluded_sites_list):

    YandexWhiteList = []
    
    AlwaysOn = ['m.yandex.ru', 'yandex.ru', 'yandex.by', 'm.yandex.by', 'yandex.kz', 'yandex.ua', 'm.yandex.kz', 'm.yandex.ua']
    
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Client-Login": login,
        "Accept-Language": "ru",
        "processingMode": "auto",
        "returnMoneyInMicros": "false",
        "skipReportHeader": "true",
        "skipReportSummary": "true",
        "Use-Operator-Units": "true"
               }

    # Получаем список включенных, остановленных или закончившихся кампаний у которых в названии есть network_campaigns_nametag_string
    url = 'https://api.direct.yandex.com/json/v5/campaigns'
    
    body = {
      "method": "get",
      "params": { 
        "SelectionCriteria": {  
          "Types": ["TEXT_CAMPAIGN"],
          "States": [ "ON", "SUSPENDED", "ENDED"],
          "Statuses": ["ACCEPTED"],
        }, 
        "FieldNames": ["Id" , "Name", "State", "Status", "ExcludedSites"], 
        }
      }

    res = requests.post(url, headers=headers, json=body)

    camps = {}
    for c in res.json()['result']['Campaigns']:
        for key in c:
            if key not in camps:
                camps[key] = []
                camps[key].append(c[key])
            else:
                camps[key].append(c[key])

    df = pd.DataFrame(camps)
    data = df.copy()

    # Для РСЯ-кампаний добавляем к текущему блеклисту новые площадки
    data = data[data.Name.str.contains(network_campaigns_nametag_string)] #  Оставляем кампании с нужными названиями
    data['ExcludedSites'] = data['ExcludedSites'].map(lambda x: x['Items'])
    
    
    # Добавляем новый блеклист к текущему
    
    campaigns_to_update = ', '.join(data['Name'].tolist())
    print(f"Campaigns to update: {campaigns_to_update}\n")  
    
    for i in data['Id'].tolist():
        
        # Название кампании
        CampaignName = data[data['Id'] == i]['Name'].tolist()[0]

        # Получим актуальный блеклист кампании
        ExcludedSites = sorted(data[data['Id'] == i]['ExcludedSites'].tolist()[0])
        
        # Удалим из нового списка площадки, которые уже есть в блеклисте кампании
        BadList_New = [x for x in new_excluded_sites_list if x not in ExcludedSites]
        
        # Удалим из нового списка площадки, которые Яндекс не даёт отключить — AlwaysOn
        BadList_New = [x for x in BadList_New if x not in AlwaysOn]

        # Если площадки из нового блеклиста уже отключены — ничего не делаем
        if len(BadList_New) == 0:
            print(f"{CampaignName} (ID:{i}), Allready in list! ExcludedSites list size: {len(ExcludedSites)}\n")
            continue

        else:
            print(f"{CampaignName} (ID:{i}), ExcludedSites list size: {len(ExcludedSites)}")
            
            # Максимальное число минус-площадок = 1000. 
            # Текущий список не трогаем, новый добавляем так, чтобы в сумме было 1000.
            # Если новый список на основании статистики, приоритет минус-площадкам с бо́льшими расходами.
            LE = len(list(set(ExcludedSites))) # Размер текущего списка
            MAX_NEW_LIST_LEN = 1000-LE # Сколько максимально можно добавить
            BadList_New = BadList_New[:MAX_NEW_LIST_LEN] # Берём нужное кол-во
            
            url = 'https://api.direct.yandex.com/json/v5/campaigns'
            body = {
              "method": "update",
              "params": { 
                "Campaigns": [{  
                  "Id": i,
                 "ExcludedSites": {  
                    "Items": sorted(list(set(ExcludedSites + BadList_New)))
                         },        
                  } 
                ] 
              }
            }

            res = requests.post(url, headers=headers, json=body)
            
            if 'Errors' not in res.json()['result']['UpdateResults'][0]:
                print(f"Added {BadList_New} to ExcludedSites.\nNew ExcludedSites list size: {len(ExcludedSites) + len(BadList_New)}\n")   

                # Список «проверенных» площадок Яндекса
                # https://yandex.ru/adv/news/vklyuchenie-pokazov-reklamy-na-ploschadkakh-rsya-s-proverennym-kachestvom-trafika?fbclid=IwAR1aUWLfWJiFr2XuzdRyhMwmWIwJEHsFbuj0ciQEdVF95eQwFdqAF5_pdiw
                try:
                    for i in res.json()['result']['UpdateResults'][0]['Warnings']:
                        i = re.sub('Элемент\s(.+?)\sсписка.+', r'\1', i['Details'])
                        if i not in YandexWhiteList:
                            YandexWhiteList.append(i)
                except:
                    continue


            else:
                print(f"Error trying update ExcludedSites for {CampaignName} (ID:{i})")
                for e in res.json()['result']['UpdateResults'][0]['Errors']:
                    print (e['Details'])
                    
                continue

    if len(YandexWhiteList) > 0:      
        print('Неотключаемые площадки РСЯ:')
        for y in sorted(YandexWhiteList):
            print(y)

bad_list = BADS
addNewExcludedSites(login, access_token, network_campaigns_nametag_string, bad_list)
В интерфейсе Директа можно посмотреть изменения.

Ссылки
Документация API Директа

P. S. Неотключаемые площадки РСЯ
В конце выполнения фукнции печатается YandexWhiteList — те самые площадки с «проверенным» трафиком Яндекса.

Предупреждение, что эффекта не будет

afisha.yandex.ru
collections.yandex.ru
com.android.browser
com.edadeal.android
com.s-g-i.edadeal
com.yandex.browser
com.yandex.launcher
com.yandex.mobile.realty
com.yandex.zen
disk.yandex.ru
dsp.yandex.ru
fotki.yandex.ru
game.yandex.ru
images.yandex.by
images.yandex.com
images.yandex.kz
images.yandex.ru
images.yandex.ua
kinopoisk.ru
m.afisha.yandex.ru
m.collections.yandex.ru
m.edadeal.ru
m.games.yandex.ru
m.images.yandex.by
m.images.yandex.ru
m.local.yandex.ru
m.pogoda.yandex.ru
m.rasp.yandex.ru
m.sport.yandex.ru
m.thequestion.ru
m.tv.yandex.ru
m.video.yandex.ru
m.zen.yandex.com
m.znatoki.yandex.ru
maps.yandex.ru
metro.yandex.ru
music.yandex.ru
pogoda.yandex.ru
ru.auto.ara
ru.yandex.disk
ru.yandex.mobile
ru.yandex.mobile.search
ru.yandex.mobile.transport
ru.yandex.mobile.weather
ru.yandex.rasp
ru.yandex.searchplugin
ru.yandex.searchplugin.beta
ru.yandex.weatherplugin
ru.yandex.yandexbus
ru.yandex.yandexmaps
sport.yandex.ru
tv.yandex.ru
video.yandex.ru
zen.yandex.com
zen.yandex.ru

Поделиться
Отправить
Запинить
2020  
Популярное