{
    "version": "https:\/\/jsonfeed.org\/version\/1",
    "title": "Блог об аналитике, визуализации данных, data science и BI, заметки с тегом: аналитика",
    "home_page_url": "http:\/\/test.leftjoin.ru\/tags\/analitika\/",
    "feed_url": "http:\/\/test.leftjoin.ru\/tags\/analitika\/json\/",
    "icon": "http:\/\/test.leftjoin.ru\/user\/userpic@2x.jpg",
    "author": {
        "name": "Николай Валиотти",
        "url": "http:\/\/test.leftjoin.ru\/",
        "avatar": "http:\/\/test.leftjoin.ru\/user\/userpic@2x.jpg"
    },
    "items": [
        {
            "id": "122",
            "url": "http:\/\/test.leftjoin.ru\/all\/youtube-api\/",
            "title": "How to: YouTube API",
            "content_html": "<p>Современным аналитикам необходимо обладать навыком сбора информации из социальных сетей, ведь сейчас контент социальных сетей очень точно отражает реальную ситуацию в мире, помогает быстро распространить новости и позволяет анализировать аудиторию — подписчиков. В предыдущих постах мы уже описывали кейсы с использованием различных API: <a href=\"http:\/\/test.leftjoin.ru\/all\/get-data-from-vk\/\">Vkontakte API<\/a>, <a href=\"http:\/\/test.leftjoin.ru\/all\/collecting-costs-from-facebook-api\/\">Facebook API<\/a>, <a href=\"http:\/\/test.leftjoin.ru\/all\/github-api\/\">GitHub API<\/a>.  Сегодня мы расскажем вам о том, что представляет из себя YouTube API, как получить ключ API, а также наглядно покажем, какую информацию можно собрать с его помощью. В двух словах, с помощью YouTube API можно находить каналы по ключевым словам, выгружать данные канала, а также статистику по видео, опубликованным на этих каналах.<\/p>\n<h2>Подготовительный этап для работы с YouTube API<\/h2>\n<p>\tДля начала, нужно разобраться в том, как получить доступ к API. Этот процесс подробно изложен на сайте для разработчиков, на который вы можете перейти по ссылке. Если коротко, то необходимо иметь или завести аккаунт Google, войти в профиль для разработчиков, создать проект, получить ключ API и подключить к нему API YouTube Data API v3. Далее, с использованием этого ключа вам будет доступен весь необходимый функционал.<br \/>\n\tПосле того, как вы успешно получили ключ, можно открывать любой удобный ноутбук (Jupyter Notebook, Collab и т. д.), устанавливать и подключать нужные для работы библиотеки.<\/p>\n<pre class=\"e2-text-code\"><code># установка библиотек\r\n\tpip install --upgrade google-api-python-client\r\n\tpip install --upgrade google-auth-oauthlib google-auth-httplib2\r\n\t# импорт необходимых библиотек\r\nimport googleapiclient.discovery\r\nimport time<\/code><\/pre><h2>Квоты<\/h2>\n<p>Один важный момент, который важно знать при использовании Youtube API — это наличие дневных квот на использование функций YouTube API в бесплатном режиме.  На день дается квота 10000 юнитов, вызов функции поиска стоит 100 юнитов, вызов информации по объекту — 1 юнит, загрузка видео на YouTube стоит 1600 юнитов. Если вам недостаточно дневной квоты, то вы можете подать запрос в Google на её увеличение, в котором нужно подробно указать цели вашей деятельности c YouTube API.<\/p>\n<h2>Поиск YouTube-каналов по ключевым словам<\/h2>\n<p>Для начала заведем несколько переменных, которые понадобятся нам в процессе сбора информации.<\/p>\n<pre class=\"e2-text-code\"><code>channels_data = {}\r\nchannels_data_full = {}\r\nvideo_data = {}<\/code><\/pre><p>Дальше написан скрипт, который можно использовать для поиска перечня каналов по ключевым словам. Мы искали каналы, в названии или описании которых используются следующие слова:  s_query = ’аналитика данных data’. Сначала выводятся каналы, в названии или описании которых присутствуют все три слова, затем хотя бы любые два, затем хотя бы одно. Чем больше ключевых слов по теме мы укажем, тем точнее будет результат.<\/p>\n<pre class=\"e2-text-code\"><code>api_service_name = &quot;youtube&quot;\r\napi_version = &quot;v3&quot;\r\nDEVELOPER_KEY = &quot;&quot; #тут нужно указать ключ, который вы получите при подключении YouTube API\r\n \r\nyoutube = googleapiclient.discovery.build(\r\n   api_service_name, api_version, developerKey = DEVELOPER_KEY)\r\n#строка поиска\r\ns_query = 'аналитика данных data'\r\nnext_token = ''\r\n \r\nwhile(True): \r\n time.sleep(0.2)\r\n request = youtube.search().list(\r\n     part=&quot;snippet&quot;,\r\n     q=s_query,\r\n     relevanceLanguage=&quot;ru&quot;,\r\n     type=&quot;channel&quot;,\r\n     maxResults=25,\r\n     access_token=DEVELOPER_KEY,\r\n     pageToken = next_token\r\n )\r\n response = request.execute()\r\n for item in response['items']:\r\n   channels_data[item['snippet']['channelId']] = [item['snippet']['title'], item['snippet']['description']\r\n   ]\r\n #берем только первые 25 результатов\r\n break<\/code><\/pre><p>Добавим пару важных пояснений относительно скрипта. В начале цикла в этом скрипте (как и в двух последующих) мы вызываем функцию time.sleep(), чтобы инициировать двухсекундную задержку между вызовом функций. Это нужно для того, чтобы запросы к YouTube не были чересчур частыми (и вообще, это считается правилом хорошего тона в программировании, так что советуем взять на заметку).<br \/>\nДля простоты нашего примера мы сохранили только 25 первых каналов из всех подходящих под условия поиска. Если вам хочется найти все каналы, в которых упоминается хотя бы одно из ключевых слов, то нужно использовать следующее свойство:<\/p>\n<pre class=\"e2-text-code\"><code>try:\r\n    next_token = response[&quot;nextPageToken&quot;]\r\n  except:\r\n    break<\/code><\/pre><h2>Сбор полной информации по всем выбранным каналам<\/h2>\n<p>Теперь, когда названия и описания выбранных каналов собраны, можно переходить к следующему этапу, а именно — выгрузке всей информации об этих каналах, в том числе: количество подписчиков канала, количество видео, общее количество просмотров всех видео канала и страна в которой живет, автор канала.<\/p>\n<pre class=\"e2-text-code\"><code>scount = ''\r\nfor channel in channels_data:\r\n   #получаем данные по каждому каналу\r\n   time.sleep(0.2)\r\n   r = youtube.channels().list(\r\n         part=&quot;snippet, statistics&quot;,\r\n         id=channel,\r\n         access_token=DEVELOPER_KEY\r\n   )\r\n   resp = r.execute()\r\n        \r\n   try:\r\n     if resp['items'][0]['statistics']['hiddenSubscriberCount']:\r\n       scount = 'hidden'\r\n     else:\r\n       scount = resp['items'][0]['statistics']['subscriberCount']\r\n  \r\n     channels_data_full[channel] = [resp['items'][0]['snippet']['title'],\r\n                                  resp['items'][0]['snippet']['description'],\r\n                                  scount,\r\n                                  resp['items'][0]['statistics']['videoCount'],\r\n                                  resp['items'][0]['statistics']['viewCount'],\r\n                                  resp['items'][0]['snippet']['country']\r\n     ]\r\n      \r\n   except:\r\n     pass<\/code><\/pre><p>Теперь вся нужная информация о канале хранится в переменнной channels_data_full.<\/p>\n<h2>Получение информации о видео<\/h2>\n<p>Если у вас есть необходимость получить статистику по видео из выбранных каналов, то ниже приведен скрипт на этот случай. В итоге, вы получите словарь   video_data  с подробной информацией о каждом видео из плейлиста (список всех видео каждого канала): название канала, дата публикации, название и описание видео, количество просмотров, лайков\/дизлайков и комментариев.<\/p>\n<pre class=\"e2-text-code\"><code># получаем информацию по всем видео ранее найденных каналов\r\nfor channel in channels_data:\r\n   #анализируем каналы\r\n   time.sleep(0.2)\r\n   r = youtube.channels().list(\r\n           part=&quot;contentDetails&quot;,\r\n           id=channel,\r\n           access_token=DEVELOPER_KEY\r\n     )\r\n   resp = r.execute()           \r\n   try:\r\n     #получаем плейлист из видео для одного канала из списка\r\n     id_playlist = resp['items'][0]['contentDetails']['relatedPlaylists']['uploads']     \r\n     #получаем набор элементов плейлиста (видео)\r\n     next_token = ''\r\n     while(True):     \r\n       time.sleep(0.2)\r\n       r = youtube.playlistItems().list(\r\n             part=&quot;contentDetails&quot;,\r\n             playlistId=id_playlist,\r\n             access_token=DEVELOPER_KEY,\r\n             pageToken = next_token\r\n       )\r\n       resp = r.execute()\r\n       for i in resp['items']:\r\n         id_videos = i['contentDetails']['videoId']\r\n         r = youtube.videos().list(\r\n               part=&quot;snippet, statistics&quot;,\r\n               id=id_videos,               \r\n               access_token=DEVELOPER_KEY\r\n         )\r\n         resp1 = r.execute()       \r\n         video_data[id_videos] = [channel,\r\n                                   resp1['items'][0]['snippet']['publishedAt'],\r\n                                   resp1['items'][0]['snippet']['title'],\r\n                                   resp1['items'][0]['snippet']['description'],\r\n                                   resp1['items'][0]['statistics']['viewCount'],\r\n                                   resp1['items'][0]['statistics']['likeCount'],\r\n                                   resp1['items'][0]['statistics']['dislikeCount'],\r\n                                   resp1['items'][0]['statistics']['commentCount']\r\n          ]\r\n       break<\/code><\/pre><p>В конце мы ставим break, то есть обрабатываем только одну часть видео из плейлиста. Если вы хотите обработать все видео, то нужно использовать функцию nextpagetoken, которую мы предложили в конце первого скрипта.<br \/>\nВ итоге, если трансформировать словарь в привычный датафрейм, мы получим таблицу, которая содержит подробную информацию про все обработанные видео.<\/p>\n<pre class=\"e2-text-code\"><code>d = {'id': [x for x in video_data],\r\n      'channel_id': [video_data[x][0] for x in video_data],\r\n       'published_at': [video_data[x][1] for x in video_data],\r\n      'title': [video_data[x][2] for x in video_data],\r\n      'description': [video_data[x][3] for x in video_data],\r\n      'viewCount': [video_data[x][4] for x in video_data],\r\n      'likeCount': [video_data[x][5] for x in video_data],\r\n      'dislikeCount': [video_data[x][6] for x in video_data],\r\n      'commentCount': [video_data[x][7] for x in video_data]\r\n   }\r\ndf = pd.DataFrame(d)\r\ndf.head()<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/dataframe.png.jpg\" width=\"2560\" height=\"289\" alt=\"\" \/>\n<\/div>\n<h2>Выводы<\/h2>\n<p>Конечно, это не все способы работы с YouTube API, однако, мы надеемся, что вы получили представление о том, как сильно расширяются возможности аналитика для получения и обработки информации с помощью этого инструмента.<\/p>\n",
            "date_published": "2021-10-26T13:13:34+03:00",
            "date_modified": "2021-10-26T13:13:27+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/dataframe.png.jpg",
            "_date_published_rfc2822": "Tue, 26 Oct 2021 13:13:34 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "122",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/dataframe.png.jpg"
                ]
            }
        },
        {
            "id": "111",
            "url": "http:\/\/test.leftjoin.ru\/",
            "title": "Анализируем речь в Python: О чем говорят гости youtube-канала вДудь",
            "content_html": "<p>Сегодня при помощи ML мы будем анализировать прямую речь. В качестве данных используем интервью, которые журналист Юрий Дудь берет для своего YouTube-канала.<br \/>\nВыход практически каждого ролика на канале «вДудь» считается событием, а некоторые из этих релизов даже сопровождаются скандалами из-за неосторожных высказываний его гостей.<br \/>\nПосмотрим с помощью Python и лемматизации о чем таком интересном рассказывали герои роликов канала «вДудь».<\/p>\n<p><b>Парсим тексты субтитров<\/b><br \/>\nВ этом проекте мы будем использовать библиотеки, которые обрабатывают тексты, но сначала нам нужно эти тексты добыть. Импортируем API-интерфейс Python <span class=\"inline-code\">youtube_transcript_api<\/span>, который скачивает субтитры из видео на YouTube.<\/p>\n<pre class=\"e2-text-code\"><code>import pandas as pd\r\nimport numpy as np\r\n\r\nfrom youtube_transcript_api import YouTubeTranscriptApi\r\nimport json<\/code><\/pre><p>Предобработаем URL видео для скачивания субтитров. Всего мы собрали 100 роликов с интервью. В некоторых из интервью нет подготовленных субтитров. В файле <span class=\"inline-code\">‘dud.csv’<\/span> заранее подготовлен список гостей канала вДудь с ссылками на их интервью.<\/p>\n<pre class=\"e2-text-code\"><code>def new_url(s):\r\n    return s.replace('watch?v=','').replace('be.com','.be').replace('www.','')\r\n\r\ndef url_to_id(s):\r\n    return s.partition('be\/')[2]\r\n\r\ndf = pd.read_csv('dud.csv')\r\ndf['URL'] = df['URL'].apply(new_url)\r\ndf['video_id'] = df['URL'].apply(url_to_id)\r\ndf = df.set_index(keys='Гость')<\/code><\/pre><p>У нас теперь есть датафрейм, в котором пока только информация о гостях и ссылка на видео. Но это пока.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-06-01--10.49.04.png\" width=\"638\" height=\"407\" alt=\"\" \/>\n<\/div>\n<p>Загрузим в нашу таблицу субтитры интервью. Если субтитры найти не удалось, то выведем на экран имена людей, к интервью с которыми их нет или они отключены (или субтитры есть, но не на русском языке).<\/p>\n<pre class=\"e2-text-code\"><code>texts = []\r\nno_sub = []\r\nfor speaker in df.index:\r\n    video_id = df.loc[speaker,'video_id']\r\n    try:\r\n        data = YouTubeTranscriptApi.get_transcript(video_id, languages=['ru', 'ru'])\r\n        data = ' '.join([words['text'] for words in data])\r\n    except Exception:\r\n        print('Нет Субтитров для: ', speaker)\r\n        no_sub.append(speaker)\r\n        data = &quot;&quot;\r\n    texts.append(data)\r\ndf['text'] = texts\r\ndf.to_csv('df_dud.csv')<\/code><\/pre><p>У девяти из 100 интервьюируемых субтитров не оказалось и нам вернулся такой текст:<\/p>\n<pre class=\"e2-text-code\"><code>Нет Субтитров для:  L'one\r\nНет Субтитров для:  Шнур\r\nНет Субтитров для:  Ресторатор\r\nНет Субтитров для:  Амиран\r\nНет Субтитров для:  Ильич\r\nНет Субтитров для:  Соболев\r\nНет Субтитров для:  Иван Дорн\r\nНет Субтитров для:  Навальный\r\nНет Субтитров для:  Noize MC<\/code><\/pre><p><b>Анализируем тексты<\/b><br \/>\nАнализ текстовой информации сложен в той степени, в какой сложен язык, на котором написан текст. Самый популярный способ решения такой аналитической задачи — стемминг. Стеммингом называют процесс нахождения стема — основы слова. Для стемминга используют библиотеку NLTK (Natural Language Toolkit), которая содержит правила образования стемов.<br \/>\nЭтот метод хорошо работает с английскими словами, но у русского языка слишком сложно устроена морфология образования слов, что повышает вероятность ошибки. Стемминг будет хорошим выбором для анализа строк, содержание которых вы примерно представляете себе (например, когда пользователя просят заполнить форму).<br \/>\nДля нашего кейса лучше выбрать лемматизацию — приведение слова к его словарной форме. Проведя лемматизацию текстовых данных по правилам русского языка мы получим существительные в именительном падеже единственного числа (кошками — кошка), прилагательные в именительном падеже мужского рода (пушистая — пушистый), а глаголы в инфинитиве несовершенного вида (бежит — бежать). В этом проекте мы используем MyStem и Pymorphy. Обе библиотеки представляют собой морфологические анализаторы.<br \/>\nКроме того, поскольку при анализе  мы будем использовать алгоритмы машинного обучения, то нам нужно избавиться от слов, которые часто встречаются, но не несут какой-то ценности для анализа. В противном случае они могут повлиять на работу модели. Список таких стоп-слов возьмем из библиотеки <span class=\"inline-code\">nltk.corpus<\/span>.<br \/>\nМаксимально подробно о подготовке текста к анализу мы рассказывали в материале <a href=\"http:\/\/test.leftjoin.ru\/all\/borderline-text-analysis\/\" class=\"nu\">«<u>Python и тексты нового альбома Земфиры<\/u>»<\/a>. Тут была проведена идентичная работа подготовка текстов, после чего мы посчитали количество уникальных слов (’Unique Words’) и записали, как часто они встречаются в речи собеседников Дудя (‘PPT Unique Words’).<\/p>\n<pre class=\"e2-text-code\"><code>df['Total Words'] = df['text'].apply(number_words)\r\ndf['Unique Words'] = df['text'].apply(set).apply(len)\r\ndf['PPT Unique Words'] = df['Unique Words'] \/ df['Total Words'] * 100\r\ndf['PPT Unique Words'] = df['PPT Unique Words'].apply(lambda x: round(x,2))\r\ndf.to_csv('df_dud.csv')<\/code><\/pre><p><b>Строим облако слов<\/b><br \/>\nАвтоматизируем построение облака слов для каждого гостя Дудя. Таким образом мы узнаем какие слова встречаются в их речи чаще всего. Для визуализации инсталлируем <span class=\"inline-code\">wordcloud<\/span>, а <span class=\"inline-code\">word_tokenize<\/span> подсчитает количество слов, которые будут встречаться чаще всего.<\/p>\n<pre class=\"e2-text-code\"><code>import nltk\r\nfrom wordcloud import WordCloud\r\nimport pandas as pd\r\nimport matplotlib.pyplot as plt\r\nfrom nltk import word_tokenize, ngrams\r\n\r\ndef word_cloud(df, occup=None, general=True):\r\n    if occup:\r\n        df = df[df['Род деятельности'] == occup] \r\n    if general:\r\n        data_source = zip([occup], [' '.join([el for el in df['Prepared Text']])])\r\n        col_count, row_count = 1, 1\r\n    else:\r\n        data_source = zip([el for el in df.index], df['Prepared Text']) \r\n        col_count = max(1, df.shape[0] \/\/ 3)\r\n        row_count = df.shape[0] \/\/ col_count + 1\r\n        \r\n    fig = plt.figure()\r\n    plt.figure(figsize=(10, 10))\r\n    fig.patch.set_facecolor('white')\r\n    plt.subplots_adjust(wspace=0.3, hspace=0.2)\r\n    i = 1\r\n    for name, text in data_source:\r\n        tokens = word_tokenize(text)\r\n        text_raw = &quot; &quot;.join(tokens)\r\n        wordcloud = WordCloud(colormap='PuBu', background_color='white', contour_width=10).generate(text_raw)\r\n        plt.subplot(row_count, col_count, i, label=name,frame_on=True)\r\n        plt.tick_params(labelsize=10)\r\n        plt.imshow(wordcloud)\r\n        plt.axis(&quot;off&quot;)\r\n        plt.title(name,fontdict={'fontsize':12,'color':'grey'},y=1.0)\r\n        plt.tick_params(labelsize=10)\r\n        i += 1\r\n    plt.savefig(f'.\/word_cloud\/{occup}.png', dpi=900)<\/code><\/pre><p>У нас получились вот такие облака слов по каждому из гостей программы:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-06-02--18.58.17.png\" width=\"1198\" height=\"678\" alt=\"\" \/>\n<\/div>\n<p><b>Работа с Word2vec<\/b><br \/>\nС помощью библиотеки <span class=\"inline-code\">gensim<\/span> вызываем модуль, который должен представить слова в наших текстах как векторы.<\/p>\n<pre class=\"e2-text-code\"><code>import plotly.graph_objects as go\r\nimport plotly.figure_factory as ff\r\nfrom scipy import spatial\r\nimport collections\r\nimport pymorphy2\r\nimport gensim\r\n\r\nmorph = pymorphy2.MorphAnalyzer()<\/code><\/pre><p>Для работы модели используем бинарный файл <span class=\"inline-code\">‘model.bin’<\/span>:<\/p>\n<pre class=\"e2-text-code\"><code>model = gensim.models.KeyedVectors.load_word2vec_format('model.bin', binary=True)<\/code><\/pre><p>Модель <b>Word2Vec<\/b> основана на нейронных сетях и позволяет представлять слова в виде векторов, учитывая семантическую составляющую. Ее мы уже использовали в анализе лирики Земфиры. Косинусная мера семантически схожих слов будет стремиться к 1, а  у двух слов, не имеющих ничего общего по смыслу, она близка к 0.<br \/>\nНапишем функцию, которая будет принимать список слов из наших интервью, распознавать для каждого часть речи, а затем получать и суммировать вектора — так мы сможем находить вектора не для одного слова, а для целых предложений и текстов.<\/p>\n<pre class=\"e2-text-code\"><code>def get_vector(word_list):\r\n    vector = 0\r\n    for word in word_list:\r\n        pos = morph.parse(word)[0].tag.POS\r\n        if pos == 'INFN':\r\n            pos = 'VERB'\r\n        if pos in ['ADJF', 'PRCL', 'ADVB', 'NPRO']:\r\n            pos = 'NOUN'\r\n        if word and pos:\r\n            try:\r\n                word_pos = word + '_' + pos\r\n                this_vector = model.word_vec(word_pos)\r\n                vector += this_vector\r\n            except KeyError:\r\n                continue\r\n    return vector<\/code><\/pre><p>Для каждого интервью находим вектор и собираем соответствующий столбец в датафрейм:<\/p>\n<pre class=\"e2-text-code\"><code>vec_list = []\r\nfor word in df['Prepared Text']:\r\n    vec_list.append(get_vector(word.split()))\r\ndf['Vector'] = vec_list<\/code><\/pre><p>Напишем функцию, который будет подсчитывать N-граммы для каждого гостя:<\/p>\n<pre class=\"e2-text-code\"><code>def get_top_five_ngrams(text, n):\r\n    counter = collections.Counter()\r\n    bigrams = list(ngrams(text, n))\r\n    counter.update(bigrams)\r\n    return counter.most_common()[:10]<\/code><\/pre><p>Построим топ N-грамм в соответствии с группой:<\/p>\n<pre class=\"e2-text-code\"><code>top_words = dict.fromkeys(df.index)\r\nfor person in df.index:\r\n    text = df.loc[person,'Prepared Text']\r\n    n_gram = get_top_five_ngrams(text.split(), 1)\r\n    n_list = []\r\n    for item in n_gram:\r\n        n_list.append(item[0][0])\r\n    top_words[person] = n_list\r\nordered_pesrons = df.index\r\ntop_2_words = []\r\nfor person in ordered_pesrons:\r\n    top_2_words.append(top_words[person])\r\ndf['Top bigramms'] = top_2_words<\/code><\/pre><p>Напишем функцию, которая будет добавлять самые часто встречающиеся слова в речи интервьюируемого:<\/p>\n<pre class=\"e2-text-code\"><code>def top_similar(df, occup=None, agg='Person'):\r\n    if occup:\r\n        df = df[df['Род деятельности'] == occup]\r\n    if agg == 'Person':\r\n        top_words_person = dict.fromkeys(df.index)\r\n        for person in df.index:\r\n            vec = df.loc[person, 'Vector']\r\n            words = model.similar_by_vector(vec, topn=10)\r\n            top_words_person[person] = [el[0].split('_')[0] for el in words]\r\n        df_person_words = pd.DataFrame(columns=[agg,'Top Words'])\r\n    elif agg == 'Total':\r\n        top_words_person = {'Total':0}\r\n        vec = df['Vector'].sum()\r\n        words = model.similar_by_vector(vec, topn=10)\r\n        top_words_person['Total'] = [el[0].split('_')[0] for el in words]\r\n        df_person_words = pd.DataFrame(columns=[agg,'Top Words'])\r\n    \r\n    for k,v in top_words_person.items():\r\n        df_person_words = df_person_words.append({agg:k, 'Top Words':v},ignore_index=True)\r\n    df_person_words = df_person_words.set_index(keys=agg) \r\n    \r\n    return df_person_words<\/code><\/pre><p>Для дальнейшей работы группируем гостей по цеховой принадлежности. Наверное, можно ожидать, что режиссеры будут обсуждать кино и все, что с ним связано, а музыканты — музыку.<\/p>\n<pre class=\"e2-text-code\"><code>df_occup = pd.DataFrame(columns=['Occupation', 'Top Words'])\r\nfor occup in df['Род деятельности'].unique():\r\n    words = top_similar(df, occup=occup, agg='Total')['Top Words'][0]\r\n    df_occup = df_occup.append({'Occupation':occup, 'Top Words’:words},ignore_index=True)\r\n\r\nfor i in range(10):\r\n    df_occup[f'Top {i+1} word'] = df_occup['Top Words'].apply(lambda x: x[i])<\/code><\/pre><p>У нас получится новый датафрейм с топом слов для каждой категории гостей (музыкант, политик, актер и тд).<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-06-02--20.55.04.png\" width=\"1104\" height=\"631\" alt=\"\" \/>\n<\/div>\n<p><b>Анализ риторики гостя<\/b><br \/>\nИспользуя метод <span class=\"inline-code\">similar_by_vector<\/span> для каждого из видов деятельности интервьюируемых, мы получаем список слов, которые наиболее точно описывают тематику текстов.<br \/>\nСтоит отметить, что слово «государство» стоит на первом месте не только в интервью политиков и бизнесменов, но и дизайнеров с писателями. Очевидно, что тема разговора у всех профессиональных групп смещена в сторону политики.<br \/>\nАктёры, кинокритики и музыканты описываются вполне закономерными для их сфер деятельности словами. А вот у фотографов нет ни слова про фотографию или творчество, но есть «работа», «трудоустройство», «существовать» и “семья”.<br \/>\nСравним риторику героев, построив box plot для каждой категории с помощью <span class=\"inline-code\">plotly<\/span>.<\/p>\n<pre class=\"e2-text-code\"><code>import plotly.express as px\r\n\r\nl = []\r\nfor el,ind in zip(df['Род деятельности'].value_counts(), df['Род деятельности'].value_counts().index):\r\n    if el &gt; 1:\r\n        l.append(ind)\r\n\r\ndf_kpi = df[df['Род деятельности'].isin(l)]\r\nfor kpi in ['Total Words', 'Unique Words','PPT Unique Words']:\r\n    buf_df = df_kpi[['Род деятельности',kpi]]\r\n    fig = px.box(df_kpi, \r\n                 x='Род деятельности',\r\n                 y=kpi,\r\n                )\r\n    fig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Bespalova\/3.embed?link=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Наиболее разговорчивыми гостями оказались блогеры — и в среднем, и по медиане они наговорили больше всего слов. И опередили по этому показателю даже писателей. А вот самыми немногословными оказались рэперы, хотя, казалось бы, вот кто должен быть хорош в импровизации.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Bespalova\/5.embed?link=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Что касается количества уникальных слов, то и тут блогеры значительно ушли вперед. Согласно медианным значениям, тройка лидеров выглядит так — блогер, журналист и писатель. А вот словарный запас рэперов оставляет желать лучшего.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Bespalova\/1.embed?link=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Если говорить об отношении уникальных слов к общему количеству, то у всех групп гостей примерно одинаковый медианный показатель. Наиболее вариативными оказались музыканты — усы от их ящика показываю наибольший разброс значений.<\/p>\n",
            "date_published": "2021-06-07T11:26:28+03:00",
            "date_modified": "2023-05-19T15:03:12+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/Total-Words.png",
            "_date_published_rfc2822": "Mon, 07 Jun 2021 11:26:28 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "111",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/Total-Words.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/Unique-Words.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/PPT-Unique-Words.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-06-01--10.49.04.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-06-02--18.58.17.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-06-02--20.55.04.png"
                ]
            }
        },
        {
            "id": "110",
            "url": "http:\/\/test.leftjoin.ru\/all\/parser-indeed-with-python\/",
            "title": "Парсим вакансии для аналитиков из Indeed",
            "content_html": "<p>В этом материале мы расскажем, как парсить вакансии с сайта Indeed. Indeed — это крупнейший в мире поисковик вакансий. Этим текстом мы начинаем большой проект по анализу и визуализации показателей оплаты труда в области Data Science в разных странах.<br \/>\nПодобный анализ рынка вакансий, но только в России, мы проводили в материале <a href=\"http:\/\/test.leftjoin.ru\/all\/hh-dashboard-bi-and-analysts-market\/\">Анализ рынка вакансий аналитики и BI: дашборд в Tableau<\/a>, когда парсили данные с сайта HeadHunter.<\/p>\n<p class=\"note\">А еще у нас можно почитать материал  <a href=\"http:\/\/test.leftjoin.ru\/all\/parsim-dannye-kataloga-sayta-ispolzuya-beautiful-soup-i-selenium\/\">Парсим данные каталога сайта, используя Beautiful Soup и Selenium<\/a><\/p>\n<p><b>Импорт библиотек<\/b><br \/>\nБиблиотека <span class=\"inline-code\">fake_useragent<\/span> имитирует реальный User-Agent, чтобы преодолеть защиту сайта от парсинга. Таким образом мы сможем пройти проверку HTTP заголовка User-Agent.<br \/>\nМодуль <span class=\"inline-code\">urllib.parse<\/span> разбирает URL-адрес на компоненты и записывает его как кортеж. Он пригодится для перехода на карточки вакансий. BeautifulSoup поможет разобраться в структуре html-страницы и добыть нужную нам информацию.<\/p>\n<pre class=\"e2-text-code\"><code>import requests\r\nfrom datetime import timedelta, datetime\r\nimport urllib.parse\r\nfrom fake_useragent import UserAgent\r\nfrom bs4 import BeautifulSoup\r\nimport pandas as pd\r\nimport time\r\nfrom lxml.html import fromstring\r\nfrom clickhouse_driver import Client\r\nfrom clickhouse_driver import errors\r\nimport numpy as np\r\nfrom funcs import check_title, get_skills_row, parse_salary, get_sheetname, create_table<\/code><\/pre><p><b>Создадим таблицу в Clickhouse<\/b><br \/>\nДанные, которые мы собираемся собрать, будем хранить в базе Clickhouse.<\/p>\n<pre class=\"e2-text-code\"><code>create_table = '''CREATE TABLE if not exists indeed.vacancies (\r\n    row_idx UInt16,\r\n    query_string String,\r\n    country String,\r\n    title String,\r\n    company String,\r\n    city String,\r\n    job_added Date,\r\n    easy_apply UInt8,\r\n    company_rating Nullable(Float32),\r\n    remote UInt8,\r\n    job_id String,\r\n    job_link String,\r\n    sheet String,\r\n    skills String,\r\n    added_date Date,\r\n    month_salary_from_USD Float64,\r\n    month_salary_to_USD Float64,\r\n    year_salary_from_USD Float64,\r\n    year_salary_to_USD Float64,\r\n)\r\nENGINE = ReplacingMergeTree\r\nSETTINGS index_granularity = 8192'''<\/code><\/pre><p><b>Обход блокировок<\/b><br \/>\nНам нужно обойти защиту Indeed и избежать блокировки по IP. Для этого используем анонимные прокси адреса на сайте free-proxy-list.net. Как собрать свежие прокси, мы писали в нашем предыдущем тексте <a href=\"http:\/\/test.leftjoin.ru\/all\/selenium-proxy\/\" class=\"nu\">«<u>Пишем парсер свежих прокси на Python для Selenium<\/u>»<\/a>. Прокси адреса мы запишем в массив, который понадобится в момент обращения к Indeed, когда запрос будет проверять User-Agent.<\/p>\n<p>Данный метод удаляет IP из списка с прокси в том случае, если ответ от Indeed через него так и не пришел.<\/p>\n<pre class=\"e2-text-code\"><code>def remove_proxy_from_list_and_update_if_required(proxy):\r\n    global _proxies\r\n    _proxies.remove(proxy)\r\n    if len(_proxies) == 0:\r\n        update_proxy_list()<\/code><\/pre><p>Функция, используя прокси, возвращает нам страницу Indeed, из которой мы впоследствии спарсим данные.<\/p>\n<pre class=\"e2-text-code\"><code>def get_page(updated_url, session):\r\n    proxy = get_proxy()\r\n    proxy_dict = {&quot;http&quot;: proxy, &quot;https&quot;: proxy}\r\n    logger.info(f'try with proxy: {proxy}')\r\n    try:\r\n        session.proxies = proxy_dict\r\n        return session.get(updated_url, timeout=15)\r\n    except (requests.exceptions.RequestException, requests.exceptions.ProxyError, requests.exceptions.ConnectTimeout,\r\n            requests.exceptions.ReadTimeout, requests.exceptions.SSLError,\r\n            requests.exceptions.ConnectionError, url_ex.MaxRetryError, ConnectionResetError,\r\n            socket.timeout, url_ex.ReadTimeoutError):\r\n        remove_proxy_from_list_and_update_if_required(proxy)\r\n        logger.info(f'try with proxy {proxy}')\r\n        return get_page(updated_url, session)<\/code><\/pre><p><b>Методы для парсера<\/b><br \/>\nИскомые данные нужно будет искать по тегам и атрибутам верстки с помощью BeautifulSoup. Мы заранее собрали ключевые слова, которые нас будут интересовать в вакансиях, и подготовили с ними отдельный датасет.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-05-26--10.28.08.png\" width=\"1078\" height=\"686\" alt=\"\" \/>\n<\/div>\n<p>В карточках вакансий нет точной даты публикации, указано лишь сколько дней назад она была опубликована. Сохраним точную дату публикации в традиционном формате с помощью <span class=\"inline-code\">timedelta<\/span>.<\/p>\n<pre class=\"e2-text-code\"><code>def raw_date_to_str(raw_date):\r\n    raw_date = raw_date.lower()\r\n    if '+' in raw_date or &quot;более&quot; in raw_date:\r\n        delta = timedelta(days=32)\r\n        return (datetime.now() - delta).strftime(&quot;%Y-%m-%d&quot;)\r\n    else:\r\n        parts = raw_date.split()\r\n        for part in parts:\r\n            if part.isdigit():\r\n                delta = timedelta(days=part.isdigit())\r\n                return (datetime.now() - delta).strftime(&quot;%Y-%m-%d&quot;)\r\n    return &quot;&quot;<\/code><\/pre><p>Сохраним id вакансии в системе Indeed. Подставляя id в URL страницы, мы сможем получить доступ к полному описанию вакансий.<\/p>\n<pre class=\"e2-text-code\"><code>def get_job_id_from_card(card):\r\n    try:\r\n        return card['id'].split('_')[1]\r\n    except:\r\n        return &quot;&quot;<\/code><\/pre><p>Данный метод соберет названия вакансий.<\/p>\n<pre class=\"e2-text-code\"><code>def get_title_from_card(card):\r\n    try:\r\n        job_title = card.find('a', {'class': 'jobtitle'}).text\r\n        return job_title.replace('\\n', '')\r\n    except:\r\n        return ''<\/code><\/pre><p>Аналогичным образом напишем методы, которые будут собирать данные о названии компании, времени публикации объявления, местоположении работодателя и рейтинге работодателя на портале.<\/p>\n<p>URL сайта Indeed пишется для разных стран по-разному. Для США это будет просто indeed.com, а локализации для других стран получают префиксом xx.indeed.com. Список с префиксами мы собрали в массив заранее из <a href=\"\"><a href=\"https:\/\/opensource.indeedeng.io\/api-documentation\/docs\/supported-countries\/\">https:\/\/opensource.indeedeng.io\/api-documentation\/docs\/supported-countries\/<\/a> списка<\/a> Indeed.<\/p>\n<pre class=\"e2-text-code\"><code>def get_link_from_card(card, card_country):\r\n    try:\r\n        if card_country == 'us':\r\n            return f&quot;https:\/\/indeed.com{card.find('a', {'class': 'jobtitle'})['href']}&quot;\r\n        else:\r\n            return f&quot;https:\/\/{card_country}.indeed.com{card.find('a', {'class': 'jobtitle'})['href']}&quot;\r\n    except:\r\n        return &quot;&quot;<\/code><\/pre><p>Спарсим описание вакансии, которое можно найти по тегу ’summary’. Именно там содержатся требования, которые предъявляют к кандидату.<\/p>\n<pre class=\"e2-text-code\"><code>def get_summary_from_card_and_transform_to_skills(card):\r\n    try:\r\n        smr = card.find('div', {'class': 'summary'}).text\r\n        return get_skills_row(smr)\r\n    except:\r\n        return &quot;&quot;\r\nНеобходимые hard-skills из описания вакансий будем сверять со списком 'skills'. \r\nskills = [&quot;python&quot;, &quot;tableau&quot;, &quot;etl&quot;, &quot;power bi&quot;, &quot;d3.js&quot;, &quot;qlik&quot;, &quot;qlikview&quot;, &quot;qliksense&quot;,\r\n          &quot;redash&quot;, &quot;metabase&quot;, &quot;numpy&quot;, &quot;pandas&quot;, &quot;congos&quot;, &quot;superset&quot;, &quot;matplotlib&quot;, &quot;plotly&quot;,\r\n          &quot;airflow&quot;, &quot;spark&quot;, &quot;luigi&quot;, &quot;machine learning&quot;, &quot;amplitude&quot;, &quot;sql&quot;, &quot;nosql&quot;, &quot;clickhouse&quot;,\r\n          'sas', &quot;hadoop&quot;, &quot;pytorch&quot;, &quot;tensorflow&quot;, &quot;bash&quot;, &quot;scala&quot;, &quot;git&quot;, &quot;aws&quot;, &quot;docker&quot;,\r\n          &quot;linux&quot;, &quot;kafka&quot;, &quot;nifi&quot;, &quot;ozzie&quot;, &quot;ssas&quot;, &quot;ssis&quot;, &quot;redis&quot;, 'olap', ' r ', 'bigquery', 'api', 'excel']<\/code><\/pre><p>Эта функция разобьет ’summary’ на слова пробелом и проверит их на соответствие нашему списку. В датасет будут возвращаться совпадения с нашим списком hard-skills.<\/p>\n<pre class=\"e2-text-code\"><code>def get_skills_row(summary):\r\n    summary = summary.lower()\r\n    row = []\r\n    for sk in skills:\r\n        if sk in summary:\r\n            row.append(sk)\r\n    return ','.join(row)<\/code><\/pre><p>На выходе мы получим таблицу с примерно 30 тысячами строк.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-05-21--17.29.19.png\" width=\"1105\" height=\"658\" alt=\"\" \/>\n<\/div>\n<p>Полный код проекта можно посмотреть в нашем <a href=\"https:\/\/github.com\/valiotti\/leftjoin\/tree\/master\/indeed\"> репозитории<\/a> на GitHub.<\/p>\n",
            "date_published": "2021-05-27T10:10:46+03:00",
            "date_modified": "2021-05-26T12:43:15+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/--2021-05-26--10.28.08.png",
            "_date_published_rfc2822": "Thu, 27 May 2021 10:10:46 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "110",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-05-26--10.28.08.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-05-21--17.29.19.png"
                ]
            }
        },
        {
            "id": "109",
            "url": "http:\/\/test.leftjoin.ru\/all\/data-scaling-with-sql\/",
            "title": "Нормализация данных через запрос в SQL",
            "content_html": "<p>Главный принцип анализа данных GIGO (от англ. garbage in — garbage out, дословный перевод «мусор на входе — мусор на выходе») говорит нам о том, что ошибки во входных данных всегда приводят к неверным результатам анализа. От того, насколько хорошо подготовлены  данные, зависят результаты всей вашей работы.<\/p>\n<p>Например, перед нами стоит задача подготовить выборку для использования в алгоритме машинного обучения (модели k-NN, k-means, логической регрессии и др). Признаки в исходном наборе данных могут быть в разном масштабе, как, например, возраст и рост человека. Это может привести к некорректной работе алгоритма. Такого рода данные нужно предварительно масштабировать.<\/p>\n<p>В данном материале мы рассмотрим способы масштабирования данных через запрос в SQL: масштабирование методом min-max, min-max для произвольного диапазона и z-score нормализация. Для каждого из методов мы подготовили по два примера написания запроса — один с помощью подзапроса SELECT, а второй используя оконную функцию OVER().<\/p>\n<p>Для работы возьмем таблицу <b>students<\/b> с данными о росте учащихся.<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<\/tr>\n<tr>\n<td>Иван<\/td>\n<td style=\"text-align: right\">174<\/td>\n<\/tr>\n<tr>\n<td>Петр<\/td>\n<td style=\"text-align: right\">181<\/td>\n<\/tr>\n<tr>\n<td>Денис<\/td>\n<td style=\"text-align: right\">199<\/td>\n<\/tr>\n<tr>\n<td>Ксения<\/td>\n<td style=\"text-align: right\">158<\/td>\n<\/tr>\n<tr>\n<td>Сергей<\/td>\n<td style=\"text-align: right\">179<\/td>\n<\/tr>\n<tr>\n<td>Ольга<\/td>\n<td style=\"text-align: right\">165<\/td>\n<\/tr>\n<tr>\n<td>Юлия<\/td>\n<td style=\"text-align: right\">152<\/td>\n<\/tr>\n<tr>\n<td>Кирилл<\/td>\n<td style=\"text-align: right\">188<\/td>\n<\/tr>\n<tr>\n<td>Антон<\/td>\n<td style=\"text-align: right\">177<\/td>\n<\/tr>\n<tr>\n<td>Софья<\/td>\n<td style=\"text-align: right\">165<\/td>\n<\/tr>\n<\/table>\n<\/div>\n<h2>Min-Max масштабирование<\/h2>\n<p>Подход min-max масштабирования заключается в том, что данные масштабируются до фиксированного диапазона, который обычно составляет от 0 до 1. В данном случае мы получим все данные в одном масштабе, что исключит влияние выбросов на выводы.<\/p>\n<p>Выполним масштабирование по формуле:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-05-12--16.42.52.png\" width=\"460\" height=\"104\" alt=\"\" \/>\n<\/div>\n<p>Умножаем числитель на 1.0, чтобы в результате получилось число с плавающей точкой.<\/p>\n<p>SQL-запрос с подзапросом:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       1.0 * (height-t1.min_height)\/(t1.max_height - t1.min_height) AS scaled_minmax\r\n  FROM students, \r\n      (SELECT min(height) as min_height, \r\n              max(height) as max_height \r\n         FROM students\r\n      ) as t1;<\/code><\/pre><p>SQL-запрос с оконной функцией:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       (height - MIN(height) OVER ()) * 1.0 \/ (MAX(height) OVER () - MIN(height) OVER ()) AS scaled_minmax\r\n  FROM students;<\/code><\/pre><p>В результате мы получим переменные в диапазоне [0...1], где за 0 принят рост самого невысокого учащегося, а 1 рост самого высокого.<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<td>scaled_minmax<\/td>\n<\/tr>\n<tr>\n<td>Иван<\/td>\n<td style=\"text-align: right\">174<\/td>\n<td>0.46809<\/td>\n<\/tr>\n<tr>\n<td>Петр<\/td>\n<td style=\"text-align: right\">181<\/td>\n<td>0.61702<\/td>\n<\/tr>\n<tr>\n<td>Денис<\/td>\n<td style=\"text-align: right\">199<\/td>\n<td>1<\/td>\n<\/tr>\n<tr>\n<td>Ксения<\/td>\n<td style=\"text-align: right\">158<\/td>\n<td>0.12766<\/td>\n<\/tr>\n<tr>\n<td>Сергей<\/td>\n<td style=\"text-align: right\">179<\/td>\n<td>0.57447<\/td>\n<\/tr>\n<tr>\n<td>Ольга<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>0.2766<\/td>\n<\/tr>\n<tr>\n<td>Юлия<\/td>\n<td style=\"text-align: right\">152<\/td>\n<td>0<\/td>\n<\/tr>\n<tr>\n<td>Кирилл<\/td>\n<td style=\"text-align: right\">188<\/td>\n<td>0.76596<\/td>\n<\/tr>\n<tr>\n<td>Антон<\/td>\n<td style=\"text-align: right\">177<\/td>\n<td>0.53191<\/td>\n<\/tr>\n<tr>\n<td>Софья<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>0.2766<\/td>\n<\/tr>\n<\/table>\n<\/div>\n<h2>Масштабирование для заданного диапазона<\/h2>\n<p>Вариант min-max нормализации для произвольных значений. Не всегда, когда речь идет о масштабировании данных, диапазон значений находится в промежутке между 0 и 1.<br \/>\nФормула для вычисления в этом случае такая:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-05-12--16.43.04.png\" width=\"530\" height=\"104\" alt=\"\" \/>\n<\/div>\n<p>Это даст нам возможность масштабировать данные к произвольной шкале. В нашем примере пусть а=10.0, а b=20.0.<\/p>\n<p>SQL-запрос с подзапросом:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       ((height - min_height) * (20.0 - 10.0) \/ (max_height - min_height)) + 10 AS scaled_ab\r\n  FROM students,\r\n      (SELECT MAX(height) as max_height, \r\n              MIN(height) as min_height\r\n         FROM students  \r\n      ) t1;<\/code><\/pre><p>SQL-запрос с оконной функцией:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       ((height - MIN(height) OVER() ) * (20.0 - 10.0) \/ (MAX(height) OVER() - MIN(height) OVER())) + 10.0 AS scaled_ab\r\n  FROM students;<\/code><\/pre><p>Получаем аналогичные результаты, что и в предыдущем методе, но данные распределены в диапазоне от 10 до 20.<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<td>scaled_ab<\/td>\n<\/tr>\n<tr>\n<td>Иван<\/td>\n<td style=\"text-align: right\">174<\/td>\n<td>14.68085<\/td>\n<\/tr>\n<tr>\n<td>Петр<\/td>\n<td style=\"text-align: right\">181<\/td>\n<td>16.17021<\/td>\n<\/tr>\n<tr>\n<td>Денис<\/td>\n<td style=\"text-align: right\">199<\/td>\n<td>20<\/td>\n<\/tr>\n<tr>\n<td>Ксения<\/td>\n<td style=\"text-align: right\">158<\/td>\n<td>11.2766<\/td>\n<\/tr>\n<tr>\n<td>Сергей<\/td>\n<td style=\"text-align: right\">179<\/td>\n<td>15.74468<\/td>\n<\/tr>\n<tr>\n<td>Ольга<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>12.76596<\/td>\n<\/tr>\n<tr>\n<td>Юлия<\/td>\n<td style=\"text-align: right\">152<\/td>\n<td>10<\/td>\n<\/tr>\n<tr>\n<td>Кирилл<\/td>\n<td style=\"text-align: right\">188<\/td>\n<td>17.65957<\/td>\n<\/tr>\n<tr>\n<td>Антон<\/td>\n<td style=\"text-align: right\">177<\/td>\n<td>15.31915<\/td>\n<\/tr>\n<tr>\n<td>Софья<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>12.76596<\/td>\n<\/tr>\n<\/table>\n<\/div>\n<h2>Нормализация с помощью z-score<\/h2>\n<p>В результате z-score нормализации данные будут масштабированы таким образом, чтобы они имели свойства стандартного нормального распределения — среднее (μ) равно 0, а стандартное отклонение (σ) равно 1.<\/p>\n<p>Вычисляется z-score по формуле:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-05-12--16.43.19.png\" width=\"368\" height=\"101\" alt=\"\" \/>\n<\/div>\n<p>SQL-запрос с подзапросом:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       (height - t1.mean) * 1.0 \/ t1.sigma AS zscore\r\n  FROM students,\r\n      (SELECT AVG(height) AS mean, \r\n              STDDEV(height) AS sigma\r\n         FROM students\r\n        ) t1;<\/code><\/pre><p>SQL-запрос с оконной функцией:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       (height - AVG(height) OVER()) * 1.0 \/ STDDEV(height) OVER() AS z-score\r\n  FROM students;<\/code><\/pre><p>В результате мы сразу заметим выбросы, которые выходят за пределы стандартного отклонения.<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<td>zscore<\/td>\n<\/tr>\n<tr>\n<td>Иван<\/td>\n<td style=\"text-align: right\">174<\/td>\n<td>0.01488<\/td>\n<\/tr>\n<tr>\n<td>Петр<\/td>\n<td style=\"text-align: right\">181<\/td>\n<td>0.53582<\/td>\n<\/tr>\n<tr>\n<td>Денис<\/td>\n<td style=\"text-align: right\">199<\/td>\n<td>1.87538<\/td>\n<\/tr>\n<tr>\n<td>Ксения<\/td>\n<td style=\"text-align: right\">158<\/td>\n<td>-1.17583<\/td>\n<\/tr>\n<tr>\n<td>Сергей<\/td>\n<td style=\"text-align: right\">179<\/td>\n<td>0.38698<\/td>\n<\/tr>\n<tr>\n<td>Ольга<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>-0.65489<\/td>\n<\/tr>\n<tr>\n<td>Юлия<\/td>\n<td style=\"text-align: right\">152<\/td>\n<td>-1.62235<\/td>\n<\/tr>\n<tr>\n<td>Кирилл<\/td>\n<td style=\"text-align: right\">188<\/td>\n<td>1.05676<\/td>\n<\/tr>\n<tr>\n<td>Антон<\/td>\n<td style=\"text-align: right\">177<\/td>\n<td>0.23814<\/td>\n<\/tr>\n<tr>\n<td>Софья<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>-0.65489<\/td>\n<\/tr>\n<\/table>\n<\/div>\n",
            "date_published": "2021-05-13T11:15:58+03:00",
            "date_modified": "2021-05-14T08:36:57+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/--2021-05-12--16.42.52.png",
            "_date_published_rfc2822": "Thu, 13 May 2021 11:15:58 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "109",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-05-12--16.42.52.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-05-12--16.43.04.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-05-12--16.43.19.png"
                ]
            }
        },
        {
            "id": "29",
            "url": "http:\/\/test.leftjoin.ru\/all\/metrics-for-marketing-analytics\/",
            "title": "Аналитические метрики здорового маркетолога",
            "content_html": "<h2>Введение<\/h2>\n<p><figure class=\"block-color-gray_background callout\" style=\"white-space:pre-wrap;display:flex; background: #f8f8f8; \"><\/p>\n<div style=\"font-size:1.5em\"><p><span class=\"icon\">☝<\/span><\/p>\n<\/div><div style=\"width:100%; \"><p>Сегодня у нас в выпуске лонгрид при поддержке телеграмм-канала <a href=\"https:\/\/t.me\/smmrus\">Русский маркетинг<\/a> на тему аналитических метрик в маркетинге. В рамках статьи обсудим для чего нужна маркетинговая аналитика, какими метриками следует оперировать при расчете эффективности маркетинга, как можно структурировать работу по построению маркетинговой отчетности. Коснемся ключевых верхнеуровневых KPI, обсудим популярный фреймворк и разберемся как считать важные аналитические показатели. Статья получилась довольно объемная и в ее содержании используется множество сокращений, поэтому не обошлось и без глоссария.<\/p>\n<\/div><p><\/figure><\/p>\n<ul class=\"toggle\"><li><details close=\"\"><summary><strong>Глоссарий<\/strong><\/summary>\r\n<ul id=\"dcd6802d-fbeb-46e8-8bf1-18eb3f19e1e7\" class=\"bulleted-list\">\r\n  <li>Revenue \/ Income \/ Sales — выручка, доход (руб. \/ $ \/ евро)<\/li>\r\n  <li>GMV (Margin) — маржа (% \/ руб. ) <\/li>\r\n  <li>MAU (monthly active users) — уникальное число активных пользователей за месяц (шт.)<\/li>\r\n  <li>WAU (weekly active users) — уникальное число активных пользователей за неделю (шт.)<\/li>\r\n  <li>DAU (daily active users) — уникальное число активных пользователей за день (шт.)<\/li>\r\n  <li>Requests — запросы (за рекламой) (шт.)<\/li>\r\n  <li>Impressions — показы (рекламы) (шт.)<\/li>\r\n  <li>Clicks — клики (на рекламу) (шт.)<\/li>\r\n  <li>FR (fill rate) — заполняемость ( =Impressions \/ Requests) (%)<\/li>\r\n  <li>CTR (click through rate) — кликабельность ( =Clicks \/ Impressions) (%)<\/li>\r\n  <li>С1 (conversion first purchase) — конверсия в первую покупку (%)<\/li>\r\n  <li>R, R1, R3, R7 (retention) — удержание (1го, 3го, 7го дня) (%)<\/li>\r\n  <li>RR (rolling retention) (%)<\/li>\r\n  <li>Churn — отток (%)<\/li>\r\n  <li>ARPU (average revenue per user) — средняя выручка на пользователя (руб. \/ $ \/ евро)<\/li>\r\n  <li>ARPPU (average revenue per paying user) — средняя выручка на платящего пользователя (руб. \/ $ \/ евро)<\/li>\r\n  <li>cARPU (cumulative average revenue per users) — накопленная средняя выручка на пользователя (руб. \/ $ \/ евро)<\/li>\r\n  <li>LTV (lifetime value) \/ CLV (customer lifetime value) — пожизненная ценность клиента<\/li>\r\n  <li>ROI (return of investment) — возврат инвестиций (%)<\/li>\r\n  <li>ROAS (return on advertising spend) — окупаемость расходов на рекламу (%)<\/li>\r\n  <li>ROMI (return of marketing investment) — возврат маркетинговых инвестиций (%)<\/li>\r\n  <li>CPA (cost per action) — стоимость действия (напр., покупка или установка приложения) (руб. \/ $ \/ евро)<\/li>\r\n  <li>CPC (cost per click) — стоимость клика (руб. \/ $ \/ евро)<\/li>\r\n  <li>CPO (cost per order) — стоимость заказа (руб. \/ $ \/ евро)<\/li>\r\n  <li>CPS (cost per sale) — стоимость продажи (руб. \/ $ \/ евро)<\/li>   \r\n  <li>CPM (cost per mille) — стоимость тысячи рекламных показов (руб. \/ $ \/ евро)<\/li>\r\n  <li>CAC (customer acquisition cost) — стоимость привлечения клиента (руб. \/ $ \/ евро)<\/li>\r\n  <li>CARC (customer acquision and retention cost) — стоимость привлечения и удержания клиента (руб. \/ $ \/ евро)<\/li>\r\n  <li>ДРР — доля рекламных расходов (%)<\/li>\r\n<\/ul>\n<p><\/li><\/ul><\/p>\n<h2>Для чего нужна маркетинговая аналитика?<\/h2>\n<p>Чтобы разобраться с аналитическими метриками, для начала следует разобраться зачем вообще нужна маркетинговая аналитика и на какие вопросы она может дать ответ. В целом, маркетинговая аналитика — это изучение и измерение в количественных показателях маркетинговой деятельности. При этом чаще всего цель данных действий — оценить эффективность маркетинга, посчитать окупаемость маркетинговых инвестиций в компанию.<br \/>\nМаркетинговая аналитика помогает найти ответы на следующие вопросы:<\/p>\n<ul>\n<li><i>Насколько эффективно расходуется маркетинговый бюджет?<\/i><\/li>\n<li><i>Какой ROMI дают разные маркетинговые каналы?<\/i><\/li>\n<li><i>Какая целевая аудитория наиболее эффективно конвертируется?<\/i><\/li>\n<li><i>Какие каналы коммуникаций наиболее \/ наименее прибыльны?<\/i><\/li>\n<li><i>Что является наибольшим источником дохода компании?<\/i><\/li>\n<\/ul>\n<p>Маркетинговый анализ следует начинать с определения ключевых показателей бизнеса и связей между ними, об этом мы поговорим чуть позже. В целом, работа над построением маркетинговой аналитики больше похожа на создание системы правильных метрик, их планирования, замеров и реагирования на изменения метрик. Более подробно цикл <a href=\"https:\/\/ru.wikipedia.org\/wiki\/Цикл_Деминга\">PDCA<\/a> описан в книжке <a href=\"https:\/\/www.ozon.ru\/context\/detail\/id\/3327206\/\">У. Деминга &quot;Выход из кризиса&quot;<\/a>, рекомендую для ознакомления.<\/p>\n<h2><i>Ключевые принципы построения правильной аналитики в маркетинге<\/i><\/h2>\n<p>Имея системный подход к анализу данных, влияющих на маркетинговую деятельность, можно помочь маркетологам решить проблему, устранить боль, предоставить рекомендации для дальнейших маркетинговых стратегических шагов. Системный подход подразумевает соблюдение ряда ключевых принципов, без которых аналитика окажется неполноценной.<\/p>\n<p><b>Компетентность<\/b><br \/>\nЗадачей маркетингового анализа данных должен заниматься профессионал, разбирающийся в основах математической статистики, эконометрики, разумеется, умеющий считать, интерпретировать результаты и делать выводы актуальные для конкретного бизнеса (понимающий предметную область). Только в таком случае, аналитика сможет дать плоды, в противном случае некорректные выводы из данных могут только усугубить ситуацию, вследствие, чего проийзодет не оптимизация бюджета, а его разорение.<\/p>\n<p><b>Объективность<\/b><br \/>\nНеобходимо, решая задачу, рассматривать данные, которые влияют на проблему, с разных сторон. Разные показатели, разная агрегация данных позволит взглянуть на проблему объективно, желательно, чтобы один и тот же вывод из данных, повторялся как минимум дважды.<\/p>\n<p><b>Актуальность<\/b><br \/>\nИзучая сегодняшние проблемы не следует оперировать устарелыми ретроспективными данными, мир очень быстро меняется, равно как и ситуация на рынке \/ в компании. Анализ, произведенный год назад, может дать сегодня совершенно иные результаты, поэтому необходимо регулярно освежать отчеты и данные, содержащиеся в них.<\/p>\n<p><b>Интерпретируемость<\/b><br \/>\nРезультаты анализа должны быть понятны человеку из бизнеса, не знакомому с техническими терминами. В идеале — каждый отчет помогает легко разобраться в проблеме и подталкивает читателя на очевидные выводы. Ситуация, при которой не аналитик вынужден копаться в огромной кучей графиков, непонятных диаграмм и страниц с цифрами без выводов недопустима.<\/p>\n<p>Подобные принципы однозначно помогут нанять компетентных аналитиков для построения корректной отчетности.<\/p>\n<h2>Как структурировать показатели?<\/h2>\n<p>Систему метрик, помогающих оценивать эффективность маркетинга, можно построить исходя из нескольких соображений. Один из ключевых подходов к структуризации — жизненный цикл клиента. Постараемся разобраться в нем и поговорим об одном из интересных фреймворков для работы над такой системой метрик. В жизненном цикле клиента можно выделить основные этапы:<br \/>\n1) Охват аудитории — работа маркетолога начинается еще до того момента, как потенциальная аудитория становится клиентами компании<br \/>\n2) Вовлечение — этап конверсии зашедших пользователей на сайт \/ в мобильное приложение в зарегистрированных клиентов<br \/>\n3) Монетизация — этап формирования платящих пользователей (из зарегистрированных)<br \/>\n4) Удержание \/ Отток — мероприятия направленные на развитие и удержание привлеченной аудитории, снижение уровня оттока<\/p>\n<h2>Метод AARRR \/ Pirate Metrics<\/h2>\n<p>В 2007-ом году Дейвом МакКлюром был разработан и предложен метод AARRR — система метрик, помогающая стартапам разобраться в бизнес-показателях. Другое название метода, которое также можно встретить, — &quot;пиратские метрики&quot; из-за того, что название произносится на пиратский лад: &quot;ааррр!&quot;.<br \/>\nИтак, разберемся в подходе и поговорим о метриках, соответствующих каждому этапу &quot;воронки&quot;. Аббревиатура состоит из 5-ти ключевых маркетинговых этапов:<\/p>\n<ul>\n<li>Аcquisition — привлечение (соответствует п. 1 выше)<\/li>\n<li>Аctivation — активация (соответствует п. 2 выше)<\/li>\n<li>Retention — удержание (соответствует п. 4 выше)<\/li>\n<li>Revenue — доход \/ монетизация (соответствует п. 3 выше)<\/li>\n<li>Referral — рекомендации (нововведенный этап)<\/li>\n<\/ul>\n<p>На входе в воронку располагается целевая аудитория, которую мы хотим привлечь. Затем, всеми силами мы стараемся зарегистрировать потенциального покупателя и превратить его в зарегистрированного клиента (к этому моменту человек, зашедший к нам на сайт \/ в приложение должен осознавать ценность нашего продукта). После, клиент совершает покупки и возвращается к нам снова и снова. В конечном итоге, если ему очень нравится наш продукт, то он порекомендует его своим друзьям \/ знакомым.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/pirate-metrics-AARRR-funnel.png\" width=\"750\" height=\"500\" alt=\"\" \/>\n<div class=\"e2-text-caption\">AARRR-воронка, пиратские метрики (<a href=\"https:\/\/thewayofdamasio.com\/aarrr-pirate-metrics-and-the-growth-hacking-mindset\/\">источник изображения<\/a>)<\/div>\n<\/div>\n<p>На каждом уровне воронки необходимо выбрать метрики, описывающие переход из одного состояния в другое, которые мы можем подсчитать и проанализировать. Разберемся последовательно с каждым из этапов и соответствующим ему метриками. Будем изучать каждый этап на примере живых организаций, чтобы расчет показателей был максимально понятен в прикладном смысле.<\/p>\n<h2>Привлечение<\/h2>\n<p>Охват потенциальных покупателей — ключевой этап формирования новой аудитории. Изучим этот важный этап на примере мобильного приложения <a href=\"https:\/\/apps.apple.com\/ru\/app\/grow-food-%D0%B4%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0-%D0%BF%D0%B8%D1%82%D0%B0%D0%BD%D0%B8%D1%8F\/id1368736005\">Grow Food<\/a> и каналов привлечения трафика. Зачастую, аудитория попадают в приложение Grow Food из нескольких разных источников:<\/p>\n<ol start=\"1\">\n<li>Органический трафик: поиск в Google, Yandex, Bing, etc<\/li>\n<li>Органический мобильный трафик: поиск в Apple Store \/ Google Play<\/li>\n<li>Коммерческий трафик: реклама в Facebook \/ Instagram, контекстная реклама (Adwords), мобильные рекламные сети.<\/li>\n<\/ol>\n<p>Обсудим на примере рекламы в Facebook. Каждое рекламное объявление таргетируется на потенциальную рекламную аудиторию, которая в терминах Facebook называется <i>&quot;Охват&quot;<\/i>. При этом мы можем оптимизировать показы рекламного объявления по кликам \/ конверсиям \/ etc. Наша задача получить максимально эффективную аудиторию за минимальные деньги. Следовательно, нужно выбрать метрики, которые помогут нам оценить эффективность, изучим их:<\/p>\n<ul>\n<li><i><b>Impressions<\/b><\/i> — количество показов рекламного объявления, сам показатель мало о чем скажет и очень тесно связан с объемом потенциальной аудитории, однако потребуется нам для понимания остальных метрик.<\/li>\n<li><i><b>Clicks<\/b><\/i> — число кликов на рекламное объявление, в абсолютном выражении, опять же зависит от числа показов.<\/li>\n<li><i><b>Installs<\/b><\/i> — количество клиентов, установивших мобильное приложение<\/li>\n<li><i><b>CTR<\/b><\/i> — кликабельность, рассчитывается как отношение Clicks \/ Impressions и показывает насколько эффективно наше объявление с точки зрения заинтересованности аудитории, другими словами, какова кликабельность нашего объявления<\/li>\n<li><i><b>CR<\/b><\/i> (conversion rate) (= Installs \/ Clicks) — уровень конверсии, показывает какой процент пользователей установили приложение из тех, кто кликнул на рекламное объявление<\/li>\n<li><i><b>Spend<\/b><\/i> — число денег, которые мы потратили на данное рекламное объявление<\/li>\n<li><i><b>CPC<\/b><\/i> (= Spend \/ Clicks) — показывает нам стоимость одного клика, оперировать показателем следует в сравнении с другими объявлениями \/ рыночными бенчмарками<\/li>\n<li><i><b>CPM<\/b><\/i> (= Spend \/ Impressions * 1000 ) — показывает нам стоимость тысячи показов рекламного объявления, используется для сравненияи с другими объявлениями \/ бенчмарками<\/li>\n<li><i><b>CPI<\/b><\/i> (= Spend \/ Installs) — удельная стоимость одного инсталла<\/li>\n<li><i><b>Revenue<\/b><\/i> — итоговый доход, который мы получили с данного рекламного объявления \/ кампании (необходимо иметь инструменты для правильной атрибуции)<\/li>\n<li><i><b>ROAS<\/b><\/i> (= Revenue \/ Spend) — возврат инвестиций в рекламу, валовый доход с потраченного доллара, метрика показывает эффективность рекламной кампании с точки зрения вложенных в нее денег. К примеру, ROAS равный 300% говорит о том, что на каждый потраченный 1$ заработано 3$, а ROAS равный 30%, говорит о том, что на вложенный доллар заработано 30 центов.<\/li>\n<\/ul>\n<p>Итого, мы уже имеем неплохую палитру метрик, с которыми можно работать — изучать их динамику, сравнивать объявления между собой, между разными источниками трафика. Например, простая табличка, содержащая эти показатели уже будет первым приближением к пониманию эффективности рекламы.<\/p>\n<h2>Facebook Campaign Efficiency<\/h2>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td><b>Advertisement<\/b><\/td>\n<td><b>Spend ($)<\/b><\/td>\n<td><b>Installs<\/b><\/td>\n<td><b>CPI<\/b><\/td>\n<td><b>Impressions<\/b><\/td>\n<td><b>CPM<\/b><\/td>\n<td><b>Clicks<\/b><\/td>\n<td><b>CTR<\/b><\/td>\n<td><b>CPC<\/b><\/td>\n<td><b>ROAS<\/b><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">Creative Grow Food-1<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">Creative Grow Food-2<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<td style=\"text-align: center\">x<\/td>\n<\/tr>\n<\/table>\n<\/div>\n<p>Данную таблицу можно перестроить таким образом, чтобы по вертикали оказались даты, а кампания выбиралась из фильтра, тогда мы начнем понимать изменения в динамике ключевых показателей привлечения трафика.<\/p>\n<p><b>Резюме<\/b>: мы можем измерять CTR разных баннеров и понимать какой из них интереснее для аудитории. Этот показатель можно использовать при A\/B тестировании одного и того же баннера, выбирая наиболее эффективный. При подсчете эффективности помимо CTR следует иметь в виду CPC для того, чтобы выбрать не только наиболее кликабельный баннер, но и не самый дорогой.<\/p>\n<p>Ключевые KPI, показатели эффективности с точки зрения денег — <b>CPI<\/b> \/ <b>ROAS<\/b>, первый показывает насколько дешево \/ дорого мы покупаем трафик, а второй — насколько хорошо купленный трафик монетизируется.<\/p>\n<h2>Активация<\/h2>\n<p>Предположим, что мы разрабатываем мобильную игру. Подумаем о том, что может являться активацией пользователя в этом случае? Мы привлекли пользователей, которые установили игру себе на смартфон. Наша следующая задача — зарегистрировать пользователя (сделать его игроком), предложить вводный тур для прохождения.<br \/>\nНа этом этапе две метрики можно считать ключевыми: <b>конверсия в зарегистрированного пользователя<\/b> (= Registrations \/ Installs), <b>конверсия в прошедших обучение<\/b> (=Tutorial Users \/ Installs).<\/p>\n<p>Соответственно, эти две метрики покажут нам: не слишком ли многого мы требуем от пользователя на этапе регистрации или, наоборот, регистрация дается крайне легко? Вторая метрика покажет насколько понятно введение в игру, заинтересованы ли пользователи проходить вводный тур, достаточно ли действий мы требуем от пользователя.<\/p>\n<p>Более того, последнюю метрику можно декомпозировать, если в рамках обучения пользователю необходимо осуществить несколько действий, мы можем изучить воронку конверсий в каждое из действий и понять проблемные места активации новых пользователей. После того как мы активировали аудиторию, нам необходимо удержать ее, чтобы впоследствии заработать денег.<\/p>\n<h2>Удержание<\/h2>\n<p>Любая организация хотела бы, чтобы у нее существовала активная база лояльных клиентов, которые регулярно делают повторные заказы. В этой связи, очень важно отслеживать несколько ключевых метрик: Retention rate (или Rolling retention), Churn. Подробно я разбирал построение <a href=\"http:\/\/test.leftjoin.ru\/all\/retention-rate\/\">retention и rolling retention отчетов<\/a> в одном из прошлых выпусков блога.<\/p>\n<\/p><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/Retention@2x.png\" width=\"585\" height=\"200\" alt=\"\" \/>\n<\/div>\n<p>Другой важной фундаментальной метрикой можно считать <b>Sticky Factor<\/b> — степень вовлеченности пользователей. Sticky Factor за неделю достаточно просто рассчитывается: <i>DAU \/ WAU * 100%<\/i>. Разберем, более подробно на прошлом примере. У нас как и прежде есть таблица — <i>client_session<\/i>, в которой по каждому <i>user_id<\/i> хранятся таймстемпы активности <i>created_at<\/i>. Тогда расчет Sticky-фактора довольно несложно выполняется следующим SQL-запросом:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT daily.dau\/weekly.wau*100 AS sticky\r\nFROM\r\n-- Считаем среднее DAU за неделю\r\n( SELECT avg(dau) AS dau\r\nFROM\r\n(SELECT from_unixtime(cs.created_at, &amp;quot;yyyy-MM-dd&amp;quot;) AS event_date,\r\nndv(cs.user_id) AS dau\r\nFROM client_session cs\r\nWHERE 1=1\r\nAND from_unixtime(cs.created_at)&amp;gt;=date_add(now(), -7)\r\nAND from_unixtime(cs.created_at)&amp;lt;=now()\r\nGROUP BY 1) d) daily,\r\n-- Считаем WAU за неделю\r\n( SELECT ndv(cs.user_id) AS wau\r\nFROM client_session cs\r\nWHERE 1=1\r\nAND from_unixtime(cs.created_at)&amp;gt;=date_add(now(), -7)\r\nAND from_unixtime(cs.created_at)&amp;lt;=now() ) weekly<\/code><\/pre><p>Вместе с фундаментальными метриками следует рассматривать метрики, связанные с инструментами удержания клиентской базы. Такими могут являться инструменты директ-маркетинга: sms, email, push-уведомления. У каждого из инструментов обычно бывают такие описательные метрики: <i>число отправленных сообщений<\/i> \/ <i>число доставленных сообщений<\/i> \/ <i>количество вернувшихся пользователей<\/i>. Они показывают эффективность каждого из инструментов.<\/p>\n<h2>Монетизация<\/h2>\n<p>Наконец, мы добрались до ключевой метрики, которая интересвует всех бизнес-пользователей — <b>деньги<\/b>. Доход, выручка — денежные средства, которые мы получаем от пользователей при покупке нашего продукта. В абсолютном выражении эта метрика (или результат деятельности компании) не очень показательна, хотя важна для понимания текущих трендов.<\/p>\n<p>Чаще всего оперируют рядом относительных метрик, которые описывают поведение пользователей:<br \/>\n<b>ARPU<\/b> ( = Revenue \/ Users )— средняя выручка на одного пользователя<br \/>\n<b>cARPU<\/b>( = cumulative Revenue \/ Users ) — накопленная средняя выручка на одного пользователя<br \/>\n<b>ARPPU<\/b> ( = Revenue \/ Payers ) — средняя выручка на платящего пользователя<br \/>\n<b>Avg Receipt<\/b> (= Revenue \/ Purchases ) — средний чек<br \/>\n<b>LTV \/ CLV<\/b> — совокупный доход на одного пользователя (жизненная ценность клиента)<\/p>\n<p>Вопросу LTV я планирую посвятить отдельный пост, поскольку это достаточно обширная тема. В данном посте разберем ARPU, накопленный ARPU и связь c LTV. Метрика ARPU покажет нам сколько мы в среднем зарабатываем с пользователя за какой-то период времени (обычно день или неделя). Это полезная информация, но ее может быть недостаточно. Задача эффективного маркетинга — привлекать таких пользователей, которые приносят компании больше денег, чем затрачивается на их привлечение. Таким образом, если мы модифицируем показатель ARPU и рассмотрим накопленный ARPU, например, за 30, 60, 90, 180 дней, то получим неплохое приближение к LTV пользователя. Еще лучше если мы построим кривую накопленного ARPU по дням.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/Revenue-CPI@2x.png\" width=\"585\" height=\"200\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Кривая накопленного ARPU<\/div>\n<\/div>\n<p>Добавив горизонтальной линией <i>CPI<\/i>, мы получим крайне полезный для понимания график. В точке пересечения двух линий мы получим день, начиная с которого доход от пользователя становится выше, чем затраты на его привлечение (привлечение пользователя становится эффективным). В рассматриваемом выше примере это <i>56-ой<\/i> день жизни клиента. Решение этой задачи похоже на поиск точки безубыточности, однако надо помнить, что компания также несет и другие косвенные затраты, которые необходимо заложить, чтобы корректно посчитать точку безубыточности.<\/p>\n<h2>Рекомендации<\/h2>\n<p>Наилучшим сценарием взаимодействия с клиентами и высшей степенью награды для компании можно считать рекомендацию продуктов компании друзьям, родственникам, знакомым. С точки зрения метрик можно выделить: <i>количество активированных приглашенных новых пользователей на одного клиента<\/i> и <i>NPS<\/i>.<\/p>\n<p>Количество активированных рекомендаций позволяет увеличить CAC \/ CPI. К примеру, мы привлекаем пользователя за $1 и хотим сохранить такую тенденцию. Мы разработали механику реферальных ссылок и выявили, что теперь после внедрения средний пользователь приглашает двух других. Получается, что в таком случае стоимость привлечения пользователя составит $1 \/ 3 = $0.33. Соответственно, мы можем позволить себе привлекать пользователей за $3, сохранив приемлемое для нас значение CAC.<\/p>\n<p>NPS (Net Promote Score) — метрика, которая показывает уровень потребительской лояльности. Механика расчета <a href=\"https:\/\/ru.wikipedia.org\/wiki\/%D0%98%D0%BD%D0%B4%D0%B5%D0%BA%D1%81_%D0%BF%D0%BE%D1%82%D1%80%D0%B5%D0%B1%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%BE%D0%B9_%D0%BB%D0%BE%D1%8F%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B8_NPS\">подробна описана на Википедии<\/a>, не будем на ней останавливаться. Скажем лишь о том, что рекомендуем регулярно замерять NPS, используя директ-маркетинговые каналы коммуникаций.<\/p>\n<h2>Иерархия метрик в организации<\/h2>\n<p>Мы достаточно подробно изучили важные метрики каждого этапа AARRR, осталось разобраться каким образом можно структурировать показатели, чтобы получить идеальный дашборд.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/dashboard.png\" width=\"390\" height=\"395\" alt=\"\" \/>\n<\/div>\n<p>Для решения этой задачи имеет смысл декомпозировать цели компании и соответствующие им метрики на разные уровни. Зачастую каждый следующий уровень соответствует отделу компании и является KPI этого отдела. Упрощенно, мы можем представить главную верхнеуровневую цель компании — Прибыль и декомпозировать ее на составные части: Выручка, Расходы.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/Ierarchy@2x.png\" width=\"444.5\" height=\"168.5\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Иерархия метрик организации<\/div>\n<\/div>\n<p>Хороший пример — школа английского языка SkyEng, на <a href=\"https:\/\/drive.google.com\/file\/d\/1sRCHG1LO6f2QtrkmcfdjRScCsEG0EM0k\/view\">видео<\/a> можно ознакомиться с детально проработанной структурой метрик SkyEng.<\/p>\n<p>Другой альтернативой может стать построение структуры дашборда на основании разобранного выше фреймворка AARRR. Схематично такой дашборд мог бы выглядеть таким образом:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/dashboard-example@2x.png\" width=\"275.5\" height=\"375\" alt=\"\" \/>\n<\/div>\n<h2>Заключение<\/h2>\n<p>Сегодня мы разобрались с ключевыми маркетинговыми метриками, которые помогут отслеживать изменения на каждом этапе маркетинговой воронки, расскажут об эффективности каждого этапа и станут полезным инструментом деятельности маркетолога.<\/p>\n<h2>Ссылки по теме:<\/h2>\n<ul>\n<li><a href=\"https:\/\/www.pierrelechelle.com\/aarrr-pirate-metrics\">AARRR: Pirate Metrics for SaaS<\/a><\/li>\n<li><a href=\"https:\/\/habr.com\/ru\/post\/448962\/\">The effectiveness of the marketing funnel AARRR<\/a><\/li>\n<li><a href=\"http:\/\/tilda.education\/articles-aarrr-metrics\">Продвижение по методу AARRR<\/a><\/li>\n<li><a href=\"https:\/\/rezart.agency\/blog\/analytics-marketing-guide\/\">Аналитика в маркетинге: особенности digital, инструменты и чек-лист<\/a><\/li>\n<li><a href=\"https:\/\/vc.ru\/marketing\/32256-privlech-polzovatelya-i-zarabotat-na-nem-effektivnost-marketingovoy-voronki-aarrr-na-primere-tinder\">Привлечь пользователя и заработать на нём: эффективность маркетинговой воронки AARRR<\/a><\/li>\n<li><a href=\"https:\/\/blog.promopult.ru\/management\/vazhnost-marketingovoj-analitiki.html\">Важность маркетинговой аналитики<\/a><\/li>\n<\/ul>\n<style>\r\nfigure {\r\n\tmargin: 1.25em 0;\r\n\tpage-break-inside: avoid;\r\n}\r\n.callout {\r\n\tborder-radius: 3px;\r\n\tpadding: 1rem;\r\n}\r\nol,\r\nul {\r\n\tmargin: 0;\r\n\tmargin-block-start: 0.6em;\r\n\tmargin-block-end: 0.6em;\r\n}\r\n\r\nli > ol:first-child,\r\nli > ul:first-child {\r\n\tmargin-block-start: 0.6em;\r\n}\r\n\r\nul > li {\r\n\tlist-style: disc;\r\n}\r\nul.toggle > li {\r\n\tlist-style: none;\r\n}\r\nul.bulleted-list > li {\r\n\tlist-style: disc;\r\n}\r\nul {\r\n\tpadding-inline-start: 0.8em;\r\n}\r\nul.toggle {\r\n\tpadding-inline-start: 0em;\r\n}\r\nul > li {\r\n\tpadding-left: 0.1em;\r\n}\r\n.toggle {\r\n\tpadding-inline-start: 0em;\r\n\tlist-style-type: none;\r\n}\r\n\r\n\/* Indent toggle children *\/\r\n.toggle > li > details {\r\n  padding-left: 0.0em;\r\n}\r\n\r\n.toggle > li > details > summary {\r\n\tmargin-left: -0.4em;\r\n}\r\n\r\n.block-color-gray {\r\n\tcolor: rgba(55, 53, 47, 0.6);\r\n\tfill: rgba(55, 53, 47, 0.6);\r\n}\t\r\n<\/style>\n",
            "date_published": "2019-12-17T13:25:56+03:00",
            "date_modified": "2020-05-12T17:31:40+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/dashboard@2x.png",
            "_date_published_rfc2822": "Tue, 17 Dec 2019 13:25:56 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "29",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/dashboard@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/pirate-metrics-AARRR-funnel.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/Retention@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/Revenue-CPI@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/dashboard.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/Ierarchy@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/dashboard-example@2x.png"
                ]
            }
        }
    ],
    "_e2_version": 3365,
    "_e2_ua_string": "E2 (v3365; Aegea)"
}