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

<channel>

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

<item>
<title>Mean VS median: how to choose a target metric?</title>
<guid isPermaLink="false">71</guid>
<link>https://en.leftjoin.ru/all/mean-vs-median/</link>
<comments>https://en.leftjoin.ru/all/mean-vs-median/</comments>
<description>
&lt;p&gt;In today’s article, we would like to highlight a simple, but important topic – how to choose a simple metric to evaluate a particular dataset. Everyone has been familiar with the arithmetic mean for a long time, almost every student knows very well that you should sum up all the available values, divide by their number and get the average value. However, school knowledge does not include any alternative options, of which, in fact, there are many in statistics – almost for every occasion. In solving research and marketing problems, people often take mean as a target. Is this legitimate or is there a better option? Let’s figure it out.&lt;/p&gt;
&lt;p&gt;To begin with, it is worth remembering the definitions of the two metrics that we will talk about today.&lt;br /&gt;
Mean is the most popular statistic used to calculate a data center. What is the median? Median is a value that splits data, sorted in order of increasing values, into two equal parts. This means that the median shows the central value in the sample if the number of cases is odd and the arithmetic mean of the two values ​​if the number of cases in the sample is even.&lt;/p&gt;
&lt;h2&gt;Research tasks&lt;/h2&gt;
&lt;p&gt;So, the estimation of the sample mean is often important in many research questions. For instance, specialists studying demography often study changes in the number of regions in Russia in order to track the dynamics and reflect it in reports. Let’s try to calculate the average size of the Russian city, as well as the median, and then compare the results.&lt;br /&gt;
First, you need to find and load data by connecting the pandas library for this.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
city = pd.read_csv('city.csv', sep = ';')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, you need to calculate the mean and median of the sample.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;mean_pop = round (city.population_2020.mean (), 0)
median_pop = round (city.population_2020.median (), 0)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The values, of course, are different, since the distribution of observations in the sample is different from the normal one. In order to understand whether they are very different, let’s build a distribution graph and display the mean and median.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import matplotlib.pyplot as plt
import seaborn as sns

sns.set_palette('rainbow')
fig = plt.figure(figsize = (20, 15))
ax = fig.add_subplot(1, 1, 1)
g = sns.histplot(data = city, x= 'population_2020', alpha=0.6, bins = 100, ax=ax)

g.axvline(mean_pop, linewidth=2, color='r', alpha=0.9, linestyle='--', label = 'Mean = {:,.0f}'.format(mean_pop).replace(',', ' '))
g.axvline(median_pop, linewidth=2, color='darkgreen', alpha=0.9, linestyle='--', label = 'Median = {:,.0f}'.format(median_pop).replace(',', ' '))

plt.ticklabel_format(axis='x', style='plain')
plt.xlabel(&amp;quot;Population&amp;quot;, fontsize=20)
plt.ylabel(&amp;quot;Number of cities&amp;quot;, fontsize=20)
plt.title(&amp;quot;Distribution of population of russian cities&amp;quot;, fontsize=20)
plt.legend(fontsize=&amp;quot;xx-large&amp;quot;)
plt.show()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/first.jpg" width="1440" height="1080" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Also, on this data it is worth building a boxplot for more accurate visualization with the main distribution quantiles, median, mean and outliers.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig = plt.figure(figsize = (10, 10))
sns.set_theme(style=&amp;quot;whitegrid&amp;quot;)
sns.set_palette(palette=&amp;quot;pastel&amp;quot;)

sns.boxplot(y = city['population_2020'], showfliers = False)

plt.scatter(0, 550100, marker='*', s=100, color = 'black', label = 'Outlier')
plt.scatter(0, 560200, marker='*', s=100, color = 'black')
plt.scatter(0, 570300, marker='*', s=100, color = 'black')
plt.scatter(0, mean_pop, marker='o', s=100, color = 'red', edgecolors = 'black', label = 'Mean')
plt.legend()

plt.ylabel(&amp;quot;Population&amp;quot;, fontsize=15)
plt.ticklabel_format(axis='y', style='plain')
plt.title(&amp;quot;Boxplot of population of russian cities&amp;quot;, fontsize=15)
plt.show()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/second.jpg" width="720" height="720" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;It follows from the graphs that the median is significantly less than the average, and it is also clear that this is a consequence of the presence of outliers which are Moscow and St. Petersburg. Since the arithmetic mean is an extremely sensitive metric to outliers, it is not worth relying on conclusions about the mean if they are present in the sample. An increase or decrease in the population of Moscow can greatly change the average population in Russia, but this will not affect the real regional trend.&lt;br /&gt;
Using the arithmetic mean, we say that the number of a typical (average) city in the Russian Federation is 268 thousand people. However, this misleads us, since the mean value is significantly higher than the median solely due to the size of the population of Moscow and St. Petersburg. In fact, the number of a typical Russian city is significantly less (2 times less!) and is approximately 104 thousand citizens.&lt;/p&gt;
&lt;h2&gt;Marketing tasks&lt;/h2&gt;
&lt;p&gt;In a business tasks, the difference between the mean and the median is also important, as using the wrong metric can seriously affect the results of the advertising campaign or make it difficult to achieve the goal. Let’s take a look at a real example of the difficulties an entrepreneur can face in retail if he chooses the wrong target metric.&lt;br /&gt;
To begin with, as in the previous example, let’s load a dataset about supermarket purchases. Let’s select the dataset columns necessary for analysis and rename them to simplify the code in the future. Since this data is not as well prepared as the previous ones, it is necessary to group all purchased items by receipts. In this case, it is necessary to group by two variables: by the customer’s id and by the date of purchase (the date and time is determined by the moment of closing the bill, therefore, all purchases within one bill coincide by date variable). Then, let’s name the resulting column “total_bill”, that is, the check amount and calculate the average and median.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = pd.read_excel ('invoice_data.xlsx')
df.columns = ['user', 'total_price', 'date']
groupped_df = pd.DataFrame (df.groupby (['user', 'date']). total_price.sum ())
groupped_df.columns = ['total_bill']
mean_bill = groupped_df.total_bill.mean ()
median_bill = groupped_df.total_bill.median ()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now, as in the previous example, you need to plot the distribution of customer checks and boxplot, and also display the median and mean on each of them.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sns.set_palette('rainbow')
fig = plt.figure(figsize = (20, 15))
ax = fig.add_subplot(1, 1, 1)
sns.histplot(groupped_df, x = 'total_bill', binwidth=200, alpha=0.6, ax=ax)
plt.xlabel(&amp;quot;Purchases&amp;quot;, fontsize=20)
plt.ylabel(&amp;quot;Total bill&amp;quot;, fontsize=20)
plt.title(&amp;quot;Distribution of total bills&amp;quot;, fontsize=20)
plt.axvline(mean_bill, linewidth=2, color='r', alpha=1, linestyle='--', label = 'Mean = {:.0f}'.format(mean_bill))
plt.axvline(median_bill, linewidth=2, color='darkgreen', alpha=1, linestyle='--', label = 'Median = {:.0f}'.format(median_bill))
plt.legend(fontsize=&amp;quot;xx-large&amp;quot;)
plt.show()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/third.jpg" width="1440" height="1080" alt="" /&gt;
&lt;/div&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig = plt.figure(figsize = (10, 10))
sns.set_theme(style=&amp;quot;whitegrid&amp;quot;)
sns.set_palette(palette=&amp;quot;pastel&amp;quot;)

sns.boxplot(y = groupped_df['total_bill'], showfliers = False)

plt.scatter(0, 1800, marker='*', s=100, color = 'black', label = 'Outlier')
plt.scatter(0, 1850, marker='*', s=100, color = 'black')
plt.scatter(0, 1900, marker='*', s=100, color = 'black')
plt.scatter(0, mean_bill, marker='o', s=100, color = 'red', edgecolors = 'black', label = 'Mean')
plt.legend()

plt.ticklabel_format(axis='y', style='plain')
plt.ylabel(&amp;quot;Total bill&amp;quot;, fontsize=15)
plt.title(&amp;quot;Boxplot of total bills&amp;quot;, fontsize=15)
plt.show()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/forth.jpg" width="720" height="720" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;The graphs show that the distribution is different from normal, which means that the median and mean are not equal. The median value is smaller than the average by about 220 rubles.&lt;br /&gt;
Now, imagine that marketers have a task to increase the average buyer’s bill. A marketer may decide that since the average check is 601 rubles, the following promotion can be offered: “All buyers who make a purchase over 600 rubles, will get 20% discount on any good for 100 rubles.” In general, it is a reasonable offer, however, in reality, the average check is lower – 378 rubles. Thus, the majority of buyers will not be interested in the offer, since their purchase usually does not reach the proposed threshold. This means, that they will not take advantage of the offer and will not receive a discount, and the company will not be able to achieve its goal and increase the profit. The point is that the initial assumptions were wrong.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;As you already understood, the mean often shows a more pleasant result, both for business and for research tasks, because it is always nicer to imagine the situation with the average check or the demographic situation in the country better than it really is. However, one must always remember about the shortcomings of mean in order to be able to correctly choose the appropriate analogue for assessing a particular situation.&lt;/p&gt;
</description>
<pubDate>Wed, 27 Oct 2021 14:20:56 +0300</pubDate>
</item>

<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>Bubble charts basics: area vs radius</title>
<guid isPermaLink="false">69</guid>
<link>https://en.leftjoin.ru/all/bubble-charts-basics-area-vs-radius/</link>
<comments>https://en.leftjoin.ru/all/bubble-charts-basics-area-vs-radius/</comments>
<description>
&lt;p&gt;Data visualization is a skill used in any industry where data is present, because tables are only good for storing information. When there is a need to present data, or rather certain conclusions derived from them, the data must be presented on graphs of a suitable type. So, here you are faced with two tasks: the first is to choose the right type of graph, the second is to display the results in a plausible way. Today we will tell you about one mistake that designers sometimes make when visualizing data on bubble-charts and how this mistake can be avoided.&lt;/p&gt;
&lt;h2&gt;The crux of building a bubble-chart&lt;/h2&gt;
&lt;p&gt;Firstly, let us tell you a bit of boring theory before we start analyzing the data. Bubble-chart is a convenient way to show three numerical variables without building a 3D model. The usual X and Y axes indicate the values ​​of two parameters, and the third is shown by the size of the circle that corresponds to each observation. This is what makes it possible to avoid the need to build a complex 3D chart, that is, anyone who sees a bubble-chart will be able to draw conclusions about the data much faster.&lt;/p&gt;
&lt;h2&gt;A mistake that a designer, but not a data analyst, can make&lt;/h2&gt;
&lt;p&gt;With the metrics that are displayed on the axes of the graph, no questions arise. This is the usual way of visualizing data, but with the data shown by the size of the circles there is some difficulty: how to correctly and accurately display changes in the values ​​of a variable, if the control is not a point on the axis, but the size of this point?&lt;br /&gt;
The fact is that when building such a graph without using analytical tools, for example, in a graphics editor, the author can draw circles, taking the radius of the circle as its size. At first glance, everything seems to be absolutely correct – the larger the value of the variable, the larger the radius of the circle. However, in this case, the area of ​​the circle will increase not as a linear, but as a power function, because S = π × r2. For instance, the figure below shows that if you double the radius of a circle, the area will quadruple.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt; &lt;span style = "color: # 7ea9b8"&gt; Draw a circle in Matplotlib &lt;/span&gt; &lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig = plt.figure (figsize = (10, 10))
ax = fig.add_subplot (1, 1, 1)
s = 4 * 10e3


ax.scatter (100, 100, s = s, c = 'r')
ax.scatter (100, 100, s = s / 4, c = 'b')
ax.scatter (100, 100, s = 10, c = 'g')
plt.axvline (99, c = 'black')
plt.axvline (101, c = 'black')
plt.axvline (98, c = 'black')
plt.axvline (102, c = 'black')


ax.set_xticks (np.arange (95, 106, 1))
ax.grid (alpha = 1)

plt.show ()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/example.png" width="720" height="720" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;This means that the graph will look implausible, because the dimensions will not reflect the real change in the variable, and the viewer pays attention and compares exactly the area of ​​the circles on the graph.&lt;/p&gt;
&lt;h2&gt;How to build such a graph correctly?&lt;/h2&gt;
&lt;p&gt;Fortunately, if you build bubble-charts using Python libraries (Matplotlib and Seaborn), then the size of the circle will be determined by the area, which is absolutely correct from in terms of visualization.&lt;br /&gt;
Now, using the example of real data found on Kaggle, we will show how to build a bubble-chart. The data contains the following variables: country, population size, percentage of literate population. For the chart to be readable, let’s take a subsample of the top 10 countries after sorting all the data in order of increasing GDP.&lt;/p&gt;
&lt;p&gt;First, let’s load all the necessary libraries:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, load the data, clear it from all rows with missing values ​​and transform the population of countries to millions of people:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;data = pd.read_csv ('countries of the world.csv', sep = ',')
data = data.dropna ()
data = data.sort_values ​​(by = 'Population', ascending = False)
data = data.head (10)
data ['Population'] = data ['Population']. apply (lambda x: x / 1000000)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that all the preparations are complete, you can build a bubble-chart:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sns.set (style = &amp;quot;darkgrid&amp;quot;)
fig, ax = plt.subplots (figsize = (10, 10))
g = sns.scatterplot (data = data, x = &amp;quot;Literacy (%)&amp;quot;, y = &amp;quot;GDP ($ per capita)&amp;quot;, size = &amp;quot;Population&amp;quot;, sizes = (10,1500), alpha = 0.5)
plt.xlabel (&amp;quot;Literacy (Percentage of literate citizens)&amp;quot;)
plt.ylabel (&amp;quot;GDP per Capita&amp;quot;)
plt.title ('Chart with bubbles as area', fontdict = {'fontsize': 'x-large'})

def label_point (x, y, val, ax):
    a = pd.concat ({'x': x, 'y': y, 'val': val}, axis = 1)
    for i, point in a.iterrows ():
        ax.text (point ['x'], point ['y'] + 500, str (point ['val']))

label_point (data ['Literacy (%)'], data ['GDP ($ per capita)'], data ['Country'], plt.gca ())

ax.legend (loc = 'upper left', fontsize = 'medium', title = 'Population (in mln)', title_fontsize = 'large', labelspacing = 1)

plt.show ()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/_32.png" width="720" height="720" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;This graph displays three metrics in an understandable way: the level of GDP per capita on the Y axis, the percentage of the literate population on the X axis, and the population – by the area of ​​the circle.&lt;/p&gt;
&lt;p&gt;We recommend using size of the circle to show one of the variables, if there is a need to show 3 or more variables on one chart.&lt;/p&gt;
</description>
<pubDate>Thu, 14 Oct 2021 17:58:24 +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>Target audience parsing in VK</title>
<guid isPermaLink="false">55</guid>
<link>https://en.leftjoin.ru/all/target-audience-parsing-in-vk/</link>
<comments>https://en.leftjoin.ru/all/target-audience-parsing-in-vk/</comments>
<description>
&lt;p&gt;When posting ads some platforms allow uploading the list of people who will see the ad in audience settings. There are special tools to parse ids from public pages but it’s much more interesting (and cheaper) to do it manually with Python and VK API. Today we will tell how we parsed the target audience for the LEFTJOIN promotional campaign and uploaded it to the advertising account.&lt;/p&gt;
&lt;h2&gt;Parsing of users&lt;/h2&gt;
&lt;p&gt;To send requests we will need a user token and the list of VK groups whose participants we want to get. We collected about 30 groups related to analytics, BI tools and Data Science.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import requests 
import time 

group_list = ['datacampus', '185023286', 'data_mining_in_action', '223456', '187222444', 'nta_ds_ai', 'business__intelligence', 'club1981711', 'datascience', 'ozonmasters', 'businessanalysts', 'datamining.team', 'club.shad', '174278716', 'sqlex', 'sql_helper', 'odssib', 'sapbi', 'sql_learn', 'hsespbcareer', 'smartdata', 'pomoshch_s_spss', 'dwhexpert', 'k0d_ds', 'sql_ex_ru', 'datascience_ai', 'data_club', 'mashinnoe_obuchenie_ai_big_data', 'womeninbigdata', 'introstats', 'smartdata', 'data_mining_in_action', 'dlschool_mipt'] 

token = 'your_token'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A request for getting the participants of VK groups will return a maximum of 1000 lines, to get the next 1000 ones we need to increment an offset parameter by 1. But we need to know when to stop incrementing so we will write a function that accepts an id of the group, receives the information about the number of group’s participants and returns the maximum number for the offset – the ratio of the total number of participants to 1000 as we can only get 1000 persons at a time.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_offset(group_id): 
    count = requests.get('https://api.vk.com/method/groups.getMembers', 
    params={ 
           'access_token':token, 
           'v':5.103, 
           'group_id': group_id, 
           'sort':'id_desc', 
           'offset':0, 
           'fields':'last_seen' 
    }).json()['response']['count'] 
    return count // 1000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the next step, we will write a function that accepts the group’s ID, collects all the subscribers into a list and returns it. To do this we will send requests for receiving 1000 people till the offset is over, enter the data into the list and return it. When parsing each person, we will additionally check their last visit date and if they have not logged in since the middle of November, we won’t add them. The time is indicated in unixtime format.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_users(group_id): 
    good_id_list = [] 
    offset = 0 
    max_offset = get_offset(group_id) 
    while offset &amp;lt; max_offset: 
        response = requests.get('https://api.vk.com/method/groups.getMembers', 
        params={
        'access_token':token, 
        'v':5.103, 
        'group_id': group_id, 
        'sort':'id_desc', 
        'offset':offset, 
        'fields':'last_seen' }).json()['response'] 
        offset += 1 
        for item in response['items']: 
            try: 
                if item['last_seen']['time'] &amp;gt;= 1605571200:
                    good_id_list.append(item['id']) 
            except Exception as E: 
                continue 
    return good_id_list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we will parse all groups from the list, collect the participants, and add them into the all_users list. In the end, we will transfer the list into a set and then back into a list to get rid of the duplicates as the same people might have been members of different groups. After parsing each group, we will pause the program for a second to prevent reaching the requests limit.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;all_users = [] 

for group in group_list: 
    print(group) 
    try: 
        users = get_users(group) 
        all_users.extend(users) 
        time.sleep(1) 
    except KeyError as E: 
        print(group, E) 
        continue 

all_users = list(set(all_users))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The last step will be writing each user to a file from a new line.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with open('users.txt', 'w') as f: 
    for item in all_users: 
        f.write(&amp;quot;%s\n&amp;quot; % item)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Audience in the advertising account from a file&lt;/h2&gt;
&lt;p&gt;Let’s open our VK advertising account and choose a “Retargeting” tab. Here we will find the “Create audience” button:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/Picture1.png" width="1049" height="165" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;After clicking it, a new window will pop up where we will be able to choose a file as a source and indicate the name of the audience.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/Picture2.png" width="718" height="840" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;The audience will be available some seconds after loading. First 10 minutes it will be indicated that the audience is too small, this is not true, and the panel will refresh soon if your audience really contains more than 100 people.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/Picture3.png" width="1049" height="126" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;Let’s compare the average cost of the attracted participant in our group when using the ad with automatic audience targeting and the ad with the audience that we have scraped. In the first case, the average cost is 52.4 rubles, in the second case 33.2 rubles. The selection of a quality audience by parsing data from VK helped us to reduce the average costs by 37%.&lt;br /&gt;
We have prepared this post for our advertising campaign:&lt;br /&gt;
&lt;i&gt;Hey! You see this ad because we have parsed your id and made a file targeting in VK advertising account. Do you want to know how to do this?&lt;/i&gt;&lt;br /&gt;
&lt;i&gt;LEFTJOIN – a blog about analytics, visualizations, Data Science and BI. A blog contains a lot of material on different BI and SQL tools, data visualizations and dashboards, work with different APIs (from Google Docs to social networks to the amateurs of beer) and interesting Python libraries&lt;/i&gt;.&lt;/p&gt;
</description>
<pubDate>Thu, 22 Apr 2021 16:58:41 +0300</pubDate>
</item>

<item>
<title>How to build a dashboard with Bootstrap 4 from scratch (Part 2)</title>
<guid isPermaLink="false">47</guid>
<link>https://en.leftjoin.ru/all/how-to-build-a-dashboard-with-bootstrap-4-from-scratch-part-2/</link>
<comments>https://en.leftjoin.ru/all/how-to-build-a-dashboard-with-bootstrap-4-from-scratch-part-2/</comments>
<description>
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/1-18.png" width="2000" height="1154" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Previously we shared &lt;a href="https://en.leftjoin.ru/all/how-to-build-dashboard-with-bootstrap-4-from-scratch-part-1/"&gt;how to use Bootstrap components in building dashboard layout&lt;/a&gt; and designed a simple yet flexible dashboard with a scatter plot and Russian map. In today’s material, we will continue adding more information, explore how to make Bootstrap tables responsive, and cover some complex callbacks for data acquisition.&lt;/p&gt;
&lt;h2&gt;Constructing Data Tables&lt;/h2&gt;
&lt;p&gt;All the code for populating our tables with data will be stored in &lt;span class="inline-code"&gt;get_tables.py&lt;/span&gt; , while the layout components areoutlined in &lt;span class="inline-code"&gt; application.py&lt;/span&gt;.  This article will cover the process of creating the table with top Russian Breweries,  however, you can find the code for creating the other three on Github.&lt;/p&gt;
&lt;p&gt;Data in the Top Breweries table can be filtered by city name in the dropdown menu, but the data collected in Untappd is not equally structured. Some city names are written in Latin, others in Cyrillic. So the challenge is to make the names equal for SQL queries, and here is where Google Translate comes to the rescue. Though we sill have to manually create a dictionary of city names,  since for example “Москва” can be written as “Moskva”  and not “Moscow”.  This dictionary will be used later for mapping our DataFrame before transforming it into a Bootstrap table.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
import dash_bootstrap_components as dbc
from clickhouse_driver import Client
import numpy as np
from googletrans import Translator

translator = Translator()

client = Client(host='12.34.56.78', user='default', password='', port='9000', database='')

city_names = {
   'Moskva': 'Москва',
   'Moscow': 'Москва',
   'СПБ': 'Санкт-Петербург',
   'Saint Petersburg': 'Санкт-Петербург',
   'St Petersburg': 'Санкт-Петербург',
   'Nizhnij Novgorod': 'Нижний Новгород',
   'Tula': 'Тула',
   'Nizhniy Novgorod': 'Нижний Новгород',
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Top Breweries Table&lt;/h2&gt;
&lt;p&gt;This table displays top 10 Russian breweries and their position change according to the rating. Simply put, we need to compare data for two periods, that’s [30 days ago; today] and [60 days ago; 30 days ago]. With this in mind, we will need the following headers: ranking, brewery name, position change, and number of check-ins.&lt;br /&gt;
Create the  &lt;span class="inline-code"&gt;get_top_russian_breweries&lt;/span&gt; function that would make queries to the Clickhouse DB, sort the data and return a refined Pandas DataFrame. Let’s send the following queries to obtain data for the past 30 and 60 days, ordering the results by the number of check-ins.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Querying data from the Database&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_top_russian_breweries(checkins_n=250):
   top_n_brewery_today = client.execute(f'''
      SELECT  rt.brewery_id,
              rt.brewery_name,
              beer_pure_average_mult_count/count_for_that_brewery as avg_rating,
              count_for_that_brewery as checkins FROM (
      SELECT           
              brewery_id,
              dictGet('breweries', 'brewery_name', toUInt64(brewery_id)) as brewery_name,
              sum(rating_score) AS beer_pure_average_mult_count,
              count(rating_score) AS count_for_that_brewery
          FROM beer_reviews t1
          ANY LEFT JOIN venues AS t2 ON t1.venue_id = t2.venue_id
          WHERE isNotNull(venue_id) AND (created_at &amp;gt;= (today() - 30)) AND (venue_country = 'Россия') 
          GROUP BY           
              brewery_id,
              brewery_name) rt
      WHERE (checkins&amp;gt;={checkins_n})
      ORDER BY avg_rating DESC
      LIMIT 10
      '''
   )

top_n_brewery_n_days = client.execute(f'''
  SELECT  rt.brewery_id,
          rt.brewery_name,
          beer_pure_average_mult_count/count_for_that_brewery as avg_rating,
          count_for_that_brewery as checkins FROM (
  SELECT           
          brewery_id,
          dictGet('breweries', 'brewery_name', toUInt64(brewery_id)) as brewery_name,
          sum(rating_score) AS beer_pure_average_mult_count,
          count(rating_score) AS count_for_that_brewery
      FROM beer_reviews t1
      ANY LEFT JOIN venues AS t2 ON t1.venue_id = t2.venue_id
      WHERE isNotNull(venue_id) AND (created_at &amp;gt;= (today() - 60) AND created_at &amp;lt;= (today() - 30)) AND (venue_country = 'Россия')
      GROUP BY           
          brewery_id,
          brewery_name) rt
  WHERE (checkins&amp;gt;={checkins_n})
  ORDER BY avg_rating DESC
  LIMIT 10
  '''
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;Creating two DataFrames with the received data:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;top_n = len(top_n_brewery_today)
column_names = ['brewery_id', 'brewery_name', 'avg_rating', 'checkins']

top_n_brewery_today_df = pd.DataFrame(top_n_brewery_today, columns=column_names).replace(np.nan, 0)
top_n_brewery_today_df['brewery_pure_average'] = round(top_n_brewery_today_df.avg_rating, 2)
top_n_brewery_today_df['brewery_rank'] = list(range(1, top_n + 1))

top_n_brewery_n_days = pd.DataFrame(top_n_brewery_n_days, columns=column_names).replace(np.nan, 0)
top_n_brewery_n_days['brewery_pure_average'] = round(top_n_brewery_n_days.avg_rating, 2)
top_n_brewery_n_days['brewery_rank'] = list(range(1, len(top_n_brewery_n_days) + 1))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And then calculate the position change over the period of time for each brewery received. With the try-except block, we will handle exceptions, in case, if a brewery was not yet in our database 60 days ago.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;rank_was_list = []
for brewery_id in top_n_brewery_today_df.brewery_id:
   try:
       rank_was_list.append(
           top_n_brewery_n_days[top_n_brewery_n_days.brewery_id == brewery_id].brewery_rank.item())
   except ValueError:
       rank_was_list.append('–')
top_n_brewery_today_df['rank_was'] = rank_was_list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we iterate over the columns with current and former positions. If there is no hyphen contained in, we will append an up or down arrow depending on the change.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;diff_rank_list = []
for rank_was, rank_now in zip(top_n_brewery_today_df['rank_was'], top_n_brewery_today_df['brewery_rank']):
   if rank_was != '–':
       difference = rank_was - rank_now
       if difference &amp;gt; 0:
           diff_rank_list.append(f'↑ +{difference}')
       elif difference &amp;lt; 0:
           diff_rank_list.append(f'↓ {difference}')
       else:
           diff_rank_list.append('–')
   else:
       diff_rank_list.append(rank_was)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally,  replace DataFrame headers, inserting the column with current ranking positions, where the top 3 will be displayed with the trophy emoji.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = top_n_brewery_today_df[['brewery_name', 'avg_rating', 'checkins']].round(2)
df.insert(2, 'Position change', diff_rank_list)
df.columns = ['NAME', 'RATING', 'POSITION CHANGE', 'CHECK-INS']
df.insert(0, 'RANKING', list('🏆 ' + str(i) if i in [1, 2, 3] else str(i) for i in range(1, len(df) + 1)))

return df&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Filtering data by city name&lt;/h2&gt;
&lt;p&gt;One of the main tasks we set before creating this dashboard was to find out what are the most liked breweries in a certain city. The user chooses a city in the dropdown menu and gets the results. Sound pretty simple, but is it that easy?&lt;br /&gt;
Our next step is to write a script that would update data for each city and store it in separate CSV files. As we mentioned earlier, the city names are not equally structured, so we need to use Google Translator within the if-else block, and since it may not convert some names to Cyrillic we need to explicitly specify such cases:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;en_city = venue_city
if en_city == 'Nizhnij Novgorod':
      ru_city = 'Нижний Новгород'
elif en_city == 'Perm':
      ru_city = 'Пермь'
elif en_city == 'Sergiev Posad':
      ru_city = 'Сергиев Посад'
elif en_city == 'Vladimir':
      ru_city = 'Владимир'
elif en_city == 'Yaroslavl':
      ru_city = 'Ярославль'
else:
      ru_city = translator.translate(en_city, dest='ru').text&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we need to add both city names in English and Russian to the SQL query, to receive all check-ins sent from this city.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WHERE (rt.venue_city='{ru_city}' OR rt.venue_city='{en_city}')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, we export received data into a CSV file in the following directory –  &lt;span class="inline-code"&gt;data/cities&lt;/span&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = top_n_brewery_today_df[['brewery_name', 'venue_city', 'avg_rating', 'checkins']].round(2)
df.insert(3, 'Position Change', diff_rank_list)
df.columns = ['NAME', 'CITY', 'RATING', 'POSITION CHANGE', 'CHECK-INS']
# MAPPING
df['CITY'] = df['CITY'].map(lambda x: city_names[x] if (x in city_names) else x)
# TRANSLATING
df['CITY'] = df['CITY'].map(lambda x: translator.translate(x, dest='en').text)
df.to_csv(f'data/cities/{en_city}.csv', index=False)
print(f'{en_city}.csv updated!')&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Scheduling Updates&lt;/h2&gt;
&lt;p&gt;We will use the &lt;span class="inline-code"&gt;apscheduler&lt;/span&gt;  library to automatically run the script and refresh data for each city in &lt;span class="inline-code"&gt;all_cities&lt;/span&gt; every day at 10:30 am (UTC).&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from apscheduler.schedulers.background import BackgroundScheduler
from get_tables import update_best_breweries

all_cities = sorted(['Vladimir', 'Voronezh', 'Ekaterinburg', 'Kazan', 'Red Pakhra', 'Krasnodar',
             'Kursk', 'Moscow', 'Nizhnij Novgorod', 'Perm', 'Rostov-on-Don', 'Saint Petersburg',
             'Sergiev Posad', 'Tula', 'Yaroslavl'])

scheduler = BackgroundScheduler()
@scheduler.scheduled_job('cron', hour=10, misfire_grace_time=30)
def update_data():
   for city in all_cities:
       update_best_breweries(city)
scheduler.start()&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Table from DataFrame&lt;/h2&gt;
&lt;p&gt;&lt;span class="inline-code"&gt;get_top_russian_breweries_table(venue_city, checkins_n=250)&lt;/span&gt;  will accept venue_city and checkins_n generating a Bootstrap Table with the top breweries. The second parameter value,  &lt;span class="inline-code"&gt;checkins_n&lt;/span&gt;  can be changed with the slider. If the city name is not specified, the function will return top Russian breweries table.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;if venue_city == None: 
      selected_df = get_top_russian_breweries(checkins_n)
else: 
      en_city = venue_city&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In other case the DataFrame will be constructed from a CSV file stored in &lt;span class="inline-code"&gt;data/cities/&lt;/span&gt;. Since the city column still may contain different names we should apply mapping and use a lambda expression with the &lt;span class="inline-code"&gt;map()&lt;/span&gt; method. The lambda function will compare values in the column against keys in &lt;span class="inline-code"&gt;city_names&lt;/span&gt; and if there is a match, the column value will be overwritten.&lt;br /&gt;
For instance,  if &lt;span class="inline-code"&gt;df[‘CITY’]&lt;/span&gt; contains  “СПБ”, a frequent acronym for Saint Petersburg, the value will be replaced, while for “Воронеж” it will remain unchanged.&lt;br /&gt;
And last but not least, we need to remove all duplicate rows from the table, add a column with a ranking position and return the first 10 rows. These would be the most liked breweries in a selected city.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = pd.read_csv(f'data/cities/{en_city}.csv')     
df = df.loc[df['CHECK-INS'] &amp;gt;= checkins_n]
df.drop_duplicates(subset=['NAME', 'CITY'], keep='first', inplace=True)  
df.insert(0, 'RANKING', list('🏆 ' + str(i) if i in [1, 2, 3] else str(i) for i in range(1, len(df) + 1)))
selected_df = df.head(10)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After all DataFrame manipulations, the function returns a simply styled Bootstrap table of top breweries.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Bootstrap table layout in DBC&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;table = dbc.Table.from_dataframe(selected_df, striped=False,
                                bordered=False, hover=True,
                                size='sm',
                                style={'background-color': '#ffffff',
                                       'font-family': 'Proxima Nova Regular',
                                       'text-align':'center',
                                       'fontSize': '12px'},
                                className='table borderless'
                                )

return table&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Layout structure&lt;/h2&gt;
&lt;p&gt;Add a Slider and a Dropdown menu with city names in &lt;span class="inline-code"&gt;application.py&lt;/span&gt;&lt;/p&gt;
&lt;p class="note"&gt;To learn more about the Dashboard layout structure, please refer to &lt;a href="https://en.leftjoin.ru/all/how-to-build-dashboard-with-bootstrap-4-from-scratch-part-1/"&gt;our previous guide&lt;/a&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;checkins_slider_tab_1 = dbc.CardBody(
                           dbc.FormGroup(
                               [
                                   html.H6('Number of check-ins', style={'text-align': 'center'})),
                                   dcc.Slider(
                                       id='checkin_n_tab_1',
                                       min=0,
                                       max=250,
                                       step=25,
                                       value=250,  
                                       loading_state={'is_loading': True},
                                       marks={i: i for i in list(range(0, 251, 25))}
                                   ),
                               ],
                           ),
                           style={'max-height': '80px', 
                                  'padding-top': '25px'
                                  }
                       )

top_breweries = dbc.Card(
       [
           dbc.CardBody(
               [
                   dbc.FormGroup(
                       [
                           html.H6('Filter by city', style={'text-align': 'center'}),
                           dcc.Dropdown(
                               id='city_menu',
                               options=[{'label': i, 'value': i} for i in all_cities],
                               multi=False,
                               placeholder='Select city',
                               style={'font-family': 'Proxima Nova Regular'}
                           ),
                       ],
                   ),
                   html.P(id=&amp;quot;tab-1-content&amp;quot;, className=&amp;quot;card-text&amp;quot;),
               ],
           ),
   ],
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;We’ll also need to add a callback function to update the table by dropdown menu and slider values:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;@app.callback(
   Output(&amp;quot;tab-1-content&amp;quot;, &amp;quot;children&amp;quot;), [Input(&amp;quot;city_menu&amp;quot;, &amp;quot;value&amp;quot;),
                                         Input(&amp;quot;checkin_n_tab_1&amp;quot;, &amp;quot;value&amp;quot;)]
)
def table_content(city, checkin_n):
   return get_top_russian_breweries_table(city, checkin_n)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Tada, the main table is ready! The dashboard can be used to receive up-to-date info about best Russian breweries, beers, and its rating across different regions, and help to make a better choice for an enjoyable tasting experience.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;a href="http://dashboard-final-en.us-east-2.elasticbeanstalk.com/" class="e2-text-picture-link"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/2-17.png" width="1234" height="630" alt="" /&gt;
&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;&lt;i&gt;View the code on &lt;a href="https://github.com/valiotti/leftjoin/tree/master/rutappd"&gt;GitHub&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 07 Oct 2020 16:35:17 +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>Pandas Profiling in action:  reviewing a new EDA library on Superstore Sales dataset</title>
<guid isPermaLink="false">39</guid>
<link>https://en.leftjoin.ru/all/pandas-profiling-in-action/</link>
<comments>https://en.leftjoin.ru/all/pandas-profiling-in-action/</comments>
<description>
&lt;p&gt;Before moving directly to data analysis we need to understand what type of data we are going to work with. In today’s material, we will take a closer look at the SuperStore Sales dataset, specifically at the &lt;i&gt;Orders&lt;/i&gt; column. It includes customer shopping data of a Canadian online supermarket, such as order, product and customer ids,  type of shipping, prices, product categories, names and etc. You can find more information about this dataset on &lt;a href="https://github.com/PacktPublishing/Tableau-10-Best-Practices/blob/master/Chapter%205/Sample%20-%20Superstore%20Sales%20(Excel).xls"&gt;GitHub&lt;/a&gt;. After creating a pandas DataFrame we can simply  use the &lt;span class="inline-code"&gt;describe()&lt;/span&gt; method to get a sense of our data.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd

df = pd.read_csv('superstore_sales_orders.csv', decimal=',')
df.describe(include='all')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And oftentimes it leads to such a mess:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/1-15.png" width="984" height="427" alt="" /&gt;
&lt;/div&gt;
&lt;p class="note"&gt;The source code of this library is available on &lt;a href="https://github.com/pandas-profiling/pandas-profiling"&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If we spend some time trying to get a grasp of this descriptive table,  we can find out that customers are more likely to choose “Regular air” as a shipping type or that the majority of orders were made from Ontario.  Nevertheless, there is a better tool to describe the dataset in more detail  – the pandas-profiling library.  Just pass a DataFrame to it and we will get a generated HTML page with a detailed description of our dataset:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas_profiling
profile = pandas_profiling.ProfileReport(df)
profile.to_file(&amp;quot;output.html&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/2-15.png" width="973" height="621" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;As you see, it returned a page with 6 sections, namely: overview, variables, interactions and correlations, number of missing values, and dataset samples.&lt;/p&gt;
&lt;p class="note"&gt;View a full version of the &lt;a href="http://leftjoin.ru/files/superstore.html"&gt;Pandas Profiling Report&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Data overview&lt;/h2&gt;
&lt;p&gt;Let’s move to the first subsection called “Overview”.  Pandas Profiling provided the following stats: number of variables, number of observations, missing cells, duplicates, and file size. The  &lt;span class="inline-code"&gt;Variable types&lt;/span&gt;  column shows that our DataFrame consists of 12 categorical and 9 numerical variables.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/3-14.png" width="737" height="356" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;The  “Reproduction”  subsection stores technical information,  showing how long it took to analyze the dataset,  currently installed version , configuration info and etc.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/4-12.png" width="725" height="293" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;The  “Warnings”  subsection informs about possible issues in the dataset structure. Now,  it warns us that the “Order Date” column has too many distinct values.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/5_5.png" width="712" height="485" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Variables&lt;/h2&gt;
&lt;p&gt;Moving further, this subsection contains a detailed description of each variable, displaying the number of duplicates and missing values stored, memory size, maximum and minimal values. Right next to the stats you can see the distribution of column values.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/6_6.png" width="722" height="282" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Clicking on  &lt;span class="inline-code"&gt;Toggle details&lt;/span&gt;  you will see more expanded information:  quartiles, median and other useful descriptive statistical indicators. The remaining tabs contain a histogram displayed on the main screen, top 10 frequent values and extremes.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/7_7.png" width="737" height="404" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Interactions&lt;/h2&gt;
&lt;p&gt;This section displays how variables are interconnected on a hexbin plot: The graph looks not very obvious and clear, since the legend is lacking.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/8_8.png" width="716" height="548" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Correlations&lt;/h2&gt;
&lt;p&gt;The section represents correlations between variables calculated in a variety of ways. For example, the first tab shows Pearson’s r-value. It is noticeable that &lt;span class="inline-code"&gt;Profit &lt;/span&gt; is positively correlated with  &lt;span class="inline-code"&gt;Sales&lt;/span&gt;.  You can get a detailed explanation to each coefficient by clicking on the &lt;span class="inline-code"&gt;Toggle correlation descriptions&lt;/span&gt; button.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/9_9.png" width="739" height="554" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Missing values&lt;/h2&gt;
&lt;p&gt;This section includes a bar chart, matrix, and dendrogram with the number of fields in each variable. For instance,  the  &lt;span class="inline-code"&gt;Product Base Margin&lt;/span&gt;  column is missing three values.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/10_10.png" width="739" height="411" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Samples&lt;/h2&gt;
&lt;p&gt;And the final section show the first and last 10 rows as chunks of a dataset, pretty similar to the  &lt;span class="inline-code"&gt;head()&lt;/span&gt;  method in Pandas.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://en.leftjoin.ru/pictures/11_11.png" width="661" height="469" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;p&gt;The library is definitely more focused on statistics than Pandas, one can get useful descriptive stats for each variable and see their correlation.  It provides a comprehensive report on a dataset in a user-friendly way,  allowing to undertake an initial investigation and get a sense of data.&lt;br /&gt;
Still, the library has its shortfalls. If your dataset is fairly large the report generation time may be extended up to several hours. It’s a great tool for automating EDA tasks,  however, it can’t do all the work for you and some details may be overlooked. If you are just getting started with data analysis, we would highly recommend to start it with pandas. It will solidify your knowledge and boost confidence in working with data.&lt;/p&gt;
</description>
<pubDate>Fri, 18 Sep 2020 15:37:40 +0300</pubDate>
</item>


</channel>
</rss>