<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0">

<channel>

<title>Блог об аналитике, визуализации данных, data science и BI, заметки с тегом: python</title>
<link>http://test.leftjoin.ru/tags/python/</link>
<description></description>
<generator>E2 (v3365; Aegea)</generator>

<item>
<title>Анализируем речь с помощью Python: Сколько раз в минуту матерятся на интервью YouTube-канала «вДудь»?</title>
<guid isPermaLink="false">136</guid>
<link>http://test.leftjoin.ru/all/youtube-interview-speech-analysis/</link>
<comments>http://test.leftjoin.ru/all/youtube-interview-speech-analysis/</comments>
<description>
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/13-1.jpg" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Выход практически каждого ролика на канале «вДудь» считается событием, а некоторые из этих релизов даже сопровождаются скандалами из-за неосторожных высказываний его гостей.&lt;br /&gt;
Сегодня при помощи статистических подходов и алгоритмов ML мы будем анализировать прямую речь. В качестве данных используем интервью, которые журналист Юрий Дудь (признан иностранным агентом на территории РФ) берет для своего YouTube-канала. Посмотрим с помощью Python, о чем таком интересном говорили в интервью на канале «вДудь».&lt;/p&gt;
&lt;h2&gt;Сбор данных&lt;/h2&gt;
&lt;p&gt;C помощью &lt;a href="https://developers.google.com/youtube/v3"&gt;YouTube API&lt;/a&gt; мы получили список всех видео с канала Юрия Дудя, а также их метаинформацию. О том, как это сделать, вы можете узнать, например, из &lt;a href="http://test.leftjoin.ru/all/youtube-api/"&gt;статьи нашего блога&lt;/a&gt;.&lt;br /&gt;
Если вы уже слышали знаменитое “Юрий будет дуть, дуть будет Юрий”, то наверняка знаете, что на этом канале есть документальные фильмы, а также интервью, в которых участвуют сразу несколько гостей. Нас заинтересовали только те выпуски, в которых преимущественно говорит только один гость. Поэтому нам пришлось провести фильтрацию всех видео вручную.&lt;br /&gt;
Для дальнейшего анализа нам необходимо было получить длительности роликов. Это мы сделали с помощью GET-запросов к YouTube API. Результаты приходили в специфическом формате (для примера: “PT1H49M35S”), поэтому их нам пришлось распарсить и перевести в секунды.&lt;br /&gt;
Итак, мы получили датафрейм, состоящий из 122 записей:&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/--1.png" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;На основе метаинформации по лайкам, комментариям и просмотрам мы построили следующий Bubble Chart:&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/29.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Так как наша цель — проанализировать речь в интервью, нам необходимо было получить текстовые составляющие роликов. В этом нам помог API-интерфейс &lt;a href="https://pypi.org/project/youtube-transcript-api/"&gt;youtube_transcript_api&lt;/a&gt;, который скачивает субтитры из видео на YouTube. Для каких-то роликов субтитры были прописаны вручную, но для большинства они были сгенерированы автоматически. К сожалению, для 10 видео субтитров не оказалось: беседы с L’one, Шнуром, Ресторатором, Амираном, Ильичом, Ильей Найшуллером, Соболевым, Иваном Дорном, Навальным, Noize MC. Причину их отсутствия мы, к сожалению, понять не смогли.&lt;/p&gt;
&lt;h2&gt;А гости кто?&lt;/h2&gt;
&lt;p&gt;Спектр рода деятельности гостей канала «вДудь» достаточно обширен, поэтому было решено пополнить исходные данные информацией о том, чем же в основном занимается приглашенный участник каждого интервью. К сожалению, ролики не сопровождаются четкими метками профессиональной принадлежности гостя, поэтому мы прописали эту информацию сами. На момент выгрузки данных последним видео на канале был разговор с комиком Дмитрием Романовым.&lt;br /&gt;
Если с идентификацией профессии каждого гостя мы не ошиблись, то вот такое распределение в итоге получается:&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/31.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Музыканты, рэперы и актеры — самые частые гости Юрия, скорее всего, они являются самыми интересными для автора и аудитории. Представителей научного сообщества (астрофизик, историк, экономист и т.д) наоборот, гораздо меньше, ведь научно-популярные интервью — прерогатива других интервьюеров.&lt;/p&gt;
&lt;h2&gt;Обработка текста&lt;/h2&gt;
&lt;p&gt;Анализ текстовой информации сложен в той степени, в какой сложен язык, на котором написан текст. Подробно о подготовке текста к анализу мы рассказывали в материале &lt;a href="http://test.leftjoin.ru/all/borderline-text-analysis/" class="nu"&gt;«&lt;u&gt;Python и тексты нового альбома Земфиры&lt;/u&gt;»&lt;/a&gt;. Тут была проведена идентичная работа.&lt;br /&gt;
Как и раньше, для решения аналитической задачи мы решили использовать такой подход как лемматизация, т. е. приведение слова к его словарной форме. Проведя лемматизацию текстовых данных по правилам русского языка, мы получим существительные в именительном падеже единственного числа (кошками — кошка), прилагательные в именительном падеже мужского рода (пушистая — пушистый), а глаголы в инфинитиве (бежит — бежать). В этом проекте мы опять воспользовались библиотекой &lt;a href="https://pymorphy2.readthedocs.io/en/stable/"&gt;Pymorphy&lt;/a&gt;, представляющую собой морфологический анализатор.&lt;br /&gt;
Помимо приведения к словарной форме нам потребовалось убрать из текстов часто встречающиеся слова, которые не несут ценности для анализа. Это было необходимо, потому что так называемые стоп-слова могут повлиять на работу используемой модели машинного обучения. Список таких слов мы взяли из пакета &lt;a href="https://www.nltk.org/api/nltk.corpus.html"&gt;ntlk.corpus&lt;/a&gt;, а после расширили его, изучив тексты интервью. Конечно, мы также убрали все знаки пунктуации.&lt;/p&gt;
&lt;h2&gt;Анализ словарного запаса&lt;/h2&gt;
&lt;p&gt;После обработки текста мы посчитали для каждого интервью количество всех слов, а также абсолютное и относительное количество уникальных слов. Конечно, полученные значения неидеальны, так как, во-первых, для большинства интервью были получены автоматически сгенерированные субтитры, которые являются неточными, а во-вторых, тексты были очищены от лишней информации.&lt;br /&gt;
Сперва мы решили наглядно представить основной массив лексики, которая звучит в интервью. После группировки интервью по роду деятельности гостя нам удалось это сделать и в этом нам помогла библиотека &lt;a href="http://amueller.github.io/word_cloud/"&gt;wordcloud&lt;/a&gt;. У нас получились такие облака слов:&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/All-wordclouds.png.jpg" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Лейтмотивом всех интервью Юрия являются обсуждение России (политики, социальной жизни и других особенностей), уровня заработка гостей, а также непосредственно профессиональной деятельности гостя (это особенно заметно у представителей индустрии кинопроизводства).&lt;br /&gt;
Далее мы решили построить боксплот для количества слов для каждого рода деятельности (профессии, которые были представлены единственным гостем, мы не стали учитывать):&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/33.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Наиболее разговорчивыми гостями оказались блогеры. По медиане, они наговорили больше всего слов. Чуть поодаль от них журналисты и комики, а вот самыми немногословными оказались рэперы.&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/35.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Что касается количества уникальных слов, то тут ситуация аналогичная. И рэперы опять в аутсайдерах…&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/37.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Если говорить об отношении уникальных слов к общему количеству, то тут можно увидеть совершенно иную картину. Теперь впереди оказываются, рэперы, музыканты и бизнесмены. Предыдущие же лидеры, наоборот, становятся самыми последними.&lt;br /&gt;
Конечно, стоит отметить, что такие сравнения могут быть несправедливыми, так как длительность интервью у каждого гостя Дудя разная, а потому кто-то просто мог успеть наговорить больше слов, чем остальные. Наглядно в этом можно убедиться, взглянув на распределение длительности интервью по роду деятельности (для построения использовался тот же пул гостей, что и для боксплотов выше):&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/39.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;К тому же, разные роды деятельности представляет разное количество человек, это тоже могло сказаться на результатах.&lt;br /&gt;
Далее мы составили список слов, появление которых в интервью было бы интересно отследить, и посмотрели как часто они упоминаются для каждого рода деятельности. Также мы решили учесть дисбаланс среди представителей разных профессиональных категорий и разделили полученные частоты на соответствующее количество гостей.&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/-.png" border="0" width="90%" height="90%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Первое место по упоминаниям очевидно занимает Россия. Что касается Запада, то про США гости говорили в 2,5 раза меньше. Что касается лидера РФ, то про него речь заходила достаточно часто. Его оппонент, Алексей Навальный, в этой словесной “баталии” потерпел поражение. Интересно, что политики далеко не в топе по упоминаниям Путина. Впереди оказался экономист Сергей Гуриев, после него ведущий Александр Гордон, а тройку замкнули журналисты.&lt;br /&gt;
Глагол “любить” чаще использовали люди, имеющие отношение к искусству, творчеству и гуманитарным наукам — кинокритик Антон Долин, мультипликатор Олег Куваев, историк Тамара Эйдельман, актеры, рэперы, художник Федор Павлов-Андреевич, комики, музыканты, режиссеры. Про страхи (если судить по глаголу “бояться”) гости говорили реже, чем о любви. В топ вошли историк Эйдельман, дизайнер Артемий Лебедев, кинокритик Долин и политики. Может быть в этом кроется ответ на вопрос, почему же политики не так охотно произносили имя президента России.&lt;br /&gt;
Что касается денег, то о них говорили все. Ну, за исключением человека науки, астрофизика Константина Батыгина. С церковью же имеем совершенно обратную ситуацию. О ней по большей части говорили только писатели и художник Павлов-Андреевич.&lt;/p&gt;
&lt;h2&gt;Анализ мата&lt;/h2&gt;
&lt;p&gt;Далее мы решили проанализировать то, как часто гости Юрия Дудя ругались матом. С помощью регулярных выражений мы составили словарь матерных слов со всех интервью. После этого, для каждого ролика было подсчитано суммарное количество вхождений элементов составленного словаря.&lt;br /&gt;
Мы построили диаграммы, отражающие топ-10 любителей нецензурно выражаться по количеству “запрещенных” слов в минуту.&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/43.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Как видим, рэперы и музыканты почти полностью захватили топ. Помимо них очень часто ругались такие гости как блогер Данила Поперечный и комики Иван Усович и Алексей Щербаков. Первое место в рейтинге с большим отрывом от остальных держит Morgenstern (признан иностранным агентом на территории РФ), а вот Олег Тиньков в своем последнем интервью матерился не так много, чтобы попасть в Топ-10.&lt;br /&gt;
Зато, как искрометно!&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/FSgosl8XwAAmtpw.jpg" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;После персонального анализа мы решили узнать, насколько насыщена нецензурными словами речь представителей разных профессиональных групп. Нулевые показатели при этом были опущены.&lt;/p&gt;
&lt;iframe width="730" height="480" frameborder="0" scrolling="no" src="//plotly.com/~satiukov.e/41.embed?showlink=false"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ожидаемо, что больше всех матерились рэперы. На втором месте оказались блогеры (по большей части за счет Поперечного). За ними следует Артемий Лебедев, единственный дизайнер в нашей выборке, благодаря разнообразия речи которого, представители этой профессии и попали в топ-3 этого распределения. Кстати, если вы еще не знакомы с нашим &lt;a href="https://habr.com/ru/post/596035/"&gt;анализом телеграм-канала Лебедева&lt;/a&gt;, то мы не понимаем, чего же вы ждете! Несмотря на то что генератор постов Артемия Лебедева сейчас выключен, исследование его телеграм-канала все равно заслуживает вашего внимания.&lt;/p&gt;
&lt;h2&gt;Ограничения анализа&lt;/h2&gt;
&lt;p&gt;Стоит отметить, что в нашем небольшом исследовании есть два недостатка:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Как уже говорилось ранее, мы не смогли отделить слова гостей Дудя от речи Юрия, который и сам зачастую не брезгует использовать нецензурные выражения. Однако, задача интервьюера — подстроиться под стиль речи гостя, поэтому, скорее всего, результаты бы не сильно изменились.&lt;/li&gt;
&lt;li&gt;В автосгенерированных субтитрах нам встретилось некое подобие цензуры — некоторые слова были заменены на ‘[ __ ]’. Тут можно выделить несколько интересных моментов:
&lt;ul&gt;
  &lt;li&gt;действительно некоторые матерные слова были зацензурены (по большей части слово “бл**ь”);&lt;/li&gt;
  &lt;li&gt;остальные матерные слова остались нетронутыми;&lt;/li&gt;
  &lt;li&gt;под чистку попали некоторые другие грубые слова, при этом не являющиеся матерными (“мудак”, “гавно”).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Продемонстрируем наглядно на примере следующего диалога:&lt;br /&gt;
&lt;i&gt;Дудь: Почему твои треки такое гавно?&lt;/i&gt;&lt;br /&gt;
&lt;i&gt;Гнойный: Мои треки ох**тельные, Юра, просто ты любишь гавно.&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/image.png" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Такие замены встречались в субтитрах роликов с людьми, которые не употребляли нецензурные выражения в своей речи (по крайней мере на протяжении интервью). Однозначное решение, что же делать с ‘[ __ ]’, мы не смогли принять, поэтому для некоторых гостей какая-то часть матерных слов была, увы, не подсчитана.&lt;/p&gt;
&lt;h2&gt;Работа с Word2vec&lt;/h2&gt;
&lt;p&gt;После статистического анализа интервью мы перешли к определению их контекста. Для этого мы, как и раньше, воспользовались моделью Word2vec. Она основана на нейронной сети и позволяет представлять слова в виде векторов с учетом семантической составляющей. Косинусная мера семантически схожих слов будет стремиться к 1, а у двух слов, не имеющих ничего общего по смыслу, она близка к 0. Модель можно обучать самостоятельно на подготовленном корпусе текстов, но мы решили взять готовую — от &lt;a href="https://rusvectores.org/ru/"&gt;RusVectores&lt;/a&gt;.  Для ее использования нам понадобилась библиотека &lt;a href="https://radimrehurek.com/gensim/"&gt;gensim&lt;/a&gt;.&lt;br /&gt;
Мы рассчитали векторы-представления для каждой профессиональной группы. Наверное, можно ожидать, что режиссёры обсуждали кино и все, что с ним связано, а музыканты — музыку. Поэтому для каждого рода деятельности мы получили список слов, описывающих тематику текстов соответствующих роликов. Также мы раскрасили ячейки в зависимости от того, насколько каждое полученное слово было близко к текстам соответствующей категории гостей.&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/-10-----.png" border="0" width="120%" height="120%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Можно сказать, что в целом каждая профессиональная категория описывается вполне соответствующими терминами. Конечно, некоторые слова могут показаться спорными. К примеру, на первом месте для рэперов стоит слово “джазовый”, хотя ни с 1 представителем хип-хоп течения речь о джазе не заходила. Тем не менее модель посчитала, что это слово достаточно близко к общему смыслу интервью людей, относящихся к этой категории (видимо, за счет непосредственного отношения рэперов к музыке).&lt;/p&gt;
&lt;h2&gt;P.S. Мистическое число 25.000000&lt;/h2&gt;
&lt;p&gt;Как мы уже говорили, среди скачанных субтитров некоторые были написаны вручную. Интересно, что все они начинаются с числа 25.000000, причем оно нигде не озвучивается.&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/25.000000--1.png" border="0" width="50%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Что же это за мистическое число? Если уйти в конспирологию, то можно вспомнить про 25-й кадр. К сожалению, нам об этом ничего неизвестно, мы просто оставим это как пищу для размышлений…&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/25.000000-.png" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Thu, 02 Jun 2022 11:09:33 +0300</pubDate>
</item>

<item>
<title>Где поесть? Куда сходить? Ищем ответ на вопрос с помощью пары рекомендаций и скрипта Python</title>
<guid isPermaLink="false">131</guid>
<link>http://test.leftjoin.ru/all/recommendations-from-geotags/</link>
<comments>http://test.leftjoin.ru/all/recommendations-from-geotags/</comments>
<description>
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/roman-bozhko-OXXsAafHDeo-unsplash.jpg" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Поскольку наш блог придерживается технологий, аналитики и IT-тематики, то обсуждение политики мы здесь, естественно опустим. Однако, сложно не заметить, что многие сейчас целенаправленно или волей случая оказываются в незнакомых городах и странах. Или планируют переезд, или просто путешествуют. В любом случае, где бы вы ни оказались, всегда хочется выстроить быт, сходить на завтрак или выпить вкусный кофе. Если вам интересно, чем в этих вопросах может помочь наш скрипт и проверить его в действии — продолжайте читать.&lt;/p&gt;
&lt;h2&gt;Что мы придумали?&lt;/h2&gt;
&lt;p&gt;Итак, предположим, что вы оказались в незнакомом городе. У вас есть несколько рекомендаций от друзей или просто несколько проверенных мест, где вам вкусно и красиво.&lt;br /&gt;
Наш алгоритм может быстро увеличить этот список в несколько раз, дополнив его 5-10 рекомендациями того же качества или уровня. Звучит здорово, да?&lt;/p&gt;
&lt;h2&gt;Как мы это реализовали?&lt;/h2&gt;
&lt;p&gt;Мы все еще не умеем колдовать, поэтому решили прибегнуть к более простому способу — написать Python-скрипт для решения этой задачи.&lt;br /&gt;
Начинаем, как всегда, с подготовки. Самая важная шестеренка в нашем скрипте — &lt;a href="https://github.com/adw0rd/instagrapi"&gt;Instagram API&lt;/a&gt; (деятельность социальной сети признана экстремистской и запрещена в Российской Федерации).&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from instagrapi import Client
import time
import pandas as pd&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Затем, подключаемся к API, чтобы приступить к обработке данных.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;cl = Client()
cl.login(&amp;quot;username&amp;quot;, &amp;quot;password&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Наша задача реализуется двумя небольшими скриптами. Первый собирает и обрабатывает геометки из Instagram (деятельность этой социальной сети признана экстремистской и запрещена в Российской Федерации), а также находит людей, которые отмечали эти геометки на фотографиях. Мы знаем, что эти места наверняка нравятся не только нам и предполагаем, что мы нашли тех людей, которые разделяют наши вкусы и ценности. Поэтому, мы собираем все недавние геометки в их профиле и так получаем список рекомендаций. Затем, с помощью второго скрипта мы узнаем точные адреса этих мест в Яндекс.Картах, чтобы определиться с выбором.&lt;/p&gt;
&lt;h2&gt;Сбор и обработка данных&lt;/h2&gt;
&lt;p&gt;Начинаем работу. Для того чтобы получить список рекомендаций нам нужны три подходящих примера, например кофейни Санкт-Петербурга: &lt;a href="https://www.instagram.com/smenacafe/"&gt;Смена&lt;/a&gt;, &lt;a href="https://www.instagram.com/coffee.tchk.spb/"&gt;ТЧК&lt;/a&gt;, &lt;a href="https://www.instagram.com/civil.coffeebar/?hl=ru"&gt;Civil&lt;/a&gt;. Наш скрипт принимает на вход идентификаторы геоточек заведений.&lt;br /&gt;
Как их получить?&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Переходим по ссылке в профиль заведения, например:&lt;br /&gt;
&lt;a href="https://www.instagram.com/smenacafe/"&gt;https://www.instagram.com/smenacafe/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Анализируем геометки в постах (считаем, что официальный профиль содержит правильные геометки)&lt;/li&gt;
&lt;li&gt;Находим ссылку на геометку, например:&lt;br /&gt;
&lt;a href="https://www.instagram.com/explore/locations/727911037416015/smenacafe/"&gt;https://www.instagram.com/explore/locations/727911037416015/smenacafe/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Цифры в ссылке и есть идентификатор геоточки&lt;br /&gt;
&lt;b&gt;727911037416015&lt;/b&gt;&lt;br /&gt;
После того как мы нашли геометки первичных рекомендаций, нам нужно найти тех пользователей, кто ходит в это заведение (= отмечает его на своих фотографиях). Для этого мы берем последние 150 отметок заведения.&lt;br /&gt;
Конечно, это должны быть реальные пользователи, а не бизнес-аккаунты, потому что там могут присутствовать рекламные интеграции, а мы в них не заинтересованы.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;pk_place_ids = [541835746291313, 2103750586526340, 100059475]
 
print('Getting users started')
 
# получаем пользователей, которые отметили заведение на фото
users = []
for i in pk_place_ids:
    # получаем 150 последних постов с выбранной геометкой
    medias = cl.location_medias_recent(i, amount=150)
    
    for m in medias:
        user_id = m.dict()['user']['pk']
        if not user_id in users:
            users.append(user_id)
    
count_users = len(users)
 
print(f'Getting {count_users} users finished')
 
print('Getting not business users started')
 
# отбираем тех пользователей, чьи аккаунты не являются комерческими
users_not_business = []
for u in users:
    try:        
        u_info = cl.user_info(u).dict()
        
        if not u_info['is_business']:
            users_not_business.append(u)
    except:
        next
 
count_nb_users = len(users_not_business)
 
print(f'Getting {count_nb_users} not business users finished')
 
print('Getting location started')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Мы нашли людей, которые отмечали эти места у себя в профиле. Теперь мы собираем все места, которые отмечены в профилях этих людей и выводим эти места списком, сортируя по числу упоминаний.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;# получаем места, которые посетил пользователь
locations = {}
 
end_cursor = None
for u in users_not_business:
    # скрипт работает довольно медленно, поэтому анализируем только 100 последних постов пользователя
    # посты получаем порциями 20 раз по 5 с сохранением курсора
    for page in range(20):
        u_medias, end_cursor = cl.user_medias_paginated(u, 5, end_cursor=end_cursor)
        for m in u_medias:
            # обернул в обработку исключений, т.к. иногда парсер падает
            try:
                # задержка для снижения чатсоты запросов в инстаграм
                time.sleep(1)
                # по идентификатору поста получаем данные поста (важно, что есть имя места, но нет его координат)
                info = cl.media_info(m.dict()['pk']).dict()
                if 'location' in info:               
                    loc_key = info['location']['pk']
                    
                    # вывод имени отмеченного места (для лога)
                    #print(info['location']['name'])
                    
                    # если место встретилось первый раз, то узнаем его координаты
                    if loc_key not in locations:
                        
                        # для того, чтобы узнать координаты, берем последний пост с такой геометкой
                        loc_data = cl.location_medias_recent(loc_key, amount=1)[0].dict()
                        
                        lng=''
                        lat=''
                        
                        if 'location' in loc_data:
                            lng=loc_data['location']['lng']
                            lat=loc_data['location']['lat']
                        
                        locations[info['location']['pk']] = [info['location']['name'],1,lng,lat] 
                    else:
                        locations[info['location']['pk']][1] = locations[info['location']['pk']][1] + 1
                    
                    # сохраняем результат в csv файл
                    ids = [i for i in locations]
                    names = [locations[i][0] for i in locations]
                    vizits = [locations[i][1] for i in locations]
                    lngs = [locations[i][2] for i in locations]
                    lats = [locations[i][3] for i in locations]
 
                    df = pd.DataFrame(
                        {'id': ids,
                        'name': names,
                        'vizit': vizits,     
                        'lng': lngs,
                        'lat': lats     
                        })
 
                    df.sort_values('vizit', ascending=False).to_csv('places.csv', index=False)                  
                    
            except:
                next
                
count_locations = len(locations)
 
print(f'Getting {count_locations} location finished')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Второй скрипт должен находить координаты новых рекомендаций в Яндекс.Картах.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;# получим категории из справочника организаций сервиса Яндекс.Карты
import requests
 
# ключ API можно найти в кабинете разработчика
api_key = &amp;quot;---&amp;quot;
 
addrs = []
urls = []
cat1s = []
cat2s = []
cat3s = []
 
df = pd.read_csv(&amp;quot;places.csv&amp;quot;)
 
for i, row in df.iterrows():
    
    time.sleep(1)
    
    lng = row['lng']
    lat = row['lat']
    name = row['name']
    print(name)
        
    req = f&amp;quot;https://search-maps.yandex.ru/v1/?text={name}&amp;amp;results=1&amp;amp;type=biz&amp;amp;lang=ru_RU&amp;amp;ll={lng},{lat}&amp;amp;spn=0.01,0.01&amp;amp;apikey={api_key}&amp;quot;
 
    response = requests.get(req)
 
    addr = ''
    url = ''
    cat1 = ''
    cat2 = ''
    cat3 = ''
 
    if response.status_code == 200:
        # обернули в обработку исключений, т.к. иногда падает
        try:
            company_data = response.json()['features'][0]['properties']['CompanyMetaData']
                        
            addr = company_data['address']
            url = company_data['url']
            
            count_categories = len(company_data['Categories'])
            
            # у организации может быть до 3 категорий, сохраняем их все
            if count_categories &amp;gt; 0:
                if count_categories == 1:
                    cat1 = company_data['Categories'][0]['name']
                elif count_categories == 2:
                    cat1 = company_data['Categories'][0]['name']
                    cat2 = company_data['Categories'][1]['name']
                elif count_categories == 3:
                    cat1 = company_data['Categories'][0]['name']
                    cat2 = company_data['Categories'][1]['name']
                    cat3 = company_data['Categories'][2]['name']             
            
        except:
            pass
        
    addrs.append(addr)
    urls.append(url)
    cat1s.append(cat1)
    cat2s.append(cat2)
    cat3s.append(cat3)
    
df['address'] = addrs
df['url'] = urls
df['cat1'] = cat1s
df['cat2'] = cat2s
df['cat3'] = cat3s
df.to_csv('places_24.csv', index=False)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Результаты&lt;/h2&gt;
&lt;p&gt;&lt;b&gt;Выпить кофе в Петербурге&lt;/b&gt;&lt;br /&gt;
Мы решили проверить, как работает скрипт на примере трех наших любимых кофеен в Петербурге. Получилось следующее:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2022-03-22--14.28.10.png" width="2210" height="618" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;&lt;b&gt;Поужинать в Петербурге&lt;/b&gt;&lt;br /&gt;
Также мы протестировали наш скрипт, задав три рекомендации классных ресторанов Питера: &lt;a href="https://www.instagram.com/changcafe/"&gt;Chang&lt;/a&gt;, &lt;a href="https://www.instagram.com/cafe_tiger_lily/?hl=ru"&gt;Tiger Lilly&lt;/a&gt;, &lt;a href="https://www.instagram.com/jackandchan/"&gt;Jack and Chan&lt;/a&gt;.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2022-03-22--14.30.03.png" width="1846" height="1020" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Ограничения скрипта&lt;/h2&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Скрипт сбора данных работает недостаточно быстро (примерно 100 геоточек в час).&lt;/li&gt;
&lt;li&gt;У геоточек может быть несколько дублей. Бывают геоточки с одинаковыми названиями, но отличающимися по координатам. Если честно, то по сути в геоточках просто хаос. Поэтому остается много ручной работы на этапе обработки и получения результатов.&lt;/li&gt;
&lt;li&gt;Яндекс отдает данные хорошо: база организаций очень большая, но для большинства объектов заполнена не одна, а целых три категории. У кафе может быть первая категория бар, затем кафе, затем ресторан. В общем, приходится опять же проверять многое вручную.&lt;/li&gt;
&lt;li&gt;Сделать единую процедуру пока не очень получается.&lt;br /&gt;
Пока со скриптом можно работать в два этапа:&lt;br /&gt;
— Первый этап — получение данных из инстаграма (название, частота посещений, координаты) и затем полуавтоматическая чистка от ненужных объектов + объединение дублей (это важно, так как бесплатная квота Яндекс.Карт — 500 запросов в день),&lt;br /&gt;
— Второй этап — получение из Яндекс.Карт категорий, адреса, сайта и затем ручная сверка категорий и выбор наиболее точной категории из трех.&lt;/li&gt;
&lt;/ol&gt;
</description>
<pubDate>Wed, 23 Mar 2022 10:55:34 +0300</pubDate>
</item>

<item>
<title>Решение головоломок Wordle с помощью Basic Python</title>
<guid isPermaLink="false">129</guid>
<link>http://test.leftjoin.ru/all/how-to-solve-wordle/</link>
<comments>http://test.leftjoin.ru/all/how-to-solve-wordle/</comments>
<description>
&lt;p class="note"&gt;Перевод статьи &lt;a href="https://www.inspiredpython.com/article/solving-wordle-puzzles-with-basic-python"&gt;“Solving Wordle Puzzles with Basic Python”&lt;/a&gt; автора &lt;a href="https://www.inspiredpython.com/author/mickey-petersen"&gt;Mickey Petersen&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/2022-01-24-18.26.50.jpg" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Вы наверняка слышали о Wordle? Это словесная головоломка не так проста, как кажется на первый взгляд. Вас просят угадать английское «слово дня», которое состоит из пяти букв. Если вы ошибетесь, вам дадут несколько подсказок: буква в слове будет &lt;i&gt;зеленой&lt;/i&gt;, если вы правильно угадали нужную букву в нужном месте; &lt;i&gt;желтой&lt;/i&gt;, если эта буква присутствует в слове, но не на этом месте; и серой, если буквы вообще нет в слове.&lt;/p&gt;
&lt;p&gt;На самом деле, решать эту головоломку довольно сложно! Вот как вы можете написать Wordle Solver на Python, с использованием множеств, представления списков (list comprehension) и капелькой удачи!&lt;/p&gt;
&lt;h2&gt;Суть головоломки&lt;/h2&gt;
&lt;p&gt;Каждый день Wordle генерирует новое слово, которое нужно угадать. Поскольку у нас есть только шесть попыток — сайт использует файлы cookie для отслеживания вашего прогресса — попытки нужно использовать аккуратно!&lt;/p&gt;
&lt;p&gt;На первый взгляд, есть несколько подсказок, которые упрощают решение:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Слово состоит ровно из пяти букв.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Слово из английского языка и использовать можно только алфавит — никаких знаков препинания, цифр или других символов.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Любая попытка дает подсказки:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Зеленая буква, если буква и её место в слове правильные.&lt;/li&gt;
&lt;li&gt;Желтая буква, если буква присутствует в слове, но было выбрано не то место.&lt;/li&gt;
&lt;li&gt;Серая буква, если буквы вообще нет в слове.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Существует конечное число слов, и их количество дополнительно ограничено словарем, используемым Wordle.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Поскольку я не хочу пытаться извлечь тот же словарь, что использует Wordle (это слишком просто), вместо этого я буду использовать свободно доступный словарь, который устанавливается через Linux в директорию &lt;b&gt;/usr/share/dict/american-english&lt;/b&gt;. Словарь — это текстовый файл с одним словом в каждой строке.&lt;/p&gt;
&lt;h2&gt;Загрузка и генерация слов&lt;/h2&gt;
&lt;p&gt;Для начала нам понадобится словарь — вы можете установить любой удобный вам или использовать уже установленный, если такой есть.&lt;/p&gt;
&lt;p&gt;Далее нам нужно закодировать правила игры:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import string

DICT = &amp;quot;/usr/share/dict/american-english&amp;quot;

ALLOWABLE_CHARACTERS = set(string.ascii_letters)
ALLOWED_ATTEMPTS = 6
WORD_LENGTH = 5&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;У нас есть всего шесть попыток; длина слова равна пяти, и мы можем использовать все доступные буквы английского алфавита.&lt;/p&gt;
&lt;p&gt;Я преобразовываю допустимые символы в Python set(), чтобы использовать функции, которые доступны для работы с множествами, для проверки наличия букв в слове — но об этом чуть позже.&lt;/p&gt;
&lt;p&gt;Теперь я могу сгенерировать множество тех слов, которые соответствуют правилам:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from pathlib import Path

WORDS = {
  word.lower()
  for word in Path(DICT).read_text().splitlines()
  if len(word) == WORD_LENGTH and set(word) &amp;lt; ALLOWABLE_CHARACTERS
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Здесь я использую представление списков (list comprehension) для создания множества допустимых слов. Я использую отличный класс Path для чтения непосредственно из файла. Если вы еще не знакомы с Path, &lt;a href="https://www.inspiredpython.com/article/common-path-patterns"&gt;я рекомендую вам узнать о нем&lt;/a&gt;, поскольку это очень удобный инструмент.&lt;/p&gt;
&lt;p&gt;Так, я фильтрую слова из словаря, которые имеют правильную длину и в которых набор символов в слове является подмножеством ALLOWABLE_CHARACTERS. Другими словами, выбираются только слова, которые составлены из набора допустимых символов.&lt;/p&gt;
&lt;h2&gt;Алфавитно-частотный анализ на английского языка&lt;/h2&gt;
&lt;p&gt;Особенность английского языка заключается в неравномерном распределении букв, используемых в словах. Например, буква &lt;b&gt;E&lt;/b&gt; используется гораздо чаще, чем &lt;b&gt;X&lt;/b&gt;. Поэтому, если мы сможем генерировать слова с наиболее распространенными буквами, у нас больше шансов угадать некоторые или даже все буквы в слове. Таким образом, выигрышная стратегия состоит в том, чтобы придумать систему, которая вычислит наиболее популярные буквы английского языка.&lt;/p&gt;
&lt;p&gt;К счастью, у нас есть словарь английских слов!&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from collections import Counter
from itertools import chain

LETTER_COUNTER = Counter(chain.from_iterable(WORDS))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Класс &lt;i&gt;Counter&lt;/i&gt; — полезное изобретение! Это модифицированный словарь, который считает количество повторений каждого элемента. Когда вы передаете ему список элементов, они становятся ключами, а затем он сохраняет количество появлений каждого ключа в его значение. Это как раз то, что нам нужно, чтобы посчитать популярность каждой буквы среди всех английских слов из 5 букв.&lt;/p&gt;
&lt;p&gt;Для этого я использую малоизвестную функцию chain из модуля &lt;i&gt;itertools&lt;/i&gt;. Эта функция имеет один скрытый метод &lt;i&gt;from_iterable&lt;/i&gt;, который берет один элемент и итерирует его:&lt;/p&gt;
&lt;p&gt;Я думаю, что лучше всего объяснить на конкретном примере:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; list(chain.from_iterable([&amp;quot;inspired&amp;quot;, &amp;quot;python&amp;quot;]))
['i', 'n', 's', 'p', 'i', 'r', 'e', 'd', 'p', 'y', 't', 'h', 'o', 'n']&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Поскольку строки также можно итерировать, а &lt;i&gt;WORDS&lt;/i&gt; — это множество строк, то мы можем разбить это множество (или список и т. д.) на составные элементы. Это очень полезное свойство строк; вы можете прогнать строку через &lt;i&gt;set()&lt;/i&gt; и получить все уникальные символы строки:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; set(&amp;quot;hello&amp;quot;)
{'e', 'h', 'l', 'o'}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;i&gt;Множества созданы по образцу своих математических тезок.&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;Это означает, что множества могут содержать только уникальные значения — без дубликатов — и они неупорядочены. Вот почему порядок в множестве и в строке  получился разным.&lt;/p&gt;
&lt;p&gt;Множества обладают многими полезными функциями, такими как проверка, содержится ли одно множество полностью в другом множестве (подмножестве); получение элементов, содержащихся в обоих множествах (пересечение); совмещение элементов двух множеств (объединение) и так далее.&lt;/p&gt;
&lt;p&gt;Итак, мы посчитали количество букв во всем словаре, и вот что получилось:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; LETTER_COUNTER
Counter({'h': 828,
         'o': 1888,
         'n': 1484,
         'e': 3106,
         's': 2954,
         'v': 338,
         # ...etc...
        })&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Так, мы получили только абсолютное количество букв в словаре. Теперь нужно разделить его на общее количество букв, чтобы перейти к относительным величинам. К счастью, в классе Counter есть удобный метод total, который может посчитать общее количество букв всех слов словаря.&lt;/p&gt;
&lt;p&gt;Затем составляем таблицу частот:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;LETTER_FREQUENCY = {
    character: value / LETTER_COUNTER.total()
    for character, value in LETTER_COUNTER.items()
}&lt;/code&gt;&lt;/pre&gt;&lt;p class="note"&gt;Метод Counter.total() был добавлен в Python 3.10, поэтому, если вы используете более старую версию Python, вы можете заменить его на sum(LETTER_COUNTER.values()), который делает то же самое.&lt;/p&gt;
&lt;p&gt;Здесь я использую представление словарей (&lt;i&gt;dictionary comprehension&lt;/i&gt;) для обработки каждого ключа и значения LETTER_COUNTER (это модифицированный словарь) и деления каждого значения на общее количество символов:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; LETTER_FREQUENCY
{ 'h': 0,02804403048264183,
  'o': 0,06394580863674852,
  'n': 0,050262489415749366,
  'e': 0,10519898391193903,
  's': 0.10005080440304827,
  # ...etc...
  }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И теперь у нас есть идеальный счетчик букв в подмножестве словаря, которые мы считаем существующими словами Wordle. Обратите внимание, что я не делал эти операции для всего словаря — я обработал только те части, которые нам интересны. Маловероятно, что это сильно повлияло бы на ранжирование популярности букв, но в конечном итоге мы руководствуемся именно этим набором слов.&lt;/p&gt;
&lt;p&gt;Теперь нам нужен способ оценки каждого слова, чтобы мы могли предположить, какие слова встретятся с наибольшей вероятностью. Итак, нам нужно взять нашу таблицу частоты и создать функцию подсчета популярности слов, которая оценивает, насколько «популярны» буквы в этом слове:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def calculate_word_commonality(word):
    score = 0.0
    for char in word:
        score += LETTER_FREQUENCY[char]
    return score / (WORD_LENGTH - len(set(word)) + 1)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Снова вспоминаем, что строка является итерируемой, и перебираем каждую букву в слове. Затем получаем популярность каждой буквы и суммируем их. Потом общее количество делится на длину слова минус количество уникальных символов (плюс один, чтобы предотвратить деление на ноль).&lt;/p&gt;
&lt;p&gt;Это не то что бы удивительная функция оценки слов, но она проста и взвешивает слова таким образом, что более уникальным символам придается больший вес. В идеале нам нужно как можно больше уникальных, часто встречающихся символов, чтобы максимизировать вероятность получения зеленых или желтых совпадений в Wordle.&lt;/p&gt;
&lt;p&gt;Быстрый тест подтверждает, что слова с редкими и повторяющимися символами имеют более низкий вес, чем слова с частыми и более уникальными символами.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; calculate_word_commonality(&amp;quot;fuzzy&amp;quot;)
0.04604572396274344&lt;/code&gt;&lt;/pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; calculate_word_commonality(&amp;quot;arose&amp;quot;)
0.42692633361558&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Все, что нам сейчас нужно — придумать способ сортировки и отображения этих слов, чтобы игрок мог выбирать из них:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import operator

def sort_by_word_commonality(words):
    sort_by = operator.itemgetter(1)
    return sorted(
        [(word, calculate_word_commonality(word)) for word in words],
        key=sort_by,
        reverse=True,
    )

def display_word_table(word_commonalities):
    for (word, freq) in word_commonalities:
        print(f&amp;quot;{word:&amp;lt;10} | {freq:&amp;lt;5.2}&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;С помощью sort_by_word_commonality я создаю отсортированный (от большего к меньшему) список кортежей, где каждый кортеж содержит слово и рассчитанный балл для этого слова. Ключ, по которому я сортирую, — это относительная популярность слова в словаре.&lt;/p&gt;
&lt;p&gt;Я не использую лямбду для получения первого элемента; для таких простых вещей я предпочитаю operator.itemgetter, который делает то же самое.&lt;/p&gt;
&lt;p&gt;Также, я добавил функцию быстрого отображения для форматирования слов и их оценки в простую таблицу.&lt;/p&gt;
&lt;p&gt;Теперь поговорим о самом решении головоломки.&lt;/p&gt;
&lt;h2&gt;Решение головоломки Wordle&lt;/h2&gt;
&lt;p&gt;Поскольку я создаю его как простое консольное приложение, я собираюсь использовать input() и print().&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def input_word():
    while True:
        word = input(&amp;quot;Input the word you entered&amp;gt; &amp;quot;)
        if len(word) == WORD_LENGTH and word.lower() in WORDS:
            break
    return word.lower()


def input_response():
    print(&amp;quot;Type the color-coded reply from Wordle:&amp;quot;)
    print(&amp;quot;  G for Green&amp;quot;)
    print(&amp;quot;  Y for Yellow&amp;quot;)
    print(&amp;quot;  ? for Gray&amp;quot;)
    while True:
        response = input(&amp;quot;Response from Wordle&amp;gt; &amp;quot;)
        if len(response) == WORD_LENGTH and set(response) &amp;lt;= {&amp;quot;G&amp;quot;, &amp;quot;Y&amp;quot;, &amp;quot;?&amp;quot;}:
            break
        else:
            print(f&amp;quot;Error - invalid answer {response}&amp;quot;)
    return response&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Функционал приложения очень прост. Нужно узнать у пользователя слово WORD_LENGTH, которое он ввел в игре Wordle, и записать ответ от Wordle. Поскольку есть только три возможных цвета для буквы (зеленый, желтый и серый), ответ закодирован в простую строку из трех символов: G, Y и ?.&lt;/p&gt;
&lt;p&gt;Я также добавил обработку ошибок на тот случай, если пользователь ошибается при вводе данных, повторяя цикл до тех пор, пока не будет задана правильная последовательность. Я делаю это, снова преобразовывая полученную информацию в множество, а затем проверяя, является ли это множество подмножеством допустимых ответов.&lt;/p&gt;
&lt;h2&gt;Фильтрация зеленых, желтых и серых букв с помощью вектора слова&lt;/h2&gt;
&lt;p&gt;Зеленая буква указывает на правильность буквы и ее места в слове. Желтый означает, что место неправильное, но буква в слове присутствует; а серый что буквы нигде нет.&lt;/p&gt;
&lt;p&gt;Другой способ интерпретации этой информации заключается в том, что пока Wordle не сообщит нам, какие буквы зеленые, желтые или серые, существуют все варианты.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;word_vector = [set(string.ascii_lowercase) for _ in range(WORD_LENGTH)]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Я создаю список из пяти множеств, так как нам нужно определить 5 букв слова. Каждый элемент списка — это множество всех строчных английских букв. Проходясь по каждому множеству, я могу удалять буквы, в соответствии с тем, как окрашены буквы после попытки:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Зеленая буква дает нам информацию только по текущему множеству&lt;br /&gt;
Это означает, что если я встречу зеленую букву на втором месте, то я могу изменить второе множество и оставить только эту букву.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Желтые буквы исключают возможность использовать эту букву на этом месте&lt;br /&gt;
Таким образом, все буквы, кроме этой, технически могут оказаться на этом месте. Удаление буквы из набора в этой позиции гарантирует, что мы не сможем выбрать слова, в которых эта буква стоит на этом месте.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Серые буквы подразумевают исключение буквы из всего вектора.&lt;br /&gt;
Следовательно, эта буква должна быть удалена из всех множеств в векторе слова.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Теперь нам нужна функция, которая выбирает слова, которые соответствуют текущему вектору слова. Есть несколько способов сделать это, но я предлагаю вот такой красивый и простой вариант:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def match_word_vector(word, word_vector):
    assert len(word) == len(word_vector)
    for letter, v_letter in zip(word, word_vector):
        if letter not in v_letter:
            return False
    return True&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Этот подход использует метод &lt;b&gt;zip&lt;/b&gt; для попарного сопоставления каждого символа в слове и каждого символа в векторе слова.&lt;/p&gt;
&lt;p&gt;Проходимся по каждому множеству, и, если буквы на определенном месте нет в соответствующем множестве, то цикл прерывается и слово исключается. Если все в порядке и цикл отрабатывает до последней буквы, то мы получаем ответ True и выходим из цикла, отмечая совпадение слова с вектором.&lt;/p&gt;
&lt;h2&gt;Проверяем слова на соответствие&lt;/h2&gt;
&lt;p&gt;Принимая во внимание все правила игры, теперь можем написать функцию поиска, которая фильтрует список слов с учетом ответов, которые мы получили от Wordle.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def match(word_vector, possible_words):
    return [word for word in possible_words if match_word_vector(word, word_vector)]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Эта функция объединяет в себе все правила, которые мы обсудили выше, и ищет с помощью представления списков (list comprehension). Каждое слово проверяется на соответствие с word_vector с помощью match_word_vector.&lt;/p&gt;
&lt;h2&gt;Сортировка результатов&lt;/h2&gt;
&lt;p&gt;Наконец, нам нужно создать небольшой UI, который может многократно запрашивать нужный нам ответ.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def solve():
    possible_words = WORDS.copy()
    word_vector = [set(string.ascii_lowercase) for _ in range(WORD_LENGTH)]
    for attempt in range(1, ALLOWED_ATTEMPTS + 1):
        print(f&amp;quot;Attempt {attempt} with {len(possible_words)} possible words&amp;quot;)
        display_word_table(sort_by_word_commonality(possible_words)[:15])
        word = input_word()
        response = input_response()
        for idx, letter in enumerate(response):
            if letter == &amp;quot;G&amp;quot;:
                word_vector[idx] = {word[idx]}
            elif letter == &amp;quot;Y&amp;quot;:
                try:
                    word_vector[idx].remove(word[idx])
                except KeyError:
                    pass
            elif letter == &amp;quot;?&amp;quot;:
                for vector in word_vector:
                    try:
                        vector.remove(word[idx])
                    except KeyError:
                        pass
        possible_words = match(word_vector, possible_words)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Функция solve() включает в себя некоторые элементы, которые мы уже обсудили. Затем попадаем в цикл от 1 до ALLOWED_ATTEMPTS + 1, и после каждой попытки мы отображаем номер текущей попытки и количество оставшихся возможных слов. Затем мы вызываем функцию display_word_table, которая выводит красивую таблицу из 15 совпадений с наибольшим рейтингом популярности слова. Затем функции нужно узнать слово, которое пользователь в итоге ввел и ответ Wordle.&lt;/p&gt;
&lt;p&gt;После этого мы проходимся по ответу Wordle, отмечая какой букве соответствует ответ (последовательность цветов). Код прост: мы сопоставляем каждый из трех возможных цветов с соответствующим контейнером (зеленый — с word_vector и т. д.) и применяем правила, которые мы обсудили выше.&lt;/p&gt;
&lt;p&gt;Наконец, мы заново берем possible_words и проверяем совпадение с новым вектором слова с помощью match, сокращая множество потенциальных вариантов.&lt;/p&gt;
&lt;h2&gt;Давайте проверим, как это работает&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/wordle.png" width="518" height="476" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Все начинается с запуска функции solve() (для краткости часть вывода опущена):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; Attempt 1 with 5905 possible words
arose      | 0.43
raise      | 0.42

   ... etc ...

Input the word you entered&amp;gt; arose
Type the color-coded reply from Wordle:
  G for Green
  Y for Yellow
  ? for Gray
Response from Wordle&amp;gt; ?Y??Y
Attempt 2 with 829 possible words
liter      | 0.34
liner      | 0.34

   ... etc ...

Input the word you entered&amp;gt; liter
Response from Wordle&amp;gt; ???YY
Attempt 3 with 108 possible words
nerdy      | 0.29
nehru      | 0.28

   ... etc ...

Input the word you entered&amp;gt; nerdy
Response from Wordle&amp;gt; ?YY?G
Attempt 4 with 25 possible words
query      | 0.24
chewy      | 0.21

   ... etc ...

Input the word you entered&amp;gt; query
Response from Wordle&amp;gt; GGGGG
Attempt 5 with 1 possible words
query      | 0.24&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Резюме&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Представления (Comprehensions) — мощный инструмент Python&lt;br /&gt;
Они могут совмещать итерацию с фильтрацией, но если вы злоупотребите этой функцией, добавляя слишком много циклов for или слишком много условных операторов, код может стать нечитаемым. Избегайте сильной вложенности этих операторов, если это возможно.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Множества — полезный тип объекта в Python&lt;br /&gt;
Множества их их верное использование делают код более стабильным, более математически правильным и более кратким. Главное — знать, когда и как их использовать. В нашем решении множества сыграли существенную роль — не пренебрегайте ими!&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Регулярные выражения могут помочь описать все требования к поиску&lt;br /&gt;
Хотя это не было использовано в коде, совпадение (или несовпадение) шаблона и слова — это то, что регулярные выражения делают круче всего. Подумайте, как можно переписать мэтчинг и векторизацию слов с помощью регулярных выражений.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Модули itertools и collections содержат очень полезные инструменты&lt;br /&gt;
Вы можете многого добиться с базовым знанием Python, если знаете, как именно можно использовать встроенные модули. Модуль itertools особенно полезен, если вы вам нужно провернуть итеративные вычисления.&lt;/li&gt;
&lt;/ul&gt;
</description>
<pubDate>Wed, 26 Jan 2022 15:18:49 +0300</pubDate>
</item>

<item>
<title>How to: YouTube API</title>
<guid isPermaLink="false">122</guid>
<link>http://test.leftjoin.ru/all/youtube-api/</link>
<comments>http://test.leftjoin.ru/all/youtube-api/</comments>
<description>
&lt;p&gt;Современным аналитикам необходимо обладать навыком сбора информации из социальных сетей, ведь сейчас контент социальных сетей очень точно отражает реальную ситуацию в мире, помогает быстро распространить новости и позволяет анализировать аудиторию — подписчиков. В предыдущих постах мы уже описывали кейсы с использованием различных API: &lt;a href="http://test.leftjoin.ru/all/get-data-from-vk/"&gt;Vkontakte API&lt;/a&gt;, &lt;a href="http://test.leftjoin.ru/all/collecting-costs-from-facebook-api/"&gt;Facebook API&lt;/a&gt;, &lt;a href="http://test.leftjoin.ru/all/github-api/"&gt;GitHub API&lt;/a&gt;.  Сегодня мы расскажем вам о том, что представляет из себя YouTube API, как получить ключ API, а также наглядно покажем, какую информацию можно собрать с его помощью. В двух словах, с помощью YouTube API можно находить каналы по ключевым словам, выгружать данные канала, а также статистику по видео, опубликованным на этих каналах.&lt;/p&gt;
&lt;h2&gt;Подготовительный этап для работы с YouTube API&lt;/h2&gt;
&lt;p&gt;	Для начала, нужно разобраться в том, как получить доступ к API. Этот процесс подробно изложен на сайте для разработчиков, на который вы можете перейти по ссылке. Если коротко, то необходимо иметь или завести аккаунт Google, войти в профиль для разработчиков, создать проект, получить ключ API и подключить к нему API YouTube Data API v3. Далее, с использованием этого ключа вам будет доступен весь необходимый функционал.&lt;br /&gt;
	После того, как вы успешно получили ключ, можно открывать любой удобный ноутбук (Jupyter Notebook, Collab и т. д.), устанавливать и подключать нужные для работы библиотеки.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;# установка библиотек
	pip install --upgrade google-api-python-client
	pip install --upgrade google-auth-oauthlib google-auth-httplib2
	# импорт необходимых библиотек
import googleapiclient.discovery
import time&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Квоты&lt;/h2&gt;
&lt;p&gt;Один важный момент, который важно знать при использовании Youtube API — это наличие дневных квот на использование функций YouTube API в бесплатном режиме.  На день дается квота 10000 юнитов, вызов функции поиска стоит 100 юнитов, вызов информации по объекту — 1 юнит, загрузка видео на YouTube стоит 1600 юнитов. Если вам недостаточно дневной квоты, то вы можете подать запрос в Google на её увеличение, в котором нужно подробно указать цели вашей деятельности c YouTube API.&lt;/p&gt;
&lt;h2&gt;Поиск YouTube-каналов по ключевым словам&lt;/h2&gt;
&lt;p&gt;Для начала заведем несколько переменных, которые понадобятся нам в процессе сбора информации.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;channels_data = {}
channels_data_full = {}
video_data = {}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Дальше написан скрипт, который можно использовать для поиска перечня каналов по ключевым словам. Мы искали каналы, в названии или описании которых используются следующие слова:  s_query = ’аналитика данных data’. Сначала выводятся каналы, в названии или описании которых присутствуют все три слова, затем хотя бы любые два, затем хотя бы одно. Чем больше ключевых слов по теме мы укажем, тем точнее будет результат.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;api_service_name = &amp;quot;youtube&amp;quot;
api_version = &amp;quot;v3&amp;quot;
DEVELOPER_KEY = &amp;quot;&amp;quot; #тут нужно указать ключ, который вы получите при подключении YouTube API
 
youtube = googleapiclient.discovery.build(
   api_service_name, api_version, developerKey = DEVELOPER_KEY)
#строка поиска
s_query = 'аналитика данных data'
next_token = ''
 
while(True): 
 time.sleep(0.2)
 request = youtube.search().list(
     part=&amp;quot;snippet&amp;quot;,
     q=s_query,
     relevanceLanguage=&amp;quot;ru&amp;quot;,
     type=&amp;quot;channel&amp;quot;,
     maxResults=25,
     access_token=DEVELOPER_KEY,
     pageToken = next_token
 )
 response = request.execute()
 for item in response['items']:
   channels_data[item['snippet']['channelId']] = [item['snippet']['title'], item['snippet']['description']
   ]
 #берем только первые 25 результатов
 break&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Добавим пару важных пояснений относительно скрипта. В начале цикла в этом скрипте (как и в двух последующих) мы вызываем функцию time.sleep(), чтобы инициировать двухсекундную задержку между вызовом функций. Это нужно для того, чтобы запросы к YouTube не были чересчур частыми (и вообще, это считается правилом хорошего тона в программировании, так что советуем взять на заметку).&lt;br /&gt;
Для простоты нашего примера мы сохранили только 25 первых каналов из всех подходящих под условия поиска. Если вам хочется найти все каналы, в которых упоминается хотя бы одно из ключевых слов, то нужно использовать следующее свойство:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;try:
    next_token = response[&amp;quot;nextPageToken&amp;quot;]
  except:
    break&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Сбор полной информации по всем выбранным каналам&lt;/h2&gt;
&lt;p&gt;Теперь, когда названия и описания выбранных каналов собраны, можно переходить к следующему этапу, а именно — выгрузке всей информации об этих каналах, в том числе: количество подписчиков канала, количество видео, общее количество просмотров всех видео канала и страна в которой живет, автор канала.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;scount = ''
for channel in channels_data:
   #получаем данные по каждому каналу
   time.sleep(0.2)
   r = youtube.channels().list(
         part=&amp;quot;snippet, statistics&amp;quot;,
         id=channel,
         access_token=DEVELOPER_KEY
   )
   resp = r.execute()
        
   try:
     if resp['items'][0]['statistics']['hiddenSubscriberCount']:
       scount = 'hidden'
     else:
       scount = resp['items'][0]['statistics']['subscriberCount']
  
     channels_data_full[channel] = [resp['items'][0]['snippet']['title'],
                                  resp['items'][0]['snippet']['description'],
                                  scount,
                                  resp['items'][0]['statistics']['videoCount'],
                                  resp['items'][0]['statistics']['viewCount'],
                                  resp['items'][0]['snippet']['country']
     ]
      
   except:
     pass&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь вся нужная информация о канале хранится в переменнной channels_data_full.&lt;/p&gt;
&lt;h2&gt;Получение информации о видео&lt;/h2&gt;
&lt;p&gt;Если у вас есть необходимость получить статистику по видео из выбранных каналов, то ниже приведен скрипт на этот случай. В итоге, вы получите словарь   video_data  с подробной информацией о каждом видео из плейлиста (список всех видео каждого канала): название канала, дата публикации, название и описание видео, количество просмотров, лайков/дизлайков и комментариев.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;# получаем информацию по всем видео ранее найденных каналов
for channel in channels_data:
   #анализируем каналы
   time.sleep(0.2)
   r = youtube.channels().list(
           part=&amp;quot;contentDetails&amp;quot;,
           id=channel,
           access_token=DEVELOPER_KEY
     )
   resp = r.execute()           
   try:
     #получаем плейлист из видео для одного канала из списка
     id_playlist = resp['items'][0]['contentDetails']['relatedPlaylists']['uploads']     
     #получаем набор элементов плейлиста (видео)
     next_token = ''
     while(True):     
       time.sleep(0.2)
       r = youtube.playlistItems().list(
             part=&amp;quot;contentDetails&amp;quot;,
             playlistId=id_playlist,
             access_token=DEVELOPER_KEY,
             pageToken = next_token
       )
       resp = r.execute()
       for i in resp['items']:
         id_videos = i['contentDetails']['videoId']
         r = youtube.videos().list(
               part=&amp;quot;snippet, statistics&amp;quot;,
               id=id_videos,               
               access_token=DEVELOPER_KEY
         )
         resp1 = r.execute()       
         video_data[id_videos] = [channel,
                                   resp1['items'][0]['snippet']['publishedAt'],
                                   resp1['items'][0]['snippet']['title'],
                                   resp1['items'][0]['snippet']['description'],
                                   resp1['items'][0]['statistics']['viewCount'],
                                   resp1['items'][0]['statistics']['likeCount'],
                                   resp1['items'][0]['statistics']['dislikeCount'],
                                   resp1['items'][0]['statistics']['commentCount']
          ]
       break&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В конце мы ставим break, то есть обрабатываем только одну часть видео из плейлиста. Если вы хотите обработать все видео, то нужно использовать функцию nextpagetoken, которую мы предложили в конце первого скрипта.&lt;br /&gt;
В итоге, если трансформировать словарь в привычный датафрейм, мы получим таблицу, которая содержит подробную информацию про все обработанные видео.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;d = {'id': [x for x in video_data],
      'channel_id': [video_data[x][0] for x in video_data],
       'published_at': [video_data[x][1] for x in video_data],
      'title': [video_data[x][2] for x in video_data],
      'description': [video_data[x][3] for x in video_data],
      'viewCount': [video_data[x][4] for x in video_data],
      'likeCount': [video_data[x][5] for x in video_data],
      'dislikeCount': [video_data[x][6] for x in video_data],
      'commentCount': [video_data[x][7] for x in video_data]
   }
df = pd.DataFrame(d)
df.head()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/dataframe.png.jpg" width="2560" height="289" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Выводы&lt;/h2&gt;
&lt;p&gt;Конечно, это не все способы работы с YouTube API, однако, мы надеемся, что вы получили представление о том, как сильно расширяются возможности аналитика для получения и обработки информации с помощью этого инструмента.&lt;/p&gt;
</description>
<pubDate>Tue, 26 Oct 2021 13:13:34 +0300</pubDate>
</item>

<item>
<title>Как и для чего экспортировать красивые отчеты из Jupyter Notebook в PDF</title>
<guid isPermaLink="false">121</guid>
<link>http://test.leftjoin.ru/all/export-jupyter-notebook-to-pdf/</link>
<comments>http://test.leftjoin.ru/all/export-jupyter-notebook-to-pdf/</comments>
<description>
&lt;p&gt;Если вы специалист по анализу данных и вам нужно представить отчет для заказчика, если вы ищете работу и не знаете, как оформить тестовое задание так, чтобы на вас обратили внимание, если у вас много учебных проектов, связанных с аналитикой и визуализацией данных, то сегодняшний пост будет вам очень и очень полезен. Дело в том, что смотреть на чужой код в Jupyter Notebook бывает проблематично, ведь результат часто теряется между множеством строк кода с подготовкой данных, импортом нужных библиотек и серией попыток реализовать ту или иную идею. Именно поэтому такой метод, как экспорт результатов в PDF-файл в формате LaTeX — это отличный вариант для итоговой визуализации, который сэкономит время и будет выглядеть презентабельно. В научных кругах статьи и отчеты очень часто оформляются именно с использованием LaTeX, поскольку он имеет ряд преимуществ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Математические уравнения и формулы выглядят аккуратнее.&lt;/li&gt;
&lt;li&gt;Библиография создается автоматически, на основе всех использованных в документе ссылок.&lt;/li&gt;
&lt;li&gt;Автор может сосредоточиться на содержании, а не на внешнем виде документа, так как верстка текста и других данных происходит автоматически с помощью указания необходимых параметров в коде.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Сегодня мы подробно расскажем о том, как научиться экспортировать вот такие красивые отчеты из Jupyter Notebook в PDF с использованием LaTeX.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://test.leftjoin.ru/files/sample_LaTeX_report.pdf" style="text-decoration:none; border:0"&gt;&lt;img src="http://test.leftjoin.ru/pictures/sample_latex_report.png" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Установка LaTeX&lt;/h2&gt;
&lt;p&gt;Самый важный момент в формировании отчета из Jupyter Notebook на Python — это его экспорт в финальный файл. Для этого применяется одна библиотека — &lt;a href="https://nbconvert.readthedocs.io/en/latest/install.html"&gt;nbconvert&lt;/a&gt; — которая конвертирует ваш ноутбук в любой удобный формат документа: pdf (как в нашем случае), html, latex или другой. Эту библиотеку нужно не просто установить, а провести некоторую процедуру по предустановке нескольких других пакетов: Pandoc, TeX и Chromium. По ссылке на библиотеку весь процесс описан очень подробно для каждого программного обеспечения, поэтому подробно мы на нем останавливаться не будем.&lt;br /&gt;
Как только вы завершили все предварительные шаги, нужно установить и импортировать библиотеку в ваш Jupyter Notebook.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;!pip install nbconvert
import nbconvert&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Экспорт таблиц в Markdown формат&lt;/h2&gt;
&lt;p&gt;Обычно, таблицы не представляют в отчетах, поскольку их бывает трудно быстро прочесть, но иногда все-таки необходимо добавить небольшую таблицу в итоговый документ. Для того, чтобы таблица выглядела аккуратно, нужно представить ее в Markdown формате. Это можно сделать вручную, но если в таблице много данных, то лучше придумать более удобный метод. Мы предлагаем использовать следующую простую функцию pandas_df_to_markdown_table(), которая преобразует любой датафрейм в markdown-table. Единственный нюанс: после преобразования исчезают строчные индексы, потому, если они важны (как в нашем примере), то стоит записать их в переменную в первой колонке датафрейма.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;data_g = px.data.gapminder()
summary = round(data_g.describe(),2)
summary.insert(0, 'metric', summary.index)

# Функция для преобразования dataframe в Markdown Table
def pandas_df_to_markdown_table(df):
    from IPython.display import Markdown, display
    fmt = ['---' for i in range(len(df.columns))]
    df_fmt = pd.DataFrame([fmt], columns=df.columns)
    df_formatted = pd.concat([df_fmt, df])
    display(Markdown(df_formatted.to_csv(sep=&amp;quot;|&amp;quot;, index=False)))

pandas_df_to_markdown_table(summary)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Экспорт изображения в отчет&lt;/h2&gt;
&lt;p&gt;В этом примере мы будем строить bubble-chart, про методику построения которых рассказывали &lt;a href="http://test.leftjoin.ru/all/principy-postroeniya-bubble-charts-ploschad-vs-radius/"&gt;в недавнем посте&lt;/a&gt;. В прошлый раз мы использовали пакет Seaborn, наглядно показывая, что отображение данных размером кругов на графике происходит корректно. Такие же графики можно построить и при помощи пакета Plotly.&lt;br /&gt;
Для того чтобы отобразить график, построенный в Plotly в отчете тоже нужно немного постараться. Дело в том, что plt.show() не поможет отобразить график при экспорте. Поэтому, нужно сохранить получившийся график в рабочей директории, а затем, используя библиотеку iPython.display, отобразить его с помощью функции Image().&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from IPython.display import Image
import plotly.express as px
fig = px.scatter(data_g.query(&amp;quot;year==2007&amp;quot;), x=&amp;quot;gdpPercap&amp;quot;, y=&amp;quot;lifeExp&amp;quot;,
                 size=&amp;quot;pop&amp;quot;, color=&amp;quot;continent&amp;quot;,
                 log_x=True, size_max=70)
fig.write_image('figure_1.jpg')
Image(data = 'figure_1.jpg', width = 1000)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Формирование и экспорт отчета&lt;/h2&gt;
&lt;p&gt;Когда все этапы анализа данных завершены, отчет можно экспортировать. Если вам нужны заголовки или текст в отчете, то пишите его в ячейках ноутбука, сменив формат Code на Markdown. Для экспорта можно использовать терминал, запуская там вторую строку без восклицательного знака, либо можно запустить код, написанный ниже, в ячейке ноутбука. Мы советуем не загружать отчет кодом, поэтому используем параметр TemplateExporter.exclude_input=True, чтобы ячейки с кодом не экспортировались. Также, при запуске этой ячейки код выдает стандартный поток (standard output) и, чтобы в отчете его не было видно, в начале ячейки нужно написать %%capture.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;%%capture
!jupyter nbconvert --to pdf --TemplateExporter.exclude_input=True ~/Desktop/VALIOTTI/Reports/Sample\LaTeX\ Report.ipynb
!open ~/Desktop/VALIOTTI/Reports/Sample\ LaTeX\ Report.pdf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Если вы все сделали верно и методично, то в итоге получится &lt;a href="http://test.leftjoin.ru/files/sample_LaTeX_report.pdf"&gt;вот такой отчет&lt;/a&gt;! Презентуйте данные красиво :)&lt;/p&gt;
</description>
<pubDate>Fri, 08 Oct 2021 14:56:58 +0300</pubDate>
</item>

<item>
<title>Граф телеграм-каналов по теме аналитики</title>
<guid isPermaLink="false">120</guid>
<link>http://test.leftjoin.ru/all/analytical-telegram-channels-graph/</link>
<comments>http://test.leftjoin.ru/all/analytical-telegram-channels-graph/</comments>
<description>
&lt;p&gt;&lt;a href="http://test.leftjoin.ru/files/analytics-graph.html" style="text-decoration:none; border:0"&gt;&lt;img src="http://test.leftjoin.ru/pictures/graph.png" border="0" width="100%" height="150%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Авторы самых разных блогов в телеграме часто публикуют подборки любимых каналов, которыми они хотят поделиться со своей аудиторией. Идея, конечно, не новая, но я решил не просто составить рейтинг интересных аналитических телеграм-блогов, а решить эту задачу аналитически.&lt;/p&gt;
&lt;p&gt;В рамках текущего курса моей учебы, я изучаю много современных подходов к анализу и визуализации данных. В самом начале курса было разминочное упражнение: объектно-ориентированное программирование на Python для сбора и итеративного построения графа с &lt;a href="https://www.themoviedb.org/documentation/api"&gt;TMDB API&lt;/a&gt;. В задаче этот метод применяется для построения графа связи актеров, где связь — игра в одном и том же фильме. Но я решил, что можно применить его и к другой задаче: построению графа связей аналитического сообщества.&lt;/p&gt;
&lt;p&gt;Поскольку последнее время мой временной ресурс особенно ограничен, а аналогичную задачу для курса я уже выполнил, то я решил передать эти знания кому-то еще, кто интересуется аналитикой. К счастью, в этот момент, ко мне в личку постучался кандидат на вакансию младшего аналитика данных — Андрей. Он сейчас находится в процессе постижения всех тонкостей аналитики, поэтому мы договорились на стажировку, в рамках которой Андрей спарсил данные с telegram-каналов.&lt;/p&gt;
&lt;p&gt;Основной задачей Андрея был сбор всех текстов с телеграм-канала Интернет-аналитика, выделение каналов, на которые ссылался Алексей Никушин, сбор текстов из этих телеграм-каналов и ссылок на этих каналах. Под “ссылкой” подразумевается любое упоминание канала: через @, через ссылку или репостом. В результате парсинга, у Андрея получилось два файла: nodes и edges.&lt;br /&gt;
Теперь я представлю вам &lt;a href="http://test.leftjoin.ru/files/analytics-graph.html"&gt;граф, который получился у меня на основе этих данных&lt;/a&gt; и прокомментирую результаты.&lt;/p&gt;
&lt;p&gt;Пользуясь случаем, хочу выразить мое почтение команде karpov.courses, поскольку у Андрея отличное знание языка Python!&lt;/p&gt;
&lt;p&gt;В результате топ-10 каналов по показателю degree (количество связей) выглядит так:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;a href="https://t.me/internetanalytics"&gt;Интернет-аналитика&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/revealthedata"&gt;Reveal The Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/rockyourdata"&gt;Инжиниринг Данных&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/data_events"&gt;Data Events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/datalytx"&gt;Datalytics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/chartomojka"&gt;Чартомойка&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/leftjoin"&gt;LEFT JOIN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/epicgrowth_chat"&gt;Epic Growth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/rtdlinks"&gt;RTD: ссылки и репосты&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://t.me/dashboardets"&gt;Дашбордец&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;По-моему, получилось супер-круто и визуально интересно, а Андрей — большой молодец! Кстати, он тоже начал свой канал &lt;a href="https://t.me/eto_analytica"&gt;”Это разве аналитика?”&lt;/a&gt;, где публикуются новости аналитики.&lt;/p&gt;
&lt;p&gt;Забегая вперед: у этой задачи имеется продолжение. С помощью Марковской цепи мы смоделировали в каком канале окажется пользователь, если будет переходить итеративно по всем упоминаниям в каналах. Получилось очень интересно, но об этом мы расскажем в следующий раз!&lt;/p&gt;
</description>
<pubDate>Mon, 27 Sep 2021 17:47:31 +0300</pubDate>
</item>

<item>
<title>Принципы построения bubble-charts: площадь VS радиус</title>
<guid isPermaLink="false">119</guid>
<link>http://test.leftjoin.ru/all/principy-postroeniya-bubble-charts-ploschad-vs-radius/</link>
<comments>http://test.leftjoin.ru/all/principy-postroeniya-bubble-charts-ploschad-vs-radius/</comments>
<description>
&lt;p&gt;Такой навык как визуализация данных применяется в любой отрасли, где присутствуют данные, ведь таблицы хороши лишь для хранения информации. Когда есть необходимость презентовать данные, точнее определенные выводы, полученные на их основе — данные необходимо представить на графиках подходящего типа. И тут перед вами встает две задачи: первая — правильно подобрать тип графика, вторая — правдоподобно отразить результаты на диаграмме. Сегодня мы расскажем вам об одной ошибке, которую иногда допускают дизайнеры при визуализации данных на bubble-charts и о том, как эту ошибку можно избежать.&lt;/p&gt;
&lt;h2&gt;Суть построения bubble-чарта&lt;/h2&gt;
&lt;p&gt;Немного скучной теории перед тем, как мы приступим к анализу данных. Bubble-chart — удобный способ показать три параметра наблюдения без построения трехмерной модели. По привычным осям X и Y указываются значения двух параметров, а третий показан размером круга, который соответствует каждому наблюдению. Именно это позволяет избежать необходимости построения сложного 3D графика, то есть любой, кто видит bubble-chart, гораздо быстрее сможет сделать выводы о данных изображенных на одной плоскости.&lt;/p&gt;
&lt;h2&gt;Ошибка, которую может допустить дизайнер, но не аналитик данных&lt;/h2&gt;
&lt;p&gt;С метриками, которые отображены на осях графика не возникает никаких вопросов, это привычный способ их визуализации, а вот с размерами возникает некоторая трудность: как грамотно и точно отобразить изменения в значениях переменной, если управление идет не точкой на оси, а размером этой точки?&lt;br /&gt;
Дело в том, что при построении такого графика без использования аналитических средств, например, в графическом редакторе, автор может нарисовать круги, принимая радиус круга за его размер. На первый взгляд, все кажется абсолютно корректным — чем больше значение переменной, тем больше радиус круга. Однако, в таком случае, площадь круга будет увеличиваться не как линейная, а как степенная функция, ведь S = π × r2. Например, на рисунке ниже показано, что, если увеличить радиус круга в два раза, то площадь увеличится в 4 раза.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Построение круга в Matplotlib&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1)
s = 4*10e3


ax.scatter(100, 100, s=s, c='r')
ax.scatter(100, 100, s=s/4 ,c='b')
ax.scatter(100, 100, s=10, c='g')
plt.axvline(99, c='black')
plt.axvline(101, c='black')
plt.axvline(98, c='black')
plt.axvline(102, c='black')


ax.set_xticks(np.arange(95, 106, 1))
ax.grid(alpha=1)

plt.show()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/example.png" width="720" height="720" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Это значит, что график будет выглядеть неправдоподобно, ведь размеры не будут отражать реальное изменение переменной, а человек обращает внимание и сравнивает именно площадь кругов на графике.&lt;/p&gt;
&lt;h2&gt;Как построить такой график правильно?&lt;/h2&gt;
&lt;p&gt;К счастью, если строить bubble-charts с помощью библиотек Python (Matplotlib и Seaborn), то размер круга будет определяться именно площадью, что абсолютно корректно и грамотно с точки зрения визуализации.&lt;br /&gt;
Сейчас на примере реальных данных, найденных на Kaggle, покажем, как построить bubble-chart правильно. В данных присутствуют следующие переменные: страна, численность населения, процент грамотного населения. Для того чтобы диаграмма была читаемой, возьмем подвыборку из 10 первых стран после сортировки всех данных по возрастанию ВВП.&lt;/p&gt;
&lt;p&gt;Для начала, загрузим все нужные библиотеки:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Затем, загрузим данные, очистим и от всех строк с пропущенными значениями и приведем данные по численности населения стран в миллионы:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;data = pd.read_csv('countries of the world.csv', sep = ',')
data = data.dropna()
data = data.sort_values(by = 'Population', ascending = False)
data = data.head(10)
data['Population'] = data['Population'].apply(lambda x: x/1000000)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь, когда все подготовка завершена, можно построить bubble-chart:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sns.set(style=&amp;quot;darkgrid&amp;quot;)    
fig, ax = plt.subplots(figsize=(10, 10))    
g = sns.scatterplot(data=data, x=&amp;quot;Literacy (%)&amp;quot;, y=&amp;quot;GDP ($ per capita)&amp;quot;, size = &amp;quot;Population&amp;quot;, sizes=(10,1500), alpha=0.5)
plt.xlabel(&amp;quot;Literacy (Percentage of literate citizens)&amp;quot;)
plt.ylabel(&amp;quot;GDP per Capita&amp;quot;)
plt.title('Chart with bubbles as area', fontdict= {'fontsize': 'x-large'})

def label_point(x, y, val, ax):
    a = pd.concat({'x': x, 'y': y, 'val': val}, axis=1)
    for i, point in a.iterrows():
        ax.text(point['x'], point['y']+500, str(point['val']))

label_point(data['Literacy (%)'], data['GDP ($ per capita)'], data['Country'], plt.gca()) 

ax.legend(loc='upper left', fontsize = 'medium', title = 'Population (in mln)', title_fontsize = 'large', labelspacing = 1)

plt.show()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/_32.png" width="720" height="720" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;На этом графике получилось понятным образом отобразить три метрики: уровень ВВП на душу населения по оси Y, процент грамотного населения по оси X и численность населения — площадью круга.&lt;/p&gt;
&lt;p&gt;Мы рекомендуем использовать площадь в качестве переменной, которая отвечает за размер фигуры, если есть необходимость показать несколько переменных на одном графике.&lt;/p&gt;
</description>
<pubDate>Wed, 22 Sep 2021 12:57:54 +0300</pubDate>
</item>

<item>
<title>Обзор дашборда в Dash</title>
<guid isPermaLink="false">114</guid>
<link>http://test.leftjoin.ru/all/dash-bi-guide/</link>
<comments>http://test.leftjoin.ru/all/dash-bi-guide/</comments>
<description>
&lt;p class="note"&gt;&lt;a href="http://test.leftjoin.ru/tags/plotly/"&gt;Посмотрите&lt;/a&gt; и другие наши материалы про plotly&lt;/p&gt;
&lt;p&gt;Сегодня публикуем не совсем классический выпуск обзора BI-инструментов — потому что речь пойдёт о &lt;a href="https://dash.plotly.com/"&gt;Dash&lt;/a&gt;, фреймворке для Python от &lt;a href="https://plotly.com/python/"&gt;plotly&lt;/a&gt;. Dash — гибкий инструмент, который предоставляет набор компонентов для работы с HTML и Bootstrap для создания дашбордов с графиками plotly. Дашборд, созданный при помощи Dash — это веб-страница, написанная на Python. Любую диаграмму можно настроить, изменив передаваемые параметры прямо в коде. А работать с самими данными можно любым удобным в Python способом — например, при помощи датафреймов pandas.&lt;/p&gt;
&lt;p&gt;В новом обзоре посмотрим на работу коллбэков и фильтров в Dash, а также на реализацию таблиц и диаграмм дашборда Superstore в plotly и Dash.&lt;/p&gt;
&lt;div class="e2-text-video"&gt;
&lt;iframe src="https://www.youtube.com/embed/HYDHljU1h7w" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Внутри команды мы оценили дашборд и получили следующие средние оценки (1 — худшая оценка, 10 — лучшая):&lt;br /&gt;
Отвечает ли заданным вопросам — 8,83&lt;br /&gt;
Порог входа в инструмент — 4,83&lt;br /&gt;
Функциональность инструмента — 8,66&lt;br /&gt;
Удобство пользования — 7,83&lt;br /&gt;
Соответствие результата макету — 9,00&lt;br /&gt;
Визуальная составляющая — 8,16&lt;/p&gt;
&lt;p&gt;Итог: дашборд получает 8,05 баллов из 10. &lt;a href="https://superstore-dash.herokuapp.com/"&gt;Посмотрите на полученный результат&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;i&gt;Автор дашборда, член команды Valiotti Analytics — &lt;a href="https://t.me/emazurova"&gt;Елизавета Мазурова&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
</description>
<pubDate>Tue, 03 Aug 2021 13:18:50 +0300</pubDate>
</item>

<item>
<title>Анализ альбомов Земфиры: дашборд в Tableau</title>
<guid isPermaLink="false">112</guid>
<link>http://test.leftjoin.ru/all/zemfira-tableau/</link>
<comments>http://test.leftjoin.ru/all/zemfira-tableau/</comments>
<description>
&lt;p&gt;&lt;a href="http://test.leftjoin.ru/tableau/zemfira.html" style="text-decoration:none; border:0"&gt;&lt;img src="http://test.leftjoin.ru/pictures/zemfira.png.jpg" border="0" width="150%" height="150%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;В марте мы опубликовали исследование &lt;a href="http://test.leftjoin.ru/all/borderline-text-analysis/" class="nu"&gt;«&lt;u&gt;Python и тексты нового альбома Земфиры: анализируем суть песен&lt;/u&gt;»&lt;/a&gt;, в котором при помощи Word2Vec-модели проанализировали близость песен альбома «бордерлайн» и получили самые близкие слова по духу альбома — ими оказались «пламень», «гореть», «тоска», «печаль», «сердце», «солнце» и другие.&lt;/p&gt;
&lt;p&gt;Мы продолжили работу над альбомами Земфиры и проанализировали семь из них, а затем результаты собрали в один дашборд и опубликовали его в &lt;a href="http://test.leftjoin.ru/tableau/zemfira.html"&gt;Tableau Public&lt;/a&gt;. Посмотрите, что получилось.&lt;/p&gt;
&lt;p&gt;Заглавная страница — общий анализ семи альбомов Земфиры. Переключиться на конкретный альбом можно по нажатию на его иконку внизу страницы. Для каждого альбома представлена матрица семантической близости песен, облако слов и топ схожих слов для альбома.&lt;/p&gt;
</description>
<pubDate>Thu, 08 Jul 2021 13:56:12 +0300</pubDate>
</item>

<item>
<title>Анализируем речь в Python: О чем говорят гости youtube-канала вДудь</title>
<guid isPermaLink="false">111</guid>
<link>http://test.leftjoin.ru/</link>
<comments>http://test.leftjoin.ru/</comments>
<description>
&lt;p&gt;Сегодня при помощи ML мы будем анализировать прямую речь. В качестве данных используем интервью, которые журналист Юрий Дудь берет для своего YouTube-канала.&lt;br /&gt;
Выход практически каждого ролика на канале «вДудь» считается событием, а некоторые из этих релизов даже сопровождаются скандалами из-за неосторожных высказываний его гостей.&lt;br /&gt;
Посмотрим с помощью Python и лемматизации о чем таком интересном рассказывали герои роликов канала «вДудь».&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Парсим тексты субтитров&lt;/b&gt;&lt;br /&gt;
В этом проекте мы будем использовать библиотеки, которые обрабатывают тексты, но сначала нам нужно эти тексты добыть. Импортируем API-интерфейс Python &lt;span class="inline-code"&gt;youtube_transcript_api&lt;/span&gt;, который скачивает субтитры из видео на YouTube.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
import numpy as np

from youtube_transcript_api import YouTubeTranscriptApi
import json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Предобработаем URL видео для скачивания субтитров. Всего мы собрали 100 роликов с интервью. В некоторых из интервью нет подготовленных субтитров. В файле &lt;span class="inline-code"&gt;‘dud.csv’&lt;/span&gt; заранее подготовлен список гостей канала вДудь с ссылками на их интервью.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def new_url(s):
    return s.replace('watch?v=','').replace('be.com','.be').replace('www.','')

def url_to_id(s):
    return s.partition('be/')[2]

df = pd.read_csv('dud.csv')
df['URL'] = df['URL'].apply(new_url)
df['video_id'] = df['URL'].apply(url_to_id)
df = df.set_index(keys='Гость')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;У нас теперь есть датафрейм, в котором пока только информация о гостях и ссылка на видео. Но это пока.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-06-01--10.49.04.png" width="638" height="407" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Загрузим в нашу таблицу субтитры интервью. Если субтитры найти не удалось, то выведем на экран имена людей, к интервью с которыми их нет или они отключены (или субтитры есть, но не на русском языке).&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;texts = []
no_sub = []
for speaker in df.index:
    video_id = df.loc[speaker,'video_id']
    try:
        data = YouTubeTranscriptApi.get_transcript(video_id, languages=['ru', 'ru'])
        data = ' '.join([words['text'] for words in data])
    except Exception:
        print('Нет Субтитров для: ', speaker)
        no_sub.append(speaker)
        data = &amp;quot;&amp;quot;
    texts.append(data)
df['text'] = texts
df.to_csv('df_dud.csv')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;У девяти из 100 интервьюируемых субтитров не оказалось и нам вернулся такой текст:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;Нет Субтитров для:  L'one
Нет Субтитров для:  Шнур
Нет Субтитров для:  Ресторатор
Нет Субтитров для:  Амиран
Нет Субтитров для:  Ильич
Нет Субтитров для:  Соболев
Нет Субтитров для:  Иван Дорн
Нет Субтитров для:  Навальный
Нет Субтитров для:  Noize MC&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Анализируем тексты&lt;/b&gt;&lt;br /&gt;
Анализ текстовой информации сложен в той степени, в какой сложен язык, на котором написан текст. Самый популярный способ решения такой аналитической задачи — стемминг. Стеммингом называют процесс нахождения стема — основы слова. Для стемминга используют библиотеку NLTK (Natural Language Toolkit), которая содержит правила образования стемов.&lt;br /&gt;
Этот метод хорошо работает с английскими словами, но у русского языка слишком сложно устроена морфология образования слов, что повышает вероятность ошибки. Стемминг будет хорошим выбором для анализа строк, содержание которых вы примерно представляете себе (например, когда пользователя просят заполнить форму).&lt;br /&gt;
Для нашего кейса лучше выбрать лемматизацию — приведение слова к его словарной форме. Проведя лемматизацию текстовых данных по правилам русского языка мы получим существительные в именительном падеже единственного числа (кошками — кошка), прилагательные в именительном падеже мужского рода (пушистая — пушистый), а глаголы в инфинитиве несовершенного вида (бежит — бежать). В этом проекте мы используем MyStem и Pymorphy. Обе библиотеки представляют собой морфологические анализаторы.&lt;br /&gt;
Кроме того, поскольку при анализе  мы будем использовать алгоритмы машинного обучения, то нам нужно избавиться от слов, которые часто встречаются, но не несут какой-то ценности для анализа. В противном случае они могут повлиять на работу модели. Список таких стоп-слов возьмем из библиотеки &lt;span class="inline-code"&gt;nltk.corpus&lt;/span&gt;.&lt;br /&gt;
Максимально подробно о подготовке текста к анализу мы рассказывали в материале &lt;a href="http://test.leftjoin.ru/all/borderline-text-analysis/" class="nu"&gt;«&lt;u&gt;Python и тексты нового альбома Земфиры&lt;/u&gt;»&lt;/a&gt;. Тут была проведена идентичная работа подготовка текстов, после чего мы посчитали количество уникальных слов (’Unique Words’) и записали, как часто они встречаются в речи собеседников Дудя (‘PPT Unique Words’).&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df['Total Words'] = df['text'].apply(number_words)
df['Unique Words'] = df['text'].apply(set).apply(len)
df['PPT Unique Words'] = df['Unique Words'] / df['Total Words'] * 100
df['PPT Unique Words'] = df['PPT Unique Words'].apply(lambda x: round(x,2))
df.to_csv('df_dud.csv')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Строим облако слов&lt;/b&gt;&lt;br /&gt;
Автоматизируем построение облака слов для каждого гостя Дудя. Таким образом мы узнаем какие слова встречаются в их речи чаще всего. Для визуализации инсталлируем &lt;span class="inline-code"&gt;wordcloud&lt;/span&gt;, а &lt;span class="inline-code"&gt;word_tokenize&lt;/span&gt; подсчитает количество слов, которые будут встречаться чаще всего.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import nltk
from wordcloud import WordCloud
import pandas as pd
import matplotlib.pyplot as plt
from nltk import word_tokenize, ngrams

def word_cloud(df, occup=None, general=True):
    if occup:
        df = df[df['Род деятельности'] == occup] 
    if general:
        data_source = zip([occup], [' '.join([el for el in df['Prepared Text']])])
        col_count, row_count = 1, 1
    else:
        data_source = zip([el for el in df.index], df['Prepared Text']) 
        col_count = max(1, df.shape[0] // 3)
        row_count = df.shape[0] // col_count + 1
        
    fig = plt.figure()
    plt.figure(figsize=(10, 10))
    fig.patch.set_facecolor('white')
    plt.subplots_adjust(wspace=0.3, hspace=0.2)
    i = 1
    for name, text in data_source:
        tokens = word_tokenize(text)
        text_raw = &amp;quot; &amp;quot;.join(tokens)
        wordcloud = WordCloud(colormap='PuBu', background_color='white', contour_width=10).generate(text_raw)
        plt.subplot(row_count, col_count, i, label=name,frame_on=True)
        plt.tick_params(labelsize=10)
        plt.imshow(wordcloud)
        plt.axis(&amp;quot;off&amp;quot;)
        plt.title(name,fontdict={'fontsize':12,'color':'grey'},y=1.0)
        plt.tick_params(labelsize=10)
        i += 1
    plt.savefig(f'./word_cloud/{occup}.png', dpi=900)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;У нас получились вот такие облака слов по каждому из гостей программы:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-06-02--18.58.17.png" width="1198" height="678" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;&lt;b&gt;Работа с Word2vec&lt;/b&gt;&lt;br /&gt;
С помощью библиотеки &lt;span class="inline-code"&gt;gensim&lt;/span&gt; вызываем модуль, который должен представить слова в наших текстах как векторы.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import plotly.graph_objects as go
import plotly.figure_factory as ff
from scipy import spatial
import collections
import pymorphy2
import gensim

morph = pymorphy2.MorphAnalyzer()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Для работы модели используем бинарный файл &lt;span class="inline-code"&gt;‘model.bin’&lt;/span&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;model = gensim.models.KeyedVectors.load_word2vec_format('model.bin', binary=True)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Модель &lt;b&gt;Word2Vec&lt;/b&gt; основана на нейронных сетях и позволяет представлять слова в виде векторов, учитывая семантическую составляющую. Ее мы уже использовали в анализе лирики Земфиры. Косинусная мера семантически схожих слов будет стремиться к 1, а  у двух слов, не имеющих ничего общего по смыслу, она близка к 0.&lt;br /&gt;
Напишем функцию, которая будет принимать список слов из наших интервью, распознавать для каждого часть речи, а затем получать и суммировать вектора — так мы сможем находить вектора не для одного слова, а для целых предложений и текстов.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_vector(word_list):
    vector = 0
    for word in word_list:
        pos = morph.parse(word)[0].tag.POS
        if pos == 'INFN':
            pos = 'VERB'
        if pos in ['ADJF', 'PRCL', 'ADVB', 'NPRO']:
            pos = 'NOUN'
        if word and pos:
            try:
                word_pos = word + '_' + pos
                this_vector = model.word_vec(word_pos)
                vector += this_vector
            except KeyError:
                continue
    return vector&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Для каждого интервью находим вектор и собираем соответствующий столбец в датафрейм:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;vec_list = []
for word in df['Prepared Text']:
    vec_list.append(get_vector(word.split()))
df['Vector'] = vec_list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Напишем функцию, который будет подсчитывать N-граммы для каждого гостя:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_top_five_ngrams(text, n):
    counter = collections.Counter()
    bigrams = list(ngrams(text, n))
    counter.update(bigrams)
    return counter.most_common()[:10]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Построим топ N-грамм в соответствии с группой:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;top_words = dict.fromkeys(df.index)
for person in df.index:
    text = df.loc[person,'Prepared Text']
    n_gram = get_top_five_ngrams(text.split(), 1)
    n_list = []
    for item in n_gram:
        n_list.append(item[0][0])
    top_words[person] = n_list
ordered_pesrons = df.index
top_2_words = []
for person in ordered_pesrons:
    top_2_words.append(top_words[person])
df['Top bigramms'] = top_2_words&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Напишем функцию, которая будет добавлять самые часто встречающиеся слова в речи интервьюируемого:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def top_similar(df, occup=None, agg='Person'):
    if occup:
        df = df[df['Род деятельности'] == occup]
    if agg == 'Person':
        top_words_person = dict.fromkeys(df.index)
        for person in df.index:
            vec = df.loc[person, 'Vector']
            words = model.similar_by_vector(vec, topn=10)
            top_words_person[person] = [el[0].split('_')[0] for el in words]
        df_person_words = pd.DataFrame(columns=[agg,'Top Words'])
    elif agg == 'Total':
        top_words_person = {'Total':0}
        vec = df['Vector'].sum()
        words = model.similar_by_vector(vec, topn=10)
        top_words_person['Total'] = [el[0].split('_')[0] for el in words]
        df_person_words = pd.DataFrame(columns=[agg,'Top Words'])
    
    for k,v in top_words_person.items():
        df_person_words = df_person_words.append({agg:k, 'Top Words':v},ignore_index=True)
    df_person_words = df_person_words.set_index(keys=agg) 
    
    return df_person_words&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Для дальнейшей работы группируем гостей по цеховой принадлежности. Наверное, можно ожидать, что режиссеры будут обсуждать кино и все, что с ним связано, а музыканты — музыку.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df_occup = pd.DataFrame(columns=['Occupation', 'Top Words'])
for occup in df['Род деятельности'].unique():
    words = top_similar(df, occup=occup, agg='Total')['Top Words'][0]
    df_occup = df_occup.append({'Occupation':occup, 'Top Words’:words},ignore_index=True)

for i in range(10):
    df_occup[f'Top {i+1} word'] = df_occup['Top Words'].apply(lambda x: x[i])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;У нас получится новый датафрейм с топом слов для каждой категории гостей (музыкант, политик, актер и тд).&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-06-02--20.55.04.png" width="1104" height="631" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;&lt;b&gt;Анализ риторики гостя&lt;/b&gt;&lt;br /&gt;
Используя метод &lt;span class="inline-code"&gt;similar_by_vector&lt;/span&gt; для каждого из видов деятельности интервьюируемых, мы получаем список слов, которые наиболее точно описывают тематику текстов.&lt;br /&gt;
Стоит отметить, что слово «государство» стоит на первом месте не только в интервью политиков и бизнесменов, но и дизайнеров с писателями. Очевидно, что тема разговора у всех профессиональных групп смещена в сторону политики.&lt;br /&gt;
Актёры, кинокритики и музыканты описываются вполне закономерными для их сфер деятельности словами. А вот у фотографов нет ни слова про фотографию или творчество, но есть «работа», «трудоустройство», «существовать» и “семья”.&lt;br /&gt;
Сравним риторику героев, построив box plot для каждой категории с помощью &lt;span class="inline-code"&gt;plotly&lt;/span&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import plotly.express as px

l = []
for el,ind in zip(df['Род деятельности'].value_counts(), df['Род деятельности'].value_counts().index):
    if el &amp;gt; 1:
        l.append(ind)

df_kpi = df[df['Род деятельности'].isin(l)]
for kpi in ['Total Words', 'Unique Words','PPT Unique Words']:
    buf_df = df_kpi[['Род деятельности',kpi]]
    fig = px.box(df_kpi, 
                 x='Род деятельности',
                 y=kpi,
                )
    fig.show()&lt;/code&gt;&lt;/pre&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Bespalova/3.embed?link=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;Наиболее разговорчивыми гостями оказались блогеры — и в среднем, и по медиане они наговорили больше всего слов. И опередили по этому показателю даже писателей. А вот самыми немногословными оказались рэперы, хотя, казалось бы, вот кто должен быть хорош в импровизации.&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Bespalova/5.embed?link=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;Что касается количества уникальных слов, то и тут блогеры значительно ушли вперед. Согласно медианным значениям, тройка лидеров выглядит так — блогер, журналист и писатель. А вот словарный запас рэперов оставляет желать лучшего.&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Bespalova/1.embed?link=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;Если говорить об отношении уникальных слов к общему количеству, то у всех групп гостей примерно одинаковый медианный показатель. Наиболее вариативными оказались музыканты — усы от их ящика показываю наибольший разброс значений.&lt;/p&gt;
</description>
<pubDate>Mon, 07 Jun 2021 11:26:28 +0300</pubDate>
</item>


</channel>
</rss>