Привет, Хабр!
На связи Сергей и Григорий - 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 по всему хакатону).