{
    "version": "https:\/\/jsonfeed.org\/version\/1",
    "title": "Блог об аналитике, визуализации данных, data science и BI, заметки с тегом: analysis",
    "home_page_url": "http:\/\/test.leftjoin.ru\/tags\/analysis\/",
    "feed_url": "http:\/\/test.leftjoin.ru\/tags\/analysis\/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": "118",
            "url": "http:\/\/test.leftjoin.ru\/all\/mean-vs-median\/",
            "title": "Различия между медианой и средним арифметическим как целевым показателем анализа данных",
            "content_html": "<p>В сегодняшней статье мы бы хотели осветить простую, но в то же время важную тему выбора простой метрики для оценки того или иного датасета. Со средним арифметическим все давным давно знакомы, чуть ли не каждый школьник отлично знает, что нужно просуммировать все имеющиеся значения, поделить на их количество и получить среднее значение. В школьные знания не входят никакие альтернативные варианты, которых, на самом деле, в статистике много — на любой вкус и случай. Однако, в решении исследовательских и маркетинговых задач люди часто берут именно эту метрику за основу. Правомерно ли это или есть более удачный вариант? Давайте разбираться.<\/p>\n<p>Для начала стоит вспомнить определения двух метрик, о которых мы сегодня поговорим.<br \/>\nСреднее  — самый популярный статистический показатель, который используется для измерения центра данных. А что же такое медиана? Медиана — значение, которое разбивает данные, отсортированные по порядку увеличения значений, на две равные части. Это значит, что медиана показывает центральное значение в выборке, если наблюдений нечетное количество и среднее арифметическое двух значений, если количество наблюдений в выборке четно.<\/p>\n<h2>Исследовательские задачи<\/h2>\n<p>Итак, оценка среднего значения выборки — зачастую важна во многих исследовательских вопросах. Например, специалисты, изучающие демографию часто задаются вопросом изменения численности регионов России, чтобы проследить за динамикой и отразить это в отчетностях. Давайте попробуем рассчитать среднюю численность региона России, а также медиану, а затем сравним полученные результаты.<br \/>\nДля начала, нужно найти и загрузить данные, подключив для этого библиотеку pandas.<\/p>\n<pre class=\"e2-text-code\"><code>import pandas as pd\r\ncity = pd.read_csv('city.csv')<\/code><\/pre><p>Затем, нужно посчитать среднее и медиану выборки.<\/p>\n<pre class=\"e2-text-code\"><code>mean_pop = round(city.population_2020.mean(), 0)\r\nmedian_pop = round(city.population_2020.median(), 0)<\/code><\/pre><p>Значения, естественно, получились разными, так как распределение наблюдений в выборке отлично от нормального. Для того, чтобы понять, сильно ли они отличаются, построим график распределения и отметим среднее и медиану.<\/p>\n<pre class=\"e2-text-code\"><code>import matplotlib.pyplot as plt\r\nimport seaborn as sns\r\n\r\nsns.set_palette('rainbow')\r\nfig = plt.figure(figsize = (20, 15))\r\nax = fig.add_subplot(1, 1, 1)\r\ng = sns.histplot(data = city, x= 'population_2020', alpha=0.6, bins = 100, ax=ax)\r\n\r\ng.axvline(mean_pop, linewidth=2, color='r', alpha=0.9, linestyle='--', label = 'Среднее = {:,.0f}'.format(mean_pop).replace(',', ' '))\r\ng.axvline(median_pop, linewidth=2, color='darkgreen', alpha=0.9, linestyle='--', label = 'Медиана = {:,.0f}'.format(median_pop).replace(',', ' '))\r\n\r\nplt.ticklabel_format(axis='x', style='plain')\r\nplt.xlabel(&quot;Численность населения&quot;, fontsize=25)\r\nplt.ylabel(&quot;Количество городов&quot;, fontsize=25)\r\nplt.title(&quot;Распределение численности населения российских городов&quot;, fontsize=25)\r\nplt.legend(fontsize=&quot;xx-large&quot;)\r\nplt.show()<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/avg_p2.jpg\" width=\"1440\" height=\"1080\" alt=\"\" \/>\n<\/div>\n<p>Также, на этих данных стоит построить боксплот для более точной визуализации основных квантилей распределения, медианы, среднего и выбросов.<\/p>\n<pre class=\"e2-text-code\"><code>fig = plt.figure(figsize = (10, 10))\r\nsns.set_theme(style=&quot;whitegrid&quot;)\r\nsns.set_palette(palette=&quot;pastel&quot;)\r\n\r\nsns.boxplot(y = city['population_2020'], showfliers = False)\r\n\r\nplt.scatter(0, 550100, marker='*', s=100, color = 'black', label = 'Выбросы')\r\nplt.scatter(0, 560200, marker='*', s=100, color = 'black')\r\nplt.scatter(0, 570300, marker='*', s=100, color = 'black')\r\nplt.scatter(0, mean_pop, marker='o', s=100, color = 'red', edgecolors = 'black', label = 'Среднее')\r\nplt.legend()\r\n\r\nplt.ylabel(&quot;Численность населения&quot;, fontsize=15)\r\nplt.ticklabel_format(axis='y', style='plain')\r\nplt.title(&quot;Боксплот численности населения&quot;, fontsize=15)\r\nplt.show()<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/bp_city.jpg\" width=\"720\" height=\"720\" alt=\"\" \/>\n<\/div>\n<p>Из графиков следует, что медиана существенно меньше среднего, а также, ясно, что это следствие наличия больших выбросов — Москвы и Санкт-Петербурга. Поскольку среднее арифметическое — метрика крайне чувствительная к выбросам — при их наличии в выборке опираться на выводы относительно среднего не стоит. Рост или снижение численности населения Москвы может сильно смещать среднюю численность по России, однако это не будет влиять на настоящий общерегиональный тренд.<br \/>\nИспользуя среднее арифметическое мы скажем, что численность типичного (среднего) города в РФ — 268 тысяч человек. Однако, это вводит нас в заблуждение, так как среднее значительно превышает медиану исключительно из-за численности населения Москвы и Санкт-Петербурга. На самом деле, численность типичного российского города существенно меньше (аж в 2 раза!) и составляет 104 тысячи жителей.<\/p>\n<h2>Маркетинговые задачи<\/h2>\n<p>В контексте бизнеса разница между средним арифметическим и медианой также важна, так как использование неверной метрики может серьезно сказаться на результатах проведения акции или затруднить достижение цели. Давайте посмотрим на реальном примере, с какими трудностями может столкнуться предприниматель в ритейле, если неверно выберет целевую метрику.<br \/>\nДля начала, как и в предыдущем примере, загрузим датасет о покупках в супермаркете. Выберем необходимые для анализа столбцы датасета и переименуем их, для упрощения кода в дальнейшем. Поскольку эти данные не так хорошо подготовлены, как предыдущие, необходимо сгруппировать все купленные товары по чекам. В этом случае необходима группировка по двум переменным: по id покупателя и по дате покупки (дата и время определяется моментом закрытия чека, поэтому все покупки в рамках одного чека совпадают по дате). Затем, назовем полученный столбец «total_bill», то есть сумма чека и посчитаем среднее и медиану.<\/p>\n<pre class=\"e2-text-code\"><code>df = pd.read_excel('invoice_data.xlsx')\r\ndf_nes = df[['Номер КПП', 'Сумма', 'Дата продажи']]\r\ndf_nes.columns = ['user','total_price', 'date']\r\ngroupped_df = pd.DataFrame(df_nes.groupby(['user', 'date']).total_price.sum())\r\ngroupped_df.columns = ['total_bill']\r\nmean_bill = groupped_df.total_bill.mean()\r\nmedian_bill = groupped_df.total_bill.median()<\/code><\/pre><p>Теперь, как и в предыдущем примере нужно построить график распределения чеков покупателей и боксплот, а также отметить медиану и среднее арифметическое на каждом из них.<\/p>\n<pre class=\"e2-text-code\"><code>sns.set_palette('rainbow')\r\nfig = plt.figure(figsize = (20, 15))\r\nax = fig.add_subplot(1, 1, 1)\r\nsns.histplot(groupped_df, x = 'total_bill', binwidth=200, alpha=0.6, ax=ax)\r\nplt.xlabel(&quot;Покупки&quot;, fontsize=25)\r\nplt.ylabel(&quot;Суммы чеков&quot;, fontsize=25)\r\nplt.title(&quot;Распределение суммы чеков&quot;, fontsize=25)\r\nplt.axvline(mean_bill, linewidth=2, color='r', alpha=1, linestyle='--', label = 'Среднее = {:.0f}'.format(mean_bill))\r\nplt.axvline(median_bill, linewidth=2, color='darkgreen', alpha=1, linestyle='--', label = 'Медиана = {:.0f}'.format(median_bill))\r\nplt.legend(fontsize=&quot;xx-large&quot;)\r\nplt.show()<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/avg_invoice_33.jpg\" width=\"1440\" height=\"1080\" alt=\"\" \/>\n<\/div>\n<pre class=\"e2-text-code\"><code>fig = plt.figure(figsize = (10, 10))\r\nsns.set_theme(style=&quot;whitegrid&quot;)\r\nsns.set_palette(palette=&quot;pastel&quot;)\r\n\r\nsns.boxplot(y = groupped_df['total_bill'], showfliers = False)\r\n\r\nplt.scatter(0, 1800, marker='*', s=100, color = 'black', label = 'Выбросы')\r\nplt.scatter(0, 1850, marker='*', s=100, color = 'black')\r\nplt.scatter(0, 1900, marker='*', s=100, color = 'black')\r\nplt.scatter(0, mean_bill, marker='o', s=100, color = 'red', edgecolors = 'black', label = 'Среднее')\r\nplt.legend()\r\n\r\nplt.ticklabel_format(axis='y', style='plain')\r\nplt.ylabel(&quot;Сумма чека&quot;, fontsize=15)\r\nplt.title(&quot;Боксплот суммы чеков&quot;, fontsize=15)\r\nplt.show()<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/bp_invoice.jpg\" width=\"720\" height=\"720\" alt=\"\" \/>\n<\/div>\n<p>Из графиков следует, что распределение смещено к началу координат (отличное от нормального), а значит медиана и среднее не равны. Медианное значение меньше среднего примерно на 220 рублей.<br \/>\nТеперь представим, что у маркетологов есть задача повысить средний чек покупателя. Маркетолог может решить, что поскольку средний чек равен 601 рублю, то можно предложить следующую акцию: «Всем покупателям, кто совершит покупку на 600 рублей, мы предоставляем скидку 20% на товар за 100 рублей». В целом, резонное предложение, однако, в реальности, средний чек ниже — 378 рублей. То есть большая часть покупателей не заинтересуется в предложении, поскольку их покупка обычно не достигает предложенного порога. Это значит. что они не воспользуются предложением и не получат скидку, а компания не сможет достичь поставленной цели и увеличить прибыль супермаркета. Все дело в том, что исходные предпосылки были ошибочны.<\/p>\n<h2>Выводы<\/h2>\n<p>Как вы уже поняли, среднее арифметическое зачастую показывает более значимый и приятный результат, как для бизнеса, так и для исследовательских задач, ведь руководству всегда выгоднее представить ситуацию со средним чеком или демографической ситуацией в стране лучше, чем она есть на самом деле. Однако, необходимо всегда помнить о недостатках такой метрики, как среднее арифметическое, чтобы уметь грамотно выбрать подходящий аналог для оценки той или иной ситуации.<\/p>\n",
            "date_published": "2021-09-16T21:20:47+03:00",
            "date_modified": "2021-10-27T14:06:01+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/avg_p2.jpg",
            "_date_published_rfc2822": "Thu, 16 Sep 2021 21:20:47 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "118",
            "_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\/avg_p2.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/bp_city.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/avg_invoice_33.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/bp_invoice.jpg"
                ]
            }
        },
        {
            "id": "117",
            "url": "http:\/\/test.leftjoin.ru\/all\/dashboard-newborn\/",
            "title": "Дашборд первых 8 месяцев жизни малыша",
            "content_html": "<p><a href=\"http:\/\/test.leftjoin.ru\/tableau\/newborn.html\" style=\"text-decoration:none; border:0\"><img src=\"http:\/\/test.leftjoin.ru\/pictures\/newborn.png-2.jpg\" border=\"0\" width=\"100%\" height=\"150%\"><\/a><\/p>\n<p>В декабре прошлого года я стал папой, а это значит, что наша семейная жизнь с супругой колоссально изменилась. Разумеется, я делюсь с вами этой новостью не просто так, а в контексте тех данных, которые сегодня будем изучать и исследовать. Они для меня очень личные, а потому имеют какую-то особую магию и ценность. Сегодня я хочу показать как круто меняется жизнь семьи на примере собственного анализа данных жизни первых 8 месяцев малыша.<\/p>\n<h2>Сбор данных<\/h2>\n<p>Исходные данные: трекинг основных элементов заботы о малыше в первые 8 месяцев: сон, кормление, смена подгузника. Данные были собраны с помощью <a href=\"https:\/\/nighp.com\/babytracker\/\">приложения BabyTracker<\/a>.<br \/>\nМоя жена — большая молодец, потому что в течение первых 7 месяцев она очень тщательно и исправно отслеживала все важные моменты. Она забыла отключить время кормления малыша ночью всего пару раз, но я достаточно быстро увидел в данных заметные выбросы, и датасет был от них очищен.<br \/>\nИзначально у меня в голове было несколько форматов визуализации данных, и я попробовал их сразу же внедрить в проектируемый дашборд. Мне хотелось показать интервалы сна малыша в виде вертикальной диаграммы Гантта, однако ночной сон переходил через сутки (0:00), и было совершенно непонятно как это можно исправить в Tableau. После ряда самостоятельных безуспешных попыток найти решение этой проблемы, я решил посоветоваться с <a href=\"https:\/\/t.me\/revealthedata\">Ромой Буниным<\/a>. К сожалению, мы вместе пришли к заключению, что это никак не решить. Тогда пришлось написать небольшой код на Python, который дробил такие временные отрезки и добавлял новые строки в датасет.<br \/>\nОднако, пока мы переписывались, Рома прислал идентичный моей идее пример! В этом примере утверждается, что женщина собирала данные о сне и бодрствовании своего ребенка в первый год его жизни, а затем написала код, с помощью которого получилось вышить <a href=\"https:\/\/youtu.be\/2R3dXARPH10?t=1723\">полотенце с датавизом паттернов сна малыша<\/a>. Для меня это оказалось удивительным, так как выяснилось, что подобный способ визуализации — основной метод, который позволяет показать, как непроста жизнь и сон родителей в первые месяцы появления ребенка.<br \/>\nВ моем <a href=\"http:\/\/test.leftjoin.ru\/tableau\/newborn.html\">дашборде на Tableau Public<\/a> получилось три смысловых блока и несколько “KPI”, про которые я хотел бы рассказать детально и поделиться основными житейскими мудростями. В верхней части дашборда можно увидеть ключевые средние показатели часов дневного и ночного сна, часов и частоты кормлений малыша, а также число смен подгузника в первые три месяца. Я выделил именно три месяца, поскольку я считаю это самым непростым периодом, ведь в вашей жизни происходят существенные изменения, к которым нужно адаптироваться.<\/p>\n<h2>Сон<\/h2>\n<p>Левая диаграмма — “Полотенце” — иллюстрирует сон малыша. На этой диаграмме важно обратить внимание на белые пропуски, особенно ночью. Это те часы, когда малыш бодрствует, а это значит бодрствуют и родители. Посмотрите, как меняется диаграмма, особенно в первые месяцы, когда мы отказывались от привычки ложиться спать в 1-2 часа ночи и засыпали пораньше. Грубо говоря, в первые три месяца (до марта 2021) ребенок мог заснуть в 2 или 3 часа ночи, но нам повезло, что ночной сон нашего ребенка оказался довольно длинным.<br \/>\nПравый график наглядно иллюстрирует как меняется длина сна малыша ночью и днем со временем, а боксплоты под ним показывают распределение часов дневного и ночного сна. График подтверждает вывод: “Это временно и скоро точно станет лучше!”<\/p>\n<h2>Кормление<\/h2>\n<p>Из левой диаграммы заметно, как изменяется количество и продолжительность кормлений. Это число постепенно уменьшается, а продолжительность кормлений сокращается. С середины июля мы изменили способ учета времени кормлений, поэтому в данном анализе они не валидны.<br \/>\nС моей точки зрения, полученные выводы — это прекрасная возможность для пар, планирующих беременность,  не строить иллюзий о возможности работать или заниматься какими-либо делами в первые месяцы после родов. Обратите внимание на частоту и продолжительность кормлений, все это время родитель всецело занят ребенком. Однако, не пугайтесь слишком сильно: со временем количество кормлений уменьшается.<\/p>\n<h2>Смена подгузника<\/h2>\n<p>Левая карта — это изюминка данного дашборда. Как вы понимаете, перед вами карта самых веселых моментов — смены подгузника. Звездочки — это моменты дня, когда нужно поменять подгузник, а светло-серым цветом снизу показано количество смен в сутки. Правый график показывает смены подгузников в разбивке по части дня. В целом, диаграмма не показывает каких-то интересных зависимостей, однако, она готовит к вам тому, что этот процесс, частый, регулярный и случается в любое время суток.<\/p>\n<h2>Выводы<\/h2>\n<p>Мне кажется, что использование настоящих личных данных и подобная визуализация иногда куда показательнее, чем множество видео или прочитанных книг о том, каким будет этот период. Именно поэтому я решил поделиться здесь своими выводами и наблюдениями с вами. Главный вывод, который я хотел, чтобы вы вынесли из датавиза: дети — это прекрасно! ❤️<\/p>\n",
            "date_published": "2021-09-14T13:15:59+03:00",
            "date_modified": "2021-09-29T12:31:11+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/newborn.png-2.jpg",
            "_date_published_rfc2822": "Tue, 14 Sep 2021 13:15:59 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "117",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/newborn.png-2.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": "100",
            "url": "http:\/\/test.leftjoin.ru\/all\/borderline-text-analysis\/",
            "title": "Python и тексты нового альбома Земфиры: анализируем суть песен",
            "content_html": "<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/Zemfira_borderline-2.jpg\" width=\"600\" height=\"600\" alt=\"\" \/>\n<\/div>\n<p>Неделю назад вышёл первый за 8 лет студийный альбом Земфиры «Бордерлайн». К работе помимо рок-певицы приложили руку разные люди, в том числе и её родственники — рифф для песни «таблетки» написал её племянник из Лондона. Альбом получился разнообразным: например, песня «остин» посвящена главному персонажу игры Homescapes российской студии Playrix (кстати, посмотрите свежие <a href=\"https:\/\/youtu.be\/SOx8afEUTnE\">Бизнес-секреты с братьями Бухманами<\/a>, там они тоже про это рассказывают) — Земфире нравится игра, и для трека она связалась со студией. А сингл «крым» был написан в качестве саундтрека к новой картине соратницы Земфиры — Ренаты Литвиновой.<\/p>\n<p class=\"note\">Послушать альбом в <a href=\"https:\/\/music.apple.com\/ru\/album\/бордерлайн\/1554865105\">Apple Music<\/a> \/ <a href=\"https:\/\/music.yandex.ru\/album\/14052981\">Яндекс.Музыке<\/a> \/ <a href=\"https:\/\/open.spotify.com\/album\/6khBsXmKA1FKjYVCIBy9kt\">Spotify<\/a><\/p>\n<p>Тем не менее, дух всего альбома довольно мрачен — в песнях часто повторяются слова «боль», «ад», «бесишь» и прочие по смыслу. Мы решили провести разведочный анализ нового альбома, а затем при помощи модели Word2Vec и косинусной меры посмотреть на семантическую близость песен между собой и вычислить общее настроение альбома.<\/p>\n<p>Для тех, кому скучно читать про подготовку данных и шаги анализа можно <a href=\"http:\/\/test.leftjoin.ru\/all\/borderline-text-analysis\/#result\">перейти сразу к результатам<\/a>.<\/p>\n<h2>Подготовка данных<\/h2>\n<p>Для начала работы напишем скрипт обработки данных. Цель скрипта — из множества текстовых файлов, в каждом из которых лежит по песне, собрать единую csv-таблицу. При этом текст треков очищаем от знаков пунктуации и ненужных слов.<\/p>\n<pre class=\"e2-text-code\"><code>import pandas as pd\r\nimport re\r\nimport string\r\nimport pymorphy2\r\nfrom nltk.corpus import stopwords<\/code><\/pre><p>Создаём морфологический анализатор и расширяем список всего, что нужно отбросить:<\/p>\n<pre class=\"e2-text-code\"><code>morph = pymorphy2.MorphAnalyzer()\r\nstopwords_list = stopwords.words('russian')\r\nstopwords_list.extend(['куплет', 'это', 'я', 'мы', 'ты', 'припев', 'аутро', 'предприпев', 'lyrics', '1', '2', '3', 'то'])\r\nstring.punctuation += '—'<\/code><\/pre><p>Названия песен приведены на английском — создадим словарь для перевода на русский и словарь, из которого позднее сделаем таблицу:<\/p>\n<pre class=\"e2-text-code\"><code>result_dict = dict()\r\n\r\nsongs_dict = {\r\n    'snow':'снег идёт',\r\n    'crimea':'крым',\r\n    'mother':'мама',\r\n    'ostin':'остин',\r\n    'abuse':'абьюз',\r\n    'wait_for_me':'жди меня',\r\n    'tom':'том',\r\n    'come_on':'камон',\r\n    'coat':'пальто',\r\n    'this_summer':'этим летом',\r\n    'ok':'ок',\r\n    'pills':'таблетки'\r\n}<\/code><\/pre><p>Опишем несколько функций. Первая читает целиком песню из файла и удаляет переносы строки, вторая очищает текст от ненужных символов и слов, а третья при помощи морфологического анализатора pymorphy2 приводит слова к нормальной форме. Модуль pymorphy2 не всегда хорошо справляется с неоднозначностью — для слов «ад» и «рай» потребуется дополнительная обработка.<\/p>\n<pre class=\"e2-text-code\"><code>def read_song(filename):\r\n    f = open(f'{filename}.txt', 'r').read()\r\n    f = f.replace('\\n', ' ')\r\n    return f\r\n\r\ndef clean_string(text):\r\n    text = re.split(' |:|\\.|\\(|\\)|,|&quot;|;|\/|\\n|\\t|-|\\?|\\[|\\]|!', text)\r\n    text = ' '.join([word for word in text if word not in string.punctuation])\r\n    text = text.lower()\r\n    text = ' '.join([word for word in text.split() if word not in stopwords_list])\r\n    return text\r\n\r\ndef string_to_normal_form(string):\r\n    string_lst = string.split()\r\n    for i in range(len(string_lst)):\r\n        string_lst[i] = morph.parse(string_lst[i])[0].normal_form\r\n        if (string_lst[i] == 'аду'):\r\n            string_lst[i] = 'ад'\r\n        if (string_lst[i] == 'рая'):\r\n            string_lst[i] = 'рай'\r\n    string = ' '.join(string_lst)\r\n    return string<\/code><\/pre><p>Проходим по каждой песне и читаем файл с соответствующим названием:<\/p>\n<pre class=\"e2-text-code\"><code>name_list = []\r\ntext_list = []\r\nfor song, name in songs_dict.items():\r\n    text = string_to_normal_form(clean_string(read_song(song)))\r\n    name_list.append(name)\r\n    text_list.append(text)<\/code><\/pre><p>Затем объединяем всё в DataFrame и сохраняем в виде csv-файла.<\/p>\n<pre class=\"e2-text-code\"><code>df = pd.DataFrame()\r\ndf['name'] = name_list\r\ndf['text'] = text_list\r\ndf['time'] = [290, 220, 187, 270, 330, 196, 207, 188, 269, 189, 245, 244]\r\ndf.to_csv('borderline.csv', index=False)<\/code><\/pre><p>Результат:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/1-26.png\" width=\"477\" height=\"365\" alt=\"\" \/>\n<\/div>\n<h2>Облако слов по всему альбому<\/h2>\n<p>Начнём анализ с построения облака слов — оно отобразит, какие слова чаще всего встречаются в песнях. Импортируем нужные библиотеки, читаем csv-файл и устанавливаем конфигурации:<\/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\n%matplotlib inline\r\nnltk.download('punkt')\r\ndf = pd.read_csv('borderline.csv')<\/code><\/pre><p>Теперь создаём новую фигуру, устанавливаем параметры оформления и при помощи библиотеки wordcloud отображаем слова с размером прямо пропорциональным частоте упоминания слова. Над каждым графиком дополнительно указываем название песни.<\/p>\n<pre class=\"e2-text-code\"><code>fig = plt.figure()\r\nfig.patch.set_facecolor('white')\r\nplt.subplots_adjust(wspace=0.3, hspace=0.2)\r\ni = 1\r\nfor name, text in zip(df.name, df.text):\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(4, 3, 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':7,'color':'grey'},y=0.93)\r\n    plt.tick_params(labelsize=10)\r\n    i += 1<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/2@2x-2.png.jpg\" width=\"2560\" height=\"1707\" alt=\"\" \/>\n<\/div>\n<h2>EDA текстов альбома<\/h2>\n<p>Теперь проанализируем тексты песен — импортируем библиотеки для работы с данными и визуализации:<\/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>Сначала посчитаем число слов в каждой песне, число уникальных слов и процентное соотношение:<\/p>\n<pre class=\"e2-text-code\"><code>songs = []\r\ntotal = []\r\nuniq = []\r\npercent = []\r\n\r\nfor song, text in zip(df.name, df.text):\r\n    songs.append(song)\r\n    total.append(len(text.split()))\r\n    uniq.append(len(set(text.split())))\r\n    percent.append(round(len(set(text.split())) \/ len(text.split()), 2) * 100)<\/code><\/pre><p>А теперь составим из этого DataFrame и дополнительно посчитаем число слов в минуту для каждой песни:<\/p>\n<pre class=\"e2-text-code\"><code>df_words = pd.DataFrame()\r\ndf_words['song'] = songs\r\ndf_words['total words'] = total\r\ndf_words['uniq words'] = uniq\r\ndf_words['percent'] = percent\r\ndf_words['time'] = df['time']\r\ndf_words['words per minute'] = round(total \/ (df['time'] \/\/ 60))\r\ndf_words = df_words[::-1]<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/3-21.png\" width=\"480\" height=\"369\" alt=\"\" \/>\n<\/div>\n<p>Данные хорошо бы визуализировать — построим две столбиковые диаграммы: одну для числа слов в песне, а другую для числа слов в минуту.<\/p>\n<pre class=\"e2-text-code\"><code>colors_1 = ['rgba(101,181,205,255)'] * 12\r\ncolors_2 = ['rgba(62,142,231,255)'] * 12\r\n\r\nfig = go.Figure(data=[\r\n    go.Bar(name='📝 Всего слов',\r\n           text=df_words['total words'],\r\n           textposition='auto',\r\n           x=df_words.song,\r\n           y=df_words['total words'],\r\n           marker_color=colors_1,\r\n           marker=dict(line=dict(width=0)),),\r\n    go.Bar(name='🌀 Уникальных слов',\r\n           text=df_words['uniq words'].astype(str) + '&lt;br&gt;'+ df_words.percent.astype(int).astype(str) + '%' ,\r\n           textposition='inside',\r\n           x=df_words.song,\r\n           y=df_words['uniq words'],\r\n           textfont_color='white',\r\n           marker_color=colors_2,\r\n           marker=dict(line=dict(width=0)),),\r\n])\r\n\r\nfig.update_layout(barmode='group')\r\n\r\nfig.update_layout(\r\n    title = \r\n        {'text':'&lt;b&gt;Соотношение числа уникальных слов к общему количеству&lt;\/b&gt;&lt;br&gt;&lt;span style=&quot;color:#666666&quot;&gt;&lt;\/span&gt;'},\r\n    showlegend = True,\r\n    height=650,\r\n    font={\r\n        'family':'Open Sans, light',\r\n        'color':'black',\r\n        'size':14\r\n    },\r\n    plot_bgcolor='rgba(0,0,0,0)',\r\n)\r\nfig.update_layout(legend=dict(\r\n    yanchor=&quot;top&quot;,\r\n    xanchor=&quot;right&quot;,\r\n))\r\n\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/76.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<pre class=\"e2-text-code\"><code>colors_1 = ['rgba(101,181,205,255)'] * 12\r\ncolors_2 = ['rgba(238,85,59,255)'] * 12\r\n\r\nfig = go.Figure(data=[\r\n    go.Bar(name='⏱️ Длина трека, мин.',\r\n           text=round(df_words['time'] \/ 60, 1),\r\n           textposition='auto',\r\n           x=df_words.song,\r\n           y=-df_words['time'] \/\/ 60,\r\n           marker_color=colors_1,\r\n           marker=dict(line=dict(width=0)),\r\n          ),\r\n    go.Bar(name='🔄 Слов в минуту',\r\n           text=df_words['words per minute'],\r\n           textposition='auto',\r\n           x=df_words.song,\r\n           y=df_words['words per minute'],\r\n           marker_color=colors_2,\r\n           textfont_color='white',\r\n           marker=dict(line=dict(width=0)),\r\n          ),\r\n])\r\n\r\nfig.update_layout(barmode='overlay')\r\n\r\nfig.update_layout(\r\n    title = \r\n        {'text':'&lt;b&gt;Длина трека и число слов в минуту&lt;\/b&gt;&lt;br&gt;&lt;span style=&quot;color:#666666&quot;&gt;&lt;\/span&gt;'},\r\n    showlegend = True,\r\n    height=650,\r\n    font={\r\n        'family':'Open Sans, light',\r\n        'color':'black',\r\n        'size':14\r\n    },\r\n    plot_bgcolor='rgba(0,0,0,0)'\r\n)\r\n\r\n\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/78.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<h2>Работа с Word2Vec моделью<\/h2>\n<p>При помощи модуля gensim загружаем модель, указывая на бинарный файл:<\/p>\n<pre class=\"e2-text-code\"><code>model = gensim.models.KeyedVectors.load_word2vec_format('model.bin', binary=True)<\/code><\/pre><p class=\"note\">Для материала мы использовали готовую обученную на Национальном Корпусе Русского Языка модель от сообщества <a href=\"https:\/\/rusvectores.org\/ru\/models\/\">RusVectōrēs<\/a><\/p>\n<p>Модель Word2Vec основана на нейронных сетях и позволяет представлять слова в виде векторов, учитывая семантическую составляющую. Это означает, что если мы возьмём два слова — например, «мама» и «папа», представим их в виде двух векторов и посчитаем косинус, значения будет близко к 1. Аналогично, у двух слов, не имеющих ничего общего по смыслу косинусная мера близка к 0.<\/p>\n<p>Опишем функцию get_vector: она будет принимать список слов, распознавать для каждого часть речи, а затем получать и суммировать вектора — так мы сможем находить вектора не для одного слова, а для целых предложений и текстов.<\/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>Для каждой песни находим вектор и собираем соответствующий столбец в DataFrame:<\/p>\n<pre class=\"e2-text-code\"><code>vec_list = []\r\nfor word in df['text']:\r\n    vec_list.append(get_vector(word.split()))\r\ndf['vector'] = vec_list<\/code><\/pre><p>Теперь сравним вектора между собой, посчитав их косинусную близость. Те песни, у которых косинусная метрика выше 0,5 запомним отдельно — так мы получим самые близкие пары песен. Данные о сравнении векторов запишем в двумерный список result.<\/p>\n<pre class=\"e2-text-code\"><code>similar = dict()\r\nresult = []\r\nfor song_1, vector_1 in zip(df.name, df.vector):\r\n    sub_list = []\r\n    for song_2, vector_2 in zip(df.name.iloc[::-1], df.vector.iloc[::-1]):\r\n        res = 1 - spatial.distance.cosine(vector_1, vector_2)\r\n        if res &gt; 0.5 and song_1 != song_2 and (song_1 + ' \/ ' + song_2 not in similar.keys() and song_2 + ' \/ ' + song_1 not in similar.keys()):\r\n            similar[song_1 + ' \/ ' + song_2] = round(res, 2)\r\n        sub_list.append(round(res, 2))\r\n    result.append(sub_list)<\/code><\/pre><p>Самые похожие треки соберём в отдельный DataFrame:<\/p>\n<pre class=\"e2-text-code\"><code>df_top_sim = pd.DataFrame()\r\ndf_top_sim['name'] = list(similar.keys())\r\ndf_top_sim['value'] = list(similar.values())\r\ndf_top_sim.sort_values(by='value', ascending=False)<\/code><\/pre><p>И построим такой же bar chart:<\/p>\n<pre class=\"e2-text-code\"><code>colors = ['rgba(101,181,205,255)'] * 5\r\n\r\nfig = go.Figure([go.Bar(x=df_top_sim['name'],\r\n                        y=df_top_sim['value'],\r\n                        marker_color=colors,\r\n                        width=[0.4,0.4,0.4,0.4,0.4],\r\n                        text=df_top_sim['value'],\r\n                        textfont_color='white',\r\n                        textposition='auto')])\r\n\r\nfig.update_layout(\r\n    title = \r\n        {'text':'&lt;b&gt;Топ-5 схожих песен&lt;\/b&gt;&lt;br&gt;&lt;span style=&quot;color:#666666&quot;&gt;&lt;\/span&gt;'},\r\n    showlegend = False,\r\n    height=650,\r\n    font={\r\n        'family':'Open Sans, light',\r\n        'color':'black',\r\n        'size':14\r\n    },\r\n    plot_bgcolor='rgba(0,0,0,0)',\r\n    xaxis={'categoryorder':'total descending'}\r\n)\r\n\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/80.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Имея вектор каждой песни, давайте посчитаем вектор всего альбома — сложим вектора песен. Затем для такого вектора при помощи модели получим самые близкие по духу и смыслу слова.<\/p>\n<pre class=\"e2-text-code\"><code>def get_word_from_tlist(lst):\r\n    for word in lst:\r\n        word = word[0].split('_')[0]\r\n        print(word, end=' ')\r\n\r\nvec_sum = 0\r\nfor vec in df.vector:\r\n    vec_sum += vec\r\nsim_word = model.similar_by_vector(vec_sum)\r\nget_word_from_tlist(sim_word)<\/code><\/pre><p><span style=\"color: '#65b5cd'; font-size: 1.2em\"><b>небо тоска тьма пламень плакать горе печаль сердце солнце мрак<\/b><\/span><\/p>\n<p>Наверное, это ключевой результат и описание альбома Земфиры всего лишь в 10 словах.<\/p>\n<p>Наконец, построим общую тепловую карту, каждая ячейка которой — результат сравнения косинусной мерой текстов двух треков.<\/p>\n<pre class=\"e2-text-code\"><code>colorscale=[[0.0, &quot;rgba(255,255,255,255)&quot;],\r\n            [0.1, &quot;rgba(229,232,237,255)&quot;],\r\n            [0.2, &quot;rgba(216,222,232,255)&quot;],\r\n            [0.3, &quot;rgba(205,214,228,255)&quot;],\r\n            [0.4, &quot;rgba(182,195,218,255)&quot;],\r\n            [0.5, &quot;rgba(159,178,209,255)&quot;],\r\n            [0.6, &quot;rgba(137,161,200,255)&quot;],\r\n            [0.7, &quot;rgba(107,137,188,255)&quot;],\r\n            [0.8, &quot;rgba(96,129,184,255)&quot;],\r\n            [1.0, &quot;rgba(76,114,176,255)&quot;]]\r\n\r\nfont_colors = ['black']\r\nx = list(df.name.iloc[::-1])\r\ny = list(df.name)\r\nfig = ff.create_annotated_heatmap(result, x=x, y=y, colorscale=colorscale, font_colors=font_colors)\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/82.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<h2><a name=\"result\">Результаты анализа и интерпретация данных<\/a><\/h2>\n<p>Давайте ещё раз посмотрим на всё, что у нас получилось — начнём с облака слов. Нетрудно заметить, что у слов «боль», «невозможно», «сорваться», «растерзаны», «сложно», «терпеть», «любить» размер весьма приличный — всё потому, что такие слова встречаются часто на протяжении всего текста песен:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/2@2x-2.png-1.jpg\" width=\"2560\" height=\"1707\" alt=\"\" \/>\n<\/div>\n<p>Одной из самых «разнообразных» песен оказался сингл «крым» — в нём 74% уникальных слов. А в песне «снег идёт» слов совсем мало, поэтому большинство — 82% уникальны. Самой большой песней в альбоме получился трек «таблетки» — суммарно там около 150 слов.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/76.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Как было выяснено на прошлом графике, самый «динамичный» трек — «таблетки», целых 37 слов в минуту — практически по слову на каждые две секунды. А самый длинный трек — «абъюз», в нём же и согласно предыдущему графику практически самый низкий процент уникальных слов — 46%.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/78.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Топ-5 самых семантически похожих пар текстов:<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/80.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Ещё мы получили вектор всего альбома и подобрали самые близкие слова. Только посмотрите на них — «тьма», «тоска», «плакать», «горе», «печаль», «сердце» — это же ведь и есть тот перечень слов, который характеризует лирику Земфиры!<\/p>\n<p><span style=\"color: '#65b5cd'; font-size: 1.2em\"><b>небо тоска тьма пламень плакать горе печаль сердце солнце мрак<\/b><\/span><\/p>\n<p>Финал — тепловая карта. По визуализации заметно, что практически все песни достаточно схожи между собой — косинусная мера у многих пар превышает значение в 0.4.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/82.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<h2>Выводы<\/h2>\n<p>В материале мы провели EDA всего текста нового альбома и при помощи предобученной модели Word2Vec доказали гипотезу — большинство песен «бордерлайна» пронизывают довольно мрачные и тексты. И это нормально, ведь Земфиру мы любим именно за искренность и прямолинейность.<\/p>\n",
            "date_published": "2021-03-05T19:54:06+03:00",
            "date_modified": "2021-03-08T17:47:56+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/Zemfira_borderline.jpg",
            "_date_published_rfc2822": "Fri, 05 Mar 2021 19:54:06 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "100",
            "_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",
                    "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\/Zemfira_borderline.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/Zemfira_borderline-2.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/1-26.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/2@2x-2.png.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/3-21.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/2@2x-2.png-1.jpg"
                ]
            }
        },
        {
            "id": "92",
            "url": "http:\/\/test.leftjoin.ru\/all\/instagram-python-selenium-bot\/",
            "title": "Робот для автоматизированного просмотра Instagram на Python и Selenium",
            "content_html": "<p class=\"note\">Недавно мы начали вести Instagram — <a href=\"https:\/\/www.instagram.com\/leftjoin.ru\/\">подписывайтесь<\/a>, чтобы не пропустить контент, которого нет в блоге и Telegram!<\/p>\n<p>Многие из нас ежедневно заходят в Instagram, чтобы посмотреть истории друзей и полистать ленту постов и рекомендаций. Предлагаем действенный способ сохранить своё время — напишем на Python и Selenium робота, который возьмёт на себя рутинную задачу проверки свежих новостей друзей и подсчитает число новых историй и входящих сообщений.<\/p>\n<h2>Авторизация в аккаунт<\/h2>\n<p>При переходе в браузерную версию сайта, нас встречает такое окно:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/1-22.png\" width=\"827\" height=\"619\" alt=\"\" \/>\n<\/div>\n<p>Но просто вставить логин, пароль и нажать на кнопку «Войти» недостаточно: впереди будет ещё два окна. Во-первых, предложение сохранить данные — здесь мы тактично жмём «Не сейчас». Instagram тщательно следит за каждым нашим действием и малейшие аномалии в поведении приводят к блокировке, поэтому любые предложения по сохранению данных будем на всякий случай пропускать.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/2-21.png\" width=\"414\" height=\"344\" alt=\"\" \/>\n<\/div>\n<p>Следующим препятствием будет предложение включить уведомление, которое мы тоже пропустим:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/3-19.png\" width=\"449\" height=\"381\" alt=\"\" \/>\n<\/div>\n<p>Первым делом импортируем библиотеки:<\/p>\n<pre class=\"e2-text-code\"><code>from selenium import webdriver\r\nfrom webdriver_manager.chrome import ChromeDriverManager\r\nfrom bs4 import BeautifulSoup as bs\r\nimport time\r\nimport random<\/code><\/pre><p>И описываем функцию authorize — она будет принимать driver в качестве аргумента, отправлять в нужные поля логин и пароль, нажимать на кнопку «Войти», затем ждать десять секунд на загрузку страницы, нажимать на кнопку «Не сейчас», снова ждать загрузки страницы и пропускать уведомления:<\/p>\n<pre class=\"e2-text-code\"><code>def authorize(driver):\r\n    username = 'login'\r\n    password = 'password'\r\n    driver.get('https:\/\/www.instagram.com')\r\n    time.sleep(5)\r\n    driver.find_element_by_name(&quot;username&quot;).send_keys(username)\r\n    driver.find_element_by_name(&quot;password&quot;).send_keys(password)\r\n    driver.execute_script(&quot;document.getElementsByClassName('sqdOP  L3NKy   y3zKF     ')[0].click()&quot;)\r\n    time.sleep(10)\r\n    driver.execute_script(&quot;document.getElementsByClassName('sqdOP  L3NKy   y3zKF     ')[0].click()&quot;)\r\n    time.sleep(10)\r\n    driver.execute_script(&quot;document.getElementsByClassName('aOOlW   HoLwm ')[0].click()&quot;)<\/code><\/pre><h2>Новые сообщения<\/h2>\n<p>В Instagram могут прийти сообщения двух видов. В случае, если вы не подписаны на отправителя — придёт запрос на диалог. Если подписаны — придёт входящее сообщения. Оба случая обрабатываются по-разному. Число входящих сообщений можно получить с главной страницы — это число над иконкой бумажного самолётика:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.19.31.png\" width=\"231\" height=\"70\" alt=\"\" \/>\n<\/div>\n<p>А число запросов можно забрать текстом заголовка h5 из раздела «Сообщения». Сперва перейдём в этот раздел и попробуем найти строку с запросами на сообщение. Затем вернёмся на главную страницу и возьмём то самое число новых сообщений.<\/p>\n<pre class=\"e2-text-code\"><code>def messages_count(driver):\r\n    driver.get('https:\/\/www.instagram.com\/direct\/inbox\/')\r\n    time.sleep(2)\r\n    inbox = bs(driver.page_source)\r\n    try:\r\n        queries_text = inbox.find_all('h5')[0].text\r\n    except Exception:\r\n        queries_text = None\r\n    driver.get('https:\/\/www.instagram.com')\r\n    time.sleep(2)\r\n    content = bs(driver.page_source)\r\n    try:\r\n        messages_count = int(content.find_all('div', attrs={'class':'KdEwV'})[0].text)\r\n    except Exception:\r\n        messages_count = 0\r\n    return queries_text, messages_count<\/code><\/pre><h2>Подсчёт числа новых сторис<\/h2>\n<p>Все истории хранятся в одном блоке:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.02.05.png\" width=\"637\" height=\"201\" alt=\"\" \/>\n<\/div>\n<p>Это список с одинаковым классом, но в каждом элементе списка лежит ещё один div-блок. У новых историй это класс eebAO  h_uhZ, у просмотренных — eebAO.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.03.48.png\" width=\"200\" height=\"132\" alt=\"\" \/>\n<\/div>\n<p>Ещё есть такая кнопка, которая показывает следующую пачку историй:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.05.14.png\" width=\"269\" height=\"135\" alt=\"\" \/>\n<\/div>\n<p>При этом Instagram динамически прогружает код страницы, и в нём не найти те элементы, которые вы не видите своими глазами. Поэтому мы возьмём первые 8 видимых новых историй, добавим в список, нажмём на кнопку «Показать следующие истории» и будем продолжать так, пока кнопка ещё отображается. А затем подсчитаем число уникальных элементов, чтобы избежать возможных дубликатов.<\/p>\n<pre class=\"e2-text-code\"><code>def get_stories_count(driver):\r\n    stories_divs = []\r\n    scroll = True\r\n    while scroll:\r\n        try:\r\n            content = bs(driver.page_source)\r\n            stories_divs.extend(content.find_all('div', attrs={'class':'eebAO h_uhZ'}))\r\n            driver.execute_script(&quot;document.getElementsByClassName('  _6CZji oevZr  ')[0].click()&quot;)\r\n            time.sleep(1)\r\n        except Exception as E:\r\n            scroll = False\r\n    return len(set(stories_divs))<\/code><\/pre><h2>Просмотр сторис<\/h2>\n<p>Следующее, чем может заняться реальный пользователь после авторизации — просмотр свежих историй. Для того, чтобы зайти в блок историй, нужно просто нажать на кнопку класса OE3OK:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/4-12.png\" width=\"285\" height=\"164\" alt=\"\" \/>\n<\/div>\n<p>Есть еще две кнопки, о которых мы должны знать. Это кнопка для переключения на следующую историю — она в классе FhutL и кнопка закрытия блока историй — класс wpO6b. Пускай одна история будет отнимать у нас от 10 до 15 секунд, и с вероятностью 1\/5 мы переключим на следующую. При этом зададим переменные counter и limit — пусть сейчас мы хотим посмотреть случайное число историй от 5 до 45, и если мы уже посмотрели столько, то выходим из функции и историй.<\/p>\n<pre class=\"e2-text-code\"><code>def watch_stories(driver):\r\n    watching = True\r\n    counter = 0\r\n    limit = random.randint(5, 45)\r\n    driver.execute_script(&quot;document.getElementsByClassName('OE3OK ')[0].click()&quot;)\r\n    try:\r\n        while watching:\r\n            time.sleep(random.randint(10, 15))\r\n            if random.randint(1, 5) == 5:\r\n                driver.execute_script(&quot;document.getElementsByClassName('FhutL')[0].click()&quot;)\r\n            counter += 1\r\n            if counter &gt; limit:\r\n                driver.execute_script(&quot;document.getElementsByClassName('wpO6b ')[1].click()&quot;)\r\n                watching = False\r\n    except Exception as E:\r\n        print(E)\r\n        watching = False<\/code><\/pre><h2>Скроллинг ленты<\/h2>\n<p>После просмотра актуальных историй можно поскроллить ленту — это действие ничем не отличается от классического скроллинга страниц в Selenium. Запоминаем последнюю доступную длину страницы, скроллим до неё, ожидаем прогрузки, получаем новую. Прекратим просматривать ленту в двух случаях — если в random.randint() сгенерировалась единица или если лента кончилась.<\/p>\n<pre class=\"e2-text-code\"><code>def scroll_feed(driver):\r\n    scrolling = True\r\n    last_height = driver.execute_script(&quot;return document.body.scrollHeight&quot;)\r\n    while scrolling:\r\n        driver.execute_script(&quot;window.scrollTo(0, document.body.scrollHeight);&quot;)\r\n        time.sleep(random.randint(4,10))\r\n        new_height = driver.execute_script(&quot;return document.body.scrollHeight&quot;)\r\n        if new_height == last_height or random.randint(1, 10) == 1:\r\n            scrolling = False\r\n        last_height = new_height<\/code><\/pre><h2>Просмотр рекомендуемых аккаунтов<\/h2>\n<p>Instagram в заглавной странице сам рекомендует нам для подписки некоторые аккаунты. Выглядит она так:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/5-12.png\" width=\"645\" height=\"573\" alt=\"\" \/>\n<\/div>\n<p>И на ней тоже придётся скроллить, чтобы дойти до конца. Заходим на страницу и ожидаем 5 секунд прогрузки, затем снова получаем длину страницы и скроллим вниз. Выходим тоже с вероятностью 1\/10 или если страница кончилась, но ещё с вероятностью 1\/2 подписываемся на некоторые из первых 100 аккаунтов рекомендаций:<\/p>\n<pre class=\"e2-text-code\"><code>def scroll_recomendations(driver):\r\n   driver.get('https:\/\/www.instagram.com\/explore\/people\/suggested\/')\r\n    time.sleep(5)\r\n    scrolling = True\r\n    last_height = driver.execute_script(&quot;return document.body.scrollHeight&quot;)\r\n    while scrolling:\r\n        driver.execute_script(&quot;window.scrollTo(0, document.body.scrollHeight);&quot;)\r\n        time.sleep(random.randint(4,10))\r\n        new_height = driver.execute_script(&quot;return document.body.scrollHeight&quot;)\r\n        if new_height == last_height or random.randint(1, 10) == 1:\r\n            scrolling = False\r\n        last_height = new_height\r\n        if random.randint(0, 1):\r\n            try:\r\n                driver.execute_script(f&quot;document.getElementsByClassName('sqdOP  L3NKy   y3zKF     ')[{random.randint(1,100)}].click()&quot;)\r\n            except Exception as E:\r\n                print(E)<\/code><\/pre><h2>Просмотр рекомендуемых постов<\/h2>\n<p>Помимо ленты, которая сформирована из наших подписок, Instagram собирает ленту рекомендаций. Туда входят все посты, которые потенциально могут вам понравиться — мы просто пройдём вниз по этой ленте. Выйдем с вероятностью 1\/5 или когда кончится, чтобы долго не засиживаться.<\/p>\n<pre class=\"e2-text-code\"><code>def scroll_explore(driver):\r\n    driver.get('https:\/\/www.instagram.com\/explore')\r\n    time.sleep(3)\r\n    scrolling = True\r\n    last_height = driver.execute_script(&quot;return document.body.scrollHeight&quot;)\r\n    while scrolling:\r\n        driver.execute_script(&quot;window.scrollTo(0, document.body.scrollHeight);&quot;)\r\n        time.sleep(random.randint(4,10))\r\n        new_height = driver.execute_script(&quot;return document.body.scrollHeight&quot;)\r\n        if new_height == last_height or random.randint(1, 5) == 1:\r\n            scrolling = False\r\n        last_height = new_height<\/code><\/pre><h2>Итог<\/h2>\n<p>Теперь можно собрать все функции вместе — создаём новый driver, проводим авторизацию, считаем число новых сторис и сообщений, просматриваем сторис, переходим в рекомендуемые подписки и листаем ленту. В конце печатаем полученные данные — число новых сообщений, запросов и историй друзей.<\/p>\n<pre class=\"e2-text-code\"><code>driver = webdriver.Chrome(ChromeDriverManager().install())\r\nauthorize(driver)\r\nqueries_text, messages_count = messages_count(driver)\r\nstories_count = get_stories_count(driver)\r\nwatch_stories(driver)\r\nscroll_recomendations(driver)\r\nscroll_feed(driver)\r\nscroll_explore(driver)\r\n\r\nif queries_text is not None:\r\n    print(queries_text)\r\nelse:\r\n    print('Нет новых запросов на диалог')\r\nprint('Новых сообщений:', messages_count)\r\n\r\nprint('Новых историй:', stories_count)<\/code><\/pre>",
            "date_published": "2021-01-25T16:49:42+03:00",
            "date_modified": "2021-01-25T16:34:11+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/6-12.png",
            "_date_published_rfc2822": "Mon, 25 Jan 2021 16:49:42 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "92",
            "_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"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/6-12.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/7-7.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/8-8.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.19.08.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/1-22.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/2-21.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/3-19.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.19.31.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.02.05.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.03.48.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/--2021-01-25--15.05.14.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/4-12.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/5-12.png"
                ]
            }
        },
        {
            "id": "80",
            "url": "http:\/\/test.leftjoin.ru\/all\/free-education-for-analysts\/",
            "title": "Бесплатные курсы математики для аналитиков и инженеров данных",
            "content_html": "<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/math-(2).png\" width=\"1152\" height=\"575\" alt=\"\" \/>\n<\/div>\n<p>Сейчас в интернете доступно огромное количество платных образовательных материалов, которые обещают сделать из вас аналитика. Иногда это так, и некоторые программы действительно дают хороший набор скилл-сетов, которые необходимы аналитику.<\/p>\n<p>Порой можно встретить точку зрения, что аналитику и вовсе не надо знать SQL и Python. С моей точки зрения, наоборот, следует стараться прокачивать себя в разных направлениях, в том числе и в хард-скиллах. Другой большой спор предполагает, что аналитику не нужна математическая база, и он может эффективно решать задачи, набравшись исключительно хард-скиллов. Я считаю, что это громадное заблуждение. Спойлер — далее в тексте у меня есть таблетка от этого.<\/p>\n<p>К примеру, на мой взгляд, тяжело рассуждать о вероятности оттока, не понимая, что такое теория вероятностей. Сложно обсуждать медиану и нормальность распределения, не понимая математической статистики. Не разобраться в SVD-разложении, не понимая линейную алгебру, и не посчитать градиент функции, не разбираясь в математическом анализе. Некоторые возразят: ну так аналитику это и не требуется, ведь в Python \/ R \/ Matlab всё уже реализовано. Действительно, для стартовой позиции, наверное, всё так, можно взять готовый алгоритм, написать пару команд, и, вау, ты построил модель линейной регрессии. Но что делать дальше? Как разобраться в сути математического аппарата при изменении конкретных деталей модели?<\/p>\n<p>Сегодня интернет позволяет нам подарить себе бесплатно второе высшее образование, и начинающему аналитику следует начать именно с него, прежде чем покупать курсы по анализу данных. Буквально недавно я прослушал ещё раз все фундаментальные курсы, которыми хочу поделиться. Прелесть в том, что все эти материалы абсолютно бесплатны. И несмотря на то, что всё нижеизложенное я изучал в ВУЗе 15 лет назад, повторить изученные материалы крайне полезно (все же за 15 лет многое забывается). Кроме того, получение подобных знаний тренирует тот самый аналитический склад ума и математическую подготовку.<\/p>\n<p>В посте предлагаю вам бесплатные фундаментальные курсы от американских именитых ВУЗов, с которыми вы можете стартовать обучение по направлению аналитика данных. Единственное требование — знание английского языка. Но если у вас до сих пор нет этого, обязательно сперва изучите английский и после возвращайтесь к посту :)<\/p>\n<p>Предполагаю, что совершенно точно можно собрать аналогичный пост из русскоязычных лекций на Youtube, но мне очень нравится подача материала не одним видео на полтора часа, а отрезками с решением прикладных задач, дублированием информации текстом, именно поэтому я рекомендую эти материалы.<\/p>\n<h2>МАТЕМАТИЧЕСКИЙ АНАЛИЗ (M.I.T.)<\/h2>\n<p>В английском языке имеет название Calculus, совершенно потрясающий по подбору материалов и объяснению курс от MIT в трех частях:<\/p>\n<ol start=\"1\">\n<li><a href=\"https:\/\/www.edx.org\/course\/calculus-1a-differentiation\">Дифференцирование<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/calculus-1b-integration\">Интегрирование<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/calculus-1c-coordinate-systems-infinite-series\">Система координат и ряды<\/a><\/li>\n<\/ol>\n<h2>ЛИНЕЙНАЯ АЛГЕБРА (Georgia Tech)<\/h2>\n<p>Курс в четырех частях от одного из ведущих мировых ВУЗов в Computer Science: Georgia Tech.<\/p>\n<ol start=\"1\">\n<li><a href=\"https:\/\/www.edx.org\/course\/linear-equations-part-1\">Линейные уравнения<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/matrix-algebra\">Матричная алгебра<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/determinants-and-eigenvalues\">Детерминанты и собственные значения<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/orthogonality-symmetric-matrices-and-the-svd\">Ортогональность, симметричные матрицы и SVD<\/a><\/li>\n<\/ol>\n<h2>ТЕОРИЯ ВЕРОЯТНОСТЕЙ И МАТЕМАТИЧЕСКАЯ СТАТИСТИКА (Georgia Tech)<\/h2>\n<p>Курс в четырех частях от одного из ведущих мировых ВУЗов в Computer Science: Georgia Tech.<\/p>\n<ol start=\"1\">\n<li><a href=\"https:\/\/www.edx.org\/course\/probability-and-statistics-i-a-gentle-introduction-to-probability\">Введение в теорию вероятностей<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/probability-and-statistics-ii-random-variables-great-expectations-to-bell-curves\">Случайные величины<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/probability-and-statistics-iii-a-gentle-introduction-to-statistics\">Введение в статистику<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/probability-and-statistics-iv-confidence-intervals-and-hypothesis-tests\">Доверительные интервалы и гипотезы<\/a><\/li>\n<\/ol>\n<h2>ВЫЧИСЛЕНИЯ В PYTHON (Georgia Tech)<\/h2>\n<p>Курс в четырех частях от одного из ведущих мировых ВУЗов в Computer Science: Georgia Tech.<\/p>\n<ol start=\"1\">\n<li><a href=\"https:\/\/www.edx.org\/course\/computing-in-python-i-fundamentals-and-procedural\">Основы<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/computing-in-python-ii-control-structures\">Управляющие структуры<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/computing-in-python-iii-data-structures\">Структуры данных<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/computing-in-python-iv-objects-algorithms\">Объектные алгоритмы<\/a><\/li>\n<\/ol>\n<h2>R ДЛЯ АНАЛИЗА ДАННЫХ (Harvard)<\/h2>\n<p>Курс в семи частях от профессора из Гарварда<\/p>\n<ol start=\"1\">\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-r-basics\">Основы R<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-visualization\">Визуализация<\/a><\/li>\n<\/ol>\n<ol start=\"3\">\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-probability\">Теория вероятностей<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-inference-and-modeling\">Вывод и моделирование<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-productivity-tools\">Полезные инструменты<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-wrangling\">Форматирование данных<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-linear-regression\">Линейная регрессия<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-machine-learning\">Машинное обучение<\/a><\/li>\n<li><a href=\"https:\/\/www.edx.org\/course\/data-science-capstone\">Итоги курса<\/a><\/li>\n<\/ol>\n",
            "date_published": "2020-12-08T01:18:50+03:00",
            "date_modified": "2020-12-08T13:53:05+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/math-(2).png",
            "_date_published_rfc2822": "Tue, 08 Dec 2020 01:18:50 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "80",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/math-(2).png"
                ]
            }
        },
        {
            "id": "32",
            "url": "http:\/\/test.leftjoin.ru\/all\/kazakhstan-marketing-conference-2020\/",
            "title": "Kazakhstan Marketing Conference 2020",
            "content_html": "<p>Вчера мне удалось выступить на крупнейшей маркетинговой конференции в Казахстане: <a href=\"https:\/\/k50.kz\/sobytiya\/kmc-marketing-conference-2020.html\">Kazakhstan Marketing Conference 2020<\/a>.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/valiotti@2x.jpeg\" width=\"494.5\" height=\"348\" alt=\"\" \/>\n<\/div>\n<p>Город Алматы произвел на меня положительное впечатление, а сама конференция оказалась высокопрофессиональным мероприятием, наполненным массой умных разносторонних и доброжелательных людей.<\/p>\n<p>Приятный <i>бонус<\/i> для участников конференции: презентация моего выступления <a href=\"https:\/\/www.slideshare.net\/NikolayValiotti\/kazahstan-marketing-conference-2020-223394647\">доступна на slideshare<\/a> (осторожно, VPN!), так можно будет вспомнить о чем шла речь.<\/p>\n<p>Помимо выступления в основной секции форума я проводил мастер-класс на тему: «Как построить понятное техническое задание на аналитику?».<br \/>\nИ в рамках работы с аудиторией нам удалось сформулировать тезисы для шаблона технического задания.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/template_v2@2x.png\" width=\"449\" height=\"346\" alt=\"\" \/>\n<\/div>\n<p><a href=\"https:\/\/docs.google.com\/spreadsheets\/d\/1JdlMLrrwHDu1LuQ4apRYNnOvCwsQReugTKkb2DWddu4\/edit#gid=815634630\">Делюсь шаблоном<\/a>, он окажется полезным для всех, кто сталкивается со сложностями в переводе задачи с бизнес-языка на технический.<\/p>\n",
            "date_published": "2020-01-23T11:34:55+03:00",
            "date_modified": "2020-05-12T17:30:08+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/photo_2020-01-23-10.11.57.jpeg",
            "_date_published_rfc2822": "Thu, 23 Jan 2020 11:34:55 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "32",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/photo_2020-01-23-10.11.57.jpeg",
                    "http:\/\/test.leftjoin.ru\/pictures\/template@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/valiotti@2x.jpeg",
                    "http:\/\/test.leftjoin.ru\/pictures\/template_v2@2x.png"
                ]
            }
        },
        {
            "id": "16",
            "url": "http:\/\/test.leftjoin.ru\/all\/looker-overview\/",
            "title": "Обзор Looker",
            "content_html": "<p>Сегодня поговорим о BI-платформе Looker, над которой мне удалось поработать в течение 2019-го года.<\/p>\n<p>Представляю краткое содержание статьи для удобной и быстрой навигации:<\/p>\n<ol start=\"1\">\n<li><a href=\"http:\/\/test.leftjoin.ru\/all\/looker-overview\/#part1\">Что такое Looker?<\/a><\/li>\n<li><a href=\"http:\/\/test.leftjoin.ru\/all\/looker-overview\/#part2\">Как и к каким СУБД можно подключиться через Looker?<\/a><\/li>\n<li><a href=\"http:\/\/test.leftjoin.ru\/all\/looker-overview\/#part3\">Построение Looker ML модели данных<\/a><\/li>\n<li><a href=\"http:\/\/test.leftjoin.ru\/all\/looker-overview\/#part4\">Режим Explore (исследование данных на построенной модели<\/a>)<\/li>\n<li><a href=\"http:\/\/test.leftjoin.ru\/all\/looker-overview\/#part5\">Построение отчетов и сохранение их в Look<\/a><\/li>\n<li><a href=\"http:\/\/test.leftjoin.ru\/all\/looker-overview\/#part6\">Примеры дашбордов в Looker<\/a><\/li>\n<\/ol>\n<h2><a id=\"part1\"><\/a>Что такое Looker?<\/h2>\n<p>Создатели <a href=\"https:\/\/looker.com\/\">Looker<\/a> позиционируют его как программное обеспечение класса business intelligence и платформу big data аналитики, которая помогает исследовать, анализировать и делиться аналитикой бизнеса в режиме реального времени.<br \/>\nLooker — это действительно удобный инструмент и один из немногих продуктов класса BI, который позволяет в режиме онлайн работать с преднастроенными кубами данных (на самом деле, реляционными таблицами, которые описаны в Look ML-модели).<br \/>\nИнженеру, работающему над Looker, требуется описать модель данных на языке Look ML (что-то среднее между CSS и SQL), опубликовать эту модель данных и далее настроить отчетность и дашборды.<br \/>\nСам Look ML достаточно прост, взаимосвязи между объектами данных задаются data-инженером, что впоследствии позволяет использовать данные без знания языка SQL (если быть точным: движок Looker сам за пользователя генерирует код на языке SQL).<\/p>\n<p>Буквально недавно, в июне 2019-го года Google <a href=\"https:\/\/cloud.google.com\/blog\/topics\/inside-google-cloud\/expanding-our-platform-for-business-intelligence-and-embedded-analytics\">объявил<\/a> о покупке платформы Looker за $2.6 млрд.<\/p>\n<h2><a id=\"part2\"><\/a>Как и к каким СУБД можно подключиться через Looker?<\/h2>\n<p>Выбор СУБД, с которыми работает Looker довольно богатый. На скриншоте ниже перечислены всевозможные подключения на Октябрь 2019 г.:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/db-list@2x.png\" width=\"418\" height=\"852\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Доступные СУБД для подключения<\/div>\n<\/div>\n<p>Настроить подключение к базе данных несложно через веб-интерфейс:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/connection-setting@2x.png.jpg\" width=\"2560\" height=\"1460\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Веб-интерфейс подключения к СУБД<\/div>\n<\/div>\n<p>В вопросе соединений с базами данных отдельно хочется отметить два факта: к сожалению, на текущий момент и в ближайшем будущем отсутствует поддержка Clickhouse от Яндекса. Скорее всего поддержка не появится, учитывая тот факт, что Looker был приобретен конкурирующей компанией Google.<br \/>\nВторой досадный факт состоит в том, что построить одну модель данных, которая бы обращалась в разные СУБД нельзя. В Looker нет встроенного хранилища, которое могло бы объединять результаты запроса (кстати, в отличии даже от того же Redash).<br \/>\nЭто означает, что аналитическая архитектура данных должна быть построена в рамках одной СУБД (желательно с высоким быстродействием или на агрегированных данных).<\/p>\n<h2><a id=\"part3\"><\/a>Построение Looker ML модели данных<\/h2>\n<p>Для того чтобы построить отчет или дашборд в Looker, предварительно необходимо настроить модель данных. Синтаксис языка Look ML достаточно подробно <a href=\"https:\/\/docs.looker.com\/data-modeling\/learning-lookml\/what-is-lookml\">описан в документации<\/a>. От себя могу добавить, что описание модели не требует долгого погружения для специалиста со знанием SQL. Скорее, необходимо перестроить подход к подготовке модели данных. Язык Look ML очень похож на CSS:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/lookml@2x.jpg\" width=\"1280\" height=\"703.5\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Консоль создания Look ML модели<\/div>\n<\/div>\n<p>В модели данных прописываются связи с таблицами, ключи, гранулярность, информация о том, какие поля являются фактами, какие измерениями. Для фактов прописывается агрегация. Разумеется, при создании модели можно использовать различные IF \/ CASE выражения.<\/p>\n<h2><a id=\"part4\"><\/a>Режим Explore<\/h2>\n<p>Наверное, это самая главная киллер-фича Looker, поскольку позволяет любым бизнес-поздразделениям самостоятельно получать данные без привлечения аналитиков \/ инженеров данных. И, видимо, поэтому использование аккаунтов с использованием режиме Explore тарифицируется отдельно.<\/p>\n<p>Фактически, режим Explore это интерфейс, который позволяет использовать настроенную Look ML модель данных, выбрать необходимые метрики и измерения и построить кастомный отчет \/ визуализацию.<br \/>\nК примеру, мы хотим понять сколько каких действий в интерфейсе Лукера было совершено на прошлой неделе. Для этого, используя режим Explore, мы выберем поле Date и поставим на него фильтр: прошлая неделя (Looker в этом смысле достаточно умный и в фильтре будет достаточно написать ‘Last week’), затем из измерений выберем «Категорию» и в качестве метрики — Количество. После нажатия кнопки <i>Run<\/i> сформируется готовый отчет.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/explore-tab@2x.png.jpg\" width=\"2560\" height=\"1012\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Построение отчета в Looker<\/div>\n<\/div>\n<p>Затем, используя полученные данные в табличной форме можно настроить визуализацию любого вида. Например, Pie chart:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/vis@2x.png\" width=\"1081\" height=\"566\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Применение визуализации к отчету<\/div>\n<\/div>\n<h2><a id=\"part5\"><\/a>Построение отчетов и сохранение их в Look<\/h2>\n<p>Полученный набор данных \/ визуализацию в режиме Explore иногда хочется сохранить и поделиться с коллегами, для этого в Looker существует отдельная сущность —  Look. Это готовый построенный отчет с выбранными фильтрами \/ измерениями \/ фактами.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/look@2x.png.jpg\" width=\"2560\" height=\"1457\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Пример сохраненного Look<\/div>\n<\/div>\n<h2><a id=\"part6\"><\/a>Примеры дашбордов в Looker<\/h2>\n<p>Систематизируя склад созданных Look зачастую хочется получить готовую композицию \/ overview ключевых метрик, которые было бы видно на одном листе.<br \/>\nДля этих целей отлично подходит создание дашборда. Дашборд создается либо на лету, либо используя ранее созданные Look. Одной из «фишек» дашборда является установка параметров, которые меняются на всем дашборде и могут быть применены ко всем Look сразу.<\/p>\n<div class=\"e2-text-picture\">\n<div class=\"fotorama\" data-width=\"2560\" data-ratio=\"1.8591140159768\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/dashboard@2x.png.jpg\" width=\"2560\" height=\"1377\" alt=\"\" \/>\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/dashboard_v2@2x.png.jpg\" width=\"2560\" height=\"1456\" alt=\"\" \/>\n<\/div>\n<\/div>\n<h2>Интересные фишки одной строкой<\/h2>\n<ul>\n<li>В Looker можно ссылаться на другие отчеты и, используя данных функционал, можно создать динамический параметр, который передается по ссылке.<br \/>\nНапример, построили отчет с разделениям выручки по странам и в этот отчете можем ссылаться на дашборд по отдельно взятой стране. Переходя по ссылке, пользователь видит дашборд по конкретной стране, на которую перешел<\/li>\n<li>На каждой странице Looker существует чат, в котором оперативно отвечает поддержка<\/li>\n<li>Looker не умеет работать с merge данных на уровне разных СУБД, однако может объединить данные на уровне готовых Look (в нашем случае эта функциональность работает очень странно).<\/li>\n<li>В рамках работы с различными моделями данных, я обнаружил крайне нетривиальное использование SQL для подсчета уникальных значений в ненормализованный таблице данных, Looker называет это <a href=\"https:\/\/help.looker.com\/hc\/en-us\/articles\/360023722974-A-Simple-Explanation-of-Symmetric-Aggregates-or-Why-On-Earth-Does-My-SQL-Look-Like-That-\">симметричными агрегатами<\/a>.<br \/>\nSQL действительно выглядит очень нетривиально:<\/li>\n<\/ul>\n<pre class=\"e2-text-code\"><code>SELECT \r\n order_items.order_id AS &quot;order_items.order_id&quot;,\r\n order_items.sale_price AS &quot;order_items.sale_price&quot;,\r\n (COALESCE(CAST( ( SUM(DISTINCT (CAST(FLOOR(COALESCE(users.age ,0)\r\n *(1000000*1.0)) AS DECIMAL(38,0))) + \r\n CAST(STRTOL(LEFT(MD5(CONVERT(VARCHAR,users.id )),15),16) AS DECIMAL(38,0))\r\n * 1.0e8 + CAST(STRTOL(RIGHT(MD5(CONVERT(VARCHAR,users.id )),15),16) AS DECIMAL(38,0)) ) \r\n - SUM(DISTINCT CAST(STRTOL(LEFT(MD5(CONVERT(VARCHAR,users.id )),15),16) AS DECIMAL(38,0))\r\n * 1.0e8 + CAST(STRTOL(RIGHT(MD5(CONVERT(VARCHAR,users.id )),15),16) AS DECIMAL(38,0))) ) \r\n AS DOUBLE PRECISION) \r\n \/ CAST((1000000*1.0) AS DOUBLE PRECISION), 0) \r\n \/ NULLIF(COUNT(DISTINCT CASE WHEN users.age IS NOT NULL THEN users.id \r\n ELSE NULL END), 0)) AS &quot;users.average_age&quot;\r\nFROM order_items AS order_items\r\nLEFT JOIN users AS users ON order_items.user_id = users.id\r\n\r\nGROUP BY 1,2\r\nORDER BY 3 DESC\r\nLIMIT 500<\/code><\/pre><ul>\n<li>При внедрении Looker к покупке обязателен JumpStart Kit, который стоит не менее $6k, в рамках которого вы получаете поддержку и консультации от Looker при внедрении инструмента.<\/li>\n<\/ul>\n",
            "date_published": "2020-01-08T11:22:02+03:00",
            "date_modified": "2020-06-09T12:04:56+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/db-list@2x.png",
            "_date_published_rfc2822": "Wed, 08 Jan 2020 11:22:02 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "16",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/fotorama\/fotorama.css",
                    "system\/library\/fotorama\/fotorama.js",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/db-list@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/connection-setting@2x.png.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/lookml@2x.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/explore-tab@2x.png.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/vis@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/look@2x.png.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/dashboard@2x.png.jpg",
                    "http:\/\/test.leftjoin.ru\/pictures\/dashboard_v2@2x.png.jpg"
                ]
            }
        },
        {
            "id": "17",
            "url": "http:\/\/test.leftjoin.ru\/all\/excel-chart-matrix-bcg\/",
            "title": "Диаграмма матрицы BCG (Boston Consulting Group)",
            "content_html": "<p>Разбавлю блог интересным отчетом, который в свое время был построен для компании Yota в ноябре 2011го года. Построить данный отчет нас вдохновила <a href=\"https:\/\/ru.wikipedia.org\/wiki\/%D0%9C%D0%B0%D1%82%D1%80%D0%B8%D1%86%D0%B0_%D0%91%D0%9A%D0%93\">матрица BCG<\/a>.<\/p>\n<p>У нас было: один пакет Excel, 75 VBA макросов, ODBC подключение к Oracle, SQL-запросы к БД всех сортов и расцветок. На таком стеке и рассмотрим построение отчета, но в начале немного о самой идее отчета.<\/p>\n<p>Матрица BCG — это матрица размером 2х2, на которой сегменты клиентов изображаются окружностями с центрами на пересечении координат, образуемых соответствующими темпами двух выбранных показателей.<\/p>\n<p>Если упростить, то нам надо было поделить всех клиентов компании на 4 сегмента: ARPU выше среднего \/ ниже среднего, потребление трафика (основной услуги) выше среднего \/ ниже среднего. Таким образом получалось, что возникает 4 квадранта, в каждый из которых необходимо поместить пузырьковую диаграмму, где размер пузырька обозначает общее количество пользователей в сегменте. Дополнительно к этому добавляется еще один пузырек в каждом квадранте (более мелкий), который показывает отток в каждом сегменте (авторское усовершенствование).<\/p>\n<p><b>Что хотелось получить на выходе?<\/b><br \/>\nГрафик подобного вида:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/bubbles-chart@2x.png\" width=\"1039\" height=\"681\" alt=\"\" \/>\n<div class=\"e2-text-caption\">Представление матрицы BCG на данных компании Yota<\/div>\n<\/div>\n<p>Постановка задачи более-менее ясна, перейдем к реализации отчёта.<br \/>\nПредположим, что мы уже собрали нужные данные (то есть научились определять средний ARPU и среднее потребление трафика, в данном посте не будем разбирать SQL-запрос), тогда первостепенная основная задача — понять как отобразить средствами Excel пузырьки в нужных местах.<\/p>\n<p>Для этого на помощь приходит базовая пузырьковая диаграмма:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/viz-type@2x.png\" width=\"252\" height=\"345\" alt=\"\" \/>\n<div class=\"e2-text-caption\"><i>Вставка — Диаграмма — Пузырьковая<\/i><\/div>\n<\/div>\n<p>Идем в меню <i>Выбор источника данных<\/i> и оцениваем, что необходимо подготовить для построения диаграммы в нужном нам виде: координаты <i>X<\/i>, координаты <i>Y<\/i>, значения размеров пузырьков.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/data-source@2x.png\" width=\"518\" height=\"529\" alt=\"\" \/>\n<\/div>\n<p>Отлично, выходит, если предположить, что наша диаграмма будет расположена в координатах по <i>X<\/i> от -1 до 1, а по <i>Y<\/i> от -1 до 1, то центр правого верхнего пузырька это точка (0.5; 0.5) на диаграмме.  Аналогичным образом, расположим все остальные основные пузырьки.<\/p>\n<p>Отдельно следует подумать о пузырьках типа <i>Churn<\/i> (для отображения оттока), они расположены правее и ниже основного пузырька и могут с ним пересекаться, поэтому правый верхний пузырек разместим в эмпирически полученных координатах (0.65; 0.35).<\/p>\n<p>Таким образом, для четырех основных и четырех дополнительных пузырьков мы можем организовать данные в следующем виде:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/bubbles-data@2x.png\" width=\"562\" height=\"97\" alt=\"\" \/>\n<\/div>\n<p>Рассмотрим подробнее, как будем их использовать:<\/p>\n<div class=\"e2-text-picture\">\n<div class=\"fotorama\" data-width=\"535\" data-ratio=\"0.9006734006734\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/data-source-active@2x.png\" width=\"535\" height=\"594\" alt=\"\" \/>\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/data-source-churn@2x.png\" width=\"534\" height=\"591\" alt=\"\" \/>\n<\/div>\n<\/div>\n<p>Итак, мы задаем по X — горизонтальные координаты центра наших пузырьков, которые лежат в ячейках <i>A9:A12<\/i>, по Y — вертикальные координаты центра наших пузырьков, которые лежат в ячейках <i>B9:B12<\/i>, а размеры пузырьков мы храним в ячейках <i>E9:E12<\/i>.<br \/>\nДалее, добавляем еще один ряд данных для Оттока и снова указываем все необходимые параметры.<\/p>\n<p>Мы получим следующий график:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/bubbles-preparing@2x.png\" width=\"503\" height=\"423\" alt=\"\" \/>\n<\/div>\n<p>Дальше наводим красоту: меняем цвета, убираем оси и получаем красивый результат.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"http:\/\/test.leftjoin.ru\/pictures\/bubbles-preparing-step2@2x.png\" width=\"568\" height=\"458\" alt=\"\" \/>\n<\/div>\n<p>Добавив необходимые подписи данных, получим то, что требовалось в задаче.<\/p>\n<p>Делитесь в комментариях — приходилось ли строить подобные графики, каким образом решали задачу?<\/p>\n",
            "date_published": "2019-11-19T10:38:11+03:00",
            "date_modified": "2020-05-12T17:32:20+03:00",
            "image": "http:\/\/test.leftjoin.ru\/pictures\/bubbles-chart@2x.png",
            "_date_published_rfc2822": "Tue, 19 Nov 2019 10:38:11 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "17",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/fotorama\/fotorama.css",
                    "system\/library\/fotorama\/fotorama.js"
                ],
                "og_images": [
                    "http:\/\/test.leftjoin.ru\/pictures\/bubbles-chart@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/viz-type@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/data-source@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/bubbles-data@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/data-source-active@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/data-source-churn@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/bubbles-preparing@2x.png",
                    "http:\/\/test.leftjoin.ru\/pictures\/bubbles-preparing-step2@2x.png"
                ]
            }
        }
    ],
    "_e2_version": 3365,
    "_e2_ua_string": "E2 (v3365; Aegea)"
}