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

<channel>

<title>LEFT JOIN: blog on analytics, visualisation &amp; data science, posts tagged: data analytics</title>
<link>https://en.leftjoin.ru/tags/data-analytics/</link>
<description></description>
<generator>E2 (v3386; Aegea)</generator>

<item>
<title>Graph of telegram channels related to analytics</title>
<guid isPermaLink="false">70</guid>
<link>https://en.leftjoin.ru/all/graph-of-telegram-channels-related-to-analytics/</link>
<comments>https://en.leftjoin.ru/all/graph-of-telegram-channels-related-to-analytics/</comments>
<description>
&lt;p&gt;&lt;a href="https://leftjoin.ru/files/analytics-graph.html" style="text-decoration:none; border-line"&gt; &lt;img src = "https://leftjoin.ru/pictures/graph.png "border =" 0 "width =" 100% "height =" 150% "&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The authors of various Telegram-blogs often publish a selection of their favorite channels so as to share their professional choice with their audience. The idea, of course, is not new, but I decided not just to make a rating of interesting analytical telegram blogs, but to solve this problem analytically.&lt;/p&gt;
&lt;p&gt;As part of the current course of my studies, I am learning many modern approaches to data analysis and visualization. At the very beginning of the course, there was a warm-up exercise: object-oriented programming in Python for collecting and iterative building a graph with &lt;a href="https://www.themoviedb.org/documentation/api"&gt;TMDB API&lt;/a&gt;. Usually, this method is used to construct a connection graph of actors, where the connection is a role in the same film. However, I decided that I could apply it to another problem: building a graph of connections for the analytic community.&lt;/p&gt;
&lt;p&gt;Since my time has been particularly limited recently, and I have already completed a similar task for the course, I decided to transfer this knowledge to someone else who is interested in analytics. Fortunately, at that moment, a candidate for the vacancy of a junior data analyst, Andrey, texted me in direct messages. He is now in the process of comprehending all the intricacies of analytics, so we agreed on an internship, in which Andrey parsed data from telegram channels.&lt;/p&gt;
&lt;p&gt;Andrey’s main task was to collect all texts from the Internet analyst’s telegram channel, select the channels, which Aleksey Nikushin linked, collect texts from these telegram channels and links on these channels. “Link” means any mention of the channel: through @, through a link or repost. As a result of parsing, Andrey got two files: nodes and edges.&lt;br /&gt;
Now I will present to you &lt;a href="https://leftjoin.ru/files/analytics-graph.html"&gt;the graph that I got based on this data&lt;/a&gt; and comment on the results.&lt;/p&gt;
&lt;p&gt;I would like to take this opportunity to express my compliments to the karpov.courses team, as Andrey has excellent knowledge of the Python language!&lt;/p&gt;
&lt;p&gt;As a result, the top 10 channels in terms of degree (number of connections) looks like this:&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;In my opinion, it turned out extremely exciting and visually interesting, and Andrey is a great fellow! By the way, he also started his own channel &lt;a href="https://t.me/eto_analytica"&gt;”Это разве аналитика?”&lt;/a&gt;, where analytics news is published.&lt;/p&gt;
&lt;p&gt;Looking ahead, this problem has a continuation. With the help of the Markov chain, we modeled where the user ends up if he iteratively navigates through all the mentions in the channels. It turned out unexpectedly, but we will tell about it next time!&lt;/p&gt;
</description>
<pubDate>Wed, 20 Oct 2021 14:03:07 +0300</pubDate>
</item>

<item>
<title>How and why should you export reports from Jupyter Notebook to PDF</title>
<guid isPermaLink="false">68</guid>
<link>https://en.leftjoin.ru/all/export-jupyter-notebook-to-pdf/</link>
<comments>https://en.leftjoin.ru/all/export-jupyter-notebook-to-pdf/</comments>
<description>
&lt;p&gt;If you are a data analyst and you need to present a report to a client, if you are looking for a job and do not know how to draw up a test task in such a way that people will pay attention to you, if you have a lot of educational projects related to data analytics and visualization, this post will be very, very useful to you.&lt;br /&gt;
Looking at someone else’s code in a Jupyter Notebook can be problematic, because the result is often lost between lines of code with data preparation, importing the necessary libraries and a series of attempts to implement the idea. That is why a method such as exporting results to a PDF file in LaTeX format is a great option for final visualization. It will save time and look presentable. In scientific circles, articles and reports are very often formatted using LaTeX, since it has a number of advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Math equations and formulas look neater.&lt;/li&gt;
&lt;li&gt;The bibliography is automatically generated based on all references used in the document.&lt;/li&gt;
&lt;li&gt;The author can focus on the content (not on the appearance of the document), since the layout of the text and other data is set automatically by specifying the necessary parameters in the code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Today we will talk in detail about how to export such beautiful reports from Jupyter Notebook to PDF using LaTeX.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://leftjoin.ru/files/sample_LaTeX_report.pdf" style="text-decoration:none; border"&gt; &lt;img src = "https://leftjoin.ru/pictures/sample_latex_report.png "border =" 0 "width =" 150% "height =" 150% "&gt; &lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Installing LaTeX&lt;/h2&gt;
&lt;p&gt;The most important point in generating a report from a Jupyter Notebook in Python is exporting it to the final file. The main library you need to install is – &lt;a href="https://nbconvert.readthedocs.io/en/latest/install.html"&gt;nbconvert&lt;/a&gt; – which converts your notebook into any convenient document format: pdf (as in our case), html, latex, etc. This library needs not only to be installed, but some preinstalling of several other packages as well: Pandoc, TeX, and Chromium. According to the link to the library, the whole process is described in detail for each software, so we will not dwell on it here.&lt;br /&gt;
Once you have completed all the preliminary steps, you need to install and import the library into your 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;Export tables to Markdown format&lt;/h2&gt;
&lt;p&gt;Usually, tables look a bit odd in reports, as they can be difficult to read quickly, but sometimes it is still necessary to add a small table to the final document. In order for the table to look neat, you need to save it in Markdown format. This can be done manually, but if there is a lot of data in the table, it is better to come up with a more convenient method. We suggest using the following simple pandas_df_to_markdown_table () function, which converts any dataframe to a markdown-table. Note: after the conversion, indices disappear, therefore, if they are important (as in our example), it is worth saving them into a variable in the first column of the dataframe.&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)

# Function to convert dataframe to 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;Export image to report&lt;/h2&gt;
&lt;p&gt;In this example, we will build a bubble-chart, the construction method of which was described &lt;a href="https://leftjoin.ru/all/principy-postroeniya-bubble-charts-ploschad-vs-radius/"&gt;in a recent post&lt;/a&gt;. Previously we used the Seaborn library, which shown that the display of data with the size of circles on the graph is correct. The same graphs can be created using the Plotly library.&lt;br /&gt;
In order to display the plot in the final report, you also need to complete an additional step. The point is that plt.show () will not help to display the graph when exporting. Therefore, you need to save the graph in the working directory, and then, using the iPython.display library, display it using the Image () function.&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;Formation and export of the report&lt;/h2&gt;
&lt;p&gt;When all stages of data analysis are completed, the report can be exported. If you need headings or text in the report, then write them in the cells of the notebook, changing the format from Code to Markdown. For export, you can use the terminal, running the second line there without an exclamation mark, or you can run the code written below in the cell of the Jupiter Notebook. We advise you not to load the report with code and use TemplateExporter.exclude_input = True parameter so that the cells with the code are not exported. Also, when you run this cell in your notebook, the code produces a standard output, and you need to write %% capture at the beginning of the cell not to export it.&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;If you did everything correctly and methodically, then you will end up with a report &lt;a href="https://leftjoin.ru/files/sample_LaTeX_report.pdf"&gt;similar to this one&lt;/a&gt;!&lt;br /&gt;
Present your data nicely :)&lt;/p&gt;
</description>
<pubDate>Mon, 11 Oct 2021 15:46:40 +0300</pubDate>
</item>

<item>
<title>Dashboard for the first 8 months of a child’s life</title>
<guid isPermaLink="false">67</guid>
<link>https://en.leftjoin.ru/all/newborn/</link>
<comments>https://en.leftjoin.ru/all/newborn/</comments>
<description>
&lt;p&gt;&lt;a href="https://en.leftjoin.ru/tableau/newborn.html" style="text-decoration:none; border:0"&gt;&lt;img src="https://en.leftjoin.ru/pictures/newborn-1.png-4.jpg" border="0" width="100%" height="150%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In December 2020, I became a dad, which means that our family life with my wife has changed drastically. Of course, I am sharing this news with you for a reason, but in the context of the data that we will study and research today. They are very personal for me, and therefore have some special magic and value. Today I want to show how dramatically the life of a family is changing by the example of my own analysis of the life data of the first 8 months of a baby.&lt;/p&gt;
&lt;h2&gt;Data collection&lt;/h2&gt;
&lt;p&gt;Initial data: tracking the main elements of caring for a baby in the first 8 months: sleep, nursing, changing a diaper. The data was collected using &lt;a href="https://nighp.com/babytracker/"&gt;BabyTracker app&lt;/a&gt;.&lt;br /&gt;
My wife is a great fellow, because during the first 7 months she carefully and regularly monitored all the important points. She forgot to turn off the timer for nursing the baby at night only a couple of times, but I quickly saw noticeable outliers in the data, and the dataset was cleared of them.&lt;br /&gt;
Initially, I had several data visualization ideas in my head, and I tried to immediately implement them into the projected dashboard. I wanted to show the baby’s sleep intervals in the form of a vertical Gantt chart, but the night’s sleep went through the midnight (0:00), and it was completely incomprehensible how this could be corrected in Tableau. After a number of unsuccessful independent attempts to find a solution to this problem, I decided to consult with &lt;a href="https://t.me/revealthedata"&gt;Roman Bunin&lt;/a&gt;. Unfortunately, we came to the conclusion together that there is no way to solve this. Then I had to write a little Python code that splits such time intervals and adds new lines to the dataset.&lt;br /&gt;
However, while we were texting, Roma sent an example identical to my idea! This example claims that a woman collected data on her child’s sleep and wakefulness in the first year of child’s life, and then wrote the code with which it turned out to be embroidered &lt;a href="https://youtu.be/2R3dXARPH10?t=1723"&gt;towel with pattern datavis baby sleep&lt;/a&gt;. For me, this was surprising, since it turned out that this way of visualization is the main method that allows you to show how difficult life and sleep of parents is in the first months of the birth of a child.&lt;br /&gt;
In my &lt;a href="https://en.leftjoin.ru/tableau/newborn.html"&gt;dashboard on Tableau Public&lt;/a&gt; I got three semantic blocks and several “KPIs” about which I would like to tell you in detail and share the basic everyday wisdom. At the top of the dashboard, you can see the key averages of the daytime and nighttime sleep hours, nursing hours, frequency of nursing, and the number of diaper changes in the first three months. I have allocated exactly three months, because I think this is the most difficult period, because significant changes that require serious adaptation are taking place in your life .&lt;/p&gt;
&lt;h2&gt;Dream&lt;/h2&gt;
&lt;p&gt;The left diagram – called “Towel” – illustrates the baby’s sleeping periods. In this diagram, it is important to pay attention to white gaps, especially at night. These are the hours when the baby is awake, which means that the parents are also awake. Look at how the chart changes, especially in the early months, when we gave up the habit of going to bed at 1 or 2 in the morning and fell asleep earlier. Roughly speaking, in the first three months (until March 2021), the child could fall asleep at 2 or 3 in the morning, but we were lucky that our child’s night sleep was quite long.&lt;br /&gt;
The right graph clearly illustrates how the baby’s day and night sleep length changes over time, and the boxplots show the distribution of the hours of daytime and nighttime sleep. The graph confirms the conclusion: “This is temporary and will definitely get better soon!”&lt;/p&gt;
&lt;h2&gt;Nursing&lt;/h2&gt;
&lt;p&gt;From the left diagram, you can see how the number and duration of nursing change. This number is gradually decreasing, and the duration of nursing periods is shortened. Since mid-July, we have changed the way we track nursing periods, so they are not valid for this analysis.&lt;br /&gt;
From my point of view, the findings are a great opportunity for couples planning a pregnancy, not to create illusions about the opportunity to work or do any other business in the first months after giving birth. Pay attention to the frequency and duration of nursing, all this time the parent is completely busy with the child. However, do not be overly alarmed: over time, the number of nursing periods will decrease.&lt;/p&gt;
&lt;h2&gt;Diaper change&lt;/h2&gt;
&lt;p&gt;The left graph is the highlight of this dashboard. As you can imagine, this is a map of the most fun moments – changing a diaper. The stars represent the moments of the day when you need to change the diaper, and the light gray color below shows the number of changes per day. The graph on the right shows diaper changes counted by part of the day. In general, the diagram does not show any interesting dependencies, however, it prepares you for the fact that this process is frequent, regular and happens at any time of the day.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;It seems to me that the use of real personal data and such visualization is sometimes much more revealing than a lot of videos or books about what this period will be like. That is why I decided to share my findings and observations with you here. The main conclusion that I wanted you to draw from the dataviz: children are great! ❤️&lt;/p&gt;
</description>
<pubDate>Wed, 29 Sep 2021 15:52:15 +0300</pubDate>
</item>

<item>
<title>Python and lyrics of Zemfira’s new album: capturing the spirit of her songs</title>
<guid isPermaLink="false">66</guid>
<link>https://en.leftjoin.ru/all/python-and-lyrics-of-zemfiras-new-album-capturing-the-spirit-of/</link>
<comments>https://en.leftjoin.ru/all/python-and-lyrics-of-zemfiras-new-album-capturing-the-spirit-of/</comments>
<description>
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/header.jpg" width="600" height="600" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Zemfira’s latest studio album, Borderline, was released in February, 8 years after the previous one. For this album, various people cooperated with her, including her relatives – the riff for the song “Таблетки” was written by her nephew from London. The album turned out to be diverse: for instance, the song “Остин” is dedicated to the main character of the Homescapes game by the Russian studio Playrix (by the way, check out the latest &lt;a href="https://youtu.be/SOx8afEUTnE"&gt;Business Secrets with the Bukhman brothers&lt;/a&gt;, they also mention it there). Zemfira liked the game a lot, thus, she contacted Playrix to create this song. Also, the song “Крым” was written as a soundtrack to a new film by Zemfira’s colleague Renata Litvinova.&lt;/p&gt;
&lt;p class="note"&gt;Listen new album in &lt;a href="https://music.apple.com/ru/album/бордерлайн/1554865105"&gt;Apple Music&lt;/a&gt; / &lt;a href="https://music.yandex.ru/album/14052981"&gt;Яндекс.Музыке&lt;/a&gt; / &lt;a href="https://open.spotify.com/album/6khBsXmKA1FKjYVCIBy9kt"&gt;Spotify&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Nevertheless, the spirit of the whole album is rather gloomy – the songs often repeat the words ‘боль’, ‘ад’, ‘бесишь’ and other synonyms. We decided to conduct an exploratory analysis of her album, and then, using the Word2Vec model and a cosine measure, look at the semantic closeness of the songs and calculate the general mood of the album.&lt;/p&gt;
&lt;p&gt;For those who are bored with reading about data preparation and analysis steps, you can &lt;a href="https://leftjoin.ru/all/borderline-text-analysis/#result"&gt;go directly to the results&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Data preparation&lt;/h2&gt;
&lt;p&gt;For starters, we write a data processing script. The purpose of the script is to collect a united csv-table from a set of text files, each of which contains a song. At the same time, we have to get rid of all punctuation marks and unnecessary words as we need to focus only on significant content.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
import re
import string
import pymorphy2
from nltk.corpus import stopwords&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we create a morphological analyzer and expand the list of everything that needs to be discarded:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;morph = pymorphy2.MorphAnalyzer()
stopwords_list = stopwords.words('russian')
stopwords_list.extend(['куплет', 'это', 'я', 'мы', 'ты', 'припев', 'аутро', 'предприпев', 'lyrics', '1', '2', '3', 'то'])
string.punctuation += '—'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The names of the songs are given in English, so we have to create a dictionary for translation into Russian and a dictionary, from which we will later make a table:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;result_dict = dict()

songs_dict = {
    'snow':'снег идёт',
    'crimea':'крым',
    'mother':'мама',
    'ostin':'остин',
    'abuse':'абьюз',
    'wait_for_me':'жди меня',
    'tom':'том',
    'come_on':'камон',
    'coat':'пальто',
    'this_summer':'этим летом',
    'ok':'ок',
    'pills':'таблетки'
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let’s define several necessary functions. The first one reads the entire song from the file and removes line breaks, the second clears the text from unnecessary characters and words, and the third one converts the words to normal form, using the pymorphy2 morphological analyzer. The pymorphy2 module does not always handle ambiguity well – additional processing is required for the words ‘ад’ and ‘рай’.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def read_song(filename):
    f = open(f'{filename}.txt', 'r').read()
    f = f.replace('\n', ' ')
    return f

def clean_string(text):
    text = re.split(' |:|\.|\(|\)|,|&amp;quot;|;|/|\n|\t|-|\?|\[|\]|!', text)
    text = ' '.join([word for word in text if word not in string.punctuation])
    text = text.lower()
    text = ' '.join([word for word in text.split() if word not in stopwords_list])
    return text

def string_to_normal_form(string):
    string_lst = string.split()
    for i in range(len(string_lst)):
        string_lst[i] = morph.parse(string_lst[i])[0].normal_form
        if (string_lst[i] == 'аду'):
            string_lst[i] = 'ад'
        if (string_lst[i] == 'рая'):
            string_lst[i] = 'рай'
    string = ' '.join(string_lst)
    return string&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After all this preparation, we can get back to the data and process each song and read the file with the corresponding name:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;name_list = []
text_list = []
for song, name in songs_dict.items():
    text = string_to_normal_form(clean_string(read_song(song)))
    name_list.append(name)
    text_list.append(text)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we combine everything into a DataFrame and save it as a csv-file.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = pd.DataFrame()
df['name'] = name_list
df['text'] = text_list
df['time'] = [290, 220, 187, 270, 330, 196, 207, 188, 269, 189, 245, 244]
df.to_csv('borderline.csv', index=False)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Result:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/2-table.png" width="477" height="365" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Word cloud for the whole album&lt;/h2&gt;
&lt;p&gt;To begin with the analysis, we have to construct a word cloud, because it can display the most common words found in these songs. We import the required libraries, read the csv-file and set the configurations:&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

%matplotlib inline
nltk.download('punkt')
df = pd.read_csv('borderline.csv')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we create a new figure, set the design parameters and, using the word cloud library, display words with the size directly proportional to the frequency of the word. We additionally indicate the name of the song above the corresponding graph.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig = plt.figure()
fig.patch.set_facecolor('white')
plt.subplots_adjust(wspace=0.3, hspace=0.2)
i = 1
for name, text in zip(df.name, df.text):
    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(4, 3, 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':7,'color':'grey'},y=0.93)
    plt.tick_params(labelsize=10)
    i += 1&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/3-wordcloud.jpg" width="2560" height="1707" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;EDA of the lyrics&lt;/h2&gt;
&lt;p&gt;Let us move to the next part and analyze the lyrics. To do this, we have to import special libraries to deal with data and visualization:&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;Firstly, we should count the overall number of words in each song, the number of unique words, and their percentage:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;songs = []
total = []
uniq = []
percent = []

for song, text in zip(df.name, df.text):
    songs.append(song)
    total.append(len(text.split()))
    uniq.append(len(set(text.split())))
    percent.append(round(len(set(text.split())) / len(text.split()), 2) * 100)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All this information should be written in a DataFrame and additionally we want to count the number of words per minute for each song:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df_words = pd.DataFrame()
df_words['song'] = songs
df_words['total words'] = total
df_words['uniq words'] = uniq
df_words['percent'] = percent
df_words['time'] = df['time']
df_words['words per minute'] = round(total / (df['time'] // 60))
df_words = df_words[::-1]&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/4-table.png" width="480" height="369" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;It would be great to visualize the data, so let us build two bar charts: one for the number of words in the song, and the other one for the number of words per minute.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;colors_1 = ['rgba(101,181,205,255)'] * 12
colors_2 = ['rgba(62,142,231,255)'] * 12

fig = go.Figure(data=[
    go.Bar(name='📝 Total number of words,
           text=df_words['total words'],
           textposition='auto',
           x=df_words.song,
           y=df_words['total words'],
           marker_color=colors_1,
           marker=dict(line=dict(width=0)),),
    go.Bar(name='🌀 Unique words',
           text=df_words['uniq words'].astype(str) + '&amp;lt;br&amp;gt;'+ df_words.percent.astype(int).astype(str) + '%' ,
           textposition='inside',
           x=df_words.song,
           y=df_words['uniq words'],
           textfont_color='white',
           marker_color=colors_2,
           marker=dict(line=dict(width=0)),),
])

fig.update_layout(barmode='group')

fig.update_layout(
    title = 
        {'text':'&amp;lt;b&amp;gt;The ratio of the number of unique words to the total&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&amp;lt;span style=&amp;quot;color:#666666&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;'},
    showlegend = True,
    height=650,
    font={
        'family':'Open Sans, light',
        'color':'black',
        'size':14
    },
    plot_bgcolor='rgba(0,0,0,0)',
)
fig.update_layout(legend=dict(
    yanchor=&amp;quot;top&amp;quot;,
    xanchor=&amp;quot;right&amp;quot;,
))

fig.show()&lt;/code&gt;&lt;/pre&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/96.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;colors_1 = ['rgba(101,181,205,255)'] * 12
colors_2 = ['rgba(238,85,59,255)'] * 12

fig = go.Figure(data=[
    go.Bar(name='⏱️ Track length, min.',
           text=round(df_words['time'] / 60, 1),
           textposition='auto',
           x=df_words.song,
           y=-df_words['time'] // 60,
           marker_color=colors_1,
           marker=dict(line=dict(width=0)),
          ),
    go.Bar(name='🔄 Words per minute',
           text=df_words['words per minute'],
           textposition='auto',
           x=df_words.song,
           y=df_words['words per minute'],
           marker_color=colors_2,
           textfont_color='white',
           marker=dict(line=dict(width=0)),
          ),
])

fig.update_layout(barmode='overlay')

fig.update_layout(
    title = 
        {'text':'&amp;lt;b&amp;gt;Track length and words per minute&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&amp;lt;span style=&amp;quot;color:#666666&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;'},
    showlegend = True,
    height=650,
    font={
        'family':'Open Sans, light',
        'color':'black',
        'size':14
    },
    plot_bgcolor='rgba(0,0,0,0)'
)


fig.show()&lt;/code&gt;&lt;/pre&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/98.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;h2&gt;Working with Word2Vec model&lt;/h2&gt;
&lt;p&gt;Using the gensim module, load the model pointing to a binary file:&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 class="note"&gt;Для материала мы использовали готовую обученную на Национальном Корпусе Русского Языка модель от сообщества &lt;a href="https://rusvectores.org/ru/models/"&gt;RusVectōrēs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Word2Vec model is based on neural networks and allows you to represent words in the form of vectors, taking into account the semantic component. It means that if we take two words – for instance, “mom” and “dad”, then represent them as two vectors and calculate the cosine, the values ​​will be close to 1. Similarly, two words that have nothing in common in their meaning have a cosine measure close to 0.&lt;/p&gt;
&lt;p&gt;Now we will define the get_vector function: it will take a list of words, recognize a part of speech for each word, and then receive and summarize vectors, so that we can find vectors even for whole sentences and texts.&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;For each song, find a vector and select the corresponding column in the DataFrame:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;vec_list = []
for word in df['text']:
    vec_list.append(get_vector(word.split()))
df['vector'] = vec_list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, now we should compare these vectors with one another, calculating their cosine proximity. Those songs with a cosine metric higher than 0.5 will be saved separately – this way we will get the closest pairs of songs. We will write the information about the comparison of vectors into the two-dimensional list result.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;similar = dict()
result = []
for song_1, vector_1 in zip(df.name, df.vector):
    sub_list = []
    for song_2, vector_2 in zip(df.name.iloc[::-1], df.vector.iloc[::-1]):
        res = 1 - spatial.distance.cosine(vector_1, vector_2)
        if res &amp;gt; 0.5 and song_1 != song_2 and (song_1 + ' / ' + song_2 not in similar.keys() and song_2 + ' / ' + song_1 not in similar.keys()):
            similar[song_1 + ' / ' + song_2] = round(res, 2)
        sub_list.append(round(res, 2))
    result.append(sub_list)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Moreover, we can construct the same bar chart:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df_top_sim = pd.DataFrame()
df_top_sim['name'] = list(similar.keys())
df_top_sim['value'] = list(similar.values())
df_top_sim.sort_values(by='value', ascending=False)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И построим такой же bar chart:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;colors = ['rgba(101,181,205,255)'] * 5

fig = go.Figure([go.Bar(x=df_top_sim['name'],
                        y=df_top_sim['value'],
                        marker_color=colors,
                        width=[0.4,0.4,0.4,0.4,0.4],
                        text=df_top_sim['value'],
                        textfont_color='white',
                        textposition='auto')])

fig.update_layout(
    title = 
        {'text':'&amp;lt;b&amp;gt;Топ-5 closest songs&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&amp;lt;span style=&amp;quot;color:#666666&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;'},
    showlegend = False,
    height=650,
    font={
        'family':'Open Sans, light',
        'color':'black',
        'size':14
    },
    plot_bgcolor='rgba(0,0,0,0)',
    xaxis={'categoryorder':'total descending'}
)

fig.show()&lt;/code&gt;&lt;/pre&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/100.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;Given the vector of each song, let us calculate the vector of the entire album – add the vectors of the songs. Then, for such a vector, using the model, we get the words that are the closest in spirit and meaning.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_word_from_tlist(lst):
    for word in lst:
        word = word[0].split('_')[0]
        print(word, end=' ')

vec_sum = 0
for vec in df.vector:
    vec_sum += vec
sim_word = model.similar_by_vector(vec_sum)
get_word_from_tlist(sim_word)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="color: '#65b5cd'; font-size: 1.2em"&gt;&lt;b&gt;небо тоска тьма пламень плакать горе печаль сердце солнце мрак&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;This is probably the key result and the description of Zemfira’s album in just 10 words.&lt;/p&gt;
&lt;p&gt;Finally, we build a general heat map, each cell of which is the result of comparing the texts of two tracks with a cosine measure.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;colorscale=[[0.0, &amp;quot;rgba(255,255,255,255)&amp;quot;],
            [0.1, &amp;quot;rgba(229,232,237,255)&amp;quot;],
            [0.2, &amp;quot;rgba(216,222,232,255)&amp;quot;],
            [0.3, &amp;quot;rgba(205,214,228,255)&amp;quot;],
            [0.4, &amp;quot;rgba(182,195,218,255)&amp;quot;],
            [0.5, &amp;quot;rgba(159,178,209,255)&amp;quot;],
            [0.6, &amp;quot;rgba(137,161,200,255)&amp;quot;],
            [0.7, &amp;quot;rgba(107,137,188,255)&amp;quot;],
            [0.8, &amp;quot;rgba(96,129,184,255)&amp;quot;],
            [1.0, &amp;quot;rgba(76,114,176,255)&amp;quot;]]

font_colors = ['black']
x = list(df.name.iloc[::-1])
y = list(df.name)
fig = ff.create_annotated_heatmap(result, x=x, y=y, colorscale=colorscale, font_colors=font_colors)
fig.show()&lt;/code&gt;&lt;/pre&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/82.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;h2&gt;&lt;a name="result"&gt;Results and data interpretation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To give valuable conclusions, we would like to take another look at everything we got. First of all, let us focus on the word cloud. It is easy to see that the words ‘боль’, ‘невозможно’, ‘сорваться’, ‘растерзаны’, ‘сложно’, ‘терпеть’, ‘любить’ have a very decent size, because such words are often found throughout the entire lyrics:&lt;/p&gt;
&lt;p&gt;Давайте ещё раз посмотрим на всё, что у нас получилось — начнём с облака слов. Нетрудно заметить, что у слов «боль», «невозможно», «сорваться», «растерзаны», «сложно», «терпеть», «любить» размер весьма приличный — всё потому, что такие слова встречаются часто на протяжении всего текста песен:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/9-wordcloud.jpg" width="2560" height="1707" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;The song “Крым” turned out to be one of the most diverse songs – it contains 74% of unique words. Also, the song “Снег идет” contains very few words, so the majority, which is 82%, are unique. The largest song on the album in terms of amount of words is the track “Таблетки” – there are about 150 words in total.&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/96.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;As it was shown on the last chart, the most dynamic track is “Таблетки”, as much as 37 words per minute – nearly one word for every two seconds – and the longest track is “Абьюз”, and according to the previous chart, it also has the lowest percentage of unique words – 46%.&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/98.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;Top 5 most semantically similar text pairs:&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/100.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;We also got the vector of the entire album and found the closest words. Just take a look at them – ‘тьма’, ‘тоска’, ‘плакать’, ‘горе’, ‘печаль’, ‘сердце’ – this is the list of words that characterizes Zemfira’s lyrics!&lt;/p&gt;
&lt;p&gt;&lt;span style="color: '#65b5cd'; font-size: 1.2em"&gt;&lt;b&gt;небо тоска тьма пламень плакать горе печаль сердце солнце мрак&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The final result is a heat map. From the visualization, it is noticeable that almost all songs are quite similar to each other – the cosine measure for many pairs exceeds the value of 0.4.&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~Elisejj/82.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;In the material, we carried out an EDA of the entire text of the new album and, using the pre-trained Word2Vec model, we proved the hypothesis – most of the “Borderline” songs are permeated with rather dark lyrics. However, this is normal, because we love Zemfira precisely for her sincerity and straightforwardness.&lt;/p&gt;
</description>
<pubDate>Tue, 07 Sep 2021 13:46:21 +0300</pubDate>
</item>

<item>
<title>Building frequency counts and bigrams using the posts of traders</title>
<guid isPermaLink="false">57</guid>
<link>https://en.leftjoin.ru/all/building-frequency-counts-and-bigrams-using-the-posts-of-traders/</link>
<comments>https://en.leftjoin.ru/all/building-frequency-counts-and-bigrams-using-the-posts-of-traders/</comments>
<description>
&lt;p&gt;Stocktwits is the largest social network for investors and traders of all levels which allows us to see what is happening in the financial markets. Today we will build a frequency dictionary and bigrams of the users’ posts and divide them by the number of followers. This will allow us to see the difference between the posts of different types of traders.&lt;/p&gt;
&lt;p&gt;This is how the feed on the CCIV security looks at Stocktwits:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/--2021-04-27-155729.png" width="735" height="679" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Some users have the status of officials:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/--2021-04-27-160235.png" width="745" height="432" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Scraping the posts&lt;/h2&gt;
&lt;p&gt;Stocktwits has an API that allows getting 30 posts at a time. The API request returns a JSON file, so we will write a get_30_messages function that reads the JSON file and writes all the entries into the list called rows. The information about posts already contains the information about users, so we will not create separate tables and will save everything in one DataFrame. For this purpose, we will create a list with the names of columns and initiate an empty list called rows where we will append all the scraped posts.&lt;/p&gt;
&lt;p&gt;Some posts don’t have a “likes” key in the JSON file which results in KeyError. To avoid the error, we will assign 0 to the “likes” in such posts.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;cols = ['post_id', 'text', 'created_at', 'user_id', 'likes', 'sentiment', 'identity','followers', 'following', 'ideas', 'watchlist_stocks_count', 'like_count', 'plus_tier']
rows = []
 
def get_30_messages(data):
    for p in data['messages']:
        try:
            likes = p['likes']['total']
        except KeyError:
            likes = 0
        rows.append({'id': p['id'], 
                    'text': p['body'], 
                    'created_at': p['created_at'], 
                    'user_id': p['user']['id'], 
                    'likes': likes,
                    'sentiment': p['entities']['sentiment'], 
                    'symbol': symbol,
                    'identity': p['user']['identity'],
                    'followers': p['user']['followers'], 
                    'following': p['user']['following'], 
                    'ideas': p['user']['ideas'], 
                    'watchlist_stocks_count': p['user']['watchlist_stocks_count'], 
                    'like_count': p['user']['like_count'], 
                    'plus_tier': p['user']['like_count']
                    })&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We will scrap the posts from the pages of 16 most trending securities.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;symbols = ['DIA', 'SPY', 'QQQ', 'INO', 'OCGN', 'BTC.X', 'SNAP', 'INTC', 'VXX', 'ASTS', 'SKLZ', 'RIOT', 'DJIA', 'GOLD', 'GGII', 'COIN']&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As the API request returns only 30 most recent posts, to get older posts, we need to save the id of the last post into a dictionary and insert it as the max parameter during the next request. Unfortunately, the API allows us to make only 200 requests per hour, so in order to stay within the limits, we will run the for loop for each security only 11 times.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;last_id_values = dict()
        
for symbol in symbols:
    file = requests.get(f&amp;quot;https://api.stocktwits.com/api/2/streams/symbol/{symbol}.json&amp;quot;)
    data = json.loads(file.content)
    
    for i in range(10):
        get_30_messages(data)
            
        last_id = data['cursor']['max']
        last_id_values[symbol] = last_id
        
        file = requests.get(f&amp;quot;https://api.stocktwits.com/api/2/streams/symbol/{symbol}.json?max={last_id}&amp;quot;)
        data = json.loads(file.content)
    
    get_30_messages(data)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Thus, we have collected only about 6000 posts, which is not enough for the analysis. That’s why, we will create a timer to run the same code after 1 hour and 5 minutes for 11 cycles.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_older_posts():
    for symbol in symbols:
        for i in range(12):
            file = requests.get(f&amp;quot;https://api.stocktwits.com/api/2/streams/symbol/{symbol}.json?max={last_id_values[symbol]}&amp;quot;)
            data = json.loads(file.content)        
            get_30_messages(data)
 
            last_id = data['cursor']['max']
            last_id_values[symbol] = last_id
 
for i in range(11):
    time.sleep(3900)
    get_older_posts()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After all the data is collected, let’s create a DataFrame.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = pd.DataFrame(rows, columns = cols)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The resulting table will look like this:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/--2021-04-27-183708.png" width="1452" height="379" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;It is important to check that the post_id doesn’t have duplicate values. By looking at the number of unique values and the number of total values in posts_id we can notice that we have about 10000 duplicate values.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df.posts_id.nunique(), len(df.posts_id)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This happened because some posts get posted on multiple pages. So the last step will be dropping the duplicate values.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df.drop_duplicates(subset=&amp;quot;posts_id&amp;quot;, inplace=True)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Frequency counts and bigrams&lt;/h2&gt;
&lt;p&gt;First of all, let’s create a frequency count for posts without dividing them into groups.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df.text.str.split(expand=True).stack().value_counts()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can see that articles, conjunctions, and prepositions prevail over the other words:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/--2021-04-27-174112.png" width="373" height="307" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Thus, we need to remove them from the dataset. However, even if the dataset is cleaned, the results will look like this. Apart from the fact that 39 is the most frequent word, the data is not very informative and it’s difficult to make any conclusions based on it.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/--2021-04-27-174622.png" width="250" height="141" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;In this case, we will need to build bigrams. One bigram is a sequence of two elements, that is two words standing next to each other. There are many algorithms for building n-grams with different optimization levels. We will use a built-in function in nltk to create a bigram for one group. First, let’s import the additional libraries, download stop words for the English language, and clean the data. Then we will add more stop words including the names of the stock tickers that are used in every post.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import nltk
from nltk.corpus import stopwords
from string import punctuation
import unicodedata
import collections
import nltk
from nltk.stem import WordNetLemmatizer
 
nltk.download('stopwords')
nltk.download('punkt')
nltk.download('wordnet')

english_stopwords = stopwords.words(&amp;quot;english&amp;quot;)
symbols = ['DIA', 'SPY', 'QQQ', 'INO', 'OCGN', 'BTC.X', 'SNAP', 'INTC', 'VXX', 'ASTS', 'SKLZ', 'RIOT', 'DJIA', 'GOLD', 'GGII', 'COIN']
symbols_lower = [sym.lower() for sym in symbols]
append_stopword = ['https', 'www', 'btc', 'x', 's', 't', 'p', 'amp', 'utm', 'm', 'gon', 'na', '’', '2021', '04', 'stocktwits', 'com', 'spx', 'ndx', 'gld', 'slv', 'es', 'f', '...', '--', 'cqginc', 'cqgthom', 'gt']
english_stopwords.extend(symbols_lower)
english_stopwords.extend(append_stopword)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let’s define a function to prepare the text that will translate all the words to lowercase, bring them to the base form and remove stop words and punctuation.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;wordnet_lemmatizer = WordNetLemmatizer()
 
def preprocess_text(text):
    tokens = nltk.word_tokenize(text.lower())
    tokens = [wordnet_lemmatizer.lemmatize(token) for token in tokens if token not in english_stopwords\
              and token != &amp;quot; &amp;quot; \
              and token.strip() not in punctuation]
    
    text = &amp;quot; &amp;quot;.join(tokens)
    
    return text
    
    df.text = df.text.apply(process_text)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For example, let’s take the group of the least popular users with less than 300 followers, build bigrams and output the most frequent ones.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;non_pop_df = df[(df['followers'] &amp;lt; 300)]
 
non_pop_counts = collections.Counter()
for sent in non_pop_df.text:
    words = nltk.word_tokenize(sent)
    non_pop_counts.update(nltk.bigrams(words))
non_pop_counts.most_common()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/--2021-04-27-184020.png" width="411" height="405" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Results of the bigrams study&lt;/h2&gt;
&lt;p&gt;Users with less than 300 followers mostly write about their personal plans on making money. This is shown by the collocations like short term, long term, and make money.&lt;br /&gt;
Less than 300 followers:&lt;br /&gt;
1.	look like, 439&lt;br /&gt;
2.	next week, 422&lt;br /&gt;
3.	let 39, 364&lt;br /&gt;
4.	capital gain, 306&lt;br /&gt;
5.	long term, 274&lt;br /&gt;
6.	let go, 261&lt;br /&gt;
7.	stock market, 252&lt;br /&gt;
8.	buy dip, 252&lt;br /&gt;
9.	gain tax, 221&lt;br /&gt;
10.	make money, 203&lt;br /&gt;
11.	short term, 201&lt;br /&gt;
12.	buy buy, 192&lt;/p&gt;
&lt;p&gt;More popular users with 300 to 3000 followers discuss more abstract issues like sweep premium, stock price and artificial intelligence.&lt;br /&gt;
From 300 to 3000 followers:&lt;br /&gt;
1.	sweep premium, 166&lt;br /&gt;
2.	price target, 165&lt;br /&gt;
3.	total day, 140&lt;br /&gt;
4.	stock market, 139&lt;br /&gt;
5.	ask premium, 132&lt;br /&gt;
6.	stock price, 129&lt;br /&gt;
7.	current stock, 117&lt;br /&gt;
8.	money trade, 114&lt;br /&gt;
9.	trade option, 114&lt;br /&gt;
10.	activity alert, 113&lt;br /&gt;
11.	trade volume, 113&lt;br /&gt;
12.	artificial intelligence, 113&lt;/p&gt;
&lt;p&gt;Popular users that have below 30000 followers discuss their observations as well as promote their accounts or articles.&lt;br /&gt;
From 3000 to 30000 followers:&lt;br /&gt;
1.	unusual option, 632&lt;br /&gt;
2.	print size, 613&lt;br /&gt;
3.	option activity, 563&lt;br /&gt;
4.	large print, 559&lt;br /&gt;
5.	activity alerted, 355&lt;br /&gt;
6.	observed unusual, 347&lt;br /&gt;
7.	sweepcast observed, 343&lt;br /&gt;
8.	|🎯 see, 311&lt;br /&gt;
9.	see profile, 253&lt;br /&gt;
10.	profile link, 241&lt;br /&gt;
11.	call expiring, 235&lt;br /&gt;
12.	new article, 226&lt;/p&gt;
&lt;p&gt;Very popular traders with more than 30000 followers mostly act as information sources and post about changes at the stock market. This is indicated by the frequent up and down arrows and collocations like “stock x-day” or “moving average”.&lt;br /&gt;
Users with more than 30000 followers:&lt;br /&gt;
1.	dow stock, 69&lt;br /&gt;
2.	elliottwave trading, 53&lt;br /&gt;
3.	⇩ indexindicators.com, 51&lt;br /&gt;
4.	⇧ indexindicators.com, 50&lt;br /&gt;
5.	u stock, 47&lt;br /&gt;
6.	stock 5-day, 36&lt;br /&gt;
7.	moving average, 29&lt;br /&gt;
8.	stock moving, 28&lt;br /&gt;
9.	stock x-day, 27&lt;br /&gt;
10.	⇧ 10-day, 26&lt;br /&gt;
11.	stock daily, 25&lt;br /&gt;
12.	daily rsi, 25&lt;/p&gt;
&lt;p&gt;We have also built the bigrams of officials, but the results turned out to be very similar to the most popular users.&lt;/p&gt;
</description>
<pubDate>Wed, 28 Apr 2021 15:48:22 +0300</pubDate>
</item>

<item>
<title>How to build Animated Charts like Hans Rosling in Plotly</title>
<guid isPermaLink="false">53</guid>
<link>https://en.leftjoin.ru/all/how-to-build-animated-charts-like-hans-rosling-in-plotly/</link>
<comments>https://en.leftjoin.ru/all/how-to-build-animated-charts-like-hans-rosling-in-plotly/</comments>
<description>
&lt;p&gt;Hans Rosling’s work on world countries economic growth presented in 2007 at TEDTalks can be attributed to one of the most iconic data visualizations, ever created. Just check out this video, in case you don’t know what we’re talking about:&lt;/p&gt;
&lt;div class="e2-text-video"&gt;
&lt;iframe src="https://www.youtube.com/embed/hVimVzgtD6w" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Sometimes we want to compare standards of living in other countries. One way to do this is to refer to the Big Mac index, which the Economist magazine has kept track of since 1986. The key idea this index represents is to measure purchasing power parity (PPP) in different countries, considering costs of domestic production. To make a standard burger, one would need the following ingredients: cheese, meat, bread and vegetables. Considering that all these ingredients can be produced locally, we can compare the production cost of one Big Mac in different countries, and measure purchasing power. Plus, McDonald’s is the world’s most popular franchise network,  its restaurants are almost everywhere around the globe.&lt;/p&gt;
&lt;p&gt;In today’s material, we will build a Motion Chart for the Big Mac index using Plotly. Following Hann Rosling’s idea, the chart will display country population along the X-axis and GDP per capita in US dollars along the Y. The size of the dots is going to be proportional to the Big Mac Index for a given country. And the color of the dots will represent the continent where the country is located.&lt;/p&gt;
&lt;h2&gt;Preparing Data&lt;/h2&gt;
&lt;p&gt;Even though The Economist has been updating it for over 30 years and sharing its observations publicly, the dataset contains many missing values. It also lacks continents names, but we can handle it by supplementing the data with some more datasets that can be found in our repo.&lt;/p&gt;
&lt;p&gt;Let’s start by importing the libraries:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
from pandas.errors import ParserError
import plotly.graph_objects as go
import numpy as np
import requests
import io&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can access the dataset directly from GitHub. Just use the following function to send a GET request to a CSV file and create a Pandas DataFrame. However, in some cases, this may raise a  ParseError because of the caption title, so we will add a try block:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def read_raw_file(link):
    raw_csv = requests.get(link).content
    try:
        df = pd.read_csv(io.StringIO(raw_csv.decode('utf-8')))
    except ParserError:
        df = pd.read_csv(io.StringIO(raw_csv.decode('utf-8')), skiprows=3)
    return df

bigmac_df = read_raw_file('https://github.com/valiotti/leftjoin/raw/master/motion-chart-big-mac/big-mac.csv')
population_df = read_raw_file('https://github.com/valiotti/leftjoin/raw/master/motion-chart-big-mac/population.csv')
dgp_df = read_raw_file('https://github.com/valiotti/leftjoin/raw/master/motion-chart-big-mac/gdp.csv')
continents_df = read_raw_file('https://github.com/valiotti/leftjoin/raw/master/motion-chart-big-mac/continents.csv')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From The Economist dataset we will need these columns: country name, local price, dollar exchange rate, country code (iso_a3) and record date. Take the timeline from 2005 to 2020, as the records are most complete for this span. And divide the local price by the exchange rate to calculate the price of one Big Mac in US dollars.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;bigmac_df = bigmac_df[['name', 'local_price', 'dollar_ex', 'iso_a3', 'date']]
bigmac_df = bigmac_df[bigmac_df['date'] &amp;gt;= '2005-01-01']
bigmac_df = bigmac_df[bigmac_df['date'] &amp;lt; '2020-01-01']
bigmac_df['date'] = pd.DatetimeIndex(bigmac_df['date']).year
bigmac_df = bigmac_df.drop_duplicates(['date', 'name'])
bigmac_df = bigmac_df.reset_index(drop=True)
bigmac_df['dollar_price'] = bigmac_df['local_price'] / bigmac_df['dollar_ex']&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Take a look at the result:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/1_1.png" width="465" height="182" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Next, let’s try adding a new column called continents. To ease the task, leave only two columns containing country code and continent name. Then we need to iterate through the bigmac_df[‘iso_a3’] column, adding a continent name for the corresponding values. However some cases may raise an error, because it’s not really clear, whether a country belongs to Europe or Asia, we will consider such cases as Europe by default.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;continents_df = continents_df[['Continent_Name', 'Three_Letter_Country_Code']]
continents_list = []
for country in bigmac_df['iso_a3']:
    try:
        continents_list.append(continents_df.loc[continents_df['Three_Letter_Country_Code'] == country]['Continent_Name'].item())
    except ValueError:
        continents_list.append('Europe')
bigmac_df['continent'] = continents_list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we can drop unnecessary columns,  apply sorting by country names and date, convert values in the date column into integers, and view the current result:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;bigmac_df = bigmac_df.drop(['local_price', 'iso_a3', 'dollar_ex'], axis=1)
bigmac_df = bigmac_df.sort_values(by=['name', 'date'])
bigmac_df['date'] = bigmac_df['date'].astype(int)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/2-20.png" width="321" height="176" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Then we need to fill up missing values for The Big Mac index with zeros and remove the Republic of China, since this partially recognized state is not included in the &lt;a href="https://data.worldbank.org"&gt;World Bank&lt;/a&gt; datasets. The UAE occurs several times, this can lead to issues.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;countries_list = list(bigmac_df['name'].unique())
years_set = {i for i in range(2005, 2020)}
for country in countries_list:
    if len(bigmac_df[bigmac_df['name'] == country]) &amp;lt; 15:
        this_continent = bigmac_df[bigmac_df['name'] == country].continent.iloc[0]
        years_of_country = set(bigmac_df[bigmac_df['name'] == country]['date'])
        diff = years_set - years_of_country
        dict_to_df = pd.DataFrame({
                      'name':[country] * len(diff),
                      'date':list(diff),
                      'dollar_price':[0] * len(diff),
                      'continent': [this_continent] * len(diff)
                     })
        bigmac_df = bigmac_df.append(dict_to_df)
bigmac_df = bigmac_df[bigmac_df['name'] != 'Taiwan']
bigmac_df = bigmac_df[bigmac_df['name'] != 'United Arab Emirates']&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next,  let’s augment the data with GDP per capita and population from other datasets. Both datasets have differences in country names, so we need to specify such cases explicitly and replace them.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;years = [str(i) for i in range(2005, 2020)]

countries_replace_dict = {
    'Russian Federation': 'Russia',
    'Egypt, Arab Rep.': 'Egypt',
    'Hong Kong SAR, China': 'Hong Kong',
    'United Kingdom': 'Britain',
    'Korea, Rep.': 'South Korea',
    'United Arab Emirates': 'UAE',
    'Venezuela, RB': 'Venezuela'
}
for key, value in countries_replace_dict.items():
    population_df['Country Name'] = population_df['Country Name'].replace(key, value)
    gdp_df['Country Name'] = gdp_df['Country Name'].replace(key, value)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, extract population data and GDP for the given years, adding the data to the bigmac_df DataFrame:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;countries_list = list(bigmac_df['name'].unique())

population_list = []
gdp_list = []
for country in countries_list:
    population_for_country_df = population_df[population_df['Country Name'] == country][years]
    population_list.extend(list(population_for_country_df.values[0]))
    gdp_for_country_df = gdp_df[gdp_df['Country Name'] == country][years]
    gdp_list.extend(list(gdp_for_country_df.values[0]))
    
bigmac_df['population'] = population_list
bigmac_df['gdp'] = gdp_list
bigmac_df['gdp_per_capita'] = bigmac_df['gdp'] / bigmac_df['population']&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here is our final dataset:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/3-16.png" width="590" height="179" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Creating a chart in Plotly&lt;/h2&gt;
&lt;p&gt;The population in China or India, on average, is 10 times more than in other countries. That’s why we need to transform X-axis to Log Scale, to make the chart easier for interpreting. The log-transformation is a common way to address skewness in data.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig_dict = {
    &amp;quot;data&amp;quot;: [],
    &amp;quot;layout&amp;quot;: {},
    &amp;quot;frames&amp;quot;: []
}

fig_dict[&amp;quot;layout&amp;quot;][&amp;quot;xaxis&amp;quot;] = {&amp;quot;title&amp;quot;: &amp;quot;Population&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;log&amp;quot;}
fig_dict[&amp;quot;layout&amp;quot;][&amp;quot;yaxis&amp;quot;] = {&amp;quot;title&amp;quot;: &amp;quot;GDP per capita (in $)&amp;quot;, &amp;quot;range&amp;quot;:[-10000, 120000]}
fig_dict[&amp;quot;layout&amp;quot;][&amp;quot;hovermode&amp;quot;] = &amp;quot;closest&amp;quot;
fig_dict[&amp;quot;layout&amp;quot;][&amp;quot;updatemenus&amp;quot;] = [
    {
        &amp;quot;buttons&amp;quot;: [
            {
                &amp;quot;args&amp;quot;: [None, {&amp;quot;frame&amp;quot;: {&amp;quot;duration&amp;quot;: 500, &amp;quot;redraw&amp;quot;: False},
                                &amp;quot;fromcurrent&amp;quot;: True, &amp;quot;transition&amp;quot;: {&amp;quot;duration&amp;quot;: 300,
                                                                    &amp;quot;easing&amp;quot;: &amp;quot;quadratic-in-out&amp;quot;}}],
                &amp;quot;label&amp;quot;: &amp;quot;Play&amp;quot;,
                &amp;quot;method&amp;quot;: &amp;quot;animate&amp;quot;
            },
            {
                &amp;quot;args&amp;quot;: [[None], {&amp;quot;frame&amp;quot;: {&amp;quot;duration&amp;quot;: 0, &amp;quot;redraw&amp;quot;: False},
                                  &amp;quot;mode&amp;quot;: &amp;quot;immediate&amp;quot;,
                                  &amp;quot;transition&amp;quot;: {&amp;quot;duration&amp;quot;: 0}}],
                &amp;quot;label&amp;quot;: &amp;quot;Pause&amp;quot;,
                &amp;quot;method&amp;quot;: &amp;quot;animate&amp;quot;
            }
        ],
        &amp;quot;direction&amp;quot;: &amp;quot;left&amp;quot;,
        &amp;quot;pad&amp;quot;: {&amp;quot;r&amp;quot;: 10, &amp;quot;t&amp;quot;: 87},
        &amp;quot;showactive&amp;quot;: False,
        &amp;quot;type&amp;quot;: &amp;quot;buttons&amp;quot;,
        &amp;quot;x&amp;quot;: 0.1,
        &amp;quot;xanchor&amp;quot;: &amp;quot;right&amp;quot;,
        &amp;quot;y&amp;quot;: 0,
        &amp;quot;yanchor&amp;quot;: &amp;quot;top&amp;quot;
    }
]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We will also add a slider to filter data within a certain range:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sliders_dict = {
    &amp;quot;active&amp;quot;: 0,
    &amp;quot;yanchor&amp;quot;: &amp;quot;top&amp;quot;,
    &amp;quot;xanchor&amp;quot;: &amp;quot;left&amp;quot;,
    &amp;quot;currentvalue&amp;quot;: {
        &amp;quot;font&amp;quot;: {&amp;quot;size&amp;quot;: 20},
        &amp;quot;prefix&amp;quot;: &amp;quot;Year: &amp;quot;,
        &amp;quot;visible&amp;quot;: True,
        &amp;quot;xanchor&amp;quot;: &amp;quot;right&amp;quot;
    },
    &amp;quot;transition&amp;quot;: {&amp;quot;duration&amp;quot;: 300, &amp;quot;easing&amp;quot;: &amp;quot;cubic-in-out&amp;quot;},
    &amp;quot;pad&amp;quot;: {&amp;quot;b&amp;quot;: 10, &amp;quot;t&amp;quot;: 50},
    &amp;quot;len&amp;quot;: 0.9,
    &amp;quot;x&amp;quot;: 0.1,
    &amp;quot;y&amp;quot;: 0,
    &amp;quot;steps&amp;quot;: []
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By default, the chart will display data for 2005 before we click on the “Play” button.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;continents_list_from_df = list(bigmac_df['continent'].unique())
year = 2005
for continent in continents_list_from_df:
    dataset_by_year = bigmac_df[bigmac_df[&amp;quot;date&amp;quot;] == year]
    dataset_by_year_and_cont = dataset_by_year[dataset_by_year[&amp;quot;continent&amp;quot;] == continent]
    
    data_dict = {
        &amp;quot;x&amp;quot;: dataset_by_year_and_cont[&amp;quot;population&amp;quot;],
        &amp;quot;y&amp;quot;: dataset_by_year_and_cont[&amp;quot;gdp_per_capita&amp;quot;],
        &amp;quot;mode&amp;quot;: &amp;quot;markers&amp;quot;,
        &amp;quot;text&amp;quot;: dataset_by_year_and_cont[&amp;quot;name&amp;quot;],
        &amp;quot;marker&amp;quot;: {
            &amp;quot;sizemode&amp;quot;: &amp;quot;area&amp;quot;,
            &amp;quot;sizeref&amp;quot;: 200000,
            &amp;quot;size&amp;quot;:  np.array(dataset_by_year_and_cont[&amp;quot;dollar_price&amp;quot;]) * 20000000
        },
        &amp;quot;name&amp;quot;: continent,
        &amp;quot;customdata&amp;quot;: np.array(dataset_by_year_and_cont[&amp;quot;dollar_price&amp;quot;]).round(1),
        &amp;quot;hovertemplate&amp;quot;: '&amp;lt;b&amp;gt;%{text}&amp;lt;/b&amp;gt;' + '&amp;lt;br&amp;gt;' +
                         'GDP per capita: %{y}' + '&amp;lt;br&amp;gt;' +
                         'Population: %{x}' + '&amp;lt;br&amp;gt;' +
                         'Big Mac price: %{customdata}$' +
                         '&amp;lt;extra&amp;gt;&amp;lt;/extra&amp;gt;'
    }
    fig_dict[&amp;quot;data&amp;quot;].append(data_dict)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we need to fill up the frames field, which will be used for animating the data. Each frame represents a certain data point from 2005 to 2019.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;for year in years:
    frame = {&amp;quot;data&amp;quot;: [], &amp;quot;name&amp;quot;: str(year)}
    for continent in continents_list_from_df:
        dataset_by_year = bigmac_df[bigmac_df[&amp;quot;date&amp;quot;] == int(year)]
        dataset_by_year_and_cont = dataset_by_year[dataset_by_year[&amp;quot;continent&amp;quot;] == continent]

        data_dict = {
            &amp;quot;x&amp;quot;: list(dataset_by_year_and_cont[&amp;quot;population&amp;quot;]),
            &amp;quot;y&amp;quot;: list(dataset_by_year_and_cont[&amp;quot;gdp_per_capita&amp;quot;]),
            &amp;quot;mode&amp;quot;: &amp;quot;markers&amp;quot;,
            &amp;quot;text&amp;quot;: list(dataset_by_year_and_cont[&amp;quot;name&amp;quot;]),
            &amp;quot;marker&amp;quot;: {
                &amp;quot;sizemode&amp;quot;: &amp;quot;area&amp;quot;,
                &amp;quot;sizeref&amp;quot;: 200000,
                &amp;quot;size&amp;quot;: np.array(dataset_by_year_and_cont[&amp;quot;dollar_price&amp;quot;]) * 20000000
            },
            &amp;quot;name&amp;quot;: continent,
            &amp;quot;customdata&amp;quot;: np.array(dataset_by_year_and_cont[&amp;quot;dollar_price&amp;quot;]).round(1),
            &amp;quot;hovertemplate&amp;quot;: '&amp;lt;b&amp;gt;%{text}&amp;lt;/b&amp;gt;' + '&amp;lt;br&amp;gt;' +
                             'GDP per capita: %{y}' + '&amp;lt;br&amp;gt;' +
                             'Population: %{x}' + '&amp;lt;br&amp;gt;' +
                             'Big Mac price: %{customdata}$' +
                             '&amp;lt;extra&amp;gt;&amp;lt;/extra&amp;gt;'
        }
        frame[&amp;quot;data&amp;quot;].append(data_dict)

    fig_dict[&amp;quot;frames&amp;quot;].append(frame)
    slider_step = {&amp;quot;args&amp;quot;: [
        [year],
        {&amp;quot;frame&amp;quot;: {&amp;quot;duration&amp;quot;: 300, &amp;quot;redraw&amp;quot;: False},
         &amp;quot;mode&amp;quot;: &amp;quot;immediate&amp;quot;,
         &amp;quot;transition&amp;quot;: {&amp;quot;duration&amp;quot;: 300}}
    ],
        &amp;quot;label&amp;quot;: year,
        &amp;quot;method&amp;quot;: &amp;quot;animate&amp;quot;}
    sliders_dict[&amp;quot;steps&amp;quot;].append(slider_step)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Just a few finishing touches left, instantiate the chart, set colors, fonts and title.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig_dict[&amp;quot;layout&amp;quot;][&amp;quot;sliders&amp;quot;] = [sliders_dict]

fig = go.Figure(fig_dict)

fig.update_layout(
    title = 
        {'text':'&amp;lt;b&amp;gt;Motion chart&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&amp;lt;span style=&amp;quot;color:#666666&amp;quot;&amp;gt;The Big Mac index from 2005 to 2019&amp;lt;/span&amp;gt;'},
    font={
        'family':'Open Sans, light',
        'color':'black',
        'size':14
    },
    plot_bgcolor='rgba(0,0,0,0)'
)
fig.update_yaxes(nticks=4)
fig.update_xaxes(tickfont=dict(family='Open Sans, light', color='black', size=12), nticks=4, gridcolor='lightgray', gridwidth=0.5)
fig.update_yaxes(tickfont=dict(family='Open Sans, light', color='black', size=12), nticks=4, gridcolor='lightgray', gridwidth=0.5)

fig.show()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Bingo! The Motion Chart is done:&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;" seamless="seamless" src="https://plotly.com/~i-bond/32.embed?showlink=false" height="650" width="100%"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;i&gt;View the code on &lt;a href="https://github.com/valiotti/leftjoin/tree/master/motion-chart-big-mac"&gt;GitHub&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
</description>
<pubDate>Fri, 30 Oct 2020 15:35:40 +0300</pubDate>
</item>

<item>
<title>Collecting Social Media Data for Top ML, AI &amp;amp; Data Science related accounts on Instagram</title>
<guid isPermaLink="false">46</guid>
<link>https://en.leftjoin.ru/all/collecting-social-media-data-for-top-ml-ai-data-science-related/</link>
<comments>https://en.leftjoin.ru/all/collecting-social-media-data-for-top-ml-ai-data-science-related/</comments>
<description>
&lt;p&gt;Instagram is in the top 5 most visited websites, perhaps not for our industry. Nevertheless, we are going to test this hypothesis using Python and our data analytics skills. In this post, we will share how to collect social media data using the Instagram API.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Data collection method&lt;/b&gt;&lt;br /&gt;
The Instagram API won’t let us collect data about other platform users for no reason, but there is always a way. Try sending the following request:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;https://instagram.com/leftjoin/?__a=1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The request returns a JSON object with detailed user information, for instance, we can easily get an account name, number of posts, followers, subscriptions, as well as the first ten user posts with likes count, comments and etc. The &lt;a href="https://github.com/OlegYurchik/pyInstagram"&gt;pyInstagram&lt;/a&gt; library allows sending such requests.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;SQL schema&lt;/b&gt;&lt;br /&gt;
Data will be collected into thee Clickhouse tables: users,  posts, comments. The users table will contain user data, such as user id, username,  user’s first and last name, account description, number of followers, subscriptions, posts, comments, and likes, whether an account is verified or not, and so on.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;CREATE TABLE instagram.users
(
    `added_at` DateTime,
    `user_id` UInt64,
    `user_name` String,
    `full_name` String,
    `base_url` String,
    `biography` String,
    `followers_count` UInt64,
    `follows_count` UInt64,
    `media_count` UInt64,
    `total_comments` UInt64,
    `total_likes` UInt64,
    `is_verified` UInt8,
    `country_block` UInt8,
    `profile_pic_url` Nullable(String),
    `profile_pic_url_hd` Nullable(String),
    `fb_page` Nullable(String)
)
ENGINE = ReplacingMergeTree
ORDER BY added_at&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The posts table will be populated with the post owner name, post id, caption, comments coun, and so on. To check whether a post is an advertisement,  Instagram carousel, or a video we can use these fields: &lt;span class="inline-code"&gt;is_ad&lt;/span&gt;, &lt;span class="inline-code"&gt;is_album&lt;/span&gt; and &lt;span class="inline-code"&gt;is_video&lt;/span&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;CREATE TABLE instagram.posts
(
    `added_at` DateTime,
    `owner` String,
    `post_id` UInt64,
    `caption` Nullable(String),
    `code` String,
    `comments_count` UInt64,
    `comments_disabled` UInt8,
    `created_at` DateTime,
    `display_url` String,
    `is_ad` UInt8,
    `is_album` UInt8,
    `is_video` UInt8,
    `likes_count` UInt64,
    `location` Nullable(String),
    `recources` Array(String),
    `video_url` Nullable(String)
)
ENGINE = ReplacingMergeTree
ORDER BY added_at&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the comments table, we store each comment separately with the comment owner and text.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;CREATE TABLE instagram.comments
(
    `added_at` DateTime,
    `comment_id` UInt64,
    `post_id` UInt64,
    `comment_owner` String,
    `comment_text` String
)
ENGINE = ReplacingMergeTree
ORDER BY added_at&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Writing the script&lt;/b&gt;&lt;br /&gt;
Import the following classes from the library: &lt;span class="inline-code"&gt;Account&lt;/span&gt;, &lt;span class="inline-code"&gt;Media&lt;/span&gt;, &lt;span class="inline-code"&gt;WebAgent&lt;/span&gt; and &lt;span class="inline-code"&gt;Comment&lt;/span&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from instagram import Account, Media, WebAgent, Comment
from datetime import datetime
from clickhouse_driver import Client
import requests
import pandas as pd&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, create an instance of the &lt;span class="inline-code"&gt;WebAgent&lt;/span&gt; class required for some library methods and data updating. To collect any meaningful information we need to have at least account names. Since we don’t have them yet, send the following request to search for porifles by the  keywords specified in queries_list. The search results will be composed of Instagram pages that match any keyword in the list.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;agent = WebAgent()
queries_list = ['machine learning', 'data science', 'data analytics', 'analytics', 'business intelligence',
                'data engineering', 'computer science', 'big data', 'artificial intelligence',
                'deep learning', 'data scientist','machine learning engineer', 'data engineer']
client = Client(host='12.34.56.789', user='default', password='', port='9000', database='instagram')
url = 'https://www.instagram.com/web/search/topsearch/?context=user&amp;amp;count=0'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let’s iterate the keywords collecting all matching accounts. Then remove duplicates from the obtained list by converting it to set and back.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;response_list = []
for query in queries_list:
    response = requests.get(url, params={
        'query': query
    }).json()
    response_list.extend(response['users'])
instagram_pages_list = []
for item in response_list:
    instagram_pages_list.append(item['user']['username'])
instagram_pages_list = list(set(instagram_pages_list))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we need to loop through the list of pages and request detailed information about an account if it’s not in the table yet. Create an instance of the Account class and pass username as a parameter.&lt;br /&gt;
Then update the account information using the agent.update()&lt;br /&gt;
method. We will collect only the first 100 posts to keep it moving. Next, create a list named  &lt;span class="inline-code"&gt;media_list&lt;/span&gt; to store received post ids after calling the &lt;span class="inline-code"&gt;agent.get_media()&lt;/span&gt; method.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Collecting user media data&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;all_posts_list = []
username_count = 0
for username in instagram_pages_list:
    if client.execute(f&amp;quot;SELECT count(1) FROM users WHERE user_name='{username}'&amp;quot;)[0][0] == 0:
        print('username:', username_count, '/', len(instagram_pages_list))
        username_count += 1
        account_total_likes = 0
        account_total_comments = 0
        try:
            account = Account(username)
        except Exception as E:
            print(E)
            continue
        try:
            agent.update(account)
        except Exception as E:
            print(E)
            continue
        if account.media_count &amp;lt; 100:
            post_count = account.media_count
        else:
            post_count = 100
        print(account, post_count)
        media_list, _ = agent.get_media(account, count=post_count, delay=1)
        count = 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;Because we need to count the total number of likes and comments  before adding a new user to our database, we’ll start with them first. Almost all required fields belong to the &lt;span class="inline-code"&gt;Media&lt;/span&gt; class:&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Collecting user posts&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;for media_code in media_list:
            if client.execute(f&amp;quot;SELECT count(1) FROM posts WHERE code='{media_code}'&amp;quot;)[0][0] == 0:
                print('posts:', count, '/', len(media_list))
                count += 1

                post_insert_list = []
                post = Media(media_code)
                agent.update(post)
                post_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
                post_insert_list.append(str(post.owner))
                post_insert_list.append(post.id)
                if post.caption is not None:
                    post_insert_list.append(post.caption.replace(&amp;quot;'&amp;quot;,&amp;quot;&amp;quot;).replace('&amp;quot;', ''))
                else:
                    post_insert_list.append(&amp;quot;&amp;quot;)
                post_insert_list.append(post.code)
                post_insert_list.append(post.comments_count)
                post_insert_list.append(int(post.comments_disabled))
                post_insert_list.append(datetime.fromtimestamp(post.date).strftime('%Y-%m-%d %H:%M:%S'))
                post_insert_list.append(post.display_url)
                try:
                    post_insert_list.append(int(post.is_ad))
                except TypeError:
                    post_insert_list.append('cast(Null as Nullable(UInt8))')
                post_insert_list.append(int(post.is_album))
                post_insert_list.append(int(post.is_video))
                post_insert_list.append(post.likes_count)
                if post.location is not None:
                    post_insert_list.append(post.location)
                else:
                    post_insert_list.append('')
                post_insert_list.append(post.resources)
                if post.video_url is not None:
                    post_insert_list.append(post.video_url)
                else:
                    post_insert_list.append('')
                account_total_likes += post.likes_count
                account_total_comments += post.comments_count
                try:
                    client.execute(f'''
                        INSERT INTO posts VALUES {tuple(post_insert_list)}
                    ''')
                except Exception as E:
                    print('posts:')
                    print(E)
                    print(post_insert_list)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;Store comments in the variable with the same name after calling the &lt;span class="inline-code"&gt;get_comments()&lt;/span&gt; method:&lt;br /&gt;
&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Collecting post comments&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;comments = agent.get_comments(media=post)
                for comment_id in comments[0]:
                    comment_insert_list = []
                    comment = Comment(comment_id)
                    comment_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
                    comment_insert_list.append(comment.id)
                    comment_insert_list.append(post.id)
                    comment_insert_list.append(str(comment.owner))
                    comment_insert_list.append(comment.text.replace(&amp;quot;'&amp;quot;,&amp;quot;&amp;quot;).replace('&amp;quot;', ''))
                    try:
                        client.execute(f'''
                            INSERT INTO comments VALUES {tuple(comment_insert_list)}
                        ''')
                    except Exception as E:
                        print('comments:')
                        print(E)
                        print(comment_insert_list)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;And now, when we have obtained user posts and comments new information can be added to the table.&lt;br /&gt;
&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Collecting user data&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;user_insert_list = []
        user_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        user_insert_list.append(account.id)
        user_insert_list.append(account.username)
        user_insert_list.append(account.full_name)
        user_insert_list.append(account.base_url)
        user_insert_list.append(account.biography)
        user_insert_list.append(account.followers_count)
        user_insert_list.append(account.follows_count)
        user_insert_list.append(account.media_count)
        user_insert_list.append(account_total_comments)
        user_insert_list.append(account_total_likes)
        user_insert_list.append(int(account.is_verified))
        user_insert_list.append(int(account.country_block))
        user_insert_list.append(account.profile_pic_url)
        user_insert_list.append(account.profile_pic_url_hd)
        if account.fb_page is not None:
            user_insert_list.append(account.fb_page)
        else:
            user_insert_list.append('')
        try:
            client.execute(f'''
                INSERT INTO users VALUES {tuple(user_insert_list)}
            ''')
        except Exception as E:
            print('users:')
            print(E)
            print(user_insert_list)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Conclusion&lt;/b&gt;&lt;br /&gt;
To sum up, we have collected data of 500 users, with nearly 20K posts and 40K comments. As the database will be updated, we can write a simple query to get the top 10 ML, AI &amp; Data Science related most followed accounts for today.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT *
FROM users
ORDER BY followers_count DESC
LIMIT 10&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And as a bonus, here is a list of the most interesting Instagram accounts on this  topic:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/ai_machine_learning/"&gt;@ai_machine_learning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/neuralnine/"&gt;@neuralnine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/datascienceinfo/"&gt;@datascienceinfo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/compscistuff/"&gt;@compscistuff&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/computersciencelife/"&gt;@computersciencelife&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/welcome.ai/"&gt;@welcome.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/papa_programmer/"&gt;@papa_programmer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/data_science_learn/"&gt;@data_science_learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/neuralnet.ai/"&gt;@neuralnet.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/techno_thinkers/"&gt;@techno_thinkers&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;i&gt;View the code on &lt;a href="https://github.com/valiotti/leftjoin/tree/master/instagram"&gt;GitHub&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 30 Sep 2020 16:06:11 +0300</pubDate>
</item>

<item>
<title>Future Data Conference Review</title>
<guid isPermaLink="false">45</guid>
<link>https://en.leftjoin.ru/all/future-data-conference-review/</link>
<comments>https://en.leftjoin.ru/all/future-data-conference-review/</comments>
<description>
&lt;p&gt;&lt;a href="https://www.futuredata.org"&gt;The Future Data Conference&lt;/a&gt;, which I happened to participate in, took place on September 8-9. And in today’s post, I’d like to share my observations about thoughts about presented ideas. Before we get started, I apologize for the poor quality of some images, I tried to make the most meaningful screens straight from the video.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;a href="https://www.futuredata.org/agenda/session/304194"&gt;Featured Keynote: Automating Analysis&lt;/a&gt;&lt;/b&gt;&lt;br /&gt;
&lt;i&gt;Speaker: Pat Hanrahan&lt;/i&gt;&lt;br /&gt;
The report was presented by the Stanford Professor and Tableau Co-Founder and mostly touched the use of AI and analytics.  Pat discussed where we are now, today’s AI use cases, although the report alone was kind of repetitive, the Q&amp;A part turned out to be interesting.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;a href="https://www.futuredata.org/agenda/session/304197"&gt;The Modern Data Stack: Past, Present, and Future&lt;/a&gt;&lt;/b&gt;&lt;br /&gt;
&lt;i&gt;Speaker: Tristan Handy&lt;/i&gt;&lt;br /&gt;
The main builder of dbt and author of the well-known  &lt;a href="https://thinkgrowth.org/the-startup-founders-guide-to-analytics-1d2176f20ac1"&gt;post serving as a guide to data analytics for startup founders&lt;/a&gt;, spoke about changes in modern data-stack from 2012 to 2020. Personally, I think it was one of the best conference reports since Tristan made predictions about growing tendencies and the future of data-stack.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="844" data-ratio="1.7805907172996"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/1-17.png" width="844" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/2-16.png" width="843" height="476" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/3-15.png" width="844" height="473" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/4-13.png" width="844" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/5-7.png" width="842" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/6-6.png" width="845" height="478" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/7-7.png" width="843" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/8-6.png" width="844" height="475" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/9-6.png" width="844" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/10-1.png" width="842" height="475" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/11-1.png" width="841" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/12.png" width="843" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/13.png" width="842" height="473" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/14.png" width="843" height="474" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/15.png" width="843" height="474" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;b&gt;&lt;a href="https://www.futuredata.org/agenda/session/321811"&gt;Making Enterprise Data Timelier and More Reliable with Lakehouse Technology&lt;/a&gt;&lt;/b&gt;&lt;br /&gt;
&lt;i&gt;Speaker: Matei Zaharia&lt;/i&gt;&lt;br /&gt;
This report belongs to the CTO of DataBricks. Unfortunately, the audio part had sound issues, but Matei considered the problems of modern Data Lake, promoting a new technology of DataBricks – DeltaLake. The report was more promotional but still interesting to listen to.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="1723" data-ratio="1.7762886597938"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/16.png" width="1723" height="970" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/17.png" width="1726" height="966" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/18.png" width="1718" height="965" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/19.png" width="1725" height="968" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/20.png" width="1725" height="969" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/21.png" width="1727" height="966" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/22.png" width="1724" height="968" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/23.png" width="1727" height="970" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/24.png" width="1723" height="979" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;b&gt;&lt;a href="https://www.futuredata.org/agenda/session/304204"&gt;How to Close the Analytic Divide&lt;/a&gt;&lt;/b&gt;&lt;br /&gt;
&lt;i&gt;Speaker: Alan Jacobson&lt;/i&gt;&lt;br /&gt;
The Chief Data Officer of Alteryx went on about the Data Scientist job and wages statistics, citing that the average salary of a data scientist is significantly higher than others in this field. By the way, our &lt;a href="https://leftjoin.ru/all/hh-dashboard-bi-and-analysts-market/"&gt;recent research&lt;/a&gt; with Roman Bunin also confirms this. Alan discussed the revenue of companies at different stages of analytical growth.  Companies with more advanced analytical approaches grow faster (surprising fact). A separate part was focused on changes in modern approaches to working with data. Overall, it’s a great report that was easy to listen to.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="1900" data-ratio="1.7723880597015"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/25.png" width="1900" height="1072" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/26.png" width="1764" height="1076" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/27.png" width="1917" height="1080" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/28.png" width="1891" height="1055" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/29.png" width="1749" height="1069" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/30.png" width="1751" height="1069" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/31.png" width="1753" height="1080" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;b&gt;&lt;a href="https://www.futuredata.org/agenda/session/304200"&gt;Hot Analytics — Handle with Care&lt;/a&gt;&lt;/b&gt;&lt;br /&gt;
&lt;i&gt;Speaker: Gian Merlino&lt;/i&gt;&lt;br /&gt;
The Co-Founder and CTO of Impy compared hot &amp; cold data (a clue to&lt;br /&gt;
Snowflake?). Then he demonstrated some BI tool with drag-n-drop in a simple interface. Gian went on talking about possible analytic architectures and overviewed some features of Apache Druid.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="1920" data-ratio="1.8879056047198"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/32.png" width="1920" height="1017" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/33.png" width="1845" height="1069" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/34.png" width="1872" height="1080" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/35.png" width="1863" height="1071" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/36.png" width="1877" height="1078" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/37.png" width="1869" height="1077" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/38.png" width="1885" height="1029" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/39.png" width="1843" height="1078" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
</description>
<pubDate>Fri, 25 Sep 2020 15:35:04 +0300</pubDate>
</item>

<item>
<title>Analyzing Business Intelligence (BI) and Analytics  job market in Tableau</title>
<guid isPermaLink="false">44</guid>
<link>https://en.leftjoin.ru/all/analyzing-business-intelligence-bi-and-analytics-job-market-in-t/</link>
<comments>https://en.leftjoin.ru/all/analyzing-business-intelligence-bi-and-analytics-job-market-in-t/</comments>
<description>
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/1.1.png" width="2000" height="1154" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;According to the &lt;a href="https://www.similarweb.com/website/hh.ru/"&gt;SimilarWeb rating&lt;/a&gt;,  &lt;a href="https://hh.ru/"&gt;hh.ru&lt;/a&gt; is the third among the most popular job search websites in the world. In one of the conversations with &lt;a href="https://t.me/revealthedata"&gt;Roman Bunin&lt;/a&gt;, we came up with the idea of making a common project and collect data using the HeadHunter API  for later analysis and visualization in Tableau Public. Our goal was to understand the dependency between salary and skills specified in a job posting and compare how things are in Moscow, Saint Petersburg, and other regions.&lt;/p&gt;
&lt;h2&gt;Data Collection Process&lt;/h2&gt;
&lt;p&gt;Our scheme is based on fetching a  &lt;a href="https://github.com/hhru/api/blob/master/docs/vacancies.md#короткое-представление-вакансии"&gt;brief job description&lt;/a&gt;,  returned by &lt;a href="https://github.com/hhru/api/blob/master/docs/vacancies.md#item"&gt;the GET /vacancies&lt;/a&gt; method. According to the structure we need to create the following columns: vacancy type, id,  vacancy rate (‘premium’), pre-employment testing (‘has_test’),  company address,  salary,  work schedule, and so forth. We created a table using the following CREATE query down below:&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Query for creating the vacancies_short  table in ClickHouse&lt;br /&gt;
&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;CREATE TABLE headhunter.vacancies_short
(
    `added_at` DateTime,
    `query_string` String,
    `type` String,
    `level` String,
    `direction` String,
    `vacancy_id` UInt64,
    `premium` UInt8,
    `has_test` UInt8,
    `response_url` String,
    `address_city` String,
    `address_street` String,
    `address_building` String,
    `address_description` String,
    `address_lat` String,
    `address_lng` String,
    `address_raw` String,
    `address_metro_stations` String,
    `alternate_url` String,
    `apply_alternate_url` String,
    `department_id` String,
    `department_name` String,
    `salary_from` Nullable(Float64),
    `salary_to` Nullable(Float64),
    `salary_currency` String,
    `salary_gross` Nullable(UInt8),
    `name` String,
    `insider_interview_id` Nullable(UInt64),
    `insider_interview_url` String,
    `area_url` String,
    `area_id` UInt64,
    `area_name` String,
    `url` String,
    `published_at` DateTime,
    `employer_url` String,
    `employer_alternate_url` String,
    `employer_logo_urls_90` String,
    `employer_logo_urls_240` String,
    `employer_logo_urls_original` String,
    `employer_name` String,
    `employer_id` UInt64,
    `response_letter_required` UInt8,
    `type_id` String,
    `type_name` String,
    `archived` UInt8,
    `schedule_id` Nullable(String)
)
ENGINE = ReplacingMergeTree
ORDER BY vacancy_id&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;The first script collects data from the HeadHunter website through API and inserts to our Database using the following libraries:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import requests
from clickhouse_driver import Client
from datetime import datetime
import pandas as pd
import re&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we create a DataFrame and connect to the Database in ClickHouse:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;queries = pd.read_csv('hh_data.csv')
client = Client(host='1.234.567.890', user='default', password='', port='9000', database='headhunter')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The queries table stores a list of our search queries, having the following columns: query type,  level,  career field, and search phrase.  The last column contains logical operators, for instance, we can get more results by putting logical ANDs between “Python”, “data” and “analysis”.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/edata@2x.png" width="825.5" height="205" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;The search results may not always match the expectations,  chiefs, marketers, and administrators can accidentally get into our database. To prevent this, we will write a function named check_name(name),  it will accept a vacancy name and return a boolean value, depending on the match.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def check_name(name):
    bad_names = [r'курьер', r'грузчик', r'врач', r'менеджер по закупу',
           r'менеджер по продажам', r'оператор', r'повар', r'продавец',
          r'директор магазина', r'директор по продажам', r'директор по маркетингу',
          r'кабельщик', r'начальник отдела продаж', r'заместитель', r'администратор магазина', 
          r'категорийный', r'аудитор', r'юрист', r'контент', r'супервайзер', r'стажер-ученик', 
          r'су-шеф', r'маркетолог$', r'региональный', r'ревизор', r'экономист', r'ветеринар', 
          r'торговый', r'клиентский', r'начальник цеха', r'территориальный', r'переводчик', 
          r'маркетолог /', r'маркетолог по']
    for item in bad_names:
        if re.match(item, name):
            return True&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Moving further, we need to create a while loop to collect data non-stop.  Iterate over the Dataframe queries selecting the type, level, field, and search phrase columns. Send a GET request using a keyword to get the number of pages.  Then we loop through the number of pages sending the same requests and populating  vacancies_from_response with job descriptions. In the per_page parameter we specified 10, this is the max limit for the HH API. Since we didn’t pass any value to the area field,  the results are collected worldwide.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;while True:
   for query_type, level, direction, query_string in zip(queries['Query Type'], queries['Level'], queries['Career Field'], queries['Seach Phrase']):
           print(f'seach phrase: {query_string}')
           url = 'https://api.hh.ru/vacancies'
           par = {'text': query_string, 'per_page':'10', 'page':0}
           r = requests.get(url, params=par).json()
           added_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
           pages = r['pages']
           found = r['found']
           vacancies_from_response = []

           for i in range(0, pages + 1):
               par = {'text': query_string, 'per_page':'10', 'page':i}
               r = requests.get(url, params=par).json()
               try:
                   vacancies_from_response.append(r['items'])
               except Exception as E:
                   continue&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Create a for loop to escape duplicate rows in our table. First, send a query to the database, verifying whether there is a vacancy with the same id and search phrase. If the verification was successful we then&lt;br /&gt;
pass the job title to check_name() and move on to the next one.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;for item in vacancies_from_response:
               for vacancy in item:
                   if client.execute(f&amp;quot;SELECT count(1) FROM vacancies_short WHERE vacancy_id={vacancy['id']} AND query_string='{query_string}'&amp;quot;)[0][0] == 0:
                       name = vacancy['name'].replace(&amp;quot;'&amp;quot;,&amp;quot;&amp;quot;).replace('&amp;quot;','')
                       if check_name(name):
                           continue&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we need to extract all the necessary data from a job description.  The table will contain empty cells, since some data may be missing.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;View the code for extracting job description data&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;vacancy_id = vacancy['id']
                       is_premium = int(vacancy['premium'])
                       has_test = int(vacancy['has_test'])
                       response_url = vacancy['response_url']
                       try:
                           address_city = vacancy['address']['city']
                           address_street = vacancy['address']['street']
                           address_building = vacancy['address']['building']
                           address_description = vacancy['address']['description']
                           address_lat = vacancy['address']['lat']
                           address_lng = vacancy['address']['lng']
                           address_raw = vacancy['address']['raw']
                           address_metro_stations = str(vacancy['address']['metro_stations']).replace(&amp;quot;'&amp;quot;,'&amp;quot;')
                       except TypeError:
                           address_city = &amp;quot;&amp;quot;
                           address_street = &amp;quot;&amp;quot;
                           address_building = &amp;quot;&amp;quot;
                           address_description = &amp;quot;&amp;quot;
                           address_lat = &amp;quot;&amp;quot;
                           address_lng = &amp;quot;&amp;quot;
                           address_raw = &amp;quot;&amp;quot;
                           address_metro_stations = &amp;quot;&amp;quot;
                       alternate_url = vacancy['alternate_url']
                       apply_alternate_url = vacancy['apply_alternate_url']
                       try:
                           department_id = vacancy['department']['id']
                       except TypeError as E:
                           department_id = &amp;quot;&amp;quot;
                       try:
                           department_name = vacancy['department']['name']
                       except TypeError as E:
                           department_name = &amp;quot;&amp;quot;
                       try:
                           salary_from = vacancy['salary']['from']
                       except TypeError as E:
                           salary_from = &amp;quot;cast(Null as Nullable(UInt64))&amp;quot;
                       try:
                           salary_to = vacancy['salary']['to']
                       except TypeError as E:
                           salary_to = &amp;quot;cast(Null as Nullable(UInt64))&amp;quot;
                       try:
                           salary_currency = vacancy['salary']['currency']
                       except TypeError as E:
                           salary_currency = &amp;quot;&amp;quot;
                       try:
                           salary_gross = int(vacancy['salary']['gross'])
                       except TypeError as E:
                           salary_gross = &amp;quot;cast(Null as Nullable(UInt8))&amp;quot;
                       try:
                           insider_interview_id = vacancy['insider_interview']['id']
                       except TypeError:
                           insider_interview_id = &amp;quot;cast(Null as Nullable(UInt64))&amp;quot;
                       try:
                           insider_interview_url = vacancy['insider_interview']['url']
                       except TypeError:
                           insider_interview_url = &amp;quot;&amp;quot;
                       area_url = vacancy['area']['url']
                       area_id = vacancy['area']['id']
                       area_name = vacancy['area']['name']
                       url = vacancy['url']
                       published_at = vacancy['published_at']
                       published_at = datetime.strptime(published_at,'%Y-%m-%dT%H:%M:%S%z').strftime('%Y-%m-%d %H:%M:%S')
                       try:
                           employer_url = vacancy['employer']['url']
                       except Exception as E:
                           print(E)
                           employer_url = &amp;quot;&amp;quot;
                       try:
                           employer_alternate_url = vacancy['employer']['alternate_url']
                       except Exception as E:
                           print(E)
                           employer_alternate_url = &amp;quot;&amp;quot;
                       try:
                           employer_logo_urls_90 = vacancy['employer']['logo_urls']['90']
                           employer_logo_urls_240 = vacancy['employer']['logo_urls']['240']
                           employer_logo_urls_original = vacancy['employer']['logo_urls']['original']
                       except Exception as E:
                           print(E)
                           employer_logo_urls_90 = &amp;quot;&amp;quot;
                           employer_logo_urls_240 = &amp;quot;&amp;quot;
                           employer_logo_urls_original = &amp;quot;&amp;quot;
                       employer_name = vacancy['employer']['name'].replace(&amp;quot;'&amp;quot;,&amp;quot;&amp;quot;).replace('&amp;quot;','')
                       try:
                           employer_id = vacancy['employer']['id']
                       except Exception as E:
                           print(E)
                       response_letter_required = int(vacancy['response_letter_required'])
                       type_id = vacancy['type']['id']
                       type_name = vacancy['type']['name']
                       is_archived = int(vacancy['archived'])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;The last field is the work schedule.  If there is mentioned a fly-in-fly-out method, these kinds of job postings will be skipped.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;try:
    schedule = vacancy['schedule']['id']
except Exception as E:
    print(E)
    schedule = ''&amp;quot;
if schedule == 'flyInFlyOut':
    continue&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we create a list of obtained variables,  replacing None values with empty strings to escape errors with Clickhouse and insert them into the table.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;vacancies_short_list = [added_at, query_string, query_type, level, direction, vacancy_id, is_premium, has_test, response_url, address_city, address_street, address_building, address_description, address_lat, address_lng, address_raw, address_metro_stations, alternate_url, apply_alternate_url, department_id, department_name,
salary_from, salary_to, salary_currency, salary_gross, insider_interview_id, insider_interview_url, area_url, area_name, url, published_at, employer_url, employer_logo_urls_90, employer_logo_urls_240,  employer_name, employer_id, response_letter_required, type_id, type_name, is_archived, schedule]
for index, item in enumerate(vacancies_short_list):
    if item is None:
        vacancies_short_list[index] = &amp;quot;&amp;quot;
tuple_to_insert = tuple(vacancies_short_list)
print(tuple_to_insert)
client.execute(f'INSERT INTO vacancies_short VALUES {tuple_to_insert}')&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Connecting Tableau  to the data source&lt;/h2&gt;
&lt;p&gt;Unfortunately, we can’t work with databases in  Tableau Public, that’s why we decided to connect our  Clickhouse Database to Google Sheets.  With this in mind, we picked the following libraries: gspread and oauth2client for accessing Google Spreadsheets API,  and schedule for task scheduling.&lt;/p&gt;
&lt;p class="note"&gt;Refer to our previous article where we used  Google Spreadseets API  for  &lt;a href="https://en.leftjoin.ru/all/collecting-data-on-ad-campaigns-from-vkontakte/" class="nu"&gt;“&lt;u&gt;Collecting Data on Ad Campaigns from VK.com&lt;/u&gt;”&lt;/a&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import schedule
from clickhouse_driver import Client
import gspread
import pandas as pd
from oauth2client.service_account import ServiceAccountCredentials
from datetime import datetime

scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
client = Client(host='54.227.137.142', user='default', password='', port='9000', database='headhunter')
creds = ServiceAccountCredentials.from_json_keyfile_name('credentials.json', scope)
gc = gspread.authorize(creds)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The update_sheet() function will transfer all data from Clickhouse to a Google Sheets table:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def update_sheet():
   print('Updating cell at', datetime.now())
   columns = []
   for item in client.execute('describe table headhunter.vacancies_short'):
       columns.append(item[0])
   vacancies = client.execute('SELECT * FROM headhunter.vacancies_short')
   df_vacancies = pd.DataFrame(vacancies, columns=columns)
   df_vacancies.to_csv('vacancies_short.csv', index=False)
   content = open('vacancies_short.csv', 'r').read()
   gc.import_csv('1ZWS2kqraPa4i72hzp0noU02SrYVo0teD7KZ0c3hl-UI', content.encode('utf-8'))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Using schedule to run our function every day at 1:00 PM (UTC):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;schedule.every().day.at(&amp;quot;13:00&amp;quot;).do(update_sheet)
while True:
   schedule.run_pending()&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;What’s the final point?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://t.me/revealthedata"&gt;Roman&lt;/a&gt; created an informative dashboard based on this data.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;a href="https://revealthedata.com/examples/hh/" class="e2-text-picture-link"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/3-4.jpg" width="2560" height="1439" alt="" /&gt;
&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;And made a &lt;a href="https://youtu.be/jaJdG7kZ7BI"&gt;youtube video &lt;/a&gt; with a detailed explanation of the dashboard features.&lt;/p&gt;
&lt;h2&gt;Key Insights&lt;/h2&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Data Analysts specializing in BI  are most in-demand in the job market since the highest number of search results were returned with this query. However, the average salary is higher in Product Analyst and BI-analyst openings.&lt;/li&gt;
&lt;li&gt;Most of the postings were found In Moscow,  where the average salary is 10-30K RUB higher than in Saint Petersburg and 30-40K  higher than in other regions.&lt;/li&gt;
&lt;li&gt;Top highly paid positions: Head of Analytics (110K RUB per month on avg.), Database Engineer (138K RUB per month), and Head of Machine Learning (250K RUB per month).&lt;/li&gt;
&lt;li&gt;The most useful skills to have are a solid knowledge of Python with Pandas and Numpy, Tableau, Power BI, ETL, and Spark. Most of the posings found contained these requirements and were highly paid than any others.  For Python programmers, it’s more valuable to have expertise with Matplotlib than Plotly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;i&gt;View the code on  &lt;a href="https://github.com/valiotti/leftjoin/tree/master/headhunter"&gt;GitHub&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 23 Sep 2020 16:24:41 +0300</pubDate>
</item>

<item>
<title>How to build a dashboard with Bootstrap 4 from scratch (Part 1)</title>
<guid isPermaLink="false">43</guid>
<link>https://en.leftjoin.ru/all/how-to-build-dashboard-with-bootstrap-4-from-scratch-part-1/</link>
<comments>https://en.leftjoin.ru/all/how-to-build-dashboard-with-bootstrap-4-from-scratch-part-1/</comments>
<description>
&lt;p&gt;In previous articles we reviewed  Plotly’s Dash Framework,  &lt;a href="https://en.leftjoin.ru/all/building-a-scatter-plot-for-untappd-breweries/"&gt;learned to build scatter plots&lt;/a&gt; and &lt;a href="https://en.leftjoin.ru/all/visualizing-covid-19-in-russia-with-plotly/"&gt; create a map visualization&lt;/a&gt;. This time we will summarize our knowledge and put all the pieces together to design a dashboard layout using the Bootstrap 4 grid system.&lt;br /&gt;
To facilitate the development, we’ll refer to the &lt;a href="https://dash-bootstrap-components.opensource.faculty.ai/"&gt;dash-bootstrap-components&lt;/a&gt; library. This is a great tool that integrates Bootstrap in Dash, allowing us to write web pages in pure Python, and add any Bootstrap components and styling.&lt;/p&gt;
&lt;h2&gt;Draft Layout&lt;/h2&gt;
&lt;p&gt;Before we begin coding it’s crucial to have a plan of our app, a rough layout that would help us to see the big picture and quickly modify the structure.  We used &lt;a href="https://app.diagrams.net/"&gt;draw.io&lt;/a&gt; to make a dashboard draft, this application enables to create diagrams, graphs,  flowcharts, and forms at the click of a button. The dashboard will be built according to this template:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/template_1@2x.png" width="872" height="946" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Like the dashboard itself,  the top header will be colored in gold and white, the main colors of &lt;a href="https://untappd.com/"&gt;Untappd&lt;/a&gt;.  Just below the header, there is a section with breweries, which includes a scatter plot and a control panel.  And at the bottom of the page, there will be a map showing beverage rating across the regions of Russia.&lt;/p&gt;
&lt;p&gt;All right, let’s get started, first create a new python file with the name application.py. The file will store all the front end components of the dashboard, and create a new directory named assets. The directory structure should be similar:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;- application.py
- assets/
    |-- typography.css
    |-- header.css
    |-- custom-script.js
    |-- image.png&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we import the libraries and initialize our application:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
import pandas as pd
from get_ratio_scatter_plot import get_plot
from get_russian_map import get_map
from clickhouse_driver import Client
from dash.dependencies import Input, Output

standard_BS = dbc.themes.BOOTSTRAP
app = dash.Dash(__name__, external_stylesheets=[standard_BS])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Main parameters of the app:&lt;br /&gt;
&lt;span class="inline-code"&gt;__name__&lt;/span&gt;  — to enable access to static elements stored in the assets folder (such as images, CSS and JS files)&lt;br /&gt;
&lt;span class="inline-code"&gt;external_stylesheets&lt;/span&gt; — external CSS styling,  here we are using a standard Bootstrap theme, however you can create your own theme  or use any of &lt;a href="https://www.bootstrapcdn.com/bootswatch/"&gt; the availables ones&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Hook up a few more things to work with local files  and connect to the Clickhouse Database:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;app.scripts.config.serve_locally = True
app.css.config.serve_locally = True

client = Client(host='ec2-3-16-148-63.us-east-2.compute.amazonaws.com',
                user='default',
                password='',
                port='9000',
                database='default')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Add a palette of colors:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;colors = ['#ffcc00', 
          '#f5f2e8', 
          '#f8f3e3',
          '#ffffff', 
          ]&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Creating a layout&lt;/h2&gt;
&lt;p&gt;All the dashboard elements will be placed within a Bootstrap container,  which is in the  &amp;lt;div&amp;gt block:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;- app 
    |-- div
     |-- container
      |-- logo&amp;amp;header
     |-- container
      |-- div
       |-- controls&amp;amp;scatter
       |-- map&lt;/code&gt;&lt;/pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;app.layout = html.Div(
                    [
                        dbc.Container(

                                         &amp;lt; header&amp;gt;
                         
                        dbc.Container(       
                            html.Div(
                                [
                        
                                    &amp;lt; body &amp;gt;
                        
                                ],
                            ),
                            fluid=False, style={'max-width': '1300px'},
                        ),
                    ],
                    style={'background-color': colors[1], 'font-family': 'Proxima Nova Bold'},
                )&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we set a fixed container width, background color, and font style of the page that is stored in typography.css in the assets folder. Let’s take a closer look at the first element in the div block,  that’s the top header with the Untappd logo:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;logo = html.Img(src=app.get_asset_url('logo.png'),
                        style={'width': &amp;quot;128px&amp;quot;, 'height': &amp;quot;128px&amp;quot;,
                        }, className='inline-image')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and the header:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;header = html.H3(&amp;quot;Russian breweries stats from Untappd&amp;quot;, style={'text-transform': &amp;quot;uppercase&amp;quot;})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We used Bootstrap Forms to position these two elements on the same level.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;logo_and_header = dbc.FormGroup(
        [
            logo,
            html.Div(
                [
                    header
                ],
                className=&amp;quot;p-5&amp;quot;
            )
        ],
        className='form-row',
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The class name  ‘p-5’ allows to increase padding and vertically align the title while specifying ‘form-row’  as the form class name we put the logo and header in one row.  At this point, the top header should  look the following:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/logo_and_header.png" width="2132" height="242" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Now we need to center the elements and add some colors.  Create a separate container that will take one row. Specify &lt;span class="inline-code"&gt;‘d-flex justify-content-center’&lt;/span&gt; in the &lt;span class="inline-code"&gt;className&lt;/span&gt;  to achieve the same output.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;dbc.Container(
                    dbc.Row(
                        [
                            dbc.Col(
                                html.Div(
                                    logo_and_header,
                                ),
                            ),
                        ],
                        style={'max-height': '128px',
                               'color': 'white',
                       }

                    ),
                    className='d-flex justify-content-center',
                    style={'max-width': '100%',
                           'background-color': colors[0]},
                ),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And now the top header is done:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/top-header.png" width="2200" height="245" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;We’re approaching the main part, create the next Bootstrap Container and add a subheading:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;dbc.Container(
                    html.Div(
                        [
                            html.Br(),
                            html.H5(&amp;quot;Breweries&amp;quot;, style={'text-align':'center', 'text-transform': 'uppercase'}),
                            html.Hr(), # horizontal  break&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The main body will consist of Bootstrap Cards, they can provide a structured layout of all parts,  giving each element a clear border and saving the white space. Create the next element, a control panel with sliders:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;slider_day_values = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
slider_top_breweries_values = [5, 25, 50, 75, 100, 125, 150, 175, 200]

controls = dbc.Card(
    [
       dbc.CardBody(
           [
               dbc.FormGroup(
                    [
                        dbc.Label(&amp;quot;Time Period&amp;quot;, style={'text-align': 'center', 'font-size': '100%', 'text-transform': 'uppercase'}),
                        dcc.Slider(
                            id='slider-day',
                            min=1,
                            max=100,
                            step=10,
                            value=100,
                            marks={i: i for i in slider_day_values}
                        ),
                    ], style={'text-align': 'center'}
               ),
               dbc.FormGroup(
                    [
                        dbc.Label(&amp;quot;Number of breweries&amp;quot;, style={'text-align': 'center', 'font-size': '100%', 'text-transform': 'uppercase'}),
                        dcc.Slider(
                            id='slider-top-breweries',
                            min=5,
                            max=200,
                            step=5,
                            value=200,
                            marks={i: i for i in slider_top_breweries_values}
                        ),
                    ], style={'text-align': 'center'}
               ),
           ],
       )
    ],
    style={'height': '32.7rem', 'background-color': colors[3]}
)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/2@2x.png" width="291.5" height="149" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;The control panel consists of two sliders that can be used to change the view on the scatter, they are positioned one below the other in a Bootstrap Form. The sliders were put inside the dbc.CardBody block, other elements will be added in the same way. It allows to eliminate alignment problem and achieve clear borders.  By default, the sliders are painted in blue, but we can easily customize them by changing the properties of the class in sliders.css.  Add the control panel with the scatter plot as follows:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;dbc.Row(
                [
                    dbc.Col(controls, width={&amp;quot;size&amp;quot;: 4,
                                     &amp;quot;order&amp;quot;: 'first',
                                             &amp;quot;offset&amp;quot;: 0},
                     ),
                     dbc.Col(dbc.Card(
                                [
                                    dbc.CardBody(
                                        [
                                            html.H6(&amp;quot;The ratio between the number of reviews and the average brewery rating&amp;quot;,
                                                    className=&amp;quot;card-title&amp;quot;,
                                                    style={'text-transform': 'uppercase'}), 
                                            dcc.Graph(id='ratio-scatter-plot'),
                                        ],
                                    ),
                                ],
                                style={'background-color': colors[2], 'text-align':'center'}
                             ),
                     md=8),
                ],
                align=&amp;quot;start&amp;quot;,
                justify='center',
            ),
html.Br(),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And at the bottom of the page we will position the map:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;html.H5(&amp;quot;Venues and Regions&amp;quot;, style={'text-align':'center', 'text-transform': 'uppercase',}),
                            html.Hr(), # horizontal  break
                            dbc.Row(
                                [
                                    dbc.Col(
                                        dbc.Card(
                                            [
                                                dbc.CardBody(
                                                    [
                                                        html.H6(&amp;quot;Average beer rating across regions&amp;quot;,
                                                                className=&amp;quot;card-title&amp;quot;,
                                                                style={'text-transform': 'uppercase'},
                                                        ),  
                                                        dcc.Graph(figure=get_map())
                                                    ],
                                                ),
                                            ],
                                        style={'background-color': colors[2], 'text-align': 'center'}
                                        ),
                                md=12),
                                ]
                            ),
                            html.Br(),&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Callbacks in Dash&lt;/h2&gt;
&lt;p&gt;Callback functions allow making dashboard elements interactive through the  Input and Output properties of a particular component.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;@app.callback(
    Output('ratio-scatter-plot', 'figure'),
    [Input('slider-day', 'value'),
     Input('slider-top-breweries', 'value'),
     ]
)
def get_scatter_plots(n_days=100, top_n=200):
    if n_days == 100 and top_n == 200:
        df = pd.read_csv('data/ratio_scatter_plot.csv')
        return get_plot(n_days, top_n, df)
    else:
        return get_plot(n_days, top_n)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this example, our inputs are the “value” properties of the components that have the ids “slider-day’” and  “slider-top-breweries”. Our output is the “children” property of the component with the id “ratio-scatter-plot”. When the input values are changed, the decorator function will be called automatically and the output on the scatter is updated. Learn more about callbacks from &lt;a href="https://dash.plotly.com/basic-callbacks/"&gt;the examples in the docs.&lt;/a&gt;&lt;br /&gt;
It’s worth noting, that the scatter plot may not be displayed correctly when the page is loaded. To avoid this scenario we need to specify its initial state and produce a scatter plot from the saved CSV file stored in the data folder.  Then, when changing the slider values, all data will be taken directly from the Clickhouse tables.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="833" data-ratio="1.595785440613"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/scatter_empty_2@2x.png" width="833" height="522" alt="" /&gt;
&lt;img src="https://en.leftjoin.ru/pictures/scatter_2@2x.png" width="828" height="515" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Add a few more lines responsible for deployment and our app is ready to run:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;application = app.server

if __name__ == '__main__':
    application.run(debug=True, port=8000)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we need to  &lt;a href="https://en.leftjoin.ru/all/deploying-analytical-web-app-with-aws-elastic-beanstalk/"&gt;deploy our app to AWS BeansTalk&lt;/a&gt; and &lt;a href="http://unappd-part-1-en.us-east-2.elasticbeanstalk.com/"&gt;the first part of our Bootstrap Dashboard is completed&lt;/a&gt;:&lt;/p&gt;
&lt;div class="embed-responsive embed-responsive-4by3" style="min-width:500"&gt;&lt;p&gt;&lt;iframe id="igraph" scrolling="yes" style="border:none;"seamless="seamless" src='http://unappd-part-1-en.us-east-2.elasticbeanstalk.com/' height="1360px" width="1100px"&lt;/p&gt;
&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;Thanks for reading the first part of our series about Bootstrap Dashboards, in the next one we are going to add more new components, improved callbacks, and talk about tables in Bootstrap.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/valiotti/leftjoin/tree/master/untappd_dashboard_en%20(part_1)/"&gt; View the code on Github&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 16 Sep 2020 16:21:28 +0300</pubDate>
</item>


</channel>
</rss>