[Веб-аналитика, Медийная реклама, Аналитика мобильных приложений, Социальные сети и сообщества] Майним еще больше данных: настраиваем сбор рекламной статистики TikTok за день

Автор Сообщение
news_bot ®

Стаж: 6 лет 2 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
11-Июн-2021 09:30

Привет, меня зовут Маша, я работаю маркетинговым аналитиком в Ozon. Наша команда "питонит" и "эскьюэлит" во все руки и ноги во благо всего маркетинга компании. Одной из моих обязанностей является поддержка аналитики для команды медийной рекламы Ozon.Медийная реклама Ozon представлена на разных площадках: Facebook, Google, MyTarget, TikTok и другие. Для эффективной работы любой рекламной кампании необходима оперативная аналитика. В данной статье речь пойдет о моём опыте сбора рекламных данных с площадки TikTok без посредников и лишних заморочек.Задача на сбор статистики: вводныеУ команды медийной рекламы Ozon есть бизнес-аккаунт TikTok, в котором они управляют всей рекламой на этой площадке. Они долго терпели, сами собирали данные из рекламных кабинетов, но всё-таки настало время, когда терпеть уже больше было нельзя. Так у меня появилась задача на автоматизацию сбора статистики из TikTok.У нас в базах уже были данные о заказах по кампаниям из TikTok, для эффективной аналитики не хватало данных о расходах. Итак, весь процесс от "нам нужны данные по расходам из TikTok" до "у нас есть данные по расходам из TikTok" разделился для нас на следующие этапы:
  • регистрация аккаунта разработчика,
  • создание приложения,
  • авторизация бизнес-аккаунта в приложении,
  • запрос, получение, обработка и загрузка данных.
Рассмотрим каждый из этапов подробнее.Регистрация разработчикаМы зарегестрировали аккаунт разработчика на нашего бизнес-менеджера. Перешли на портал TikTok Marketing API, нажали на "My Apps", далее кликнули на "Become a Developer", и началась череда заполнения форм.
TikTok – не Facebook, у нас ничего ни разу не отклонял, но всё равно мы были очень внимательны при заполнении полей и не добавляли то, что нам не нужно прямо сейчас. Например, в поле "What services do you provide?" добавили только "Reporting".
Последним пунктом был "Create App". Процесс создания аккаунта разработчика и приложения в первый раз происходит вместе.Создание приложенияЗаполняем имя и описание приложение, callback-address. Далее нужно выбрать разрешения, которые приложение будет запрашивать у авторизирующегося в нем аккаунта. Так же, как и при заполнении полей для аккаунта разработчика, выбрали только пункт "Reporting". Указали ID рекламного аккаунта. После этого отправили приложение на проверку.
Как сообщает TikTok в своей документации, проверка может занять от двух до трех рабочих дней. Мы отправили приложение на проверку в пятницу, в понедельник с утра у нас уже было одобренное приложение и можно было продолжить работу.К сожалению, у меня нет для вас советов на тот случай, если ваше приложение не одобрили. Главное, о чём нужно помнить – это правильно заполнять все обязательные поля и запрашивать разрешения только на то, что действительно необходимо: ни больше, ни меньше.Авторизация бизнес-аккаунта в приложенииИз всей рутинной работы по заполнению форм, эта часть оказалось самой интересной. У нас не было web-приложения, которое бы отлавливало редирект с авторизационным кодом, поэтому автоматическую авторизацию бизнес-аккаунта сделать не получилось. Но мы оперативно потыкали в кнопки и получили заветный Access Token, с помощью которого собираем данные всех рекламных аккаунтов нашего бизнес-менеджера.Итак, по порядку, что мы делали не имея сайта, который бы отлавливал callback с авторизационным кодом.
  • Зашли в приложение и указали Callback Address https://www.ozon.ru.
  • Скопировали Authorized URL, перешли по нему, авторизовались под аккаунтом бизнес-менеджера.
  • Согласились на предоставление разрешений для приложения, нажали "Confirm".
  • Далее нас перекинуло на сайт Ozon, но с дополнительными аргументами в url. Получилось наподобие такого https://www.ozon.ru/?auth_code=XXXXXXXXXXX.
  • Скопировали значение auth_code, в приложении скопировали secret и app_id и отправили запрос к TikTok на получение long-term Access Token.
curl -H "Content-Type:application/json" -X POST \
-d '{
    "secret": "SECRET",
    "app_id": "APP_ID",
    "auth_code": "AUTH_CODE"
}' \
https://ads.tiktok.com/open_api/v1.2/oauth2/access_token
Получили ответ такого вида:
{
    "message": "OK",
    "code": 0,
    "data": {
        "access_token": "XXXXXXXXXXXXXXXXXXXX",
        "scope": [4],
        "advertiser_ids": [
            1111111111111111111,
            2222222222222222222]
    },
    "request_id": "XXXXXXXXXXXXXXX"
}
Важно было успеть отправить запрос на получение long-term Access Token как можно быстрее, после редиректа на сайт Ozon. Связано это с временем жизни auth_code – 10 минут.Из полученного ответа необходимо сохранить значения access_token, его нужно использовать при каждом запросе. Если access_token будет потерян или, того хуже, скомпрометирован, нужно будет заново выполнять все пункты по аваторизации аккаунта бизнес-менеджера.Так же при запросах нам понадобиться список advertiser_ids, но его не обязательно сахранять прямо сейчас – список ID аккаунтов всегда можно посмотреть в аккаунте бизнес-менеджера.Всё, мы готовы писать запросы!Получение статистикиКогда мы только начинали собирать данные из TikTok, я пользовалась методом, который сейчас depricated, поэтому сразу расскажу о новом.Итак, у нас есть всё необходимое для получения данных, а именно:
  • access_token,
  • список advertiser_ids.
В результате нужно получить расходы по кампаниям, в группировке до названия рекламного объявления.media source -> campaign -> adset -> ad_nameЗначение media source всегда неизменно, так как источник один – TikTok. По остальным параметрам можно запросить данные из API TikTok.Теперь нужно было решить, с какой детализацией по времени будем тянуть данные. TikTok позволяет загружать детализацию по часу и дню. Если выгружать детализацию по часу, то, максимум, за один запрос можно получить данные только за один день; если запрашивать детализацию по дням – максимум, на один запрос мы получим 30 дней. Конверсии в покупки анализируются за целый день, поэтому и расходы решили собирать за день.В новом методе получения данных добавили фильтр по типу размещения рекламы: AUCTION и RESERVATION. Ozon использует только AUCTION в своей стратегии ведения кампаний.Кроме расходов мы собирали также и другую рекламную статистику по кампаниям: просмотры, клики, количество уникальных пользователей смотревших рекламу и другое. В итоге получился такой список метрик:
METRICS = [
    "campaign_name", # название кампании
    "adgroup_name", # название группы объявлений
    "ad_name", # название объявления
    "spend", # потраченные деньги (валюта задаётся в рекламном кабинете)
    "impressions", # просмотры
    "clicks", # клики
    "reach", # количество уникальных пользователей, смотревших рекламу
    "video_views_p25", # количество просмотров 25% видео
    "video_views_p50", # количество просмотров 50% видео
    "video_views_p75", # количество просмотров 75% видео
    "video_views_p100", # количество просмотров 100% видео
    "frequency" # среднее количество просмотра рекламы каждым пользователем
]
В документации TikTok для каждого метода API описан пример на языках Java, Python, PHP и также curl-запрос. Я использовала пример на Python с небольшими изменениями.В примерах из документации TikTok используются две дополнительные библиотеки:
pip install requests
pip install six
Библиотека requests необходима для удобной отправки get-запросов. Библиотека six используется для генерации url-адреса запроса.И еще две библиотеки, которые я уже добавила сама для того, чтобы записать данные в базу:
pip install pandas
pip install sqlalchemy
В нашей компании для хранения данных используются SQL-подобные хранилища, поэтому я использую pandas для преобразования данных в DataFrame и sqlalchemy для записи DataFrame в базу.Я использовала функции из примера в документации TikTok для генерации url и отправки запроса.
# генерирует url на основе словаря args с аргументами запроса
def build_url(args: dict) -> str:
    query_string = urlencode({k: v if isinstance(v, string_types) else json.dumps(v) for k, v in args.items()})
    scheme = "https"
    netloc = "ads.tiktok.com"
    path = "/open_api/v1.1/reports/integrated/get/"
    return urlunparse((scheme, netloc, path, "", query_string, ""))
# отправляет запрос к TikTok Marketing API,
# возвращает результат в виде преобразованного json в словарь
def get(args: dict, access_token: str) -> dict:
    url = build_url(args)
    headers = {
        "Access-Token": access_token,
    }
    rsp = requests.get(url, headers=headers)
    return rsp.json()
На вход функции get нужно передать список аргументов и access token. Список аргументов под наши цели выглядит следующим образом:
args = {
    "metrics": METRICS, # список метрик, описанный выше
    "data_level": "AUCTION_AD", # тип рекламы
    "start_date": 'YYYY-MM-DD', # начальный день запроса
    "end_date": 'YYYY-MM-DD', # конечный день запроса
    "page_size": 1000, # размер страницы - количество объектов, которое возвращается за один запрос
    "page": 1, # порядковый номер страницы (если данные не поместились в один запрос, аргумент инкрементируется)
    "advertiser_id": advertiser_id, # один из ID из advertiser_ids, который мы получили при генерации access token
    "report_type": "BASIC", # тип отчета
    "dimensions": ["ad_id", "stat_time_day"] # аргументы группировки, вплоть до объявления и за целый день
}
Подробнее про page_size: ответ на запрос может содержать большое количество информации и загружать всё это за один раз не эффективно. Поэтому у TikTok есть ограничение на максимальное количество объектов в ответе – 1000. Чтобы получить следующую порцию данных, нужно отправить запрос с теми же входными аргументами на следующую страницу. Подробнее о постраничных запросах ниже.В ответ на запуск функции get получаем словарь подобного вида.
{
    # маркер успешности ответа
    "message": "OK",
    "code": 0,
    "data": {
        # информация о странице данных
        "page_info": {
            # общее количество объектов
            "total_number": 3000,
            # текущая страница
            "page": 1,
            # количество объектов на одной странице ответа
            "page_size": 1000,
            # общее количество страниц
            "total_page": 3
        },
        # массив объектов
        "list": [
            # первый объект
            {
                # метрики
                "metrics": {
                    "video_views_p25": "0",
                    "video_views_p100": "0",
                    "adgroup_name": "adgroup_name",
                    "reach": "0",
                    "spend": "0.0",
                    "frequency": "0.0",
                    "video_views_p75": "0",
                    "video_views_p50": "0",
                    "ad_name": "ad_name",
                    "campaign_name": "campaign_name",
                    "impressions": "0",
                    "clicks": "0"
                },
                # измерения (по каким параметрам группируем результаты)
                "dimensions": {
                    "stat_time_day": "YYYY-MM-DD HH: mm: ss",
                    "ad_id": 111111111111111
                }
            },
...
        ]
    },
    # id ответа
    "request_id": "11111111111111111111111"
}
Как я описывала выше, если в ответе получается более 1000 объектов, ответ будет разбит на несколько страниц. В данном случае поле total_page говорит о том, что для получения полного набора данных по указанным параметрам, нужны будут три страницы. Следовательно, запускаем и коллекционируем ответы пока не выгрузим все страницы.
page = 1 # сначала всегда получаем данные по первой странице
result_dict = {} # словарь, в который будем записывать ответы
result = get(args, access_token) # первый запрос
result_dict[advertiser_id] = result['data']['list'] # сохраняем ответ на запрос к первой странице
# пока текущая полученная страница page меньше
# чем общее количество страниц в последнем ответе result
while page < result['data']['page_info']['total_page']:
    # увеличиваем значение страницы на 1
    page += 1
    # обновляем значение текущей страницы в словаре аргументов запроса
    args['page'] = page
    # запрашиваем ответ по текущей странице page
    result = get(args, access_token)
    # накапливаем ответ
    result_dict[advertiser_id] += result['data']['list']
Такое необходимо повторить для каждого рекламного аккаунта из списка advertiser_ids.В результате всех вышеописанных манипуляций мы получили для каждого рекламного аккаунта данные по рекламным метрикам. Осталось только преобразовать словарь в pandas.DataFrame и отправить их в базу.
# результирующий DataFrame, который будем записывать в базу
data_df = pd.DataFrame()
# для каждого рекламного аккаунта выполнить преобразование
for adv_id in advertiser_ids:
    # получаем накопленные разультаты для аккаунта из словаря
    adv_input_list = result_dict[adv_id]
    # временный список
    adv_result_list = []
    # для каждого объекта
    for adv_input_row in adv_input_list:
        # берём словарь метрик
        metrics = adv_input_row['metrics']
        # насыщаем этот словарь словарём измерений
        metrics.update(adv_input_row['dimensions'])
        # добавляем полученный объект во временный список
        adv_result_list.append(metrics)
    # преобразуем временный словарь в DataFrame
    result_df = pd.DataFrame(adv_result_list)
    # добавляем колонку со значением id аккаунта
    result_df['account'] = adv_id
    # добавляем получившийся DataFrame в результирующий
    data_df = data_df.append(
        result_df,
        ignore_index=True
    )
#
# здесь пропущены некоторые манипуляции
# по преобразованию строк в числа
#
# запись данных из результирующего DataFrame в базу
data_df.to_sql(
    schema=schema,
    name=table,
    con=connection,
    if_exists = 'append',
    index = False
)
TikTok утверждает, что исторические данные по статистике не меняеются, а если и меняются, то это должна быть экстроординарная ситуации, наподобие аварии в ЦОД. Но на основе опыта получения данных от Facebook, я решила что всё равно буду перезаписывать семь последних дней (цифра семь появилась эмпирически).В итоге получился вот такой скрипт, который каждый день обновляется данные по TikTok кампаниям за последние семь дней.Полный текст скрипта.
# импорт библиотек
import json
from datetime import datetime
from datetime import timedelta
import requests
from six import string_types
from six.moves.urllib.parse import urlencode
from six.moves.urllib.parse import urlunparse
import pandas as pd
import sqlalchemy
# генерирует url на основе словаря args с аргументами запроса
def build_url(args: dict) -> str:
    query_string = urlencode({k: v if isinstance(v, string_types) else json.dumps(v) for k, v in args.items()})
    scheme = "https"
    netloc = "ads.tiktok.com"
    path = "/open_api/v1.1/reports/integrated/get/"
    return urlunparse((scheme, netloc, path, "", query_string, ""))
# отправляет запрос к TikTok Marketing API,
# возвращает результат в виде преобразованного json в словарь
def get(args: dict, access_token: str) -> dict:
    url = build_url(args)
    headers = {
        "Access-Token": access_token,
    }
    rsp = requests.get(url, headers=headers)
    return rsp.json()
# обновляет данные в базе за последние семь дней
# (или, если указаны start_date и end_date, для периода [start_date, end_date])
def update_tiktik_data(
    # словарь с доступами к API TikTok
    tiktok_conn: dict,
    # словарь с доступами к базе данных
    db_conn: dict,
    # список id рекламных кабинетов
    advertiser_ids: list,
    # необязательное поле: начало периода
    start_date:datetime=None,
    # необязательное поле: окончание периода
    end_date:datetime=None
):
    access_token = tiktok_conn['password']
    start_date = datetime.now() - timedelta(7) if start_date is None else start_date
    end_date = datetime.now() - timedelta(1) if end_date is None else end_date
    START_DATE = datetime.strftime(start_date, '%Y-%m-%d')
    END_DATE = datetime.strftime(end_date, '%Y-%m-%d')
    SCHEMA = "schema"
    TABLE = "table"
    PAGE_SIZE = 1000
    METRICS = [
        "campaign_name", # название кампании
        "adgroup_name", # название группы объявлений
        "ad_name", # название объявления
        "spend", # потраченные деньги (валюта задаётся в рекламном кабинете)
        "impressions", # просмотры
        "clicks", # клики
        "reach", # количество уникальных пользователей, смотревших рекламу
        "video_views_p25", # количество просмотров 25% видео
        "video_views_p50", # количество просмотров 50% видео
        "video_views_p75", # количество просмотров 75% видео
        "video_views_p100", # количество просмотров 100% видео
        "frequency" # среднее количество просмотра рекламы каждым пользователем
    ]
    result_dict = {} # словарь, в который будем записывать ответы
    for advertiser_id in advertiser_ids:
        page = 1 # сначала всегда получаем данные по первой странице
        args = {
            "metrics": METRICS, # список метрик, описанный выше
            "data_level": "AUCTION_AD", # тип рекламы
            "start_date": START_DATE, # начальный день запроса
            "end_date": END_DATE, # конечный день запроса
            "page_size": PAGE_SIZE, # размер страницы - количество объектов, которое возвращается за один запрос
            "page": 1, # порядковый номер страницы (если данные не поместились в один запрос, аргумент инкрементируется)
            "advertiser_id": advertiser_id, # один из ID из advertiser_ids, который мы получили при генерации access token
            "report_type": "BASIC", # тип отчета
            "dimensions": ["ad_id", "stat_time_day"] # аргументы группировки, вплоть до объявления и за целый день
        }
        result = get(args, access_token) # первый запрос
        result_dict[advertiser_id] = result['data']['list'] # сохраняем ответ на запрос к первой странице
        # пока текущая полученная страница page меньше,
        # чем общее количество страниц в последнем ответе result
        while page < result['data']['page_info']['total_page']:
            # увеличиваем значение страницы на 1
            page += 1
            # обновляем значение текущей страницы в словаре аргументов запроса
            args['page'] = page
            # запрашиваем ответ по текущей странице page
            result = get(args, access_token)
            # накапливаем ответ
            result_dict[advertiser_id] += result['data']['list']
    # результирующий DataFrame, который будем записывать в базу
    data_df = pd.DataFrame()
    # для каждого рекламного аккаунта выполнить преобразование
    for adv_id in advertiser_ids:
        # получаем накопленные разультаты для аккаунта из словаря
        adv_input_list = result_dict[adv_id]
        # временный список
        adv_result_list = []
        # для каждого объекта
        for adv_input_row in adv_input_list:
            # берем словарь метрик
            metrics = adv_input_row['metrics']
            # насыщаем этот словарь словарём измерений
            metrics.update(adv_input_row['dimensions'])
            # добавляем полученный объект во временный список
            adv_result_list.append(metrics)
        # преобразуем временный словарь в DataFrame
        result_df = pd.DataFrame(adv_result_list)
        # добавляем колонку со значением id аккаунта
        result_df['account'] = adv_id
        # добавляем получившийся DataFrame в результирующий
        data_df = data_df.append(
            result_df,
            ignore_index=True
        )
    #
    # здесь пропущены некоторые манипуляции
    # по преобразованию строк в числа
    #
    # создание подключения к базе
    connection = sqlalchemy.create_engine(
        '{db_type}://{user}:{pswd}@{host}:{port}/{path}'.format(
            db_type=db_conn['db_type'],
            user=db_conn['user'],
            pswd=db_conn['password'],
            host=db_conn['host'],
            port=db_conn['port'],
            path=db_conn['path']
        )
    )
    # удаление последних семи дней из базы
    with connection.connect() as conn:
        conn.execute(f"""delete from {SCHEMA}.{TABLE}
        where date >= '{START_DATE}' and date <= '{END_DATE}'""")
    # запись данных из результирующего DataFrame в базу
    data_df.to_sql(
        schema=SCHEMA,
        name=TABLE,
        con=connection,
        if_exists = 'append',
        index = False
    )
Миссия выполнена!Подведем итогиИтого, на всевышеописанные действия было потрачено менее одного рабочего дня (не считая времени, которое приложение было на проверке). Надеюсь, мне удалось показать, что уровень вхождения в API TikTok достаточно низкий, и для настройки автоматического сбора данных не нужно обязательно привлекать разработчика или блуждать по лабиринтам документации и запутанной логики. К слову о лабиринтах, в Facebook тот же самый один рабочий день уходит на то, чтобы создать аккаунт разработчика, протыкать все галочки о политике конфидециальности и условий использования, создать приложение, настроить его и т.д. И в итоге к концу дня у тебя не работающий ETL по сбору данных, а очередной Permission Denied и распухшая голова, в которой крутится только одна мысль – "что я делаю не так".Конечно, сравнивать Facebook и TikTok не очень правильно: второй ещё относительно молод и ему еще только предстоит быть обвешанным хитрыми условиями, запретами и всеми возможными сложностями. Но сейчас всего этого пока нет, так что пользоваться TikTok Marketing API крайне удобно. Надеюсь, моя статья вам немного в этом поможет.Полезные ссылки
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_vebanalitika (Веб-аналитика), #_medijnaja_reklama (Медийная реклама), #_analitika_mobilnyh_prilozhenij (Аналитика мобильных приложений), #_sotsialnye_seti_i_soobschestva (Социальные сети и сообщества), #_tiktok, #_sotsialnye_seti (социальные сети), #_marketing (маркетинг), #_mobilnaja_reklama (мобильная реклама), #_mobilnyj_marketing (мобильный маркетинг), #_facebook_api, #_blog_kompanii_ozon_tech (
Блог компании Ozon Tech
)
, #_vebanalitika (
Веб-аналитика
)
, #_medijnaja_reklama (
Медийная реклама
)
, #_analitika_mobilnyh_prilozhenij (
Аналитика мобильных приложений
)
, #_sotsialnye_seti_i_soobschestva (
Социальные сети и сообщества
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 01-Май 07:04
Часовой пояс: UTC + 5