Как провести анализ рекламных креативов с помощью генеративных сетей
Привет, Хабр!
На связи Сергей и Григорий — Data Scientist’ы.
Сегодня расскажем, как заняли 2 место в общем зачете AI Generative Product Hackathon, инициированного Napoleon IT, и 1 место в кейсе по анализу рекламных креативов для крупной российской фармацевтической компании.
Проблематика
Компания, занимающаяся промышленным производством и продажей медицинских препаратов, представила на хакатон кейс «Медиа корреляции», в котором требовалось проводить анализ рекламных креативов.
Даже самые лучшие креативы перестают приносить конверсии, если крутятся слишком долго. Отслеживание «выгорания» креатива позволяет маркетологу грамотно распределить бюджет и заблаговременно подготовить вариант на смену.
Для компании важно анализировать медиакомпании, выбирать лучше кампании (CTR), которые откликаются у аудитории и использовать эти практики в разработке новых ключевых баннеров по всей России.
Постановка задачи
Кейс включал в себя две разные задачи:
Проанализировать ход рекламной кампании и предоставить рекомендации для маркетолога.
Предложить, основываясь на прошлом опыте, реализацию определенного способа коммуникации. К примеру, текст сообщения для рассылки.
По итогу мы остановились на первом варианте, в котором стояла задача предоставить маркетологу компании дополнительный способ анализа его медиапроектов. Показ рекламы осуществлялся исключительно для уже сформированных баз данных, включающих в себя врачей различных направлений. У нас не было информации о количестве повторных показов для одного пользователя, однако было понятно, что со временем их так или иначе станет больше.
Маркетологи люди занятые, поэтому нам необходимо было забрать у них пару обязанностей, и выкатить решение, которое позволило бы сворачивать уже идущие к своему финалу кампании в идеальное для этого время.
Исходные данные
Исходные данные состояли из csv-файла с данными о предыдущих рекламных кампаниях различных препаратов. Данные были разделены по дням, и для каждого было известно общее количество показа данного типа рекламы («impressions») и количество кликов («clicks»).
Дополнительную информацию о препарате, ЦА и типе рекламы мы получали из столбца «campaign_name».
В качестве целевой метрики использовался CTR, как единственно возможный. Для эффективного отслеживания изменений CTR было испробовано несколько кастомных оценок основанных на скользящих средних, среди которых наиболее эффективным и устойчивым к выбросам оказалось значение скользящего среднего по 3 последним значениям скользящего среднего CTR по последним пяти дням. Вот так выглядел код для преобразования данных:
def pretty_dataframe (dataframe):
dataframe.columns = dataframe.iloc[0]
dataframe.drop (dataframe.index[0], inplace=True)
dataframe.drop ('campaign_id', axis=1, inplace=True)
dataframe.drop ('platform', axis=1, inplace=True)
dataframe = type_dataframe (dataframe)
dataframe = clear_zeros (dataframe)
dataframe = add_click_rate_to_dataframe (dataframe)
dataframe = add_drug_name_to_dataframe (dataframe)
dataframe = add_medic_group_to_dataframe (dataframe)
dataframe = add_adv_format_to_dataframe (dataframe)
dataframe = add_campaign_numbers_to_dataframe (dataframe)
dataframe = add_rolling_mean_to_dataframe (dataframe, window=3)
dataframe = add_rolling_mean_to_dataframe (dataframe, window=5)
dataframe = add_rolling_mean_to_dataframe (dataframe, window=7)
dataframe = add_trend_flag_to_dataframe (dataframe)
dataframe = add_moving_average_change_to_dataframe (dataframe)
dataframe = add_day_of_campaign (dataframe)
return dataframe
Финальный набор новых признаков выглядел примерно так:
Техническое решение
На протяжении всего хакатона мы работали над проектом в тесном сотрудничестве с представителем фармацевтической компании Максимом Кочержинским, руководителем проектов омниканального продвижения. В ходе работы было принято решение по разработке бота для Телеграм, так как данный вариант, в отличие от первоначальной идеи с сайтом или приложением, предполагал легкое взаимодействие с системой без необходимости скачивать дополнительный софт и возможность получать автоматические уведомления при существенных изменениях в показателях рекламной кампании.
Начало взаимодействия с ботом. Как видно, бот умеет парсить Google Sheets и для каждого юзера записывать их в небольшую бд-шку.
В процессе работы работы с данными пользователя система анализирует показатели рекламной кампании на наличие существенных изменений: взлетов или падений и при их наличии формирует автоматический отчёт на основе сравнения данных в рамках текущей рекламной кампании с историческими данными, результаты данного сравнения передаются для в языковую модель для формирования гипотез. Также в боте предусмотрена возможность формирования мгновенного отчёта по запросу пользователя. Вот так примерно выглядел код для формирования отчета:
def generate_report_text (self): # Текст отчёта
if (self.user_timeseries.trend.tail (1).values[0] == 'Increase') & (
self.user_timeseries.moving_average_change.tail (1).values[0] > 0):
trend = f'''\nПоказатель CTR за последние дни вырос на {self.user_timeseries.moving_average_change.tail (1).values[0]:.2f}%!\nДля получения дополнительно информации, можете ознакомиться с графиками.
'''
elif (self.user_timeseries.trend.tail (1).values[0] == 'Decrease') & (
self.user_timeseries.moving_average_change.tail (1).values[0] < 0):
trend = f'''\nПоказатель CTR за последние дни упал на {self.user_timeseries.moving_average_change.tail (1).values[0]:.2f}%.\nРекомендуется ознакомиться с графиком общей тенденции CTR, чтобы принять решение.\n
'''
else:
trend = f'''\nПоказатель CTR колеблется, либо остаётся на одном уровне.\nОзнакомьтесь с другими графиками, чтобы принять взвешенное решение.
'''
impressions = self.user_timeseries.impressions.sum ()
ctr = self.user_timeseries.clicks.sum () / self.user_timeseries.impressions.sum ()
return (
f«Отчёт по креативу:\n»
f«Название препарата: {self.user_timeseries.drug_name[1]}\n»
f«Целевая группа: {self.user_timeseries.medic_group[1]}\n»
f«Тип креатива: {self.user_timeseries.adv_format[1]}\n»
f»{trend}\n»
f«Общее количество показов: {impressions}, это больше чем у »
f»{(self.history_timeseries_for_this.groupby (['campaign_name']).impressions.sum () < impressions).sum() * 100 / (self.history_timeseries_for_this.groupby(['campaign_name']).impressions.sum() < impressions).count():.1f}% "
f«кампаний для данных препарата/ца/формата креатива, а так же больше чем у »
f»{(self.history_timeseries.groupby (['campaign_name']).impressions.sum () < impressions).sum() * 100 / (self.history_timeseries.groupby(['campaign_name']).impressions.sum() < impressions).count():.1f}% "
f«всех предыдущих рекламных кампаний.\n\n»
f«СTR кампании за всё время составил {ctr * 100:.3f}%. Для данных препарата/ца/формата креатива средний показатель CTR составляет »
f»{(self.history_timeseries_for_this.groupby (['campaign_name']).clicks.sum () * 100 / self.history_timeseries_for_this.groupby (['campaign_name']).impressions.sum ()).median ():.3f}% »
f«Средний показатель CTR для всех предыдущих рекламных кампаний составляет »
f»{(self.history_timeseries.groupby (['campaign_name']).clicks.sum () * 100 / self.history_timeseries.groupby (['campaign_name']).impressions.sum ()).median ():.3f}%\n\n»
f«На данный момент кампания длится {self.user_timeseries.day.iloc[-1]} дней.»
f«Обычно кампания для данных препарата/ца/формата креатива длится »
f»{self.history_timeseries_for_this.groupby (['campaign_name', 'campaign_number'])['day'].max ().median ():.0f} дня/дней, а »
f«средняя длительность всех рекламных кампаний составляет {self.history_timeseries.groupby (['campaign_name', 'campaign_number'])['day'].max ().median ():.0f} дня/дней.\n\n»
f«В ходе этой кампании значение CTR росло {(self.user_timeseries.trend == 'Increase').sum ()} дня/дней, падало {(self.user_timeseries.trend == 'Decrease').sum ()} дня/дней и стабилизровалось, либо было стабильно {(self.user_timeseries.trend == 'Plateau').sum ()} дня/дней.»
)
def generate_report_images (self):
report_images = []
buffer = BytesIO ()
fig2, ax = plt.subplots (figsize=(15, 9))
image2 = sns.lineplot (data=self.user_timeseries[self.user_timeseries['day'] > 5], x='day', y='rolled_5', ax=ax,
label=«Текущая кампания»)
history = select_campaign_metrics (drug_name=None, medic_group=self.user_timeseries.medic_group.iloc[0],
adv_format=None)
history = history[history['click_rate'] < 0.015]
history = history[(history['day'] > 5) & (history['day'] < self.user_timeseries.day.max() + 5)]
image2 = sns.lineplot (data=history, x='day', y='rolled_5', ax=ax, label=«Усредненная история кампаний»)
image2.set_ylabel («Среднее CTR»)
image2.set_xlabel («День кампании»)
image2.set_title («Сравнение среднего CTR для данной ГРУППЫ ВРАЧЕЙ в ходе прошлых кампаниях и нынешней»)
ax.legend ()
ax.grid (True)
sns.set_style («whitegrid»)
image2.get_figure ().savefig (buffer, format='png')
buffer.seek (0)
report_images.append (buffer)
buffer.flush ()
buffer2 = BytesIO ()
fig2, ax2 = plt.subplots (figsize=(15, 9))
image2 = sns.lineplot (data=self.user_timeseries[self.user_timeseries['day'] > 5], x='day', y='rolled_5', ax=ax2,
label=«Текущая кампания»)
history = select_campaign_metrics (drug_name=None, medic_group=None, adv_format=None)
history = history[history['click_rate'] < 0.015]
history = history[(history['day'] > 5) & (history['day'] < self.user_timeseries.day.max() + 5)]
image2 = sns.lineplot (data=history, x='day', y='rolled_5', ax=ax2, label=«Усредненная история кампаний»)
image2.set_ylabel («Среднее CTR»)
image2.set_xlabel («День кампании»)
image2.set_title («Сравнение среднего CTR для данного ТИПА КРЕАТИВА в ходе прошлых кампаниях и нынешней»)
ax2.legend ()
ax2.grid (True)
sns.set_style («whitegrid»)
image2.get_figure ().savefig (buffer2, format='png')
buffer2.seek (0)
report_images.append (buffer2)
buffer2.flush ()
buffer3 = BytesIO ()
fig3, ax3 = plt.subplots (figsize=(15, 9))
image3 = sns.lineplot (data=self.user_timeseries[self.user_timeseries['day'] > 5], x='day', y='rolled_5', ax=ax3,
label=«Текущая кампания»)
history = select_campaign_metrics (drug_name=self.user_timeseries.drug_name.iloc[0], medic_group=None,
adv_format=None)
history = history[history['click_rate'] < 0.015]
history = history[(history['day'] > 5) & (history['day'] < self.user_timeseries.day.max() + 5)]
image3 = sns.lineplot (data=history, x='day', y='rolled_5', ax=ax3, label=«Усредненная история кампаний»)
image3.set_ylabel («Среднее CTR»)
image3.set_ylabel («День кампании»)
image2.set_title («Информация об изменении CTR в ходе кампании»)
image3.set_title («Сравнение среднего CTR данного ПРЕПАРАТА в ходе прошлых кампаниях и нынешней»)
ax3.legend ()
ax3.grid (True)
sns.set_style («whitegrid»)
image3.get_figure ().savefig (buffer3, format='png')
buffer3.seek (0)
report_images.append (buffer3)
buffer3.flush ()
return report_images
В окончательный отчет входят:
Шаблонная часть с текущими показателями рекламной кампании.
Гипотезы и рекомендации, сгенерированные языковой моделью.
Визуализация динамики показателей на фоне средней динамики по соответствующей группе.
Ниже приведен пример отчета, полученного от бота в автоматическом режиме:
Отчет по креативу:
Название препарата: -
Целевая группа: pharmacy
Тип креатива: banner
Показатель CTR за последние дни упал на -3.45%.
Рекомендуется ознакомиться с графиком общей тенденции CTR, чтобы принять решение.
Общее количество показов: 355187, это больше чем у 84.6% кампаний для данных препарата/ца/формата креатива, а также больше чем у 84.9% всех предыдущих рекламных кампаний.
СTR кампании за всё время составил 0.365%. Для данных препарата/ца/формата креатива средний показатель CTR составляет 0.441% Средний показатель CTR для всех предыдущих рекламных кампаний составляет 0.372%
На данный момент кампания длится 38 дней.Обычно кампания для данных препарата/ца/формата креатива длится 126 дня/дней, а средняя длительность всех рекламных кампаний составляет 47 дня/дней.
В ходе этой кампании значение CTR росло 11 дня/дней, падало 19 дня/дней и стабилизировалось, либо было стабильно 8 дня/дней.
Гипотеза 1: Кампания находится на этапе спада
За: CTR кампании в данный момент находится ниже среднего показателя как для данного препарата/формата, так и для всех проведенных кампаний. Также за последние дни наблюдается падение этого показателя, что свидетельствует о том, что кампания может быть менее эффективной.
Против: Длительность кампании для данного препарата/формата обычно составляет около 126 дней, кампания длится только 38 дней, что говорит о том, что она еще не достигла своего среднего срока жизни и есть потенциал для роста.
Гипотеза 2: Кампания имеет перспективы на улучшение показателей
За: CTR кампании уже превышает средний показатель по всем проведенным кампаниям. Большое количество показов говорит о хорошей достигаемости и масштабируемости кампании.
Против: В течение длительного периода (19 дней) значение CTR падало и текущий показатель является относительно низким. Это может говорить о неэффективности кампании в текущих рамках.
Целостный вывод: В целом, кампания показывает средний уровень показателя CTR по сравнению со всеми проведенными кампаниями, однако относительно данного препарата/формата креатива показатель ниже среднего. Это, вместе с тем, что кампания находится в стадии спада, говорит о том, что возможно стоит пересмотреть стратегию кампании или принять решение о ее сворачивании. Малое количество дней роста CTR кампании (11 дней) по сравнению с днями падения (19 дней) также свидетельствует о возможности пересмотра выбранной стратегии.
Полный код решения посмотреть здесь
Итог
В рамках реализации проекта мы тесно сотрудничали с экспертами и представителями компании, обсуждая множество идей реализации функционала.
Наша команда постаралась представить максимально подходящее кейсодержателю решение, поэтому в конечном продукте для хакатона не было обучено какой-либо предсказательной модели, весь упор делался на расчёт понятных и хорошо интерпретируемых метрик, которые будут полезны в анализе любому маркетологу. Думаем, что именно длительное и основательное выявление желаний и требований кейсодателя и принесло нам в результате 1 место по кейсу и 2 по всему хакатону)