{
    "version": "https:\/\/jsonfeed.org\/version\/1",
    "title": "LEFT JOIN: blog on analytics, visualisation & data science, posts tagged: Analytics engineering",
    "home_page_url": "https:\/\/en.leftjoin.ru\/tags\/analytics-engineering\/",
    "feed_url": "https:\/\/en.leftjoin.ru\/tags\/analytics-engineering\/json\/",
    "icon": "https:\/\/en.leftjoin.ru\/user\/userpic@2x.jpg",
    "author": {
        "name": "Nikolay Valiotti",
        "url": "https:\/\/en.leftjoin.ru\/",
        "avatar": "https:\/\/en.leftjoin.ru\/user\/userpic@2x.jpg"
    },
    "items": [
        {
            "id": "66",
            "url": "https:\/\/en.leftjoin.ru\/all\/python-and-lyrics-of-zemfiras-new-album-capturing-the-spirit-of\/",
            "title": "Python and lyrics of Zemfira’s new album: capturing the spirit of her songs",
            "content_html": "<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/header.jpg\" width=\"600\" height=\"600\" alt=\"\" \/>\n<\/div>\n<p>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 <a href=\"https:\/\/youtu.be\/SOx8afEUTnE\">Business Secrets with the Bukhman brothers<\/a>, 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.<\/p>\n<p class=\"note\">Listen new album in <a href=\"https:\/\/music.apple.com\/ru\/album\/бордерлайн\/1554865105\">Apple Music<\/a> \/ <a href=\"https:\/\/music.yandex.ru\/album\/14052981\">Яндекс.Музыке<\/a> \/ <a href=\"https:\/\/open.spotify.com\/album\/6khBsXmKA1FKjYVCIBy9kt\">Spotify<\/a><\/p>\n<p>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.<\/p>\n<p>For those who are bored with reading about data preparation and analysis steps, you can <a href=\"https:\/\/leftjoin.ru\/all\/borderline-text-analysis\/#result\">go directly to the results<\/a>.<\/p>\n<h2>Data preparation<\/h2>\n<p>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.<\/p>\n<pre class=\"e2-text-code\"><code>import pandas as pd\r\nimport re\r\nimport string\r\nimport pymorphy2\r\nfrom nltk.corpus import stopwords<\/code><\/pre><p>Then we create a morphological analyzer and expand the list of everything that needs to be discarded:<\/p>\n<pre class=\"e2-text-code\"><code>morph = pymorphy2.MorphAnalyzer()\r\nstopwords_list = stopwords.words('russian')\r\nstopwords_list.extend(['куплет', 'это', 'я', 'мы', 'ты', 'припев', 'аутро', 'предприпев', 'lyrics', '1', '2', '3', 'то'])\r\nstring.punctuation += '—'<\/code><\/pre><p>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:<\/p>\n<pre class=\"e2-text-code\"><code>result_dict = dict()\r\n\r\nsongs_dict = {\r\n    'snow':'снег идёт',\r\n    'crimea':'крым',\r\n    'mother':'мама',\r\n    'ostin':'остин',\r\n    'abuse':'абьюз',\r\n    'wait_for_me':'жди меня',\r\n    'tom':'том',\r\n    'come_on':'камон',\r\n    'coat':'пальто',\r\n    'this_summer':'этим летом',\r\n    'ok':'ок',\r\n    'pills':'таблетки'\r\n}<\/code><\/pre><p>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 ‘рай’.<\/p>\n<pre class=\"e2-text-code\"><code>def read_song(filename):\r\n    f = open(f'{filename}.txt', 'r').read()\r\n    f = f.replace('\\n', ' ')\r\n    return f\r\n\r\ndef clean_string(text):\r\n    text = re.split(' |:|\\.|\\(|\\)|,|&quot;|;|\/|\\n|\\t|-|\\?|\\[|\\]|!', text)\r\n    text = ' '.join([word for word in text if word not in string.punctuation])\r\n    text = text.lower()\r\n    text = ' '.join([word for word in text.split() if word not in stopwords_list])\r\n    return text\r\n\r\ndef string_to_normal_form(string):\r\n    string_lst = string.split()\r\n    for i in range(len(string_lst)):\r\n        string_lst[i] = morph.parse(string_lst[i])[0].normal_form\r\n        if (string_lst[i] == 'аду'):\r\n            string_lst[i] = 'ад'\r\n        if (string_lst[i] == 'рая'):\r\n            string_lst[i] = 'рай'\r\n    string = ' '.join(string_lst)\r\n    return string<\/code><\/pre><p>After all this preparation, we can get back to the data and process each song and read the file with the corresponding name:<\/p>\n<pre class=\"e2-text-code\"><code>name_list = []\r\ntext_list = []\r\nfor song, name in songs_dict.items():\r\n    text = string_to_normal_form(clean_string(read_song(song)))\r\n    name_list.append(name)\r\n    text_list.append(text)<\/code><\/pre><p>Then we combine everything into a DataFrame and save it as a csv-file.<\/p>\n<pre class=\"e2-text-code\"><code>df = pd.DataFrame()\r\ndf['name'] = name_list\r\ndf['text'] = text_list\r\ndf['time'] = [290, 220, 187, 270, 330, 196, 207, 188, 269, 189, 245, 244]\r\ndf.to_csv('borderline.csv', index=False)<\/code><\/pre><p>Result:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/2-table.png\" width=\"477\" height=\"365\" alt=\"\" \/>\n<\/div>\n<h2>Word cloud for the whole album<\/h2>\n<p>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:<\/p>\n<pre class=\"e2-text-code\"><code>import nltk\r\nfrom wordcloud import WordCloud\r\nimport pandas as pd\r\nimport matplotlib.pyplot as plt\r\nfrom nltk import word_tokenize, ngrams\r\n\r\n%matplotlib inline\r\nnltk.download('punkt')\r\ndf = pd.read_csv('borderline.csv')<\/code><\/pre><p>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.<\/p>\n<pre class=\"e2-text-code\"><code>fig = plt.figure()\r\nfig.patch.set_facecolor('white')\r\nplt.subplots_adjust(wspace=0.3, hspace=0.2)\r\ni = 1\r\nfor name, text in zip(df.name, df.text):\r\n    tokens = word_tokenize(text)\r\n    text_raw = &quot; &quot;.join(tokens)\r\n    wordcloud = WordCloud(colormap='PuBu', background_color='white', contour_width=10).generate(text_raw)\r\n    plt.subplot(4, 3, i, label=name,frame_on=True)\r\n    plt.tick_params(labelsize=10)\r\n    plt.imshow(wordcloud)\r\n    plt.axis(&quot;off&quot;)\r\n    plt.title(name,fontdict={'fontsize':7,'color':'grey'},y=0.93)\r\n    plt.tick_params(labelsize=10)\r\n    i += 1<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/3-wordcloud.jpg\" width=\"2560\" height=\"1707\" alt=\"\" \/>\n<\/div>\n<h2>EDA of the lyrics<\/h2>\n<p>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:<\/p>\n<pre class=\"e2-text-code\"><code>import plotly.graph_objects as go\r\nimport plotly.figure_factory as ff\r\nfrom scipy import spatial\r\nimport collections\r\nimport pymorphy2\r\nimport gensim\r\n\r\nmorph = pymorphy2.MorphAnalyzer()<\/code><\/pre><p>Firstly, we should count the overall number of words in each song, the number of unique words, and their percentage:<\/p>\n<pre class=\"e2-text-code\"><code>songs = []\r\ntotal = []\r\nuniq = []\r\npercent = []\r\n\r\nfor song, text in zip(df.name, df.text):\r\n    songs.append(song)\r\n    total.append(len(text.split()))\r\n    uniq.append(len(set(text.split())))\r\n    percent.append(round(len(set(text.split())) \/ len(text.split()), 2) * 100)<\/code><\/pre><p>All this information should be written in a DataFrame and additionally we want to count the number of words per minute for each song:<\/p>\n<pre class=\"e2-text-code\"><code>df_words = pd.DataFrame()\r\ndf_words['song'] = songs\r\ndf_words['total words'] = total\r\ndf_words['uniq words'] = uniq\r\ndf_words['percent'] = percent\r\ndf_words['time'] = df['time']\r\ndf_words['words per minute'] = round(total \/ (df['time'] \/\/ 60))\r\ndf_words = df_words[::-1]<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/4-table.png\" width=\"480\" height=\"369\" alt=\"\" \/>\n<\/div>\n<p>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.<\/p>\n<pre class=\"e2-text-code\"><code>colors_1 = ['rgba(101,181,205,255)'] * 12\r\ncolors_2 = ['rgba(62,142,231,255)'] * 12\r\n\r\nfig = go.Figure(data=[\r\n    go.Bar(name='📝 Total number of words,\r\n           text=df_words['total words'],\r\n           textposition='auto',\r\n           x=df_words.song,\r\n           y=df_words['total words'],\r\n           marker_color=colors_1,\r\n           marker=dict(line=dict(width=0)),),\r\n    go.Bar(name='🌀 Unique words',\r\n           text=df_words['uniq words'].astype(str) + '&lt;br&gt;'+ df_words.percent.astype(int).astype(str) + '%' ,\r\n           textposition='inside',\r\n           x=df_words.song,\r\n           y=df_words['uniq words'],\r\n           textfont_color='white',\r\n           marker_color=colors_2,\r\n           marker=dict(line=dict(width=0)),),\r\n])\r\n\r\nfig.update_layout(barmode='group')\r\n\r\nfig.update_layout(\r\n    title = \r\n        {'text':'&lt;b&gt;The ratio of the number of unique words to the total&lt;\/b&gt;&lt;br&gt;&lt;span style=&quot;color:#666666&quot;&gt;&lt;\/span&gt;'},\r\n    showlegend = True,\r\n    height=650,\r\n    font={\r\n        'family':'Open Sans, light',\r\n        'color':'black',\r\n        'size':14\r\n    },\r\n    plot_bgcolor='rgba(0,0,0,0)',\r\n)\r\nfig.update_layout(legend=dict(\r\n    yanchor=&quot;top&quot;,\r\n    xanchor=&quot;right&quot;,\r\n))\r\n\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/96.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<pre class=\"e2-text-code\"><code>colors_1 = ['rgba(101,181,205,255)'] * 12\r\ncolors_2 = ['rgba(238,85,59,255)'] * 12\r\n\r\nfig = go.Figure(data=[\r\n    go.Bar(name='⏱️ Track length, min.',\r\n           text=round(df_words['time'] \/ 60, 1),\r\n           textposition='auto',\r\n           x=df_words.song,\r\n           y=-df_words['time'] \/\/ 60,\r\n           marker_color=colors_1,\r\n           marker=dict(line=dict(width=0)),\r\n          ),\r\n    go.Bar(name='🔄 Words per minute',\r\n           text=df_words['words per minute'],\r\n           textposition='auto',\r\n           x=df_words.song,\r\n           y=df_words['words per minute'],\r\n           marker_color=colors_2,\r\n           textfont_color='white',\r\n           marker=dict(line=dict(width=0)),\r\n          ),\r\n])\r\n\r\nfig.update_layout(barmode='overlay')\r\n\r\nfig.update_layout(\r\n    title = \r\n        {'text':'&lt;b&gt;Track length and words per minute&lt;\/b&gt;&lt;br&gt;&lt;span style=&quot;color:#666666&quot;&gt;&lt;\/span&gt;'},\r\n    showlegend = True,\r\n    height=650,\r\n    font={\r\n        'family':'Open Sans, light',\r\n        'color':'black',\r\n        'size':14\r\n    },\r\n    plot_bgcolor='rgba(0,0,0,0)'\r\n)\r\n\r\n\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/98.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<h2>Working with Word2Vec model<\/h2>\n<p>Using the gensim module, load the model pointing to a binary file:<\/p>\n<pre class=\"e2-text-code\"><code>model = gensim.models.KeyedVectors.load_word2vec_format('model.bin', binary=True)<\/code><\/pre><p class=\"note\">Для материала мы использовали готовую обученную на Национальном Корпусе Русского Языка модель от сообщества <a href=\"https:\/\/rusvectores.org\/ru\/models\/\">RusVectōrēs<\/a><\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<pre class=\"e2-text-code\"><code>def get_vector(word_list):\r\n    vector = 0\r\n    for word in word_list:\r\n        pos = morph.parse(word)[0].tag.POS\r\n        if pos == 'INFN':\r\n            pos = 'VERB'\r\n        if pos in ['ADJF', 'PRCL', 'ADVB', 'NPRO']:\r\n            pos = 'NOUN'\r\n        if word and pos:\r\n            try:\r\n                word_pos = word + '_' + pos\r\n                this_vector = model.word_vec(word_pos)\r\n                vector += this_vector\r\n            except KeyError:\r\n                continue\r\n    return vector<\/code><\/pre><p>For each song, find a vector and select the corresponding column in the DataFrame:<\/p>\n<pre class=\"e2-text-code\"><code>vec_list = []\r\nfor word in df['text']:\r\n    vec_list.append(get_vector(word.split()))\r\ndf['vector'] = vec_list<\/code><\/pre><p>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.<\/p>\n<pre class=\"e2-text-code\"><code>similar = dict()\r\nresult = []\r\nfor song_1, vector_1 in zip(df.name, df.vector):\r\n    sub_list = []\r\n    for song_2, vector_2 in zip(df.name.iloc[::-1], df.vector.iloc[::-1]):\r\n        res = 1 - spatial.distance.cosine(vector_1, vector_2)\r\n        if res &gt; 0.5 and song_1 != song_2 and (song_1 + ' \/ ' + song_2 not in similar.keys() and song_2 + ' \/ ' + song_1 not in similar.keys()):\r\n            similar[song_1 + ' \/ ' + song_2] = round(res, 2)\r\n        sub_list.append(round(res, 2))\r\n    result.append(sub_list)<\/code><\/pre><p>Moreover, we can construct the same bar chart:<\/p>\n<pre class=\"e2-text-code\"><code>df_top_sim = pd.DataFrame()\r\ndf_top_sim['name'] = list(similar.keys())\r\ndf_top_sim['value'] = list(similar.values())\r\ndf_top_sim.sort_values(by='value', ascending=False)<\/code><\/pre><p>И построим такой же bar chart:<\/p>\n<pre class=\"e2-text-code\"><code>colors = ['rgba(101,181,205,255)'] * 5\r\n\r\nfig = go.Figure([go.Bar(x=df_top_sim['name'],\r\n                        y=df_top_sim['value'],\r\n                        marker_color=colors,\r\n                        width=[0.4,0.4,0.4,0.4,0.4],\r\n                        text=df_top_sim['value'],\r\n                        textfont_color='white',\r\n                        textposition='auto')])\r\n\r\nfig.update_layout(\r\n    title = \r\n        {'text':'&lt;b&gt;Топ-5 closest songs&lt;\/b&gt;&lt;br&gt;&lt;span style=&quot;color:#666666&quot;&gt;&lt;\/span&gt;'},\r\n    showlegend = False,\r\n    height=650,\r\n    font={\r\n        'family':'Open Sans, light',\r\n        'color':'black',\r\n        'size':14\r\n    },\r\n    plot_bgcolor='rgba(0,0,0,0)',\r\n    xaxis={'categoryorder':'total descending'}\r\n)\r\n\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/100.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>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.<\/p>\n<pre class=\"e2-text-code\"><code>def get_word_from_tlist(lst):\r\n    for word in lst:\r\n        word = word[0].split('_')[0]\r\n        print(word, end=' ')\r\n\r\nvec_sum = 0\r\nfor vec in df.vector:\r\n    vec_sum += vec\r\nsim_word = model.similar_by_vector(vec_sum)\r\nget_word_from_tlist(sim_word)<\/code><\/pre><p><span style=\"color: '#65b5cd'; font-size: 1.2em\"><b>небо тоска тьма пламень плакать горе печаль сердце солнце мрак<\/b><\/span><\/p>\n<p>This is probably the key result and the description of Zemfira’s album in just 10 words.<\/p>\n<p>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.<\/p>\n<pre class=\"e2-text-code\"><code>colorscale=[[0.0, &quot;rgba(255,255,255,255)&quot;],\r\n            [0.1, &quot;rgba(229,232,237,255)&quot;],\r\n            [0.2, &quot;rgba(216,222,232,255)&quot;],\r\n            [0.3, &quot;rgba(205,214,228,255)&quot;],\r\n            [0.4, &quot;rgba(182,195,218,255)&quot;],\r\n            [0.5, &quot;rgba(159,178,209,255)&quot;],\r\n            [0.6, &quot;rgba(137,161,200,255)&quot;],\r\n            [0.7, &quot;rgba(107,137,188,255)&quot;],\r\n            [0.8, &quot;rgba(96,129,184,255)&quot;],\r\n            [1.0, &quot;rgba(76,114,176,255)&quot;]]\r\n\r\nfont_colors = ['black']\r\nx = list(df.name.iloc[::-1])\r\ny = list(df.name)\r\nfig = ff.create_annotated_heatmap(result, x=x, y=y, colorscale=colorscale, font_colors=font_colors)\r\nfig.show()<\/code><\/pre><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/82.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<h2><a name=\"result\">Results and data interpretation<\/a><\/h2>\n<p>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:<\/p>\n<p>Давайте ещё раз посмотрим на всё, что у нас получилось — начнём с облака слов. Нетрудно заметить, что у слов «боль», «невозможно», «сорваться», «растерзаны», «сложно», «терпеть», «любить» размер весьма приличный — всё потому, что такие слова встречаются часто на протяжении всего текста песен:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/9-wordcloud.jpg\" width=\"2560\" height=\"1707\" alt=\"\" \/>\n<\/div>\n<p>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.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/96.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>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%.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/98.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>Top 5 most semantically similar text pairs:<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/100.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p>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!<\/p>\n<p><span style=\"color: '#65b5cd'; font-size: 1.2em\"><b>небо тоска тьма пламень плакать горе печаль сердце солнце мрак<\/b><\/span><\/p>\n<p>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.<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~Elisejj\/82.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<h2>Conclusions<\/h2>\n<p>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.<\/p>\n",
            "date_published": "2021-09-07T13:46:21+03:00",
            "date_modified": "2021-09-07T13:46:08+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/header.jpg",
            "_date_published_rfc2822": "Tue, 07 Sep 2021 13:46:21 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "66",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/header.jpg",
                    "https:\/\/en.leftjoin.ru\/pictures\/2-table.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-wordcloud.jpg",
                    "https:\/\/en.leftjoin.ru\/pictures\/4-table.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/9-wordcloud.jpg"
                ]
            }
        },
        {
            "id": "64",
            "url": "https:\/\/en.leftjoin.ru\/all\/data-normalization-with-sql\/",
            "title": "Data normalization with SQL",
            "content_html": "<p>According to GIGO (garbage in, garbage out) principle, errors in input data lead to erroneous analysis results. The results of our work directly depend on the quality of data preparation.<\/p>\n<p>For instance, when we need to prepare data to use in ML algorithms (like k-NN, k-means, logistic regression, etc.), features in the original dataset may vary in scale like the age and height of a person. This may lead to the incorrect performance of the algorithm. Thus, such data needs to be rescaled first.<\/p>\n<p>In this tutorial, we will consider the ways to scale the data using SQL query: min-max normalization, min-max normalization for an arbitrary range, and z-score normalization. For each of these methods we have prepared two SQL query options: one using a SELECT subquery and another using a window function OVER().<\/p>\n<p>We will work with the simple table <b>students<\/b> that contains the data on the height of the students:<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<\/tr>\n<tr>\n<td>Ivan<\/td>\n<td style=\"text-align: right\">174<\/td>\n<\/tr>\n<tr>\n<td>Peter<\/td>\n<td style=\"text-align: right\">181<\/td>\n<\/tr>\n<tr>\n<td>Dan<\/td>\n<td style=\"text-align: right\">199<\/td>\n<\/tr>\n<tr>\n<td>Kate<\/td>\n<td style=\"text-align: right\">158<\/td>\n<\/tr>\n<tr>\n<td>Mike<\/td>\n<td style=\"text-align: right\">179<\/td>\n<\/tr>\n<tr>\n<td>Silvia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<\/tr>\n<tr>\n<td>Giulia<\/td>\n<td style=\"text-align: right\">152<\/td>\n<\/tr>\n<tr>\n<td>Robert<\/td>\n<td style=\"text-align: right\">188<\/td>\n<\/tr>\n<tr>\n<td>Steven<\/td>\n<td style=\"text-align: right\">177<\/td>\n<\/tr>\n<tr>\n<td>Sophia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<\/tr>\n<\/table>\n<\/div>\n<h2>Min-max rescaling<\/h2>\n<p>Min-max scaling approach scales the data using the fixed range from 0 to 1. In this case, all the data is on the same scale which will exclude the impact of outliers on the conclusions.<\/p>\n<p>The formula for the min-max scaling is given as:<\/p>\n<p>We multiply the numerator by 1.0 in order to get a floating point number at the end.<\/p>\n<p>SQL-query with a subquery:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       1.0 * (height-t1.min_height)\/(t1.max_height - t1.min_height) AS scaled_minmax\r\n  FROM students, \r\n      (SELECT min(height) as min_height, \r\n              max(height) as max_height \r\n         FROM students\r\n      ) as t1;<\/code><\/pre><p>SQL-query with a window function:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       (height - MIN(height) OVER ()) * 1.0 \/ (MAX(height) OVER () - MIN(height) OVER ()) AS scaled_minmax\r\n  FROM students;<\/code><\/pre><p>As a result, we get the values in [0, 1] range where 0 is the height of the shortest student and 1 is the height of the tallest one.<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<td>scaled_minmax<\/td>\n<\/tr>\n<tr>\n<td>Ivan<\/td>\n<td style=\"text-align: right\">174<\/td>\n<td>0.46809<\/td>\n<\/tr>\n<tr>\n<td>Peter<\/td>\n<td style=\"text-align: right\">181<\/td>\n<td>0.61702<\/td>\n<\/tr>\n<tr>\n<td>Dan<\/td>\n<td style=\"text-align: right\">199<\/td>\n<td>1<\/td>\n<\/tr>\n<tr>\n<td>Kate<\/td>\n<td style=\"text-align: right\">158<\/td>\n<td>0.12766<\/td>\n<\/tr>\n<tr>\n<td>Mike<\/td>\n<td style=\"text-align: right\">179<\/td>\n<td>0.57447<\/td>\n<\/tr>\n<tr>\n<td>Silvia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>0.2766<\/td>\n<\/tr>\n<tr>\n<td>Giulia<\/td>\n<td style=\"text-align: right\">152<\/td>\n<td>0<\/td>\n<\/tr>\n<tr>\n<td>Robert<\/td>\n<td style=\"text-align: right\">188<\/td>\n<td>0.76596<\/td>\n<\/tr>\n<tr>\n<td>Steven<\/td>\n<td style=\"text-align: right\">177<\/td>\n<td>0.53191<\/td>\n<\/tr>\n<tr>\n<td>Sophia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>0.2766<\/td>\n<\/tr>\n<\/table>\n<\/div>\n<h2>Rescaling within a given range<\/h2>\n<p>This is an option of min-max normalization between an arbitrary set of values. When it comes to data scaling, the values do not always need to be in the range between 0 and 1. In these cases, the following formula is applied.<\/p>\n<p>This allows us to scale the data to an arbitrary scale. In our example, let’s assume a=10.0 and b=20.0.<\/p>\n<p>SQL-query with subquery:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       ((height - min_height) * (20.0 - 10.0) \/ (max_height - min_height)) + 10 AS scaled_ab\r\n  FROM students,\r\n      (SELECT MAX(height) as max_height, \r\n              MIN(height) as min_height\r\n         FROM students  \r\n      ) t1;<\/code><\/pre><p>SQL-query with a window function:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       ((height - MIN(height) OVER() ) * (20.0 - 10.0) \/ (MAX(height) OVER() - MIN(height) OVER())) + 10.0 AS scaled_ab\r\n  FROM students;<\/code><\/pre><p>We receive similar results as before, but with data spread between 10 and 20.<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<td>scaled_ab<\/td>\n<\/tr>\n<tr>\n<td>Ivan<\/td>\n<td style=\"text-align: right\">174<\/td>\n<td>14.68085<\/td>\n<\/tr>\n<tr>\n<td>Peter<\/td>\n<td style=\"text-align: right\">181<\/td>\n<td>16.17021<\/td>\n<\/tr>\n<tr>\n<td>Dan<\/td>\n<td style=\"text-align: right\">199<\/td>\n<td>20<\/td>\n<\/tr>\n<tr>\n<td>Kate<\/td>\n<td style=\"text-align: right\">158<\/td>\n<td>11.2766<\/td>\n<\/tr>\n<tr>\n<td>Mike<\/td>\n<td style=\"text-align: right\">179<\/td>\n<td>15.74468<\/td>\n<\/tr>\n<tr>\n<td>Silvia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>12.76596<\/td>\n<\/tr>\n<tr>\n<td>Giulia<\/td>\n<td style=\"text-align: right\">152<\/td>\n<td>10<\/td>\n<\/tr>\n<tr>\n<td>Robert<\/td>\n<td style=\"text-align: right\">188<\/td>\n<td>17.65957<\/td>\n<\/tr>\n<tr>\n<td>Steven<\/td>\n<td style=\"text-align: right\">177<\/td>\n<td>15.31915<\/td>\n<\/tr>\n<tr>\n<td>Sophia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>12.76596<\/td>\n<\/tr>\n<\/table>\n<\/div>\n<h2>Z-score normalization<\/h2>\n<p>Using Z-score normalization, the data will be scaled so that it has the properties of a standard normal distribution where the mean (μ) is equal to 0 and the standard deviation (σ) to 1.<\/p>\n<p>Z-score is calculated using the formula:<\/p>\n<p>SQL-query with a subquery:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       (height - t1.mean) * 1.0 \/ t1.sigma AS zscore\r\n  FROM students,\r\n      (SELECT AVG(height) AS mean, \r\n              STDDEV(height) AS sigma\r\n         FROM students\r\n        ) t1;<\/code><\/pre><p>SQL-query with a window function:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT height, \r\n       (height - AVG(height) OVER()) * 1.0 \/ STDDEV(height) OVER() AS z-score\r\n  FROM students;<\/code><\/pre><p>As a result, we can easily notice the outliers that exceed the standard deviation.<\/p>\n<div class=\"e2-text-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n<tr>\n<td>name<\/td>\n<td>height<\/td>\n<td>zscore<\/td>\n<\/tr>\n<tr>\n<td>Ivan<\/td>\n<td style=\"text-align: right\">174<\/td>\n<td>0.01488<\/td>\n<\/tr>\n<tr>\n<td>Peter<\/td>\n<td style=\"text-align: right\">181<\/td>\n<td>0.53582<\/td>\n<\/tr>\n<tr>\n<td>Dan<\/td>\n<td style=\"text-align: right\">199<\/td>\n<td>1.87538<\/td>\n<\/tr>\n<tr>\n<td>Kate<\/td>\n<td style=\"text-align: right\">158<\/td>\n<td>-1.17583<\/td>\n<\/tr>\n<tr>\n<td>Mike<\/td>\n<td style=\"text-align: right\">179<\/td>\n<td>0.38698<\/td>\n<\/tr>\n<tr>\n<td>Silvia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>-0.65489<\/td>\n<\/tr>\n<tr>\n<td>Giulia<\/td>\n<td style=\"text-align: right\">152<\/td>\n<td>-1.62235<\/td>\n<\/tr>\n<tr>\n<td>Robert<\/td>\n<td style=\"text-align: right\">188<\/td>\n<td>1.05676<\/td>\n<\/tr>\n<tr>\n<td>Steven<\/td>\n<td style=\"text-align: right\">177<\/td>\n<td>0.23814<\/td>\n<\/tr>\n<tr>\n<td>Sophia<\/td>\n<td style=\"text-align: right\">165<\/td>\n<td>-0.65489<\/td>\n<\/tr>\n<\/table>\n<\/div>\n",
            "date_published": "2021-05-21T16:58:48+03:00",
            "date_modified": "2021-05-21T16:58:37+03:00",
            "_date_published_rfc2822": "Fri, 21 May 2021 16:58:48 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "64",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": []
            }
        },
        {
            "id": "53",
            "url": "https:\/\/en.leftjoin.ru\/all\/how-to-build-animated-charts-like-hans-rosling-in-plotly\/",
            "title": "How to build Animated Charts like Hans Rosling in Plotly",
            "content_html": "<p>Hans Rosling’s work on world countries economic growth presented in 2007 at TEDTalks can be attributed to one of the most iconic data visualizations, ever created. Just check out this video, in case you don’t know what we’re talking about:<\/p>\n<div class=\"e2-text-video\">\n<iframe src=\"https:\/\/www.youtube.com\/embed\/hVimVzgtD6w\" frameborder=\"0\" allowfullscreen><\/iframe><\/div>\n<p>Sometimes we want to compare standards of living in other countries. One way to do this is to refer to the Big Mac index, which the Economist magazine has kept track of since 1986. The key idea this index represents is to measure purchasing power parity (PPP) in different countries, considering costs of domestic production. To make a standard burger, one would need the following ingredients: cheese, meat, bread and vegetables. Considering that all these ingredients can be produced locally, we can compare the production cost of one Big Mac in different countries, and measure purchasing power. Plus, McDonald’s is the world’s most popular franchise network,  its restaurants are almost everywhere around the globe.<\/p>\n<p>In today’s material, we will build a Motion Chart for the Big Mac index using Plotly. Following Hann Rosling’s idea, the chart will display country population along the X-axis and GDP per capita in US dollars along the Y. The size of the dots is going to be proportional to the Big Mac Index for a given country. And the color of the dots will represent the continent where the country is located.<\/p>\n<h2>Preparing Data<\/h2>\n<p>Even though The Economist has been updating it for over 30 years and sharing its observations publicly, the dataset contains many missing values. It also lacks continents names, but we can handle it by supplementing the data with some more datasets that can be found in our repo.<\/p>\n<p>Let’s start by importing the libraries:<\/p>\n<pre class=\"e2-text-code\"><code>import pandas as pd\r\nfrom pandas.errors import ParserError\r\nimport plotly.graph_objects as go\r\nimport numpy as np\r\nimport requests\r\nimport io<\/code><\/pre><p>We can access the dataset directly from GitHub. Just use the following function to send a GET request to a CSV file and create a Pandas DataFrame. However, in some cases, this may raise a  ParseError because of the caption title, so we will add a try block:<\/p>\n<pre class=\"e2-text-code\"><code>def read_raw_file(link):\r\n    raw_csv = requests.get(link).content\r\n    try:\r\n        df = pd.read_csv(io.StringIO(raw_csv.decode('utf-8')))\r\n    except ParserError:\r\n        df = pd.read_csv(io.StringIO(raw_csv.decode('utf-8')), skiprows=3)\r\n    return df\r\n\r\nbigmac_df = read_raw_file('https:\/\/github.com\/valiotti\/leftjoin\/raw\/master\/motion-chart-big-mac\/big-mac.csv')\r\npopulation_df = read_raw_file('https:\/\/github.com\/valiotti\/leftjoin\/raw\/master\/motion-chart-big-mac\/population.csv')\r\ndgp_df = read_raw_file('https:\/\/github.com\/valiotti\/leftjoin\/raw\/master\/motion-chart-big-mac\/gdp.csv')\r\ncontinents_df = read_raw_file('https:\/\/github.com\/valiotti\/leftjoin\/raw\/master\/motion-chart-big-mac\/continents.csv')<\/code><\/pre><p>From The Economist dataset we will need these columns: country name, local price, dollar exchange rate, country code (iso_a3) and record date. Take the timeline from 2005 to 2020, as the records are most complete for this span. And divide the local price by the exchange rate to calculate the price of one Big Mac in US dollars.<\/p>\n<pre class=\"e2-text-code\"><code>bigmac_df = bigmac_df[['name', 'local_price', 'dollar_ex', 'iso_a3', 'date']]\r\nbigmac_df = bigmac_df[bigmac_df['date'] &gt;= '2005-01-01']\r\nbigmac_df = bigmac_df[bigmac_df['date'] &lt; '2020-01-01']\r\nbigmac_df['date'] = pd.DatetimeIndex(bigmac_df['date']).year\r\nbigmac_df = bigmac_df.drop_duplicates(['date', 'name'])\r\nbigmac_df = bigmac_df.reset_index(drop=True)\r\nbigmac_df['dollar_price'] = bigmac_df['local_price'] \/ bigmac_df['dollar_ex']<\/code><\/pre><p>Take a look at the result:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/1_1.png\" width=\"465\" height=\"182\" alt=\"\" \/>\n<\/div>\n<p>Next, let’s try adding a new column called continents. To ease the task, leave only two columns containing country code and continent name. Then we need to iterate through the bigmac_df[‘iso_a3’] column, adding a continent name for the corresponding values. However some cases may raise an error, because it’s not really clear, whether a country belongs to Europe or Asia, we will consider such cases as Europe by default.<\/p>\n<pre class=\"e2-text-code\"><code>continents_df = continents_df[['Continent_Name', 'Three_Letter_Country_Code']]\r\ncontinents_list = []\r\nfor country in bigmac_df['iso_a3']:\r\n    try:\r\n        continents_list.append(continents_df.loc[continents_df['Three_Letter_Country_Code'] == country]['Continent_Name'].item())\r\n    except ValueError:\r\n        continents_list.append('Europe')\r\nbigmac_df['continent'] = continents_list<\/code><\/pre><p>Now we can drop unnecessary columns,  apply sorting by country names and date, convert values in the date column into integers, and view the current result:<\/p>\n<pre class=\"e2-text-code\"><code>bigmac_df = bigmac_df.drop(['local_price', 'iso_a3', 'dollar_ex'], axis=1)\r\nbigmac_df = bigmac_df.sort_values(by=['name', 'date'])\r\nbigmac_df['date'] = bigmac_df['date'].astype(int)<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/2-20.png\" width=\"321\" height=\"176\" alt=\"\" \/>\n<\/div>\n<p>Then we need to fill up missing values for The Big Mac index with zeros and remove the Republic of China, since this partially recognized state is not included in the <a href=\"https:\/\/data.worldbank.org\">World Bank<\/a> datasets. The UAE occurs several times, this can lead to issues.<\/p>\n<pre class=\"e2-text-code\"><code>countries_list = list(bigmac_df['name'].unique())\r\nyears_set = {i for i in range(2005, 2020)}\r\nfor country in countries_list:\r\n    if len(bigmac_df[bigmac_df['name'] == country]) &lt; 15:\r\n        this_continent = bigmac_df[bigmac_df['name'] == country].continent.iloc[0]\r\n        years_of_country = set(bigmac_df[bigmac_df['name'] == country]['date'])\r\n        diff = years_set - years_of_country\r\n        dict_to_df = pd.DataFrame({\r\n                      'name':[country] * len(diff),\r\n                      'date':list(diff),\r\n                      'dollar_price':[0] * len(diff),\r\n                      'continent': [this_continent] * len(diff)\r\n                     })\r\n        bigmac_df = bigmac_df.append(dict_to_df)\r\nbigmac_df = bigmac_df[bigmac_df['name'] != 'Taiwan']\r\nbigmac_df = bigmac_df[bigmac_df['name'] != 'United Arab Emirates']<\/code><\/pre><p>Next,  let’s augment the data with GDP per capita and population from other datasets. Both datasets have differences in country names, so we need to specify such cases explicitly and replace them.<\/p>\n<pre class=\"e2-text-code\"><code>years = [str(i) for i in range(2005, 2020)]\r\n\r\ncountries_replace_dict = {\r\n    'Russian Federation': 'Russia',\r\n    'Egypt, Arab Rep.': 'Egypt',\r\n    'Hong Kong SAR, China': 'Hong Kong',\r\n    'United Kingdom': 'Britain',\r\n    'Korea, Rep.': 'South Korea',\r\n    'United Arab Emirates': 'UAE',\r\n    'Venezuela, RB': 'Venezuela'\r\n}\r\nfor key, value in countries_replace_dict.items():\r\n    population_df['Country Name'] = population_df['Country Name'].replace(key, value)\r\n    gdp_df['Country Name'] = gdp_df['Country Name'].replace(key, value)<\/code><\/pre><p>Finally, extract population data and GDP for the given years, adding the data to the bigmac_df DataFrame:<\/p>\n<pre class=\"e2-text-code\"><code>countries_list = list(bigmac_df['name'].unique())\r\n\r\npopulation_list = []\r\ngdp_list = []\r\nfor country in countries_list:\r\n    population_for_country_df = population_df[population_df['Country Name'] == country][years]\r\n    population_list.extend(list(population_for_country_df.values[0]))\r\n    gdp_for_country_df = gdp_df[gdp_df['Country Name'] == country][years]\r\n    gdp_list.extend(list(gdp_for_country_df.values[0]))\r\n    \r\nbigmac_df['population'] = population_list\r\nbigmac_df['gdp'] = gdp_list\r\nbigmac_df['gdp_per_capita'] = bigmac_df['gdp'] \/ bigmac_df['population']<\/code><\/pre><p>And here is our final dataset:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/3-16.png\" width=\"590\" height=\"179\" alt=\"\" \/>\n<\/div>\n<h2>Creating a chart in Plotly<\/h2>\n<p>The population in China or India, on average, is 10 times more than in other countries. That’s why we need to transform X-axis to Log Scale, to make the chart easier for interpreting. The log-transformation is a common way to address skewness in data.<\/p>\n<pre class=\"e2-text-code\"><code>fig_dict = {\r\n    &quot;data&quot;: [],\r\n    &quot;layout&quot;: {},\r\n    &quot;frames&quot;: []\r\n}\r\n\r\nfig_dict[&quot;layout&quot;][&quot;xaxis&quot;] = {&quot;title&quot;: &quot;Population&quot;, &quot;type&quot;: &quot;log&quot;}\r\nfig_dict[&quot;layout&quot;][&quot;yaxis&quot;] = {&quot;title&quot;: &quot;GDP per capita (in $)&quot;, &quot;range&quot;:[-10000, 120000]}\r\nfig_dict[&quot;layout&quot;][&quot;hovermode&quot;] = &quot;closest&quot;\r\nfig_dict[&quot;layout&quot;][&quot;updatemenus&quot;] = [\r\n    {\r\n        &quot;buttons&quot;: [\r\n            {\r\n                &quot;args&quot;: [None, {&quot;frame&quot;: {&quot;duration&quot;: 500, &quot;redraw&quot;: False},\r\n                                &quot;fromcurrent&quot;: True, &quot;transition&quot;: {&quot;duration&quot;: 300,\r\n                                                                    &quot;easing&quot;: &quot;quadratic-in-out&quot;}}],\r\n                &quot;label&quot;: &quot;Play&quot;,\r\n                &quot;method&quot;: &quot;animate&quot;\r\n            },\r\n            {\r\n                &quot;args&quot;: [[None], {&quot;frame&quot;: {&quot;duration&quot;: 0, &quot;redraw&quot;: False},\r\n                                  &quot;mode&quot;: &quot;immediate&quot;,\r\n                                  &quot;transition&quot;: {&quot;duration&quot;: 0}}],\r\n                &quot;label&quot;: &quot;Pause&quot;,\r\n                &quot;method&quot;: &quot;animate&quot;\r\n            }\r\n        ],\r\n        &quot;direction&quot;: &quot;left&quot;,\r\n        &quot;pad&quot;: {&quot;r&quot;: 10, &quot;t&quot;: 87},\r\n        &quot;showactive&quot;: False,\r\n        &quot;type&quot;: &quot;buttons&quot;,\r\n        &quot;x&quot;: 0.1,\r\n        &quot;xanchor&quot;: &quot;right&quot;,\r\n        &quot;y&quot;: 0,\r\n        &quot;yanchor&quot;: &quot;top&quot;\r\n    }\r\n]<\/code><\/pre><p>We will also add a slider to filter data within a certain range:<\/p>\n<pre class=\"e2-text-code\"><code>sliders_dict = {\r\n    &quot;active&quot;: 0,\r\n    &quot;yanchor&quot;: &quot;top&quot;,\r\n    &quot;xanchor&quot;: &quot;left&quot;,\r\n    &quot;currentvalue&quot;: {\r\n        &quot;font&quot;: {&quot;size&quot;: 20},\r\n        &quot;prefix&quot;: &quot;Year: &quot;,\r\n        &quot;visible&quot;: True,\r\n        &quot;xanchor&quot;: &quot;right&quot;\r\n    },\r\n    &quot;transition&quot;: {&quot;duration&quot;: 300, &quot;easing&quot;: &quot;cubic-in-out&quot;},\r\n    &quot;pad&quot;: {&quot;b&quot;: 10, &quot;t&quot;: 50},\r\n    &quot;len&quot;: 0.9,\r\n    &quot;x&quot;: 0.1,\r\n    &quot;y&quot;: 0,\r\n    &quot;steps&quot;: []\r\n}<\/code><\/pre><p>By default, the chart will display data for 2005 before we click on the “Play” button.<\/p>\n<pre class=\"e2-text-code\"><code>continents_list_from_df = list(bigmac_df['continent'].unique())\r\nyear = 2005\r\nfor continent in continents_list_from_df:\r\n    dataset_by_year = bigmac_df[bigmac_df[&quot;date&quot;] == year]\r\n    dataset_by_year_and_cont = dataset_by_year[dataset_by_year[&quot;continent&quot;] == continent]\r\n    \r\n    data_dict = {\r\n        &quot;x&quot;: dataset_by_year_and_cont[&quot;population&quot;],\r\n        &quot;y&quot;: dataset_by_year_and_cont[&quot;gdp_per_capita&quot;],\r\n        &quot;mode&quot;: &quot;markers&quot;,\r\n        &quot;text&quot;: dataset_by_year_and_cont[&quot;name&quot;],\r\n        &quot;marker&quot;: {\r\n            &quot;sizemode&quot;: &quot;area&quot;,\r\n            &quot;sizeref&quot;: 200000,\r\n            &quot;size&quot;:  np.array(dataset_by_year_and_cont[&quot;dollar_price&quot;]) * 20000000\r\n        },\r\n        &quot;name&quot;: continent,\r\n        &quot;customdata&quot;: np.array(dataset_by_year_and_cont[&quot;dollar_price&quot;]).round(1),\r\n        &quot;hovertemplate&quot;: '&lt;b&gt;%{text}&lt;\/b&gt;' + '&lt;br&gt;' +\r\n                         'GDP per capita: %{y}' + '&lt;br&gt;' +\r\n                         'Population: %{x}' + '&lt;br&gt;' +\r\n                         'Big Mac price: %{customdata}$' +\r\n                         '&lt;extra&gt;&lt;\/extra&gt;'\r\n    }\r\n    fig_dict[&quot;data&quot;].append(data_dict)<\/code><\/pre><p>Next, we need to fill up the frames field, which will be used for animating the data. Each frame represents a certain data point from 2005 to 2019.<\/p>\n<pre class=\"e2-text-code\"><code>for year in years:\r\n    frame = {&quot;data&quot;: [], &quot;name&quot;: str(year)}\r\n    for continent in continents_list_from_df:\r\n        dataset_by_year = bigmac_df[bigmac_df[&quot;date&quot;] == int(year)]\r\n        dataset_by_year_and_cont = dataset_by_year[dataset_by_year[&quot;continent&quot;] == continent]\r\n\r\n        data_dict = {\r\n            &quot;x&quot;: list(dataset_by_year_and_cont[&quot;population&quot;]),\r\n            &quot;y&quot;: list(dataset_by_year_and_cont[&quot;gdp_per_capita&quot;]),\r\n            &quot;mode&quot;: &quot;markers&quot;,\r\n            &quot;text&quot;: list(dataset_by_year_and_cont[&quot;name&quot;]),\r\n            &quot;marker&quot;: {\r\n                &quot;sizemode&quot;: &quot;area&quot;,\r\n                &quot;sizeref&quot;: 200000,\r\n                &quot;size&quot;: np.array(dataset_by_year_and_cont[&quot;dollar_price&quot;]) * 20000000\r\n            },\r\n            &quot;name&quot;: continent,\r\n            &quot;customdata&quot;: np.array(dataset_by_year_and_cont[&quot;dollar_price&quot;]).round(1),\r\n            &quot;hovertemplate&quot;: '&lt;b&gt;%{text}&lt;\/b&gt;' + '&lt;br&gt;' +\r\n                             'GDP per capita: %{y}' + '&lt;br&gt;' +\r\n                             'Population: %{x}' + '&lt;br&gt;' +\r\n                             'Big Mac price: %{customdata}$' +\r\n                             '&lt;extra&gt;&lt;\/extra&gt;'\r\n        }\r\n        frame[&quot;data&quot;].append(data_dict)\r\n\r\n    fig_dict[&quot;frames&quot;].append(frame)\r\n    slider_step = {&quot;args&quot;: [\r\n        [year],\r\n        {&quot;frame&quot;: {&quot;duration&quot;: 300, &quot;redraw&quot;: False},\r\n         &quot;mode&quot;: &quot;immediate&quot;,\r\n         &quot;transition&quot;: {&quot;duration&quot;: 300}}\r\n    ],\r\n        &quot;label&quot;: year,\r\n        &quot;method&quot;: &quot;animate&quot;}\r\n    sliders_dict[&quot;steps&quot;].append(slider_step)<\/code><\/pre><p>Just a few finishing touches left, instantiate the chart, set colors, fonts and title.<\/p>\n<pre class=\"e2-text-code\"><code>fig_dict[&quot;layout&quot;][&quot;sliders&quot;] = [sliders_dict]\r\n\r\nfig = go.Figure(fig_dict)\r\n\r\nfig.update_layout(\r\n    title = \r\n        {'text':'&lt;b&gt;Motion chart&lt;\/b&gt;&lt;br&gt;&lt;span style=&quot;color:#666666&quot;&gt;The Big Mac index from 2005 to 2019&lt;\/span&gt;'},\r\n    font={\r\n        'family':'Open Sans, light',\r\n        'color':'black',\r\n        'size':14\r\n    },\r\n    plot_bgcolor='rgba(0,0,0,0)'\r\n)\r\nfig.update_yaxes(nticks=4)\r\nfig.update_xaxes(tickfont=dict(family='Open Sans, light', color='black', size=12), nticks=4, gridcolor='lightgray', gridwidth=0.5)\r\nfig.update_yaxes(tickfont=dict(family='Open Sans, light', color='black', size=12), nticks=4, gridcolor='lightgray', gridwidth=0.5)\r\n\r\nfig.show()<\/code><\/pre><p>Bingo! The Motion Chart is done:<\/p>\n<iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\" seamless=\"seamless\" src=\"https:\/\/plotly.com\/~i-bond\/32.embed?showlink=false\" height=\"650\" width=\"100%\"><\/iframe>\n<p><i>View the code on <a href=\"https:\/\/github.com\/valiotti\/leftjoin\/tree\/master\/motion-chart-big-mac\">GitHub<\/a><\/i><\/p>\n",
            "date_published": "2020-10-30T15:35:40+03:00",
            "date_modified": "2020-10-30T15:40:47+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/remote\/youtube-hVimVzgtD6w-cover.jpg",
            "_date_published_rfc2822": "Fri, 30 Oct 2020 15:35:40 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "53",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/remote\/youtube-hVimVzgtD6w-cover.jpg",
                    "https:\/\/en.leftjoin.ru\/pictures\/1_1.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/2-20.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-16.png"
                ]
            }
        },
        {
            "id": "46",
            "url": "https:\/\/en.leftjoin.ru\/all\/collecting-social-media-data-for-top-ml-ai-data-science-related\/",
            "title": "Collecting Social Media Data for Top ML, AI &amp; Data Science related accounts on Instagram",
            "content_html": "<p>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.<\/p>\n<p><b>Data collection method<\/b><br \/>\nThe 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:<\/p>\n<pre class=\"e2-text-code\"><code>https:\/\/instagram.com\/leftjoin\/?__a=1<\/code><\/pre><p>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 <a href=\"https:\/\/github.com\/OlegYurchik\/pyInstagram\">pyInstagram<\/a> library allows sending such requests.<\/p>\n<p><b>SQL schema<\/b><br \/>\nData 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.<\/p>\n<pre class=\"e2-text-code\"><code>CREATE TABLE instagram.users\r\n(\r\n    `added_at` DateTime,\r\n    `user_id` UInt64,\r\n    `user_name` String,\r\n    `full_name` String,\r\n    `base_url` String,\r\n    `biography` String,\r\n    `followers_count` UInt64,\r\n    `follows_count` UInt64,\r\n    `media_count` UInt64,\r\n    `total_comments` UInt64,\r\n    `total_likes` UInt64,\r\n    `is_verified` UInt8,\r\n    `country_block` UInt8,\r\n    `profile_pic_url` Nullable(String),\r\n    `profile_pic_url_hd` Nullable(String),\r\n    `fb_page` Nullable(String)\r\n)\r\nENGINE = ReplacingMergeTree\r\nORDER BY added_at<\/code><\/pre><p>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: <span class=\"inline-code\">is_ad<\/span>, <span class=\"inline-code\">is_album<\/span> and <span class=\"inline-code\">is_video<\/span>.<\/p>\n<pre class=\"e2-text-code\"><code>CREATE TABLE instagram.posts\r\n(\r\n    `added_at` DateTime,\r\n    `owner` String,\r\n    `post_id` UInt64,\r\n    `caption` Nullable(String),\r\n    `code` String,\r\n    `comments_count` UInt64,\r\n    `comments_disabled` UInt8,\r\n    `created_at` DateTime,\r\n    `display_url` String,\r\n    `is_ad` UInt8,\r\n    `is_album` UInt8,\r\n    `is_video` UInt8,\r\n    `likes_count` UInt64,\r\n    `location` Nullable(String),\r\n    `recources` Array(String),\r\n    `video_url` Nullable(String)\r\n)\r\nENGINE = ReplacingMergeTree\r\nORDER BY added_at<\/code><\/pre><p>In the comments table, we store each comment separately with the comment owner and text.<\/p>\n<pre class=\"e2-text-code\"><code>CREATE TABLE instagram.comments\r\n(\r\n    `added_at` DateTime,\r\n    `comment_id` UInt64,\r\n    `post_id` UInt64,\r\n    `comment_owner` String,\r\n    `comment_text` String\r\n)\r\nENGINE = ReplacingMergeTree\r\nORDER BY added_at<\/code><\/pre><p><b>Writing the script<\/b><br \/>\nImport the following classes from the library: <span class=\"inline-code\">Account<\/span>, <span class=\"inline-code\">Media<\/span>, <span class=\"inline-code\">WebAgent<\/span> and <span class=\"inline-code\">Comment<\/span>.<\/p>\n<pre class=\"e2-text-code\"><code>from instagram import Account, Media, WebAgent, Comment\r\nfrom datetime import datetime\r\nfrom clickhouse_driver import Client\r\nimport requests\r\nimport pandas as pd<\/code><\/pre><p>Next, create an instance of the <span class=\"inline-code\">WebAgent<\/span> 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.<\/p>\n<pre class=\"e2-text-code\"><code>agent = WebAgent()\r\nqueries_list = ['machine learning', 'data science', 'data analytics', 'analytics', 'business intelligence',\r\n                'data engineering', 'computer science', 'big data', 'artificial intelligence',\r\n                'deep learning', 'data scientist','machine learning engineer', 'data engineer']\r\nclient = Client(host='12.34.56.789', user='default', password='', port='9000', database='instagram')\r\nurl = 'https:\/\/www.instagram.com\/web\/search\/topsearch\/?context=user&amp;count=0'<\/code><\/pre><p>Let’s iterate the keywords collecting all matching accounts. Then remove duplicates from the obtained list by converting it to set and back.<\/p>\n<pre class=\"e2-text-code\"><code>response_list = []\r\nfor query in queries_list:\r\n    response = requests.get(url, params={\r\n        'query': query\r\n    }).json()\r\n    response_list.extend(response['users'])\r\ninstagram_pages_list = []\r\nfor item in response_list:\r\n    instagram_pages_list.append(item['user']['username'])\r\ninstagram_pages_list = list(set(instagram_pages_list))<\/code><\/pre><p>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.<br \/>\nThen update the account information using the agent.update()<br \/>\nmethod. We will collect only the first 100 posts to keep it moving. Next, create a list named  <span class=\"inline-code\">media_list<\/span> to store received post ids after calling the <span class=\"inline-code\">agent.get_media()<\/span> method.<\/p>\n<p><details><br \/>\n<summary><span style=\"color:#7ea9b8\">Collecting user media data<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>all_posts_list = []\r\nusername_count = 0\r\nfor username in instagram_pages_list:\r\n    if client.execute(f&quot;SELECT count(1) FROM users WHERE user_name='{username}'&quot;)[0][0] == 0:\r\n        print('username:', username_count, '\/', len(instagram_pages_list))\r\n        username_count += 1\r\n        account_total_likes = 0\r\n        account_total_comments = 0\r\n        try:\r\n            account = Account(username)\r\n        except Exception as E:\r\n            print(E)\r\n            continue\r\n        try:\r\n            agent.update(account)\r\n        except Exception as E:\r\n            print(E)\r\n            continue\r\n        if account.media_count &lt; 100:\r\n            post_count = account.media_count\r\n        else:\r\n            post_count = 100\r\n        print(account, post_count)\r\n        media_list, _ = agent.get_media(account, count=post_count, delay=1)\r\n        count = 0<\/code><\/pre><p><\/details><\/p>\n<p>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 <span class=\"inline-code\">Media<\/span> class:<\/p>\n<p><details><br \/>\n<summary><span style=\"color:#7ea9b8\">Collecting user posts<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>for media_code in media_list:\r\n            if client.execute(f&quot;SELECT count(1) FROM posts WHERE code='{media_code}'&quot;)[0][0] == 0:\r\n                print('posts:', count, '\/', len(media_list))\r\n                count += 1\r\n\r\n                post_insert_list = []\r\n                post = Media(media_code)\r\n                agent.update(post)\r\n                post_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))\r\n                post_insert_list.append(str(post.owner))\r\n                post_insert_list.append(post.id)\r\n                if post.caption is not None:\r\n                    post_insert_list.append(post.caption.replace(&quot;'&quot;,&quot;&quot;).replace('&quot;', ''))\r\n                else:\r\n                    post_insert_list.append(&quot;&quot;)\r\n                post_insert_list.append(post.code)\r\n                post_insert_list.append(post.comments_count)\r\n                post_insert_list.append(int(post.comments_disabled))\r\n                post_insert_list.append(datetime.fromtimestamp(post.date).strftime('%Y-%m-%d %H:%M:%S'))\r\n                post_insert_list.append(post.display_url)\r\n                try:\r\n                    post_insert_list.append(int(post.is_ad))\r\n                except TypeError:\r\n                    post_insert_list.append('cast(Null as Nullable(UInt8))')\r\n                post_insert_list.append(int(post.is_album))\r\n                post_insert_list.append(int(post.is_video))\r\n                post_insert_list.append(post.likes_count)\r\n                if post.location is not None:\r\n                    post_insert_list.append(post.location)\r\n                else:\r\n                    post_insert_list.append('')\r\n                post_insert_list.append(post.resources)\r\n                if post.video_url is not None:\r\n                    post_insert_list.append(post.video_url)\r\n                else:\r\n                    post_insert_list.append('')\r\n                account_total_likes += post.likes_count\r\n                account_total_comments += post.comments_count\r\n                try:\r\n                    client.execute(f'''\r\n                        INSERT INTO posts VALUES {tuple(post_insert_list)}\r\n                    ''')\r\n                except Exception as E:\r\n                    print('posts:')\r\n                    print(E)\r\n                    print(post_insert_list)<\/code><\/pre><p><\/details><\/p>\n<p>Store comments in the variable with the same name after calling the <span class=\"inline-code\">get_comments()<\/span> method:<br \/>\n<details><br \/>\n<summary><span style=\"color:#7ea9b8\">Collecting post comments<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>comments = agent.get_comments(media=post)\r\n                for comment_id in comments[0]:\r\n                    comment_insert_list = []\r\n                    comment = Comment(comment_id)\r\n                    comment_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))\r\n                    comment_insert_list.append(comment.id)\r\n                    comment_insert_list.append(post.id)\r\n                    comment_insert_list.append(str(comment.owner))\r\n                    comment_insert_list.append(comment.text.replace(&quot;'&quot;,&quot;&quot;).replace('&quot;', ''))\r\n                    try:\r\n                        client.execute(f'''\r\n                            INSERT INTO comments VALUES {tuple(comment_insert_list)}\r\n                        ''')\r\n                    except Exception as E:\r\n                        print('comments:')\r\n                        print(E)\r\n                        print(comment_insert_list)<\/code><\/pre><p><\/details><\/p>\n<p>And now, when we have obtained user posts and comments new information can be added to the table.<br \/>\n<details><br \/>\n<summary><span style=\"color:#7ea9b8\">Collecting user data<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>user_insert_list = []\r\n        user_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))\r\n        user_insert_list.append(account.id)\r\n        user_insert_list.append(account.username)\r\n        user_insert_list.append(account.full_name)\r\n        user_insert_list.append(account.base_url)\r\n        user_insert_list.append(account.biography)\r\n        user_insert_list.append(account.followers_count)\r\n        user_insert_list.append(account.follows_count)\r\n        user_insert_list.append(account.media_count)\r\n        user_insert_list.append(account_total_comments)\r\n        user_insert_list.append(account_total_likes)\r\n        user_insert_list.append(int(account.is_verified))\r\n        user_insert_list.append(int(account.country_block))\r\n        user_insert_list.append(account.profile_pic_url)\r\n        user_insert_list.append(account.profile_pic_url_hd)\r\n        if account.fb_page is not None:\r\n            user_insert_list.append(account.fb_page)\r\n        else:\r\n            user_insert_list.append('')\r\n        try:\r\n            client.execute(f'''\r\n                INSERT INTO users VALUES {tuple(user_insert_list)}\r\n            ''')\r\n        except Exception as E:\r\n            print('users:')\r\n            print(E)\r\n            print(user_insert_list)<\/code><\/pre><p><\/details><\/p>\n<p><b>Conclusion<\/b><br \/>\nTo 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 & Data Science related most followed accounts for today.<\/p>\n<pre class=\"e2-text-code\"><code>SELECT *\r\nFROM users\r\nORDER BY followers_count DESC\r\nLIMIT 10<\/code><\/pre><p>And as a bonus, here is a list of the most interesting Instagram accounts on this  topic:<\/p>\n<ol start=\"1\">\n<li><a href=\"https:\/\/www.instagram.com\/ai_machine_learning\/\">@ai_machine_learning<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/neuralnine\/\">@neuralnine<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/datascienceinfo\/\">@datascienceinfo<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/compscistuff\/\">@compscistuff<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/computersciencelife\/\">@computersciencelife<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/welcome.ai\/\">@welcome.ai<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/papa_programmer\/\">@papa_programmer<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/data_science_learn\/\">@data_science_learn<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/neuralnet.ai\/\">@neuralnet.ai<\/a><\/li>\n<li><a href=\"https:\/\/www.instagram.com\/techno_thinkers\/\">@techno_thinkers<\/a><\/li>\n<\/ol>\n<p><i>View the code on <a href=\"https:\/\/github.com\/valiotti\/leftjoin\/tree\/master\/instagram\">GitHub<\/a><\/i><\/p>\n",
            "date_published": "2020-09-30T16:06:11+03:00",
            "date_modified": "2020-09-30T16:13:40+03:00",
            "_date_published_rfc2822": "Wed, 30 Sep 2020 16:06:11 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "46",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": []
            }
        },
        {
            "id": "44",
            "url": "https:\/\/en.leftjoin.ru\/all\/analyzing-business-intelligence-bi-and-analytics-job-market-in-t\/",
            "title": "Analyzing Business Intelligence (BI) and Analytics  job market in Tableau",
            "content_html": "<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/1.1.png\" width=\"2000\" height=\"1154\" alt=\"\" \/>\n<\/div>\n<p>According to the <a href=\"https:\/\/www.similarweb.com\/website\/hh.ru\/\">SimilarWeb rating<\/a>,  <a href=\"https:\/\/hh.ru\/\">hh.ru<\/a> is the third among the most popular job search websites in the world. In one of the conversations with <a href=\"https:\/\/t.me\/revealthedata\">Roman Bunin<\/a>, we came up with the idea of making a common project and collect data using the HeadHunter API  for later analysis and visualization in Tableau Public. Our goal was to understand the dependency between salary and skills specified in a job posting and compare how things are in Moscow, Saint Petersburg, and other regions.<\/p>\n<h2>Data Collection Process<\/h2>\n<p>Our scheme is based on fetching a  <a href=\"https:\/\/github.com\/hhru\/api\/blob\/master\/docs\/vacancies.md#короткое-представление-вакансии\">brief job description<\/a>,  returned by <a href=\"https:\/\/github.com\/hhru\/api\/blob\/master\/docs\/vacancies.md#item\">the GET \/vacancies<\/a> method. According to the structure we need to create the following columns: vacancy type, id,  vacancy rate (‘premium’), pre-employment testing (‘has_test’),  company address,  salary,  work schedule, and so forth. We created a table using the following CREATE query down below:<\/p>\n<p><details><br \/>\n<summary><span style=\"color:#7ea9b8\">Query for creating the vacancies_short  table in ClickHouse<br \/>\n<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>CREATE TABLE headhunter.vacancies_short\r\n(\r\n    `added_at` DateTime,\r\n    `query_string` String,\r\n    `type` String,\r\n    `level` String,\r\n    `direction` String,\r\n    `vacancy_id` UInt64,\r\n    `premium` UInt8,\r\n    `has_test` UInt8,\r\n    `response_url` String,\r\n    `address_city` String,\r\n    `address_street` String,\r\n    `address_building` String,\r\n    `address_description` String,\r\n    `address_lat` String,\r\n    `address_lng` String,\r\n    `address_raw` String,\r\n    `address_metro_stations` String,\r\n    `alternate_url` String,\r\n    `apply_alternate_url` String,\r\n    `department_id` String,\r\n    `department_name` String,\r\n    `salary_from` Nullable(Float64),\r\n    `salary_to` Nullable(Float64),\r\n    `salary_currency` String,\r\n    `salary_gross` Nullable(UInt8),\r\n    `name` String,\r\n    `insider_interview_id` Nullable(UInt64),\r\n    `insider_interview_url` String,\r\n    `area_url` String,\r\n    `area_id` UInt64,\r\n    `area_name` String,\r\n    `url` String,\r\n    `published_at` DateTime,\r\n    `employer_url` String,\r\n    `employer_alternate_url` String,\r\n    `employer_logo_urls_90` String,\r\n    `employer_logo_urls_240` String,\r\n    `employer_logo_urls_original` String,\r\n    `employer_name` String,\r\n    `employer_id` UInt64,\r\n    `response_letter_required` UInt8,\r\n    `type_id` String,\r\n    `type_name` String,\r\n    `archived` UInt8,\r\n    `schedule_id` Nullable(String)\r\n)\r\nENGINE = ReplacingMergeTree\r\nORDER BY vacancy_id<\/code><\/pre><p><\/details><\/p>\n<p>The first script collects data from the HeadHunter website through API and inserts to our Database using the following libraries:<\/p>\n<pre class=\"e2-text-code\"><code>import requests\r\nfrom clickhouse_driver import Client\r\nfrom datetime import datetime\r\nimport pandas as pd\r\nimport re<\/code><\/pre><p>Next, we create a DataFrame and connect to the Database in ClickHouse:<\/p>\n<pre class=\"e2-text-code\"><code>queries = pd.read_csv('hh_data.csv')\r\nclient = Client(host='1.234.567.890', user='default', password='', port='9000', database='headhunter')<\/code><\/pre><p>The queries table stores a list of our search queries, having the following columns: query type,  level,  career field, and search phrase.  The last column contains logical operators, for instance, we can get more results by putting logical ANDs between “Python”, “data” and “analysis”.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/edata@2x.png\" width=\"825.5\" height=\"205\" alt=\"\" \/>\n<\/div>\n<p>The search results may not always match the expectations,  chiefs, marketers, and administrators can accidentally get into our database. To prevent this, we will write a function named check_name(name),  it will accept a vacancy name and return a boolean value, depending on the match.<\/p>\n<pre class=\"e2-text-code\"><code>def check_name(name):\r\n    bad_names = [r'курьер', r'грузчик', r'врач', r'менеджер по закупу',\r\n           r'менеджер по продажам', r'оператор', r'повар', r'продавец',\r\n          r'директор магазина', r'директор по продажам', r'директор по маркетингу',\r\n          r'кабельщик', r'начальник отдела продаж', r'заместитель', r'администратор магазина', \r\n          r'категорийный', r'аудитор', r'юрист', r'контент', r'супервайзер', r'стажер-ученик', \r\n          r'су-шеф', r'маркетолог$', r'региональный', r'ревизор', r'экономист', r'ветеринар', \r\n          r'торговый', r'клиентский', r'начальник цеха', r'территориальный', r'переводчик', \r\n          r'маркетолог \/', r'маркетолог по']\r\n    for item in bad_names:\r\n        if re.match(item, name):\r\n            return True<\/code><\/pre><p>Moving further, we need to create a while loop to collect data non-stop.  Iterate over the Dataframe queries selecting the type, level, field, and search phrase columns. Send a GET request using a keyword to get the number of pages.  Then we loop through the number of pages sending the same requests and populating  vacancies_from_response with job descriptions. In the per_page parameter we specified 10, this is the max limit for the HH API. Since we didn’t pass any value to the area field,  the results are collected worldwide.<\/p>\n<pre class=\"e2-text-code\"><code>while True:\r\n   for query_type, level, direction, query_string in zip(queries['Query Type'], queries['Level'], queries['Career Field'], queries['Seach Phrase']):\r\n           print(f'seach phrase: {query_string}')\r\n           url = 'https:\/\/api.hh.ru\/vacancies'\r\n           par = {'text': query_string, 'per_page':'10', 'page':0}\r\n           r = requests.get(url, params=par).json()\r\n           added_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r\n           pages = r['pages']\r\n           found = r['found']\r\n           vacancies_from_response = []\r\n\r\n           for i in range(0, pages + 1):\r\n               par = {'text': query_string, 'per_page':'10', 'page':i}\r\n               r = requests.get(url, params=par).json()\r\n               try:\r\n                   vacancies_from_response.append(r['items'])\r\n               except Exception as E:\r\n                   continue<\/code><\/pre><p>Create a for loop to escape duplicate rows in our table. First, send a query to the database, verifying whether there is a vacancy with the same id and search phrase. If the verification was successful we then<br \/>\npass the job title to check_name() and move on to the next one.<\/p>\n<pre class=\"e2-text-code\"><code>for item in vacancies_from_response:\r\n               for vacancy in item:\r\n                   if client.execute(f&quot;SELECT count(1) FROM vacancies_short WHERE vacancy_id={vacancy['id']} AND query_string='{query_string}'&quot;)[0][0] == 0:\r\n                       name = vacancy['name'].replace(&quot;'&quot;,&quot;&quot;).replace('&quot;','')\r\n                       if check_name(name):\r\n                           continue<\/code><\/pre><p>Now we need to extract all the necessary data from a job description.  The table will contain empty cells, since some data may be missing.<\/p>\n<p><details><br \/>\n<summary><span style=\"color:#7ea9b8\">View the code for extracting job description data<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>vacancy_id = vacancy['id']\r\n                       is_premium = int(vacancy['premium'])\r\n                       has_test = int(vacancy['has_test'])\r\n                       response_url = vacancy['response_url']\r\n                       try:\r\n                           address_city = vacancy['address']['city']\r\n                           address_street = vacancy['address']['street']\r\n                           address_building = vacancy['address']['building']\r\n                           address_description = vacancy['address']['description']\r\n                           address_lat = vacancy['address']['lat']\r\n                           address_lng = vacancy['address']['lng']\r\n                           address_raw = vacancy['address']['raw']\r\n                           address_metro_stations = str(vacancy['address']['metro_stations']).replace(&quot;'&quot;,'&quot;')\r\n                       except TypeError:\r\n                           address_city = &quot;&quot;\r\n                           address_street = &quot;&quot;\r\n                           address_building = &quot;&quot;\r\n                           address_description = &quot;&quot;\r\n                           address_lat = &quot;&quot;\r\n                           address_lng = &quot;&quot;\r\n                           address_raw = &quot;&quot;\r\n                           address_metro_stations = &quot;&quot;\r\n                       alternate_url = vacancy['alternate_url']\r\n                       apply_alternate_url = vacancy['apply_alternate_url']\r\n                       try:\r\n                           department_id = vacancy['department']['id']\r\n                       except TypeError as E:\r\n                           department_id = &quot;&quot;\r\n                       try:\r\n                           department_name = vacancy['department']['name']\r\n                       except TypeError as E:\r\n                           department_name = &quot;&quot;\r\n                       try:\r\n                           salary_from = vacancy['salary']['from']\r\n                       except TypeError as E:\r\n                           salary_from = &quot;cast(Null as Nullable(UInt64))&quot;\r\n                       try:\r\n                           salary_to = vacancy['salary']['to']\r\n                       except TypeError as E:\r\n                           salary_to = &quot;cast(Null as Nullable(UInt64))&quot;\r\n                       try:\r\n                           salary_currency = vacancy['salary']['currency']\r\n                       except TypeError as E:\r\n                           salary_currency = &quot;&quot;\r\n                       try:\r\n                           salary_gross = int(vacancy['salary']['gross'])\r\n                       except TypeError as E:\r\n                           salary_gross = &quot;cast(Null as Nullable(UInt8))&quot;\r\n                       try:\r\n                           insider_interview_id = vacancy['insider_interview']['id']\r\n                       except TypeError:\r\n                           insider_interview_id = &quot;cast(Null as Nullable(UInt64))&quot;\r\n                       try:\r\n                           insider_interview_url = vacancy['insider_interview']['url']\r\n                       except TypeError:\r\n                           insider_interview_url = &quot;&quot;\r\n                       area_url = vacancy['area']['url']\r\n                       area_id = vacancy['area']['id']\r\n                       area_name = vacancy['area']['name']\r\n                       url = vacancy['url']\r\n                       published_at = vacancy['published_at']\r\n                       published_at = datetime.strptime(published_at,'%Y-%m-%dT%H:%M:%S%z').strftime('%Y-%m-%d %H:%M:%S')\r\n                       try:\r\n                           employer_url = vacancy['employer']['url']\r\n                       except Exception as E:\r\n                           print(E)\r\n                           employer_url = &quot;&quot;\r\n                       try:\r\n                           employer_alternate_url = vacancy['employer']['alternate_url']\r\n                       except Exception as E:\r\n                           print(E)\r\n                           employer_alternate_url = &quot;&quot;\r\n                       try:\r\n                           employer_logo_urls_90 = vacancy['employer']['logo_urls']['90']\r\n                           employer_logo_urls_240 = vacancy['employer']['logo_urls']['240']\r\n                           employer_logo_urls_original = vacancy['employer']['logo_urls']['original']\r\n                       except Exception as E:\r\n                           print(E)\r\n                           employer_logo_urls_90 = &quot;&quot;\r\n                           employer_logo_urls_240 = &quot;&quot;\r\n                           employer_logo_urls_original = &quot;&quot;\r\n                       employer_name = vacancy['employer']['name'].replace(&quot;'&quot;,&quot;&quot;).replace('&quot;','')\r\n                       try:\r\n                           employer_id = vacancy['employer']['id']\r\n                       except Exception as E:\r\n                           print(E)\r\n                       response_letter_required = int(vacancy['response_letter_required'])\r\n                       type_id = vacancy['type']['id']\r\n                       type_name = vacancy['type']['name']\r\n                       is_archived = int(vacancy['archived'])<\/code><\/pre><p><\/details><\/p>\n<p>The last field is the work schedule.  If there is mentioned a fly-in-fly-out method, these kinds of job postings will be skipped.<\/p>\n<pre class=\"e2-text-code\"><code>try:\r\n    schedule = vacancy['schedule']['id']\r\nexcept Exception as E:\r\n    print(E)\r\n    schedule = ''&quot;\r\nif schedule == 'flyInFlyOut':\r\n    continue<\/code><\/pre><p>Next, we create a list of obtained variables,  replacing None values with empty strings to escape errors with Clickhouse and insert them into the table.<\/p>\n<pre class=\"e2-text-code\"><code>vacancies_short_list = [added_at, query_string, query_type, level, direction, vacancy_id, is_premium, has_test, response_url, address_city, address_street, address_building, address_description, address_lat, address_lng, address_raw, address_metro_stations, alternate_url, apply_alternate_url, department_id, department_name,\r\nsalary_from, salary_to, salary_currency, salary_gross, insider_interview_id, insider_interview_url, area_url, area_name, url, published_at, employer_url, employer_logo_urls_90, employer_logo_urls_240,  employer_name, employer_id, response_letter_required, type_id, type_name, is_archived, schedule]\r\nfor index, item in enumerate(vacancies_short_list):\r\n    if item is None:\r\n        vacancies_short_list[index] = &quot;&quot;\r\ntuple_to_insert = tuple(vacancies_short_list)\r\nprint(tuple_to_insert)\r\nclient.execute(f'INSERT INTO vacancies_short VALUES {tuple_to_insert}')<\/code><\/pre><h2>Connecting Tableau  to the data source<\/h2>\n<p>Unfortunately, we can’t work with databases in  Tableau Public, that’s why we decided to connect our  Clickhouse Database to Google Sheets.  With this in mind, we picked the following libraries: gspread and oauth2client for accessing Google Spreadsheets API,  and schedule for task scheduling.<\/p>\n<p class=\"note\">Refer to our previous article where we used  Google Spreadseets API  for  <a href=\"https:\/\/en.leftjoin.ru\/all\/collecting-data-on-ad-campaigns-from-vkontakte\/\" class=\"nu\">“<u>Collecting Data on Ad Campaigns from VK.com<\/u>”<\/a><\/p>\n<pre class=\"e2-text-code\"><code>import schedule\r\nfrom clickhouse_driver import Client\r\nimport gspread\r\nimport pandas as pd\r\nfrom oauth2client.service_account import ServiceAccountCredentials\r\nfrom datetime import datetime\r\n\r\nscope = ['https:\/\/spreadsheets.google.com\/feeds', 'https:\/\/www.googleapis.com\/auth\/drive']\r\nclient = Client(host='54.227.137.142', user='default', password='', port='9000', database='headhunter')\r\ncreds = ServiceAccountCredentials.from_json_keyfile_name('credentials.json', scope)\r\ngc = gspread.authorize(creds)<\/code><\/pre><p>The update_sheet() function will transfer all data from Clickhouse to a Google Sheets table:<\/p>\n<pre class=\"e2-text-code\"><code>def update_sheet():\r\n   print('Updating cell at', datetime.now())\r\n   columns = []\r\n   for item in client.execute('describe table headhunter.vacancies_short'):\r\n       columns.append(item[0])\r\n   vacancies = client.execute('SELECT * FROM headhunter.vacancies_short')\r\n   df_vacancies = pd.DataFrame(vacancies, columns=columns)\r\n   df_vacancies.to_csv('vacancies_short.csv', index=False)\r\n   content = open('vacancies_short.csv', 'r').read()\r\n   gc.import_csv('1ZWS2kqraPa4i72hzp0noU02SrYVo0teD7KZ0c3hl-UI', content.encode('utf-8'))<\/code><\/pre><p>Using schedule to run our function every day at 1:00 PM (UTC):<\/p>\n<pre class=\"e2-text-code\"><code>schedule.every().day.at(&quot;13:00&quot;).do(update_sheet)\r\nwhile True:\r\n   schedule.run_pending()<\/code><\/pre><h2>What’s the final point?<\/h2>\n<p><a href=\"https:\/\/t.me\/revealthedata\">Roman<\/a> created an informative dashboard based on this data.<\/p>\n<div class=\"e2-text-picture\">\n<a href=\"https:\/\/revealthedata.com\/examples\/hh\/\" class=\"e2-text-picture-link\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/3-4.jpg\" width=\"2560\" height=\"1439\" alt=\"\" \/>\n<\/a><\/div>\n<p>And made a <a href=\"https:\/\/youtu.be\/jaJdG7kZ7BI\">youtube video <\/a> with a detailed explanation of the dashboard features.<\/p>\n<h2>Key Insights<\/h2>\n<ol start=\"1\">\n<li>Data Analysts specializing in BI  are most in-demand in the job market since the highest number of search results were returned with this query. However, the average salary is higher in Product Analyst and BI-analyst openings.<\/li>\n<li>Most of the postings were found In Moscow,  where the average salary is 10-30K RUB higher than in Saint Petersburg and 30-40K  higher than in other regions.<\/li>\n<li>Top highly paid positions: Head of Analytics (110K RUB per month on avg.), Database Engineer (138K RUB per month), and Head of Machine Learning (250K RUB per month).<\/li>\n<li>The most useful skills to have are a solid knowledge of Python with Pandas and Numpy, Tableau, Power BI, ETL, and Spark. Most of the posings found contained these requirements and were highly paid than any others.  For Python programmers, it’s more valuable to have expertise with Matplotlib than Plotly.<\/li>\n<\/ol>\n<p><i>View the code on  <a href=\"https:\/\/github.com\/valiotti\/leftjoin\/tree\/master\/headhunter\">GitHub<\/a><\/i><\/p>\n",
            "date_published": "2020-09-23T16:24:41+03:00",
            "date_modified": "2020-09-24T09:49:14+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/3-2.jpg",
            "_date_published_rfc2822": "Wed, 23 Sep 2020 16:24:41 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "44",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/3-2.jpg",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-3.jpg",
                    "https:\/\/en.leftjoin.ru\/pictures\/1.1.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/edata@2x.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-4.jpg"
                ]
            }
        },
        {
            "id": "35",
            "url": "https:\/\/en.leftjoin.ru\/all\/sentiment-analysis-of-russians-on-constitutional-amendments\/",
            "title": "Sentiment analysis of Russians on Constitutional Amendments",
            "content_html": "<p>In today’s article, we are going to use public data from vk.com to interpret and classify users’ attitudes about the 2020 amendments to the Constitution of Russia.<\/p>\n<h2>API Overview<\/h2>\n<p>First off, we need to receive data using the <a href=\"https:\/\/vk.com\/dev\/newsfeed.search\">newsfeed.search<\/a> method, this method allows us to get up to one thousand of the latest posts from the news feed by keyword.<br \/>\nThe response data contains different fields, like post ids,  user or community ids, text data, likes count, comments,  apps, geolocation, and many more. We are only needed ids and text data.<br \/>\nSome expanded information about the author will also be useful for our analysis, this includes city, gender, age, and can be received with the <a href=\"https:\/\/vk.com\/dev\/users.get\">users.get<\/a> method.<\/p>\n<h2>Create Clickhouse Tables<\/h2>\n<p>The received data should be stored somewhere, we chose to use ClickHouse, an open-source column-oriented DBMS. Let’s create two tables to store users and their posts. The first table will be populated with ids and text data, the second one will hold user data, such as their ids, age, and city. The ReplacingMergeTree () engine will remove duplicates in our tables.<\/p>\n<p class=\"note\">The article assumes that you’re familiar with how to <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/installing-clickhouse-on-aws\/\">install ClickHouse on AWS<\/a>, <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/example-of-using-dictionaries-in-clickhouse-with-untappd\/\">create external dictionaries<\/a> and <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/working-with-materialized-views-in-clickhouse\/\"> materialized views<\/a><\/p>\n<pre class=\"e2-text-code\"><code>CREATE TABLE vk_posts(\r\n   post_id UInt64,\r\n   post_date DateTime,\r\n   owner_id UInt64,\r\n   from_id UInt64,\r\n   text String\r\n) ENGINE ReplacingMergeTree()\r\nORDER BY post_date\r\n\r\nCREATE TABLE vk_users(\r\n   user_id UInt64,\r\n   user_sex Nullable(UInt8),\r\n   user_city String,\r\n   user_age Nullable(UInt16)\r\n) ENGINE ReplacingMergeTree()\r\nORDER BY user_id<\/code><\/pre><h2>Collecting user posts with the VK API<\/h2>\n<p>Let’s get to writing our script, import the libraries, and create several variables with constant values:<\/p>\n<p class=\"note\">If you don’t have an access token yet and want to create one, refer to this step by step guide:<a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/collecting-data-on-ad-campaigns-from-vkontakte\/\"> “Collecting Data on Ad Campaigns from VK.com”<\/a><\/p>\n<pre class=\"e2-text-code\"><code>from clickhouse_driver import Client\r\nfrom datetime import datetime\r\nimport requests\r\nimport pandas as pd\r\nimport time\r\n\r\ntoken = 'your_token'\r\nversion = 5.103\r\nclient = Client(host='ec1-23-456-789-1011.us-east-2.compute.amazonaws.com', user='default', password='', port='9000', database='default')      \r\ndata_list = []\r\nstart_from = 0\r\nquery_string = 'конституция' #constitution<\/code><\/pre><p>Define the get_and_insert_info_by_user function that will receive a list of user ids and expanded information about them, and send it to the vk_users table. Since the user_ids parameter takes a list as a string object, we need to change the structure and omit the square brackets.<br \/>\nMost users prefer to conceal their gender, age, and city. In such cases, we need to use Nullable values. To obtain user age we need to subtract the birth year from the current year, if the birth year is missing we can check it using the regular expression.<\/p>\n<p><details><br \/>\n<summary><span style=\"color:#7ea9b8\">get_and_insert_info_by_user() function<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>def get_and_insert_info_by_user(users):\r\n    try:\r\n        r = requests.get('https:\/\/api.vk.com\/method\/users.get', params={\r\n            'access_token':token,\r\n            'v':version,\r\n            'user_ids':str(users)[1:-2],\r\n            'fields':'sex, city, bdate'\r\n        }).json()['response']\r\n        for user in r:\r\n            user_list = []\r\n            user_list.append(user['id'])\r\n            if client.execute(f&quot;SELECT count(1) FROM vk_users where user_id={user['id']}&quot;)[0][0] == 0:\r\n                print(user['id'])\r\n                try:\r\n                    user_list.append(user['sex'])\r\n                except Exception:\r\n                    user_list.append('cast(Null as Nullable(UInt8))')\r\n                try:\r\n                    user_list.append(user['city']['title'])\r\n                except Exception:\r\n                    user_list.append('')\r\n                try:\r\n                    now = datetime.now()\r\n    \t\t\t    year = item.split('.')[-1]\r\n    \t\t\t    if re.match(r'\\d\\d\\d\\d', year):\r\n        \t\t        age = now.year - int(year)\r\n\t\t\t    \t   user_list.append(age)\r\n                except Exception:\r\n                    user_list.append('cast(Null as Nullable(UInt16))')\r\n                user_insert_tuple = tuple(user_list)\r\n                client.execute(f'INSERT INTO vk_users VALUES {user_insert_tuple}')\r\n    except KeyError:\r\n        pass<\/code><\/pre><p><\/details><br \/>\nOur script will work in a while loop to constantly update data, as we can only receive a thousand of the latest data points.The newsfeed.search method returns 200 posts per call, so we need to invoke it five times to collect all the posts.<\/p>\n<p><details><br \/>\n<summary><span style=\"color:#7ea9b8\">While loop to collect new posts<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>while True:\r\n    for i in range(5):\r\n        r = requests.get('https:\/\/api.vk.com\/method\/newsfeed.search', params={\r\n            'access_token':token,\r\n            'v':version,\r\n            'q':query_string,\r\n            'count':200,\r\n            'start_from': start_from\r\n        })\r\n        data_list.append(r.json()['response'])\r\n        try:\r\n            start_from = r.json()['response']['next_from']\r\n        except KeyError:\r\n            pass<\/code><\/pre><p><\/details><\/p>\n<p>The data we received can be parsed, VK users always have a positive id, while for communities it’s negative. We need only users data for our analysis, where from_id  > 0. The next step is to check whether a post contains any text data or not. Finally, we will collect and store unique entries by user id. Pause the script after each iteration for 180 seconds to wait for new user posts and not violate the VK API rules.<\/p>\n<p><details><br \/>\n<summary><span style=\"color:#7ea9b8\">Adding new data to Clickhouse<\/span><\/summary><\/p>\n<pre class=\"e2-text-code\"><code>user_ids = []\r\n    for data in data_list:\r\n        for data_item in data['items']:\r\n            if data_item['from_id'] &gt; 0:\r\n                post_list = []\r\n                if not data_item['text']:\r\n                    continue\r\n                if client.execute(f&quot;SELECT count(1) FROM vk_posts WHERE post_id={data_item['id']} AND from_id={data_item['from_id']}&quot;)[0][0] == 0:\r\n                    user_ids.append(data_item['from_id'])\r\n                    date = datetime.fromtimestamp(data_item['date'])\r\n                    date = datetime.strftime(date, '%Y-%m-%d %H:%M:%S')\r\n                    post_list.append(date)\r\n                    post_list.append(data_item['id'])\r\n                    post_list.append(data_item['owner_id'])\r\n                    post_list.append(data_item['from_id'])\r\npost_list.append(data_item['text'].replace(&quot;'&quot;,&quot;&quot;).replace('&quot;','').replace(&quot;\\n&quot;,&quot;&quot;))\r\n                    post_list.append(query_string)\r\n                    post_tuple = tuple(post_list)\r\n                    print(post_list)\r\n                    try:\r\n                        client.execute(f'INSERT INTO vk_posts VALUES {post_tuple}')\r\n                    except Exception as E:\r\n                        print('!!!!! try to insert into vk_post but got', E)\r\n    try:\r\n        get_and_insert_info_by_user(user_ids)\r\n    except Exception as E:\r\n        print(&quot;Try to insert user list:&quot;, user_ids, &quot;but got:&quot;, E)\r\n    time.sleep(180)<\/code><\/pre><p><\/details><\/p>\n<h2>Dostoevsky for sentiment analysis<\/h2>\n<p>For one week our script collected almost 20000 posts from VK users that mention the keyword  “constitution” (or “конституция” in Russian). It’s time to write our second script for data analysis and visualization. First, create a DataFrame with the data received, and evaluate the sentiment of each post, identifying whether it’s positive, negative, or neutral. We are going to use the <a href=\"https:\/\/pypi.org\/project\/dostoevsky\/\">Dostoevsky<\/a> library to analyze the emotion behind a text.<\/p>\n<pre class=\"e2-text-code\"><code>from dostoevsky.tokenization import RegexTokenizer\r\nfrom dostoevsky.models import FastTextSocialNetworkModel\r\nfrom clickhouse_driver import Client\r\nimport pandas as pd\r\nclient = Client(host='ec1-23-456-789-1011.us-east-2.compute.amazonaws.com', user='default', password='', port='9000', database='default')<\/code><\/pre><p>Assign all the contents of our table to the vk_posts variable with a simple query. Iterate through all the posts, select those with text data and populate our DataFrame.<\/p>\n<pre class=\"e2-text-code\"><code>vk_posts = client.execute('SELECT * FROM vk_posts')\r\nlist_of_posts = []\r\nlist_of_ids = []\r\nfor post in vk_posts:\r\n    if str(post[-2]).replace(&quot; &quot;, &quot;&quot;):\r\n        list_of_posts.append(str(post[-2]).replace(&quot;\\n&quot;,&quot;&quot;))\r\n        list_of_ids.append(int(post[2]))\r\ndf_posts = pd.DataFrame()\r\ndf_posts['post'] = list_of_posts\r\ndf_posts['id'] = list_of_ids<\/code><\/pre><p>Instantiate our model and iterate through the posts to evaluate the sentiment of each entry.<\/p>\n<pre class=\"e2-text-code\"><code>tokenizer = RegexTokenizer()\r\nmodel = FastTextSocialNetworkModel(tokenizer=tokenizer)\r\nsentiment_list = []\r\nresults = model.predict(list_of_posts, k=2)\r\nfor sentiment in results:\r\n    sentiment_list.append(sentiment)<\/code><\/pre><p>Add several boolean columns to our DataFrame that will reflect whether it’s a  positive, negative, or neutral post.<\/p>\n<pre class=\"e2-text-code\"><code>neutral_list = []\r\nnegative_list = []\r\npositive_list = []\r\nspeech_list = []\r\nskip_list = []\r\nfor sentiment in sentiment_list:\r\n    neutral = sentiment.get('neutral')\r\n    negative = sentiment.get('negative')\r\n    positive = sentiment.get('positive')\r\n    if neutral is None:\r\n        neutral_list.append(0)\r\n    else:\r\n        neutral_list.append(sentiment.get('neutral'))\r\n    if negative is None:\r\n        negative_list.append(0)\r\n    else:\r\n        negative_list.append(sentiment.get('negative'))\r\n    if positive is None:\r\n        positive_list.append(0)\r\n    else:\r\n        positive_list.append(sentiment.get('positive'))\r\ndf_posts['neutral'] = neutral_list\r\ndf_posts['negative'] = negative_list\r\ndf_posts['positive'] = positive_list<\/code><\/pre><p>That’s how the DataFrame looks now:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/1-9.png\" width=\"669\" height=\"187\" alt=\"\" \/>\n<\/div>\n<p>Let’s examine the most negative posts:<\/p>\n<pre class=\"e2-text-code\"><code>df_posts[df_posts.negative &gt; 0.9]<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/2-9.png\" width=\"631\" height=\"289\" alt=\"\" \/>\n<\/div>\n<p>Now, let’s add data about the authors of these posts by merging two tables together on the id column.<\/p>\n<pre class=\"e2-text-code\"><code>vk_users = client.execute('SELECT * FROM vk_users')\r\nvk_user_ids_list = []\r\nvk_user_sex_list = []\r\nvk_user_city_list = []\r\nvk_user_age_list = []\r\nfor user in vk_users:\r\n    vk_user_ids_list.append(user[0])\r\n    vk_user_sex_list.append(user[1])\r\n    vk_user_city_list.append(user[2])\r\n    vk_user_age_list.append(user[3])\r\ndf_users = pd.DataFrame()\r\ndf_users['id'] = vk_user_ids_list\r\ndf_users['sex'] = vk_user_sex_list\r\ndf_users['city'] = vk_user_city_list\r\ndf_users['age'] = vk_user_age_list\r\ndf = df_posts.merge(df_users, on='id')<\/code><\/pre><p>And the table now looks the following:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/3-10.png\" width=\"819\" height=\"346\" alt=\"\" \/>\n<\/div>\n<h2>Analysing data with Plotly<\/h2>\n<p class=\"note\">Check out our previous article on data visualization with Plotly: <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/building-an-interactive-waterfall-chart-in-python\/\" class=\"nu\">“<u>Building an interactive waterfall chart in Python<\/u>”<\/a><\/p>\n<p>Let’s find the percentage of posts for each group: positive, negative, neutral. Iterate through these three columns and calculate the values more than zero for each data point. Then do the same for different age categories and gender.<\/p>\n<div class=\"embed-responsive embed-responsive-4by3\" style=\"min-width:500\"><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\"seamless=\"seamless\" src=\"https:\/\/plotly.com\/~i-bond\/9.embed?showlink=false\" height=\"500\" width=\"500\" ><\/iframe>\n<\/div><p>According to our chart, 45% of recent user posts relevant to the keyword “constitution” have a negative meaning, while the other 52% are neutral. Later it’ll be known how different the Internet opinions from the voting results.<\/p>\n<div class=\"embed-responsive embed-responsive-4by3\" style=\"min-width:800\"><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none;\"seamless=\"seamless\" src=\"https:\/\/plotly.com\/~i-bond\/5.embed?showlink=false\" height=\"500\" width=\"800\"><\/iframe>\n<\/div><p>It’s noticeable that among the men audience the proportion of positive posts is less than 2%, while for women it’s 3.5%. However, the number of negative posts for each group is almost the same, 47% and 43% respectively.<\/p>\n<div class=\"embed-responsive embed-responsive-4by3\" style=\"min-width:800\"><iframe id=\"igraph\" scrolling=\"no\" style=\"border:none; position:relative;\"seamless=\"seamless\" src=\"https:\/\/plotly.com\/~i-bond\/7.embed?showlink=false\" height=\"500\" width=\"800\"><\/iframe>\n<\/div><p>According to our analysis,  posts made by younger audiences between 18-25 years have more positive sentiment, which is 6%. While users under 18 years leave mostly negative posts, this may be because most users under the age of 18 prefer to hide their real age, this makes it difficult to obtain accurate data for such a group.<br \/>\nThe proportion of negative posts is almost equal for all groups and accounts for 44%.<br \/>\nAs you can see, the data is distributed equally in all three charts. This means that half of all posts relevant to the keyword “constitution” and made by VK users over the past week mostly have a negative sentiment.<\/p>\n",
            "date_published": "2020-07-08T12:53:42+03:00",
            "date_modified": "2020-07-08T12:49:06+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/1-9.png",
            "_date_published_rfc2822": "Wed, 08 Jul 2020 12:53:42 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "35",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/1-9.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/2-9.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-10.png"
                ]
            }
        },
        {
            "id": "33",
            "url": "https:\/\/en.leftjoin.ru\/all\/handling-website-buttons-in-selenium\/",
            "title": "Handling website buttons in Selenium",
            "content_html": "<p>In our previous article, <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/parse-website-with-python-p2\/\" class=\"nu\">“<u>Parsing the data of site’s catalogue, using Beautiful Soup and Selenium<\/u>”<\/a> we have addressed the problem of working with dynamic pages, but sometimes this method doesn’t work,  as with “Show more” buttons. Today we will show how you can imitate button click with Selenium to load a whole page, collect beer IDs, ratings, and send the data to Clickhouse.<\/p>\n<h2>Webpage structure<\/h2>\n<p>Let’s take a random brewery that has 105 check-ins, or customer feedbacks. One page with check-ins displays up to 25 records and looks like this:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/1-8.png\" width=\"1186\" height=\"735\" alt=\"\" \/>\n<\/div>\n<p>If we try to scroll down to the bottom, we will encounter the same button that prevents us from getting all 105 records at once:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/2-8.png\" width=\"350\" height=\"65\" alt=\"\" \/>\n<\/div>\n<p>First off, to address this task, let’s find out the button class and just click it until it works. Since Selenium launches the browser and the next “Show more”  button may not be loaded in time, that’s why we set  2-second intervals between the clicks. As soon as the page is loaded we will take its content and parse the relevant data.<br \/>\nLet’s view the source code and  find the button, it’s assigned to the <span class=\"inline-code\">more_checkins<\/span> class.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/3-9.png\" width=\"849\" height=\"94\" alt=\"\" \/>\n<\/div>\n<p>The button has style attributes, such as <span class=\"inline-code\">display<\/span>. When the button is displayed this attribute takes the <span class=\"inline-code\">block<\/span> value. But when we scroll the page to the buttom and there is nothing left to display, the attribute takes the <span class=\"inline-code\">none<\/span> value and we can stop clicking.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/4-8.png\" width=\"562\" height=\"107\" alt=\"\" \/>\n<\/div>\n<h2>Writing our code<\/h2>\n<p>Let’s import the necessary libraries<\/p>\n<pre class=\"e2-text-code\"><code>import time\r\nfrom selenium import webdriver\r\nfrom bs4 import BeautifulSoup as bs\r\nimport re\r\nfrom datetime import datetime\r\nfrom clickhouse_driver import Client<\/code><\/pre><p class=\"note\">Chromedriver is used to run Selenium tests on Chrome and can be downloaded from <a href=\"https:\/\/chromedriver.chromium.org\/downloads\">the official website<\/a><\/p>\n<p>Connect to the database and create cookies:<\/p>\n<pre class=\"e2-text-code\"><code>client = Client(host='ec1-23-456-789-10.us-east-2.compute.amazonaws.com', user='', password='', port='9000', database='')\r\ncount = 0\r\ncookies = {\r\n    'domain':'untappd.com',\r\n    'expiry':1594072726,\r\n    'httpOnly':True,\r\n    'name':'untappd_user_v3_e',\r\n    'path':'\/',\r\n    'secure':False,\r\n    'value':'your_value'\r\n}<\/code><\/pre><p class=\"note\">You can find out more about working with cookies in Selenium from <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/parse-website-with-python-p2\/\" class=\"nu\">“<u>Parsing the data of site’s catalogue, using Beautiful Soup and Selenium<\/u>”<\/a>. We will need the untappd_user_v3_e parameter.<\/p>\n<p>As we are going to work with pages that have more than hundreds of thousands of records,  it’s pretty heavy and our instance may be overloaded. To prevent this, we will shut down unnecessary parts and then enable authentication cookie:<\/p>\n<pre class=\"e2-text-code\"><code>options = webdriver.ChromeOptions()\r\nprefs = {'profile.default_content_setting_values': {'images': 2, \r\n                            'plugins': 2, 'fullscreen': 2}}\r\noptions.add_experimental_option('prefs', prefs)\r\noptions.add_argument(&quot;start-maximized&quot;)\r\noptions.add_argument(&quot;disable-infobars&quot;)\r\noptions.add_argument(&quot;--disable-extensions&quot;)\r\ndriver = webdriver.Chrome(options=options)\r\ndriver.get('https:\/\/untappd.com\/TooSunnyBrewery')\r\ndriver.add_cookie(cookies)<\/code><\/pre><p>We  will need a function that would take a link, open it in the browser, load a whole page and return a soup object to be parsed. Get the  <span class=\"inline-code\">display<\/span> attribute, assign it to the <span class=\"inline-code\">more_checkins<\/span>: variable and click the button until the attribute is <span class=\"inline-code\">none<\/span>. Let’s set  2-second intervals between the clicks, to wait for the page to load. As soon as we received the page, converth it into a <span class=\"inline-code\">soup<\/span> object using the <span class=\"inline-code\">bs4<\/span> library.<\/p>\n<pre class=\"e2-text-code\"><code>def get_html_page(url):\r\n    driver.get(url)\r\n    driver.maximize_window()\r\n    more_checkins = driver.execute_script(&quot;var more_checkins=document.getElementsByClassName('more_checkins_logged')[0].style.display;return more_checkins;&quot;)\r\n    print(more_checkins)\r\n    while more_checkins != &quot;none&quot;:\r\n        driver.execute_script(&quot;document.getElementsByClassName('more_checkins_logged')[0].click()&quot;)\r\n        time.sleep(2)\r\n        more_checkins = driver.execute_script(&quot;var more_checkins=document.getElementsByClassName('more_checkins_logged')[0].style.display;return more_checkins;&quot;)\r\n        print(more_checkins)\r\n    source_data = driver.page_source\r\n    soup = bs(source_data, 'lxml')\r\n    return soup<\/code><\/pre><p>Write the following function that will take a page <span class=\"inline-code\">url<\/span>, pass it in the <span class=\"inline-code\">get_html_page<\/span> and receive a soup object to parse. The function returns zipped lists with beer IDs and ratings.<\/p>\n<p class=\"note\"> See how you can use <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/parse-website-with-python-p1\/\">Beautiful Soup to retrieve data from a website catalogue<\/a><\/p>\n<pre class=\"e2-text-code\"><code>def parse_html_page(url):\r\n    soup = get_html_page(url)\r\n    brewery_id = soup.find_all('a', {'class':'label',\r\n                                     'href':re.compile('https:\/\/untappd.com\/brewery\/*')})[0]['href'][28:]\r\n    items = soup.find_all('div', {'class':'item',\r\n                                  'id':re.compile('checkin_*')})\r\n    checkin_rating_list = []\r\n    beer_id_list = []\r\n    count = 0\r\n    print('Filling the lists')\r\n    for checkin in items:\r\n        print(count, '\/', len(items))\r\n        try:\r\n            checkin_rating_list.append(float(checkin.find('div', {'class':'caps'})['data-rating']))\r\n        except Exception:\r\n            checkin_rating_list.append('cast(Null as Nullable(Float32))')\r\n        try:\r\n            beer_id_list.append(int(checkin.find('a', {'class':'label'})['href'][-7:]))\r\n        except Exception:\r\n            beer_id_list.append('cast(Null as Nullable(UInt64))')\r\n        count += 1 \r\n    return zip(checkin_rating_list, beer_id_list)<\/code><\/pre><p>Finally, write a function call for the breweries. We’ve covered how to receive a list of Russian brewery IDs in this article: <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/example-of-using-dictionaries-in-clickhouse-with-untappd\/\">Example of using dictionaries in Clickhouse with Untappd<\/a>.<br \/>\nLet’s fetch it from the Clickhouse table.<\/p>\n<pre class=\"e2-text-code\"><code>brewery_list = client.execute('SELECT brewery_id FROM brewery_info')<\/code><\/pre><p>If we print out the <span class=\"inline-code\">brewery_list<\/span>,  we will find out that the data is stored in a list of tuples.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/5-6.png\" width=\"378\" height=\"141\" alt=\"\" \/>\n<\/div>\n<p>Let’s make it a bit prettier with the lambda expression:<\/p>\n<pre class=\"e2-text-code\"><code>flatten = lambda lst: [item for sublist in lst for item in sublist]\r\nbrewery_list = flatten(brewery_list)<\/code><\/pre><p>That’s much better:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/6-5.png\" width=\"252\" height=\"139\" alt=\"\" \/>\n<\/div>\n<p>Create a <span class=\"inline-code\">url<\/span> for each brewery in the list, it includes a standard link and a brewery ID in the end. Pass it to the <span class=\"inline-code\">parse_html_page<\/span> function that fetches the <span class=\"inline-code\">get_html_page<\/span> and return lists with <span class=\"inline-code\">beer_id<\/span> and <span class=\"inline-code\">rating_score<\/span>. Since the lists are zipped, we can iterate throught them, create a tuple and send it to Clickhouse.<\/p>\n<pre class=\"e2-text-code\"><code>for brewery_id in brewery_list:\r\n    print('Fetching the brewery with id', brewery_id, count, '\/', len(brewery_list))\r\n    url = 'https:\/\/untappd.com\/brewery\/' + str(brewery_id)\r\n    returned_checkins = parse_html_page(url)\r\n    for rating, beer_id in returned_checkins:\r\n        tuple_to_insert = (rating, beer_id)\r\n        try:\r\n            client.execute(f'INSERT INTO beer_reviews VALUES {tuple_to_insert}')\r\n        except errors.ServerException as E:\r\n            print(E)\r\n    count += 1<\/code><\/pre><p>That’s it about the way we can handle “Show more” buttons. Over time we will form a large dataset for further analysis, to work with in our next series.<\/p>\n",
            "date_published": "2020-06-22T10:54:56+03:00",
            "date_modified": "2020-06-22T11:13:03+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/1-8.png",
            "_date_published_rfc2822": "Mon, 22 Jun 2020 10:54:56 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "33",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/1-8.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/2-8.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-9.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/4-8.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/5-6.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/6-5.png"
                ]
            }
        },
        {
            "id": "32",
            "url": "https:\/\/en.leftjoin.ru\/all\/example-of-using-dictionaries-in-clickhouse-with-untappd\/",
            "title": "Example of using dictionaries in Clickhouse with Untappd",
            "content_html": "<p>In Clickhouse we can use internal dictionaries as well as external dictionaries, they can be an alternative to JSON that doesn’t always work fine. DIctionaries store information in memory and can be invoked with the dictGet method. Let’s review how we can create one in Clickhouse and use it for our queries.<\/p>\n<p>We will illustrate an example of data using the Untappd API. Untappd is a social network for everyone who loves craft beer. We are going to use сheck-ins of Russian-based craft breweries and start collecting information about them to analyze this data later on and to draw some conclusions. in today’s article, we will analyze how to receive metadata on Russian breweries with Untappd and store it in a Clickhouse dictionary.<\/p>\n<h2>Collecting data with Untappd<\/h2>\n<p>First off, we need to create a new app to receive <span class=\"inline-code\">client_id<\/span> and  <span class=\"inline-code\">client_secret_key<\/span> to make API calls. Follow  <a href=\"https:\/\/untappd.com\/api\/register?register=new\">this link<\/a> and fill in the fields:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/1-7.png\" width=\"734\" height=\"478\" alt=\"\" \/>\n<\/div>\n<p>Usually, it takes about 1 to 3 weeks to wait for approval.<\/p>\n<pre class=\"e2-text-code\"><code>import requests\r\nimport pandas as pd\r\nimport time<\/code><\/pre><p>We’ll be using the requests library to make API calls, view results in a Pandas DataFrame, and save them in a CSV file before sending it to a Clickhouse dictionary. Untappd has strict limits on the number of requests, prohibiting us to make more than 100 calls per hour. Therefore, we need to make our script wait for 38 seconds using the Python time module.<\/p>\n<pre class=\"e2-text-code\"><code>client_id = 'your_client_id'\r\nclient_secret = 'your_client_secret'\r\nall_brewery_of_russia = []<\/code><\/pre><p>We want to get data for one thousand Russian breweries. One request to the <a href=\"https:\/\/untappd.com\/api\/docs#brewerysearch\">Brewery Search<\/a> method enables us to view up to 50 breweries. The website gave us 3369 breweries when searching the word “Russia” manually.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/2-7.png\" width=\"728\" height=\"223\" alt=\"\" \/>\n<\/div>\n<p>Let’s check this, scroll down to the bottom, and open the page code.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/3-8.png\" width=\"405\" height=\"199\" alt=\"\" \/>\n<\/div>\n<p>Each brewery received is stored in the <span class=\"inline-code\">beer-item<\/span> class. This means we can the number of references to <span class=\"inline-code\">beer-item<\/span>:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/4-7.png\" width=\"395\" height=\"21\" alt=\"\" \/>\n<\/div>\n<p>And as it turned out, we have exactly 1000 breweries, not 3369. When searching the word “Russia” manually, the results also contain some American breweries. So, we need to make 20 calls, getting 50 breweries at a time:<\/p>\n<pre class=\"e2-text-code\"><code>for offset in range(0, 1000, 50):\r\n    try:\r\n        print('offset = ', offset)\r\n        print('remained:', 1000 - offset, '\\n')\r\n        response = requests.get(f'https:\/\/api.untappd.com\/v4\/search\/brewery?client_id={client_id}&amp;client_secret={client_secret}',\r\n                               params={\r\n                                   'q':'Russia',\r\n                                   'offset':offset,\r\n                                   'limit':50\r\n                               })\r\n        item = response.json()\r\n        print(item, '\\n')\r\n        all_brewery_of_russia.append(item)\r\n        time.sleep(37)\r\n    except Exception:\r\n        print(Exception)\r\n        continue<\/code><\/pre><p>The <span class=\"inline-code\">Brewery Search<\/span> method includes several parameters, q – a string with a country name (specify specify “Russia” to get all the breweries based in Russia),  offset – allows us to shift by 50 lines in the search to get the next list of breweries, limit – restricts the number of breweries received and can not be more than 50. Convert the answer to JSON and append data sotred in the <span class=\"inline-code\">item<\/span> object to the  <span class=\"inline-code\">all_brewery_of_russia<\/span>  list.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/5-5.png\" width=\"911\" height=\"323\" alt=\"\" \/>\n<\/div>\n<p>Our data may also include breweries from other countries. That’s why we need to filter the data. Iterate through the <span class=\"inline-code\">all_brewery_of_russia<\/span>  list and keep only those breweires, which <span class=\"inline-code\">country_name<\/span> is Russia.<\/p>\n<pre class=\"e2-text-code\"><code>brew_list = []\r\nfor element in all_brewery_of_russia:\r\n    brew = element['response']['brewery']\r\n    for i in range(brew['count']):\r\n        if brew['items'][i]['brewery']['country_name'] == 'Russia':\r\n            brew_list.append(brew['items'][i])<\/code><\/pre><p>Print out the first element in our <span class=\"inline-code\">brew_list<\/span>:<\/p>\n<pre class=\"e2-text-code\"><code>print(brew_list[0])<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/11.png\" width=\"1009\" height=\"79\" alt=\"\" \/>\n<\/div>\n<p>Create a DataFrame with the following columns: <span class=\"inline-code\">brewery_id<\/span>,  <span class=\"inline-code\">beer_count<\/span>,  <span class=\"inline-code\">brewery_name<\/span>,  <span class=\"inline-code\">brewery_slug<\/span>,  <span class=\"inline-code\">brewery_page_url<\/span>,  <span class=\"inline-code\">brewery_city<\/span>,  <span class=\"inline-code\">lat<\/span> и  <span class=\"inline-code\">lng<\/span>. And several lists to sort out the data stored in the <span class=\"inline-code\">brewery_list<\/span>:<\/p>\n<pre class=\"e2-text-code\"><code>df = pd.DataFrame()\r\nbrewery_id_list = []\r\nbeer_count_list = []\r\nbrewery_name_list = []\r\nbrewery_slug_list = []\r\nbrewery_page_url_list = []\r\nbrewery_location_city = []\r\nbrewery_location_lat = []\r\nbrewery_location_lng = []\r\nfor brewery in brew_list:\r\n    brewery_id_list.append(brewery['brewery']['brewery_id'])\r\n    beer_count_list.append(brewery['brewery']['beer_count'])\r\n    brewery_name_list.append(brewery['brewery']['brewery_name'])\r\n    brewery_slug_list.append(brewery['brewery']['brewery_slug'])\r\n    brewery_page_url_list.append(brewery['brewery']['brewery_page_url'])\r\n brewery_location_city.append(brewery['brewery']['location']['brewery_city'])\r\n    brewery_location_lat.append(brewery['brewery']['location']['lat'])\r\n    brewery_location_lng.append(brewery['brewery']['location']['lng'])<\/code><\/pre><p>Assign them as column values:<\/p>\n<pre class=\"e2-text-code\"><code>df['brewery_id'] = brewery_id_list\r\ndf['beer_count'] = beer_count_list\r\ndf['brewery_name'] = brewery_name_list\r\ndf['brewery_slug'] = brewery_slug_list\r\ndf['brewery_page_url'] = brewery_page_url_list\r\ndf['brewery_city'] = brewery_location_city\r\ndf['brewery_lat'] = brewery_location_lat\r\ndf['brewery_lng'] = brewery_location_lng<\/code><\/pre><p>And view our DataFrame:<\/p>\n<pre class=\"e2-text-code\"><code>df.head()<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/6-4.png\" width=\"866\" height=\"185\" alt=\"\" \/>\n<\/div>\n<p>Let’s sort the values by <span class=\"inline-code\">brewery_id<\/span>  and store our DataFrame as a CSV file without index column and headings:<\/p>\n<pre class=\"e2-text-code\"><code>df = df.sort_values(by='brewery_id')\r\ndf.to_csv('brewery_data.csv', index=False, header=False)<\/code><\/pre><h2>Creating a Clickhouse dictionary<\/h2>\n<p>You can create Clickouse dictionaries in many different ways. We will try to structure it in an XML file, configure the server files, and access it through our client. The XML file structure will be the following:<\/p>\n<p class=\"note\">Learn more about other ways you can create Clickhouse dictionaries <a href=\"https:\/\/clickhouse.tech\/docs\/ru\/engines\/table-engines\/special\/dictionary\/\">in the documentation<\/a><\/p>\n<pre class=\"e2-text-code\"><code>&lt;yandex&gt;\r\n&lt;dictionary&gt;\r\n        &lt;name&gt;breweries&lt;\/name&gt;\r\n        &lt;source&gt;\r\n                &lt;file&gt;\r\n                        &lt;path&gt;\/home\/ubuntu\/brewery_data.csv&lt;\/path&gt;\r\n                        &lt;format&gt;CSV&lt;\/format&gt;\r\n                &lt;\/file&gt;\r\n        &lt;\/source&gt;\r\n        &lt;layout&gt;\r\n                &lt;flat \/&gt;\r\n        &lt;\/layout&gt;\r\n        &lt;structure&gt;\r\n                &lt;id&gt;\r\n                        &lt;name&gt;brewery_id&lt;\/name&gt;\r\n                &lt;\/id&gt;\r\n                &lt;attribute&gt;\r\n                        &lt;name&gt;beer_count&lt;\/name&gt;\r\n                        &lt;type&gt;UInt64&lt;\/type&gt;\r\n                        &lt;null_value&gt;Null&lt;\/null_value&gt;\r\n                &lt;\/attribute&gt;\r\n                &lt;attribute&gt;\r\n                        &lt;name&gt;brewery_name&lt;\/name&gt;\r\n                        &lt;type&gt;String&lt;\/type&gt;\r\n                        &lt;null_value&gt;Null&lt;\/null_value&gt;\r\n                &lt;\/attribute&gt;\r\n                &lt;attribute&gt;\r\n                        &lt;name&gt;brewery_slug&lt;\/name&gt;\r\n                        &lt;type&gt;String&lt;\/type&gt;\r\n                        &lt;null_value&gt;Null&lt;\/null_value&gt;\r\n                &lt;\/attribute&gt;\r\n                &lt;attribute&gt;\r\n                        &lt;name&gt;brewery_page_url&lt;\/name&gt;\r\n                        &lt;type&gt;String&lt;\/type&gt;\r\n                        &lt;null_value&gt;Null&lt;\/null_value&gt;\r\n                &lt;\/attribute&gt;\r\n                &lt;attribute&gt;\r\n                        &lt;name&gt;brewery_city&lt;\/name&gt;\r\n                        &lt;type&gt;String&lt;\/type&gt;\r\n                        &lt;null_value&gt;Null&lt;\/null_value&gt;\r\n                &lt;\/attribute&gt;\r\n                &lt;attribute&gt;\r\n                        &lt;name&gt;lat&lt;\/name&gt;\r\n                        &lt;type&gt;String&lt;\/type&gt;\r\n                        &lt;null_value&gt;Null&lt;\/null_value&gt;\r\n                &lt;\/attribute&gt;\r\n                &lt;attribute&gt;\r\n                        &lt;name&gt;lng&lt;\/name&gt;\r\n                        &lt;type&gt;String&lt;\/type&gt;\r\n                        &lt;null_value&gt;Null&lt;\/null_value&gt;\r\n                &lt;\/attribute&gt;\r\n        &lt;\/structure&gt;\r\n        &lt;lifetime&gt;300&lt;\/lifetime&gt;\r\n&lt;\/dictionary&gt;\r\n&lt;\/yandex&gt;<\/code><\/pre><p><span class=\"inline-code\">name<\/span> is a dictionary name,  <span class=\"inline-code\">attribute<\/span>  holds the properties of the columns, <span class=\"inline-code\">id<\/span>  is a key field,   <span class=\"inline-code\">file<\/span> stores file path and format. We are going to store our file in this directory: \/home\/ubuntu.<\/p>\n<p>Let’s upload our CSV and XML files to the server, it can be done using an FTP like FileZilla. We explained how to deploy Clickhouse on an Amazon instance in our <a href=\"http:\/\/leftjoin.ru\/all\/stavim-clickhouse-na-aws\/\">previous article<\/a>, this time need to do the same. Open your FileZilla client and go to SFTP settings to add a private key:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/7-4.png\" width=\"777\" height=\"557\" alt=\"\" \/>\n<\/div>\n<p>Connect to your server address, it can be found in the EC2 management console. Specify SFTP as a protocol, your Host, and Ubuntu as a username.<\/p>\n<p class=\"note\">Your Public DNS may change in case of overload<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/8-3.png\" width=\"452\" height=\"451\" alt=\"\" \/>\n<\/div>\n<p>After connecting we will wind up in this location \/home\/ubuntu. Let’s put the files in that folder and connect via SSH using Termius. Then we need to move the files to \/etc\/clickhouse-server to view them in Clickhouse:<\/p>\n<p class=\"note\">Learn how you can connect to an AWS server using SSH client from our previous material <a href=\"http:\/\/leftjoin.ru\/all\/stavim-clickhouse-na-aws\/\" class=\"nu\">“<u>Installing Clickhouse on AWS<\/u>”<\/a><\/p>\n<pre class=\"e2-text-code\"><code>sudo mv breweries_dictionary.xml \/etc\/clickhouse server\/<\/code><\/pre><p>Go to the config file:<\/p>\n<pre class=\"e2-text-code\"><code>cd \/etc\/clickhouse-server\r\nsudo nano config.xml<\/code><\/pre><p>We need the <dictionaries_config> tag, it’s the path to a file that describes the dictionaries structure. Specify the path to our XML file:<\/p>\n<pre class=\"e2-text-code\"><code>&lt;dictionaries_config&gt;\/etc\/clickhouse-server\/breweries_dictionary.xml&lt;\/dictionaries_config&gt;<\/code><\/pre><p>Save our file and run the Clickhouse client:<\/p>\n<pre class=\"e2-text-code\"><code>clickhouse client<\/code><\/pre><p>Let’s check that the dictionary really loaded:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT * FROM system.dictionaries\\G<\/code><\/pre><p>In case of success you will get the following:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/9-3.png\" width=\"854\" height=\"344\" alt=\"\" \/>\n<\/div>\n<p>Now, let’s write a query with the  <a href=\"https:\/\/clickhouse.tech\/docs\/ru\/sql-reference\/functions\/ext-dict-functions\/#dictget\">dictGet<\/a> function to get the name of the brewery with ID 999. Pass in the dictionary name, as the first argument, then the filed name and ID.<\/p>\n<pre class=\"e2-text-code\"><code>SELECT dictGet('breweries', 'brewery_name', toUInt64(999))<\/code><\/pre><p>And our query returns this:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/10.png\" width=\"400\" height=\"60\" alt=\"\" \/>\n<\/div>\n<p>Similarly, we could use this function to get a beer name, when the table contains only IDs.<\/p>\n",
            "date_published": "2020-06-16T10:25:07+03:00",
            "date_modified": "2020-06-16T10:18:15+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/1-7.png",
            "_date_published_rfc2822": "Tue, 16 Jun 2020 10:25:07 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "32",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/1-7.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/2-7.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-8.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/4-7.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/5-5.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/11.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/6-4.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/7-4.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/8-3.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/9-3.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/10.png"
                ]
            }
        },
        {
            "id": "30",
            "url": "https:\/\/en.leftjoin.ru\/all\/sql-window-functions-cheat-sheet-with-examples\/",
            "title": "SQL Window Functions Cheat Sheet with examples",
            "content_html": "<p>Window functions are calculation functions that can increase the efficiency and reduce the complexity of SQL queries, making things much easier:<br \/>\nView as <a href=\"http:\/\/valiotti.com\/leftjoin\/files\/Wndow_Functions_Cheat_Sheet.pdf\"><i  class=\"fa fa-file-pdf-o\"> PDF <\/i><\/a><\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/Window_Functions_Cheat_Sheet-1.png\" width=\"2339\" height=\"1654\" alt=\"\" \/>\n<\/div>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/Window_Functions_Cheat_Sheet-2.png\" width=\"2339\" height=\"1654\" alt=\"\" \/>\n<\/div>\n<p>Special Thanks to <a href=\"https:\/\/learnsql.com\/\">LearnSQL<\/a>, best place to master your SQL skills !<\/p>\n",
            "date_published": "2020-06-02T12:17:13+03:00",
            "date_modified": "2020-06-05T11:36:45+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/Window_Functions_Cheat_Sheet-1.png",
            "_date_published_rfc2822": "Tue, 02 Jun 2020 12:17:13 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "30",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/Window_Functions_Cheat_Sheet-1.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/Window_Functions_Cheat_Sheet-2.png"
                ]
            }
        },
        {
            "id": "27",
            "url": "https:\/\/en.leftjoin.ru\/all\/collecting-data-on-ad-campaigns-from-vkontakte\/",
            "title": "Collecting Data on Ad Campaigns from VK.com",
            "content_html": "<p>We have a lot to share in today’s longread: we’ll retrieve data on ad campaigns from Vkontakte (widely popular social network in Russia and CIS countries)  and compare them to Google Analytics data in Redash. This time we don’t need to create a server,  as our data will be transferred to Google Docs via Google Sheets API.<\/p>\n<p><b>Getting an Access Token<\/b><br \/>\nWe need to create an app to receive our access token. Follow this link <a href=\"https:\/\/vk.com\/apps?act=manage\">https:\/\/vk.com\/apps?act=manage<\/a> and click “Create app” on the developer’s page. Choose a name for your app and check it as a “Standalone app”. Then, click Settings in the left menu and save your app ID.<\/p>\n<p class=\"note\">More details on access tokens can be found here: <a href=\"https:\/\/vk.com\/dev\/access_token\" class=\"nu\">“<u>Getting an access token<\/u>”<\/a><\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/1-4.png\" width=\"730\" height=\"292\" alt=\"\" \/>\n<\/div>\n<p>Copy this link:<\/p>\n<pre class=\"e2-text-code\"><code>https:\/\/oauth.vk.com\/authorize?client_id=YourClientID&amp;scope=ads&amp;response_type=token<\/code><\/pre><p>And change <span class=\"inline-code\">YourClientID<\/span>  to your app ID, this will allow you to get information about your advertising account. Open this link in your browser and you will be redirected to another page, which  URL address holds your generated access token.<\/p>\n<p class=\"note\">Access token expires in 86400 seconds or 24 hours. If you want to generate a token with an unlimited lifetime period, just pass scope to the offline parameter. In case if you need to generate a new token – change your password account or terminate all active sessions in security settings.<\/p>\n<p>You will also need your advertising account ID to make API requests. It can be found via this link, just copy it:  <a href=\"https:\/\/vk.com\/ads?act=settings\">https:\/\/vk.com\/ads?act=settings<\/a><\/p>\n<p><b>Using APIs to collect data<\/b><br \/>\nLet’s write a script that would allow us to retrieve information on all user’s ad campaigns: number of impressions, сlicks and costs. The script will pass this data to a DataFrame and send it to Google Docs.<\/p>\n<pre class=\"e2-text-code\"><code>from oauth2client.service_account import ServiceAccountCredentials\r\nfrom pandas import DataFrame\r\nimport requests\r\nimport gspread\r\nimport time<\/code><\/pre><p>We have several constant variables: access token, advertising account ID and Vkontakte API Version. Here we are using the most recent API version, which is 5.103.<\/p>\n<pre class=\"e2-text-code\"><code>token = 'fa258683fd418fafcab1fb1d41da4ec6cc62f60e152a63140c130a730829b1e0bc'\r\nversion = 5.103\r\nid_rk = 123456789<\/code><\/pre><p>To get advertising stats you need to use the  <span class=\"inline-code\">ads.getStatistics<\/span> method and pass your ad campaign ID to it. Since we don’t run any advertisements yet,  we’ll use the  <span class=\"inline-code\">ads.getAds<\/span> method that returns IDs of ads and campaigns.<\/p>\n<p class=\"note\">Learn more about the API methods available for Vkontakte <a href=\"https:\/\/vk.com\/dev\/methods\">here<\/a><\/p>\n<p>Use the <span class=\"inline-code\">requests<\/span>  library to send a request and convert the response to JSON.<\/p>\n<pre class=\"e2-text-code\"><code class=\"Python\">\r\ncampaign_ids = []\r\nads_ids = []\r\nr = requests.get('https:\/\/api.vk.com\/method\/ads.getAds', params={\r\n    'access_token': token,\r\n    'v': version,\r\n    'account_id': id_rk\r\n})\r\ndata = r.json()['response']\r\n<\/code>\n<\/pre>\n<p>We have a familiar list of dictionaries returned, similar to the one we have reviewed in the previous article, <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/analysing-data-on-facebook-ad-campaigns-with-redash\/\"> “Analysing data on Facebook Ad Campaigns with Redash”<\/a>.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/2-4.png\" width=\"993\" height=\"328\" alt=\"\" \/>\n<\/div>\n<p>Fill in the <span class=\"inline-code\">ad_campaign_dict<\/span> dictionary as follows: specify ad ID as a key, and campaign ID as a value,  where this ad belongs to.<\/p>\n<pre class=\"e2-text-code\"><code>ad_campaign_dict = {}\r\nfor i in range(len(data)):\r\n    ad_campaign_dict[data[i]['id']] = data[i]['campaign_id']<\/code><\/pre><p>Having ID for every ad needed we can invoke the  <span class=\"inline-code\">ads.getStatistics<\/span> method to collect data on the number of impressions,  clicks, costs, and dates for a particular ad, so create several empty lists in advance.<\/p>\n<pre class=\"e2-text-code\"><code>ads_campaign_list = []\r\nads_id_list = []\r\nads_impressions_list = []\r\nads_clicks_list = []\r\nads_spent_list = []\r\nads_day_start_list = []\r\nads_day_end_list = []<\/code><\/pre><p>We need to invoke the <span class=\"inline-code\">getStatistics<\/span>  method for each ad separately, let’s refer to the <span class=\"inline-code\">ad_campaign_dict<\/span> and iterate our requests.  Retrieve all-time data by calling the <span class=\"inline-code\">‘period’<\/span> method with the  <span class=\"inline-code\">‘overall’<\/span> value. Some ads may not have impression or clicks if they haven’t been launched yet, this may cause a  <span class=\"inline-code\">KeyError<\/span>. Let’s recall to the <span class=\"inline-code\">try — except<\/span> approach to handle this error.<\/p>\n<pre class=\"e2-text-code\"><code>for ad_id in ad_campaign_dict:\r\n        r = requests.get('https:\/\/api.vk.com\/method\/ads.getStatistics', params={\r\n            'access_token': token,\r\n            'v': version,\r\n            'account_id': id_rk,\r\n            'ids_type': 'ad',\r\n            'ids': ad_id,\r\n            'period': 'overall',\r\n            'date_from': '0',\r\n            'date_to': '0'\r\n        })\r\n        try:\r\n            data_stats = r.json()['response']\r\n            for i in range(len(data_stats)):\r\n                for j in range(len(data_stats[i]['stats'])):\r\n                    ads_impressions_list.append(data_stats[i]['stats'][j]['impressions'])\r\n                    ads_clicks_list.append(data_stats[i]['stats'][j]['clicks'])\r\n                    ads_spent_list.append(data_stats[i]['stats'][j]['spent'])\r\n                    ads_day_start_list.append(data_stats[i]['stats'][j]['day_from'])\r\n                    ads_day_end_list.append(data_stats[i]['stats'][j]['day_to'])\r\n                    ads_id_list.append(data_stats[i]['id'])\r\n                    ads_campaign_list.append(ad_campaign_dict[ad_id])\r\n        except KeyError:\r\n            continue<\/code><\/pre><p>Now, create a DataFrame and print out the first 5 data points<\/p>\n<pre class=\"e2-text-code\"><code>df = DataFrame()\r\ndf['campaign_id'] = ads_campaign_list\r\ndf['ad_id'] = ads_id_list\r\ndf['impressions'] = ads_impressions_list\r\ndf['clicks'] = ads_clicks_list\r\ndf['spent'] = ads_spent_list\r\ndf['day_start'] = ads_day_start_list\r\ndf['day_end'] = ads_day_end_list\r\nprint(df.head())<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/3-5.png\" width=\"514\" height=\"172\" alt=\"\" \/>\n<\/div>\n<p><b>Exporting Data to Google Docs<\/b><br \/>\nWe’ll need a Google API access token, navigate to <a href=\"https:\/\/console.developers.google.com\">https:\/\/console.developers.google.com<\/a>  and create one. Choose any name you like, then go to your Dashboard and click “Enable APIs and Services”. Choose Google Drive API from the list, enable it and do exactly the same for Google Sheets API.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/g_sheet.png\" width=\"886\" height=\"259\" alt=\"\" \/>\n<\/div>\n<p>After activation you will be redirected to the API control panel. Click Credentials – Create Credentials,  click choose data type and create an account. Choosing a role is optional, just proceed and specify JSON as a key type.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/service_acc.png\" width=\"670\" height=\"472\" alt=\"\" \/>\n<\/div>\n<p>After these steps you can download a JSON file with your credentials,  we’ll rename it to <span class=\"inline-code\">«credentials.json»<\/span>. On the main page you’ll find the email field – copy your email address.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/service_acc_2.png\" width=\"617\" height=\"51\" alt=\"\" \/>\n<\/div>\n<p>Go to <a href=\"https:\/\/docs.google.com\/spreadsheets\">https:\/\/docs.google.com\/spreadsheets<\/a> and create a new file named  <span class=\"inline-code\">data<\/span>, we’ll pass data from our DataFrame to it. Put the  <span class=\"inline-code\">credentials.json<\/span>  file in one directory with the script and continue coding. Add these links to the scope list:<\/p>\n<pre class=\"e2-text-code\"><code>scope = ['https:\/\/spreadsheets.google.com\/feeds', 'https:\/\/www.googleapis.com\/auth\/drive']<\/code><\/pre><p>We will use the  <span class=\"inline-code\">ServiceAccountCredentials.from_json_keyfile_name<\/span> and  <span class=\"inline-code\">gspread.authorize<\/span> methods available in the  <span class=\"inline-code\">oauth2client<\/span> and  <span class=\"inline-code\">gspread<\/span>  libraries for authenticaion process. Specify your file name and the scope variable in the <span class=\"inline-code\">ServiceAccountCredentials.from_json_keyfile_name<\/span> method. The  <span class=\"inline-code\">sheet<\/span> variable will allow us to send requests to our file in Google Docs.<\/p>\n<pre class=\"e2-text-code\"><code>creds = ServiceAccountCredentials.from_json_keyfile_name('credentials.json', scope)\r\nclient = gspread.authorize(creds)\r\nsheet = client.open('data').sheet1<\/code><\/pre><p>Apply the <span class=\"inline-code\">update_cell<\/span> method to enter new value in a table cell. It’s worth mentioning that the indexing starts at 0, not 1. With the first loop we’ll move the column names of our DataFrame. And with the following loops we’ll move the rest of our data points. The default limits allow us to make 100 loops for 100 seconds. These restrictions may cause errors and stop our script, that’s why we need to use <span class=\"inline-code\">time.sleep<\/span> and make the script sleep for 1 second after each loop.<\/p>\n<pre class=\"e2-text-code\"><code>count_of_rows = len(df)\r\ncount_of_columns = len(df.columns)\r\nfor i in range(count_of_columns):\r\n    sheet.update_cell(1, i + 1, list(df.columns)[i])\r\nfor i in range(1, count_of_rows + 1):\r\n    for j in range(count_of_columns):\r\n        sheet.update_cell(i + 1, j + 1, str(df.iloc[i, j]))\r\n        time.sleep(1)<\/code><\/pre><p>In case of success, you’ll get the same table:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/5-3.png\" width=\"831\" height=\"377\" alt=\"\" \/>\n<\/div>\n<p><b>Exporting data to Redash<\/b><\/p>\n<p class=\"note\">See how you can connect Google Analytics to Redash in this article <a href=\"https:\/\/www.valiotti.com\/leftjoin\/all\/how-to-connect-google-analytics-to-redash\/\">«How to connect Google Analytics to Redash?»<\/a>.<\/p>\n<p>Having a table with Google Analytics and ad campaigns from Vkontakte exported we can compare them by writing the following query:<\/p>\n<pre class=\"e2-text-code\"><code>SELECT\r\n    query_50.day_start,\r\n    CASE WHEN ga_source LIKE '%vk%' THEN 'vk.com' END AS source,\r\n    query_50.spent,\r\n    query_50.impressions,\r\n    query_50.clicks,\r\n    SUM(query_49.ga_sessions) AS sessions,\r\n    SUM(query_49.ga_newUsers) AS users\r\nFROM query_49\r\nJOIN query_50\r\nON query_49.ga_date = query_50.day_start\r\nWHERE query_49.ga_source LIKE '%vk%' AND DATE(query_49.ga_date) BETWEEN '2020-05-16' AND '2020-05-20'\r\nGROUP BY query_49.ga_date, source<\/code><\/pre><p><span class=\"inline-code\">ga_source<\/span> — the traffic source, from which a user was redirected. Use the  <span class=\"inline-code\">CASE<\/span> method to combine everything that contains “vk” in one column called «vk.com». With the help of <span class=\"inline-code\">JOIN<\/span> operator we can add the table with the data on ad campaigns, merging by date. Let’s take the day of the last ad campaign and a couple of days after, this will result in the following output:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/en.leftjoin.ru\/pictures\/123.png\" width=\"945\" height=\"108\" alt=\"\" \/>\n<\/div>\n<p><b>Takeaways<\/b><br \/>\nNow we have a table that reflects how much were spent in ad costs on a certain day, the number of users who viewed this ad, were engaged and redirected to our website,  and then completed the sign-up process.<\/p>\n",
            "date_published": "2020-05-26T07:15:13+03:00",
            "date_modified": "2020-05-26T07:14:10+03:00",
            "image": "https:\/\/en.leftjoin.ru\/pictures\/1-4.png",
            "_date_published_rfc2822": "Tue, 26 May 2020 07:15:13 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "27",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css",
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/en.leftjoin.ru\/pictures\/1-4.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/2-4.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/3-5.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/g_sheet.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/service_acc.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/service_acc_2.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/5-3.png",
                    "https:\/\/en.leftjoin.ru\/pictures\/123.png"
                ]
            }
        }
    ],
    "_e2_version": 3386,
    "_e2_ua_string": "E2 (v3386; Aegea)"
}