Большие выводы на маленьких выборках

Чтобы начать двигать недвижимость, достаточно сменить систему отсчёта.
Фейсбук
→ Посты за 2020, 2019, 2018, 2017, 2016

Отправляем целевые звонки из CoMagic в пиксель Фейсбука

Используем API CoMagic, Facebook Server-Side API и Python

Зачем

Фейсбук-реклама может оптимизироваться по целевым событиям на сайте — например по кликам или отправкам форм. Но, в рекламе недвижимости или автодилеров часть (обычно бо́льшая) лидов приходит со звонков — такие внешние события можно отправить в пиксель с помощью Server-Side API Фейсбука.

Пока интеграции у CoMagic нет, делаем костыли ;-)

Передаём данные о пользователе в CoMagic

Предварительно необходимо собрать все известные пользовательские параметры на момент совершения события. Больше параметров  — больше вероятность, что Facebook определит пользователя, который совершил это событие.

В GTM создаём две пользовательские переменные:

Пользовательские переменные → Основной файл cookie → Название cookie: _fbp
Пользовательские переменные → Основной файл cookie → Название cookie: _fpc

User Data Parameters
fbp and fbc Parameters

Далее создаём пользовательский HTML тег, где отправим найденые параметры в CoMagic с помощью javascript-api.

<script type="text/javascript">
Comagic.setProperty('fbp', {{fbp}});
Comagic.setProperty('fbc', {{fbc}});
</script>

Триггером может быть загрузка страницы при условии не пустого значения fbc или fbp.
Эти значения запишутся как параметры пользователя (visitor_custom_properties) в CoMagic.

Получаем данные об обращениях CoMagic

В CoMagic различные методы для получения отчётов, нам нужны:
get.communications_report — получение списка всех обращений (сюда относятся не только звонки, но и чаты, цели и офлайн обращения);
get.calls_report — получение отчёта по сессиям звонков.

Документация Data API

# Импорты
import re
import requests
import pandas as pd
import json
from datetime import datetime, timedelta

# Токен из раздела «Управление пользователями» на app.comagic.ru
access_token = '9m6xfwuofjid8xb6v68h46r1g9njax1xhe8iwyw6'

 # Идентификатор сайта
user_id = 19774

# Дата начала выборки
campaign_start = datetime(2020, 1, 1)

get.communications

# При попытке получить данные более, чем за 90 дней API выдаст ошибку: Max value of requested date interval is 90 days. Несмотря на то, что Фейсбук примет только те события, что произошли не ранее 7 дней назад, учтём это ограничение в коде — его можно использовать для других целей.
i = 0
date_from_list = []
date_till_list = []

while True:
    next_period = campaign_start + timedelta(i*90)
    if next_period > datetime.now():
        break
    else:    
        date_from_list.append((campaign_start + timedelta(i*90)).strftime('%Y-%m-%d %H:%M:%S'))
        date_till_list.append((campaign_start + timedelta(i*90) + timedelta(90)).strftime('%Y-%m-%d %H:%M:%S'))
        i += 1       
print(f'date_from_list: {date_from_list}\ndate_till_list: {date_till_list}')

Получим промежутки по 90 дней для которых будем получать данные.

date_from_list: ['2020-01-01 00:00:00', '2020-03-31 00:00:00']
date_till_list: ['2020-03-31 00:00:00', '2020-06-29 00:00:00']

Функция получения данных об обращениях.
Нужные поля:
ym_client_id — clientID Метрики,
date_time — дата и время звонка,
tags — теги,
communication_page_url — страница с которой был совершен звонок,
visitor_custom_properties — те самые параметры пользователя куда мы передаем значения fbp и fbc,
id — идентификатор обращения.

Фильтры: только звонки и идентификатор сайта.

# https://www.comagic.ru/support/api/data-api/Reports/
def getCommunications(datefrom,datetill):  
    period_data = []
    n = 0        
    while True:
        payload = \
        {'id': 1,
         'jsonrpc': '2.0',
         'method': 'get.communications_report',
         'params': {'access_token': access_token,
                    'date_from': datefrom,
                    'date_till': datetill,
                    'fields': ['ym_client_id',
                               'date_time',
                               'tags',
                               'communication_page_url',
                               'visitor_custom_properties',
                               'id'],
                    'filter': {'condition': 'and',
                               'filters': [{'field': 'communication_type',
                                            'operator': '=',
                                            'value': 'call'},
                                           {'field': 'site_id',
                                            'operator': '=',
                                            'value': site_id}]},
                    'limit': 10000,
                    'offset': 0 + n * 10000,
                    'sort': [{'field': 'date_time', 'order': 'asc'}]}}
                
        url = 'https://dataapi.comagic.ru/v2.0'
        r = requests.post(url, data=json.dumps(payload))
        data = json.loads(r.text)
        res = data['result']['data']        
        if len(res) == 0:
            break
        
        period_data += res
        n += 1

    return period_data

Пройдем циклом по временным промежуткам и создадим dataframe с данными об обращениях.

data = []
for i in range(len(date_from_list)):
    try:
        data += getCommunications(date_from_list[i], date_till_list[i])
    except KeyError:
        print(getCommunications()['error']['message']) 
        break
        
communications_df = pd.DataFrame(data)   
communications_df = communications_df.sort_index(axis=1)
total_communications = communications_df.shape[0]
print(f"Обращений: {total_communications}")
В полученной таблице будут вложенные массивы в столбцах tags и visitor_custom_properties

Запись одного тега выглядит так:

[{'tag_type': 'manual',
  'tag_name': 'Повторный',
  'tag_user_login': 'name@domen.ru',
  'tag_id': 55332,
  'tag_employee_id': None,
  'tag_employee_full_name': None,
  'tag_change_time': '2020-04-01 10:46:18',
  'tag_user_id': 111685}]

Нам нужны только tag_name и только с определенным значением, например «Целевой».

def get_clean_tags(tags):
    if tags != None:
        tags_list = []
        for tag in tags:
            tags_list.append(tag['tag_name'])
        if 'Целевой' in tags_list:
            return 'Целевой'
        else:
            return None
    else:
        return None
    
communications_df['clean_tags'] = communications_df['tags'].apply(get_clean_tags)

Дальше необходимые преобразования

# Дату в формат datetime
communications_df['date_time'] = pd.to_datetime(communications_df['date_time'], yearfirst=True) 

# Удалим параметры из url
communications_df['communication_page_url'] = communications_df['communication_page_url']\
    .map(lambda x: re.sub('(^.+?)\?.*', r'\1', str(x)))

# Заменим на None звонки без visitor_custom_properties
communications_df['visitor_custom_properties'] = communications_df['visitor_custom_properties']\
    .map(lambda x: None if len(x) == 0 else x)

# Удалим строки без целевых звонков и без данных фейсбука, удалим столбец tags и дубликаты строк
communications_df_target = communications_df.sort_values(by='date_time').\
    dropna(subset=['visitor_custom_properties', 'clean_tags']).drop('tags', axis=1).\
    drop_duplicates(subset='ym_client_id', keep='first').reset_index(drop=True)

get.calls_report

Метод для получения данных о звонках — тут можно получить телефон пользователя, в методе get.communications номеров нет. Телефон нужен для передачи в пиксель вместе со значениями fbp/fbc.

Поля:
id — идентификатор обращения,
contact_phone_number — телефон,
visitor_id — идентификатор посетителя сайта.

def getCalls(datefrom,datetill):  
    period_data = []
    n = 0        
    while True:
        payload = \
        {'id': 1,
         'jsonrpc': '2.0',
         'method': 'get.calls_report',
         'params': {'access_token': access_token,
                    'date_from': datefrom,
                    'date_till': datetill,
                    'fields': ['id',
                               'contact_phone_number', 
                               'visitor_id'
                              ],
                    'filter': {'condition': 'and',
                               'filters': [{'field': 'id',
                                            'operator': 'in',
                                            'value': communications_df_target['id'].to_list()},
                                           {'field': 'site_id',
                                            'operator': '=',
                                            'value': site_id}]},                    
                    'limit': 10000,
                    'offset': 0 + n * 10000}}
                
        url = 'https://dataapi.comagic.ru/v2.0'
        r = requests.post(url, data=json.dumps(payload))
        data = json.loads(r.text)

        res = data['result']['data']        
        if len(res) == 0:
            break
        
        period_data += res
        n += 1


    return period_data

data = []
for i in range(len(date_from_list)):
    try:
        data += getCalls(date_from_list[i],date_till_list[i])
    except KeyError:
        print(getCalls()['error']['message']) 
        break
        
comagic_calls = pd.DataFrame(data)        
comagic_calls = comagic_calls.sort_index(axis=1)
comagic_calls

Получим таблицу с номерами телефонов:

id — идентификатор обращения, ключ по которому мы объединим две таблицы

Объединям таблицу в одну

communications_calls = pd.merge(communications_df_target, comagic_calls, how='left', on='id')
Единая таблица обращений и звонков

Разберём вложенный массив visitor_custom_properties и сделаем ряд необходимых преобразований.

def get_fb_from_custom_properties(properties): # достанем данные из вложенной структуры visitor_custom_properties
    fb = {}
    for i in properties:
        fb[i['property_name']] = i['property_value']     
    fbc = fb.get('fbc', None)
    fbp = fb.get('fbp', None)
    return fbp, fbc

communications_calls['fbp'] = communications_calls['visitor_custom_properties'].\
map(lambda x: get_fb_from_custom_properties(x)[0])

communications_calls['fbc'] = communications_calls['visitor_custom_properties'].\
map(lambda x: get_fb_from_custom_properties(x)[1])

final_table = communications_calls.drop('visitor_custom_properties', axis = 1)

# в фейсбук надо отправить звонки с временем в формате timestamp
final_table['timestamp'] = final_table['date_time'].\
map(lambda x: int((x - timedelta(hours=3)).timestamp())) # часовой пояс Москвы

final_table = final_table.where(pd.notnull(final_table), None) # все пустые значения заменим на None

# Удаление лишних столбцов
final_table = final_table.drop(['clean_tags', 'id', 'visitor_id'], axis=1)

final_table = final_table.sort_index(axis=1)
final_table
Таблица с необходимыми данными для отправки

Так как важно не отпралять одно событие несколько раз, завёдем лог отправленных звонков в формате csv .

# загрузим лог уже отправленных событий
# или создадим его, если его нет
open('sent_calls_to_facebook_pixel.csv', 'a').close()

with open('sent_calls_to_facebook_pixel.csv','r') as f:
    text = f.read()
    
    sent_events = []
    for row in text.split('\n'):
        try:
            sent_events.append(row.split(';')[6])
        except:
            continue  

# оставим в таблице только те значения, которые еще не отправляли
# данные об отправленных звонках хранятся в sent_calls_to_facebook_pixel.csv
# если таблица пустая — ничего не отправится
final_table = final_table[~final_table['contact_phone_number'].isin(sent_events)]

# удалим события старше  7 дней, иначе запрос не пройдет
final_table = final_table[final_table['date_time'] > datetime.now() - timedelta(7)]
final_table.head()

Всё готово к отправке.

Facebook Server API

Как настроить и получить токен — Блог Дмитрия Осиюка
Документация Facebook

from facebook_business.adobjects.serverside.event import Event
from facebook_business.adobjects.serverside.event_request import EventRequest
from facebook_business.adobjects.serverside.user_data import UserData
from facebook_business.adobjects.serverside.custom_data import CustomData
from facebook_business.api import FacebookAdsApi

fb_access_token = 'EAAEAM1ktR1gBAJkpPqJZAA81uyrfgeLp0LIFwChKbBoXlVpEgNu6GvecLh26kFbbJpfd2JsZBqtPQWrw4i1HvkzMW4Q54zPIugQPGZBfa2wSiFU2AV7ccCZAtIJzG4llSbkm6Ioyym2enmpZCyq8SdnGbwF1ym09ra46fRKUphvWvPXqSYibf1tb8J1xckiGNbwB9nejTg8QZDZD'
pixel_id = '279923445633319'

FacebookAdsApi.init(access_token=fb_access_token)

Создаём массив events для отправки, в user_data передаются значения из подготовленной таблицы:

events = []
for x, y in final_table.iterrows():
    call_url = y[final_table.columns.get_loc('communication_page_url')]
    phone = y[final_table.columns.get_loc('contact_phone_number')]
    clientID = y[final_table.columns.get_loc('ym_client_id')]
    date_time = y[final_table.columns.get_loc('date_time')]
    fbc = y[final_table.columns.get_loc('fbc')]
    fbp = y[final_table.columns.get_loc('fbp')]
    ts = y[final_table.columns.get_loc('timestamp')]
 
#    Пишем лог clientID, отправленных в пиксель    
    with open('sent_calls_to_facebook_pixel.csv','a') as f:
        f.write(f'{date_time};{clientID};{fbc};{fbp};{ts};{call_url};{phone}\n')
        
    print(f'data_time: {date_time}; clientID: {clientID}; fbc:{fbc}; fbp: {fbp}; ts: {ts}; call_url: {call_url}; phone: {phone}\n')
    
    # https://developers.facebook.com/docs/marketing-api/server-side-api/using-the-api
    # https://developers.facebook.com/docs/marketing-api/server-side-api/parameters/server-event
    user_data = UserData(
    fbc = fbc,
    fbp = fbp,
    phone = phone
    )
    
    event = Event(
        event_name = 'ComagicCall',
        event_time = ts, # event_time can be up to 7 days before you send an event to Facebook. If any event_time in data is greater than 7 days in the past, we return an error for the entire request and process no events.
        event_source_url = call_url, # the browser URL where the event happened.
        user_data = user_data,
    )    
    
    events.append(event)

print(f'Будет отправлено событий: {len(events)}', end='\n\n')
События готовы к отправке

Отправляем в тестовом режиме:

# раскомментируй test_event_code чтобы проверить отправку событий на
# events manager → тестрование событий (test events) → серверные (server)
# test_event_code надо подставить свой
# https://business.facebook.com/events_manager/pixel/verify?business_id={}&selected_data_sources=PIXEL&selected_screen_section=DATA_SOURCES&pixel_id={}&event_type=SERVER

if len(events) > 0:
    event_request = EventRequest(
        events=events,
        test_event_code = 'TEST34220',
        pixel_id=pixel_id)
    event_response = event_request.execute()
    print(event_response)
else:
    print('Новых звоночков нет :(')
В разделе «Тестирование событий» можно проверить отправку. (IP-адрес и Агент пользователя в примере выше мы не передавали, но делается по аналогии.)

Если всё прошло удачно, закомментируйте test_event_code.

Ссылки:
Настройка Server-Side API для Facebook Pixel — Блог Дмитрия Осиюка
Документация Facebook
Документация CoMagic JavaScript API
Документация CoMagic Data API

P. S. Токены и идентификаторы вымышленные ;-)

17 апреля   CoMagic   facebook API   pandas   python

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

Используем 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  

Питон-скрипт для проверки опечаток на сайте

Опечатки бывают у всех ;-) Пик
Крост
Эталон
VALO

В дополнение к скрипту для поиска опечаток в объявлениях Директа проверяем сайт на опечатки.
Скрипт использует библиотеку Selenium для парсинга страницы и Яндекс Спеллер для проверки орфографии.

В коде замените путь до хромдрайвера:

browser = webdriver.Chrome(options=options, executable_path=r'/Users/vitalybakhvalov/Documents/chromedriver')

И url страницы для проверки:

check_url = 'http://italylov.ru/blog'

Скачать

UPD1
Вторая версия — находит на заданной странице ссылки на другие страницы сайта и проверяет все. Удобно когда страниц много..

Проверка объявлений Директа на опечатки

Опечатки случаются, и почему-то клиенты всегда успевают находить их раньше, чем авторы.
Для таких случаев сделал py-скрипт для поиска опечаток с помощью Яндекс Спеллера и API Директа. Не панацея, так как не видит пунктуацию, но орфографию и опечатки находит. Лучше пошагово запустить в Юпитере.

Плюс, скрипт наглядно печатает все уникальные заголовки, вторые заголовки, тексты объявлений, уточнения, заголовки и расширения быстрых ссылок в аккаунте.

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

Сначала получим список активных кампаний.

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

Затем получим все активные объявления и вытащим тексты.

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

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

Повторим упражнение для заголовков и описаний быстрых ссылок.

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

И для уточнений.

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

Скриншоты

Ошибки собираются в словарь. Count — сколько раз встречалось. Variants — предложения Спеллера, как правильно. Ложноположительные результаты можно добавить в ok-list, чтобы они не выводились как ошибки
Датафрейм с быстрыми ссылками
Ещё ошибочки

UPD1

В проверке быстрых ссылок заменил body запроса

body = {
      "method": "get",
      "params": { 
        "SelectionCriteria": { 
                
        "Ids" : [int(x) for x in SitelinkSetIds if x != ''] # вот тут бывали ошибки если взять просто SitelinkSetIds


        }, 
        "FieldNames" : [ "Id" ],
 "SitelinkFieldNames": [ "Title" , "Href" , "Description"]


      } 
    }

UPD2

Если аккаунт прямой, а не агентский поправьте в headers:

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

UPD3

Объявления можно не фильтровать по только активным, а проверить все.

ads_df = ads_df[ads_df['State'] == 'ON'][['Id','TextAd','CampaignId']] # Только активные объявления
# ads_df = ads_df[['Id','TextAd','CampaignId']] # Все
print(f'Кол-во активных объявлений: {len(ads_df)}')

UPD4 Получение токена

Вы зарегистрировали приложение. У вас есть ID и пароль.

#https://oauth.yandex.ru/

appid =  'ваш-ID'
apppw =  'ваш-пароль'
appcallback =  'https://oauth.yandex.ru/verification_code'
link = f'https://oauth.yandex.ru/authorize?response_type=code&client_id={appid}'
print(link)

Переходите по ссылке под вашим логином и копируете код.

code = 'ваш-код'
# Получаем токен
code_request = f'https://oauth.yandex.ru/authorize'
    
code_req = {
    'grant_type': 'authorization_code',
    'code': code,
    'client_id': appid,
    'client_secret': apppw
}

token_responce = requests.post('https://oauth.yandex.ru/token', data = code_req)
if token_responce.status_code != 200:
    raise Exception('Wrong status code')

token_resp_data = token_responce.json()
access_token = token_resp_data['access_token']
print(access_token)

Медленный парсер подсказок Яндекса (зато бесплатный)

Для тех, кого забанили в гугле и кейколекторе.

Берёт запросы из txt-файла, вбивает в Яндекс и собирает подсказки.

Как сделать

Скачайте скрипт .ipynb и создайте txt-файл с запросами. Сохраните в отдельную папку.

Будем использовать специальный дистрибутив Питона, который называется «Анаконда». Качаем версию Питона 3.6, устанавливаем, запускаем Anaconda Navigator, в нём жмем launch Jupiter Notebook.

Выберите папку со скачанными файлами, откройте файл .ipynb.

Жмём Run ALL из меню

Когда скрипт закончит работу — в папке появится новый текстовый файлик с подсказками.

P. S. ChromeDriver нужно устанавливать. Путь в скрипте указывается в строчке browser = webdriver.Chrome(chrome_options=options, executable_path=r’/Users/ПОЛЬЗОВАТЕЛЬ/Documents/chromedriver’)
Скачать драйвер

2019  

Парсим объявления в Директе-2 и делаем скриншоты

Скрипт берёт файл со списком запросов, вот такой, например:

Каждый запрос вбивает в Яндекс и записывает заголовки, тексты и отображаемые ссылки рекламных объявлений в CSV-файл + делает нотариально заверенные скриншоты выдачи каждого запроса и сохраняет в папке со скриптом — полезно для упоротых клиентов.

CSV-файл
Скриншоты в папке

Как сделать

Скачайте скрипт и файл с запросами. Сохраните в отдельную папку.

Будем использовать специальный дистрибутив Питона, который называется «Анаконда». Качаем версию Питона 3.6, устанавливаем, запускаем Anaconda Navigator, в нём жмем launch Jupiter Notebook.

Там выберите папку со скачанными файлами и файл .ipynb:

Задайте регион. Наример, Москва и Московская область — 1, а Санкт-Петербург — 2.

region = '1'

Жмём Shift + Enter (или Run из меню) для каждой ячейки — фишка Jupiter Notebook в том, что код выполняется пошагово. Когда дойдете до шага «Основной цикл» — придется подождать. Скрипт будет писать какой запрос он сейчас обрабатывает:

https://www.yandex.ru/search/ads?text=дилеры%20ауди%20+в%20москве&lr=1&p=0
Запрос 1 из 6
...
...
...
https://www.yandex.ru/search/ads?text=ауди%20официальный%20дилер%20+в%20москве&lr=1&p=0
Запрос 2 из 6
...
...
...
https://www.yandex.ru/search/ads?text=дилер%20ауди%20север%20москва&lr=1&p=0
Запрос 3 из 6
...
...
...
https://www.yandex.ru/search/ads?text=ауди%20север%20официальный%20дилер%20+в%20москве&lr=1&p=0
Запрос 4 из 6
...
...
...
https://www.yandex.ru/search/ads?text=купить%20ауди%20москва%20дилер&lr=1&p=0
Запрос 5 из 6
...
...
...
https://www.yandex.ru/search/ads?text=купить%20ауди%20+у%20официального%20дилера%20+в%20москве&lr=1&p=0
Запрос 6 из 6
...
...
...

Далее формируем датафрейм и записываем в CSV-файл. Как всё будет готово, появится табличка с первыми пятью строками результата и файл в папке со скриптом.

Первые пять строчек для первого запроса из файла. Все данные — в CSV-файле.

P. S. ChromeDriver нужно устанавливать. Путь в скрипте указывается в строчке browser = webdriver.Chrome(chrome_options=options, executable_path=r’/Users/vitalybakhvalov/Documents/chromedriver’)
Скачать драйвер

2019  

Как склеить выгрузку из колтрекинга с данными о визитах в гугл-таблицах

Пришло время, и вы вдруг начали подозревать, что ваш любимый канал programatic-cpa-want-exchange, хоть и помогает выполнить kpi по звонкам, но к визитам и сделкам не приводит.

Поскольку crm у вас ещё в стадии «почти допилили», то проверку придется делать руками: необходимо «склеить» данные о визитах с колтрекингом. Тут вас обычно ждут две проблемы: кривые телефонные номера и несколько телефонов у одного клиента.

Пример выгрузки из CRM с контактами тех, кто приехал в офис продаж

Поделюсь двумя формулами в гугл-таблицах, которые помогут навести порядок.

Приводим номера телефонов в единый вид

Удалим все не-цифры, возьмем 10 цифр справа и добавим к ним «7»
=REGEXREPLACE(REGEXREPLACE(ТЕКСТ(F9;"#");"\D";"");".*(\d{10})$";"7$1")

Избавляемся от столбцов

Конечно, проще сделать split в SQL, но иногда так быстрее
=QUERY({D18:E21\F18:F21;D18:E21\G18:G21};"select Col1,Col2,Col3 where Col3 != '' order by Col1 asc, Col2 asc label Col1 'id', Col2 'name', Col3 'phone'";0)

Итого

Теперь эти данные можно корректно подтянуть к выгрузке колтрекинга с помощью VLOOKUP/ВПР.
Гугл-таблица с примером.

2019  
2019   python

Подсчёт количества символов для Директа в Гугл-таблицах

При подсчёте количества символов в текстах объявлений Директа не учитываются некоторые символы.
Для верного подсчёта в Гугл-таблицах подойдет формула:

=ДЛСТР(REGEXREPLACE(REGEXREPLACE(A1;СИМВОЛ(34);"");"[«»,.:;!]";""))

Где A1 — ячейка с текстом.

2019   google_sheets

Big Query → DataFrame

Простой способ достать данные из Google Big Query в Jupyter Notebook.

# Установите библиотеки
# !pip install python-gflags
# !pip install httplib2
# !pip install google-api-python-client
# !pip install pandas-gbq

# Включите API: https://console.developers.google.com/apis/dashboard


import pandas as pd
from pandas.io import gbq

#  Ваш запрос в BQ
Query = """
SELECT
 *
FROM 
[your_dataset_name]
"""

# Укажите project_id и диалект
data = gbq.read_gbq(Query, project_id='your_BQ_project_id', dialect = 'standard')


data.head()

Источник

2018   big query   pandas   python
Ctrl + ↓ Ранее