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

<channel>

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

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

<item>
<title>Нормализация данных через запрос в SQL</title>
<guid isPermaLink="false">109</guid>
<link>http://test.leftjoin.ru/all/data-scaling-with-sql/</link>
<comments>http://test.leftjoin.ru/all/data-scaling-with-sql/</comments>
<description>
&lt;p&gt;Главный принцип анализа данных GIGO (от англ. garbage in — garbage out, дословный перевод «мусор на входе — мусор на выходе») говорит нам о том, что ошибки во входных данных всегда приводят к неверным результатам анализа. От того, насколько хорошо подготовлены  данные, зависят результаты всей вашей работы.&lt;/p&gt;
&lt;p&gt;Например, перед нами стоит задача подготовить выборку для использования в алгоритме машинного обучения (модели k-NN, k-means, логической регрессии и др). Признаки в исходном наборе данных могут быть в разном масштабе, как, например, возраст и рост человека. Это может привести к некорректной работе алгоритма. Такого рода данные нужно предварительно масштабировать.&lt;/p&gt;
&lt;p&gt;В данном материале мы рассмотрим способы масштабирования данных через запрос в SQL: масштабирование методом min-max, min-max для произвольного диапазона и z-score нормализация. Для каждого из методов мы подготовили по два примера написания запроса — один с помощью подзапроса SELECT, а второй используя оконную функцию OVER().&lt;/p&gt;
&lt;p&gt;Для работы возьмем таблицу &lt;b&gt;students&lt;/b&gt; с данными о росте учащихся.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;Min-Max масштабирование&lt;/h2&gt;
&lt;p&gt;Подход min-max масштабирования заключается в том, что данные масштабируются до фиксированного диапазона, который обычно составляет от 0 до 1. В данном случае мы получим все данные в одном масштабе, что исключит влияние выбросов на выводы.&lt;/p&gt;
&lt;p&gt;Выполним масштабирование по формуле:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-05-12--16.42.52.png" width="460" height="104" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Умножаем числитель на 1.0, чтобы в результате получилось число с плавающей точкой.&lt;/p&gt;
&lt;p&gt;SQL-запрос с подзапросом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       1.0 * (height-t1.min_height)/(t1.max_height - t1.min_height) AS scaled_minmax
  FROM students, 
      (SELECT min(height) as min_height, 
              max(height) as max_height 
         FROM students
      ) as t1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SQL-запрос с оконной функцией:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       (height - MIN(height) OVER ()) * 1.0 / (MAX(height) OVER () - MIN(height) OVER ()) AS scaled_minmax
  FROM students;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результате мы получим переменные в диапазоне [0...1], где за 0 принят рост самого невысокого учащегося, а 1 рост самого высокого.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;scaled_minmax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;td&gt;0.46809&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;td&gt;0.61702&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;td&gt;0.12766&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;td&gt;0.57447&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;0.2766&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;td&gt;0.76596&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;td&gt;0.53191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;0.2766&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;Масштабирование для заданного диапазона&lt;/h2&gt;
&lt;p&gt;Вариант min-max нормализации для произвольных значений. Не всегда, когда речь идет о масштабировании данных, диапазон значений находится в промежутке между 0 и 1.&lt;br /&gt;
Формула для вычисления в этом случае такая:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-05-12--16.43.04.png" width="530" height="104" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Это даст нам возможность масштабировать данные к произвольной шкале. В нашем примере пусть а=10.0, а b=20.0.&lt;/p&gt;
&lt;p&gt;SQL-запрос с подзапросом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       ((height - min_height) * (20.0 - 10.0) / (max_height - min_height)) + 10 AS scaled_ab
  FROM students,
      (SELECT MAX(height) as max_height, 
              MIN(height) as min_height
         FROM students  
      ) t1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SQL-запрос с оконной функцией:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       ((height - MIN(height) OVER() ) * (20.0 - 10.0) / (MAX(height) OVER() - MIN(height) OVER())) + 10.0 AS scaled_ab
  FROM students;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получаем аналогичные результаты, что и в предыдущем методе, но данные распределены в диапазоне от 10 до 20.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;scaled_ab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;td&gt;14.68085&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;td&gt;16.17021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;td&gt;11.2766&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;td&gt;15.74468&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;12.76596&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;td&gt;17.65957&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;td&gt;15.31915&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;12.76596&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;Нормализация с помощью z-score&lt;/h2&gt;
&lt;p&gt;В результате z-score нормализации данные будут масштабированы таким образом, чтобы они имели свойства стандартного нормального распределения — среднее (μ) равно 0, а стандартное отклонение (σ) равно 1.&lt;/p&gt;
&lt;p&gt;Вычисляется z-score по формуле:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-05-12--16.43.19.png" width="368" height="101" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;SQL-запрос с подзапросом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       (height - t1.mean) * 1.0 / t1.sigma AS zscore
  FROM students,
      (SELECT AVG(height) AS mean, 
              STDDEV(height) AS sigma
         FROM students
        ) t1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SQL-запрос с оконной функцией:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       (height - AVG(height) OVER()) * 1.0 / STDDEV(height) OVER() AS z-score
  FROM students;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результате мы сразу заметим выбросы, которые выходят за пределы стандартного отклонения.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;zscore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;td&gt;0.01488&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;td&gt;0.53582&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;td&gt;1.87538&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;td&gt;-1.17583&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;td&gt;0.38698&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;-0.65489&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;td&gt;-1.62235&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;td&gt;1.05676&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;td&gt;0.23814&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;-0.65489&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
</description>
<pubDate>Thu, 13 May 2021 11:15:58 +0300</pubDate>
</item>

<item>
<title>Обработка изображения с чеком для поиска QR-кода через библиотеку skimage</title>
<guid isPermaLink="false">44</guid>
<link>http://test.leftjoin.ru/all/obrabotka-izobrazheniya-s-chekom-dlya-poiska-qr-koda/</link>
<comments>http://test.leftjoin.ru/all/obrabotka-izobrazheniya-s-chekom-dlya-poiska-qr-koda/</comments>
<description>
&lt;p&gt;Есть много разных сканеров для QR, но не всегда изображение обладает хорошим качеством. В компьютерном зрении для этого используется Image Pre-processing: предобработка изображения. Сегодня рассмотрим, как средствами библиотеки &lt;span class="inline-code"&gt;scikit-image&lt;/span&gt; помочь QR-сканеру найти код на картинке.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from matplotlib import pyplot as plt
import skimage
from skimage import util, exposure, io, measure, feature
from scipy import ndimage as ndi
import numpy as np
import cv2&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Проблема&lt;/h2&gt;
&lt;p&gt;Попробуем просканировать чек из материала &lt;a href="http://test.leftjoin.ru/all/sobiraem-dannye-s-chekov-gipermarketov-na-python/" class="nu"&gt;«&lt;u&gt;Собираем данные с чеков гипермаркетов на Python&lt;/u&gt;»&lt;/a&gt;. Прочтём картинку методом &lt;span class="inline-code"&gt;imread&lt;/span&gt; библиотеки &lt;span class="inline-code"&gt;matplotlib&lt;/span&gt; и покажем его на экране:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;img = plt.imread('чек.jpg')
plt.imshow(img)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/1.jpg" width="273" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Кажется, в такой каше сложно что-либо разобрать. Воспользуемся готовой функцией для чтения чтения QR-кода из библиотеки &lt;span class="inline-code"&gt;opencv&lt;/span&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def qr_reader(img):
    detector = cv2.QRCodeDetector()
    data, bbox, _ = detector.detectAndDecode(img)
    if data:
        print(data)
    else:
        print('Ничего не нашлось!')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И обратимся к ней, чтобы просканировать наше изображение:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;qr_reader(img)&lt;/code&gt;&lt;/pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;Ничего не нашлось!&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И это можно понять: обилие лишних пикселей мешает сканеру распознать здесь QR-код. Тем не менее, мы можем помочь сканеру, указав где находится искомая область.&lt;/p&gt;
&lt;h2&gt;Решение&lt;/h2&gt;
&lt;p&gt;Сделаем так: уберём с картинки всё лишнее, найдём координаты прямоугольника с QR-кодом, чтобы затем передать в функцию &lt;span class="inline-code"&gt;qr_reader&lt;/span&gt; не исходное изображение, а исключительно QR-код. Первым делом уменьшим шум, используя &lt;a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.median_filter.html"&gt;медианный фильтр&lt;/a&gt; и сконвертируем изображение из rgb в gray: QR-код состоит всего из двух цветов, так что работать с остальными нам не нужно.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;image = ndi.median_filter(util.img_as_float(img), size=9)
image = skimage.color.rgb2gray(image)
plt.imshow(image, cmap='gray')&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2.jpg" width="273" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Медианный фильтр размыл изображение, и разбросанные одинокие пиксели стали менее отчётливыми, а QR теперь выделяется на их фоне. Попробуем применить &lt;a href="https://scikit-image.org/docs/dev/api/skimage.exposure.html"&gt;adjust_gamma&lt;/a&gt; к изображению. Эта функция возводит в степень gamma значение каждого пикселя: чем меньше будет этот параметр — тем меньше будет значение пикселя и тем ближе к белому он будет становиться. Попробуем взять &lt;span class="inline-code"&gt;gamma&lt;/span&gt; за 0.5.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;pores_gamma = exposure.adjust_gamma(image, gamma=0.5)
plt.imshow(pores_gamma, cmap='gray')&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/3.jpg" width="273" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Заметно, что QR стал ещё отчетливее прочего на фото. Воспользуемся этим: все пиксели, значение которых меньше 0.3 сделаем 0, а остальных — 1.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;thresholded = (pores_gamma &amp;lt;= 0.3)
plt.imshow(thresholded, cmap='gray')&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/4.jpg" width="273" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;А теперь воспользуемся &lt;a href="https://scikit-image.org/docs/dev/auto_examples/edges/plot_canny.html"&gt;детектором границ canny&lt;/a&gt; для полученного изображения &lt;span class="inline-code"&gt;thresholded&lt;/span&gt;. Этот оператор сам сглаживает изображение и ищет градиенты: границы находятся там, где градиент принимает максимальное значение. С повышением параметра sigma детектор &lt;span class="inline-code"&gt;canny&lt;/span&gt; перестает замечать менее отчетливые границы.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;edge = feature.canny(thresholded, sigma=6)
plt.imshow(edge)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/5.jpg" width="273" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Наконец, получим координаты границ: для этого нарисуем контуры. Получаем их методом &lt;span class="inline-code"&gt;find_contours&lt;/span&gt; и рисуем поверх изображения &lt;span class="inline-code"&gt;edge&lt;/span&gt;. Объекты массива &lt;span class="inline-code"&gt;contours&lt;/span&gt; — координаты по осям X и Y.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;contours = measure.find_contours(edge, 0.5)
plt.imshow(edge)
for contour in contours:
    plt.plot(contour[:,1], contour[:,0], linewidth=2)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/6.jpg" width="273" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Возьмём максимальные и минимальные координаты по X и по Y: это будут границы видимого прямоугольника.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;positions = np.concatenate(contours, axis=0)
min_pos_x = int(min(positions[:,1]))
max_pos_x = int(max(positions[:,1]))
min_pos_y = int(min(positions[:,0]))
max_pos_y = int(max(positions[:,0]))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь, имея координаты, можем на исходном изображении обвести область с кодом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;start = (min_pos_x, min_pos_y)
end = (max_pos_x, max_pos_y)
cv2.rectangle(img, start, end, (255, 0, 0), 5)
io.imshow(img)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/8.jpg" width="300" height="280" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Попробуем срезать оригинальное изображение по этим координатам:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;new_img = img[min_pos_y:max_pos_y, min_pos_x:max_pos_x]
plt.imshow(new_img)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/7.jpg" width="311" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;И передадим новое изображение в функцию &lt;span class="inline-code"&gt;qr_reader&lt;/span&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;qr_reader(new_img)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получаем в ответе:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;t=20190320T2303&amp;amp;s=5803.00&amp;amp;fn=9251440300007971&amp;amp;i=141637&amp;amp;fp=4087570038&amp;amp;n=1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Это то, чего мы и хотели. Конечно, скрипт не будет универсальным, ведь в каждом изображении будут свои недостатки: где-то шума будет больше, где-то фотография размыта, где-то не будет хватать контраста. Поэтому в отдельных случаях потребуется вносить и иные корректировки в изображение. На следующем этапе обработки фотографии мы воспользуемся уже готовой библиотекой.&lt;/p&gt;
</description>
<pubDate>Wed, 03 Jun 2020 12:35:17 +0300</pubDate>
</item>

<item>
<title>Строим модель для предсказания категории продуктов</title>
<guid isPermaLink="false">42</guid>
<link>http://test.leftjoin.ru/all/stroim-model-predskazaniya-kategorii-produktov/</link>
<comments>http://test.leftjoin.ru/all/stroim-model-predskazaniya-kategorii-produktov/</comments>
<description>
&lt;p&gt;Эта статья — продолжение серии материалов &lt;a href="http://test.leftjoin.ru/all/sobiraem-dannye-s-chekov-gipermarketov-na-python/" class="nu"&gt;«&lt;u&gt;Собираем данные с чеков гипермаркетов на Python&lt;/u&gt;»&lt;/a&gt; и &lt;a href="http://test.leftjoin.ru/all/parsim-dannye-kataloga-sayta-ispolzuya-beautiful-soup-i-selenium/" class="nu"&gt;«&lt;u&gt;Парсим данные каталога сайта&lt;/u&gt;»&lt;/a&gt;. В этот раз построим модель, которая обучится на датасете из собранного каталога и классифицирует товарные позиции чека из гипермаркета на продуктовые категории. Суть проблемы: в чеке мы видим данные о каждом товаре отдельно, а иногда хочется быстро понять сколько сегодня потратили денег на «Сладкое».&lt;/p&gt;
&lt;h2&gt;Предобработка датасета&lt;/h2&gt;
&lt;p&gt;Импортируем библиотеку pandas и прочитаем csv-файл с каталогом igoods (мы сформировали его, когда парсили каталог). Заодно посмотрим, как он выглядит:&lt;/p&gt;
&lt;p class="note"&gt;Подробнее о том, как программе эмулировать поведение человека на сайте и собрать датасет из каталога можно прочитать в материале &lt;a href="http://test.leftjoin.ru/all/parsim-dannye-kataloga-sayta-ispolzuya-beautiful-soup-i-selenium/" class="nu"&gt;«&lt;u&gt;Парсим данные каталога сайта&lt;/u&gt;»&lt;/a&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
sku = pd.read_csv('SKU_igoods.csv',sep=';')
sku.head()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/0-2.png" width="718" height="166" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;После парсинга в таблице осталось несколько ненужных колонок: например, нам ни к чему знать цену на продукт и его вес, чтобы построить модель предсказания категории товара. Избавляемся от этих колонок методом drop(), а остальные переименуем через rename() и снова смотрим на таблицу:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sku.drop(columns=['Unnamed: 0', 'Weight','Price'],inplace=True)
sku.rename(columns={&amp;quot;SKU&amp;quot;: &amp;quot;SKU&amp;quot;, &amp;quot;Category&amp;quot;: &amp;quot;Group&amp;quot;},inplace=True)
sku.head()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/1-4.png" width="539" height="180" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Сгруппируем товары по их категории и посчитаем количество функциями groupby() и agg():&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sku.groupby('Group').agg(['count'])&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2-4.png" width="276" height="632" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Наша модель должна обучиться на каталоге и, увидев наименование товара, предсказать его категорию. Но в каталоге многие названия будут непонятны модели. В русском языке, например, много предлогов, союзов и других стоп-слов: мы хотим, чтобы модель понимала, что «Мангал с ребрами жесткости» и «Мангал с 6 шампурами» — продукты одной и той же категории. Для этого почистим все названия: уберём из них союзы, предлоги, междометия, частицы и приведём слова к своим основам при помощи стеммера.&lt;/p&gt;
&lt;p class="note"&gt;Стеммер — программа, которая находит для заданного слова его основу.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import nltk
from nltk.corpus import stopwords
from pymystem3 import Mystem
from string import punctuation
nltk.download('stopwords')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Для стемминга будем использовать стеммер Яндекса из библиотеки pymystem3. Список стоп-слов необходимо расширить — каталог товаров из магазина немного отличается от бытовых ситуаций, в которых базовый набор актуален.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;mystem = Mystem() 
russian_stopwords = stopwords.words(&amp;quot;russian&amp;quot;)
russian_stopwords.extend(['лента','ассорт','разм','арт','что', 'это', 'так', 'вот', 'быть', 'как', 'в', '—', 'к', 'на'])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Опишем функцию подготовки текста. Она приводит текст стеммером к своей основе, убирает из него знаки пунктуации, цифры и стоп-слова. Этот код был найден в одном из kernel на &lt;a href="https://www.kaggle.com/alxmamaev/how-to-easy-preprocess-russian-text"&gt;kaggle&lt;/a&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def preprocess_text(text):
    text = str(text)
    tokens = mystem.lemmatize(text.lower())
    tokens = [token for token in tokens if token not in russian_stopwords\
              and token != &amp;quot; &amp;quot; \
              and len(token)&amp;gt;=3 \
              and token.strip() not in punctuation \
              and token.isdigit()==False]
    text = &amp;quot; &amp;quot;.join(tokens)
    return text&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Проверим, как работает функция:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;preprocess_text(&amp;quot;Мой дядя самых честных правил, Когда не в шутку занемог, Он уважать себя заставил И лучше выдумать не мог.&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получаем:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;'дядя самый честный правило шутка занемогать уважать заставлять выдумывать мочь'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;А значит всё работает как надо — все слова в своей морфологической основе и переведены в нижний регистр, отсутствует пунктуация и предлоги. Теперь опробуем функцию на одном из наименований товара из каталога:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;print(‘Было:’, sku['SKU'][0])
print(‘Стало:’, preprocess_text(sku['SKU'][0]))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получаем:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;Было: Фисташки соленые жареные ТМ 365 дней
Стало: фисташка соленый жареный день&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Функция справляется отлично, теперь можем применить её ко всем наименованиям и вынести обработанные названия в новый столбец processed. Посмотрим, как выглядит датасет теперь:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sku['processed']=sku['SKU'].apply(preprocess_text)
sku.head()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/3-4.png" width="840" height="166" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Строим модель предсказания категории&lt;/h2&gt;
&lt;p&gt;Для предсказания категории товара будем использовать &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html"&gt;CountVectorizer&lt;/a&gt; и &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.MultinomialNB.html?highlight=multinomialnb"&gt;наивный байесовский классификатор&lt;/a&gt;. Первый разобьёт текст на токены и посчитает их количество, а второй — простейший мультикатегорийный классификатор, позволит обучить модель предсказывать категорию товара. Также нам потребуются &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfTransformer.html"&gt;TfidfTransformer&lt;/a&gt; для подсчета весов вхождения каждого токена. Поскольку мы хотим запустить все функции одну за другой, обратимся к библиотеке Pipeline.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
from imblearn.pipeline import Pipeline&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Поделим наш датасет на X — обработанные наименования товаров и на Y — их категории. Разделим на обучающую и тестовую выборку, отдав под тесты 33% от общего числа данных.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;x = sku.processed
y = sku.Group
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.33)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Пройдём пайплайном следующие команды:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CountVectorizer() — вернет матрицу с количеством вхождений каждого токена&lt;/li&gt;
&lt;li&gt;TfidfTransformer() —  преобразует эту матрицу в нормализованное представление tf-idf&lt;/li&gt;
&lt;li&gt;MultinomialNB() — наивный байесовский классификатор для предсказания категории товара&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;text_clf = Pipeline([('vect', CountVectorizer(ngram_range=(1,2))),
                     ('tfidf', TfidfTransformer()), 
                    ('clf', MultinomialNB())])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;На выходе получим модель в text_clf, которую затем обучим по обучающей выборке и посчитаем предсказания по тестовой выборке:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;text_clf = text_clf.fit(X_train, y_train)
y_pred = text_clf.predict(X_test)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;А теперь оценим модель:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;print('Score:', text_clf.score(X_test, y_test))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получим такую точность:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;Score: 0.923949864498645&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Верификация на реальных данных&lt;/h2&gt;
&lt;p&gt;Можем проверить, как работает модель на реальных данных из свежего чека. В материале о том, &lt;a href="http://test.leftjoin.ru/all/sobiraem-dannye-s-chekov-gipermarketov-na-python/"&gt;как получить продукты из чека гипермаркета&lt;/a&gt;, на выходе мы получали DataFrame с продуктами — возьмём его и применим к названиям товаров функцию preprocess_text.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;my_products['processed']=my_products['name'].apply(preprocess_text)
my_products.head()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/5-4.png" width="807" height="205" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Заполним новый столбец prediction — он будет предсказывать категорию товара по его названию. Передаем ему колонку с обработанными названиями и создаём новую колонку с предсказаниями.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;prediction = text_clf.predict(my_products['processed'])
my_products['prediction']=prediction
my_products[['name', 'prediction']]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;DataFrame станет таким:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/6-3.png" width="666" height="483" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;И посчитаем сумму по каждой категории:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;my_products.groupby('prediction').sum()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/8-2.png" width="389" height="404" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;В целом, модель справляется неплохо с предсказаниями: сосиски уходят в мясную гастрономию, творог — в молочные продукты, багет — в хлеб и выпечку. И всё же заметно, что киви почему-то относится к молочным продуктам, а груши — к эко-продуктам. Проблема в том, что в каталоге в этих разделах много товаров «со вкусом груши» или «со вкусом киви», из-за чего наивный байесовский классификатор отдаёт предпочтение тому классу, экземпляров которого в датасете больше. Это известная проблема несбалансированных классов, которую можно победить ресемплингом исходного датасета или задав нужные веса в модели.&lt;/p&gt;
</description>
<pubDate>Fri, 29 May 2020 10:40:02 +0300</pubDate>
</item>

<item>
<title>Карта Machine Learning алгоритмов</title>
<guid isPermaLink="false">12</guid>
<link>http://test.leftjoin.ru/all/karta-machine-learning-algoritmov/</link>
<comments>http://test.leftjoin.ru/all/karta-machine-learning-algoritmov/</comments>
<description>
&lt;p&gt;На днях попалась замечательная карта machine learning алгоритмов от &lt;i&gt;scikit-learn&lt;/i&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/ml_map@2x.png" width="1061" height="661.5" alt="" /&gt;
&lt;/div&gt;
</description>
<pubDate>Sat, 13 Apr 2019 16:20:56 +0300</pubDate>
</item>

<item>
<title>Собираем данные с чеков гипермаркетов на Python</title>
<guid isPermaLink="false">10</guid>
<link>http://test.leftjoin.ru/all/sobiraem-dannye-s-chekov-gipermarketov-na-python/</link>
<comments>http://test.leftjoin.ru/all/sobiraem-dannye-s-chekov-gipermarketov-na-python/</comments>
<description>
&lt;p&gt;&lt;b&gt;Update:&lt;/b&gt; к сожалению, информация в данном посте устарела. Рекомендуем изучить &lt;a href="http://test.leftjoin.ru/all/nalog-ru-client/"&gt;наш новый пост&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Недавно, покупая в очередной раз продукты в гипермаркете, вспомнил, что согласно ФЗ-54 любой оператор торговли, который пробивает кассовый чек, обязан отправлять данные чека в налоговую.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/lenta-receipt@2x.jpg" width="787" height="762" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;Чек из гипермаркета «Лента», QR-код, который нас интересует, обведен&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Что это значит для нас, аналитиков данных? Что мы можем лучше узнать себя, свои потребности и получить интересные данные о собственных покупках.&lt;/p&gt;
&lt;p&gt;Попробуем в рамках серии постов собрать небольшой прототип приложения, которое позволит строить динамику своих покупок.  Итак, начнем с того, что в каждом чеке есть QR-code, если его распознать, то мы получим следующую строку:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;i&gt;t=20190320T2303&amp;s=5803.00&amp;fn=9251440300007971&amp;i=141637&amp;fp=4087570038&amp;n=1&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;В данной строке содержатся:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;i&gt;t&lt;/i&gt; — timestamp, время, когда вы осуществили покупку&lt;br /&gt;
&lt;i&gt;s&lt;/i&gt; — сумма чека&lt;br /&gt;
&lt;i&gt;fn&lt;/i&gt; — кодовый номер fss, потребуется далее в запросе к API&lt;br /&gt;
&lt;i&gt;i&lt;/i&gt; — номер чека, он нам потребуется далее в запросе к API&lt;br /&gt;
&lt;i&gt;fp&lt;/i&gt; — параметр fiscalsign, потребуется далее в запросе к API&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;В рамках решения первого шага нашей задачи мы будем парсить данные чека и собирать их в pandas dataframe, используя модули Python.&lt;/p&gt;
&lt;p&gt;Мы воспользуемся &lt;a href="https://habr.com/ru/post/358966/"&gt;API&lt;/a&gt;, который отдает данные по чеку с сайта налоговой.&lt;/p&gt;
&lt;p&gt;В начале получим аутентификационные данные:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import requests
your_phone = '+7XXXYYYZZZZ' #нужно указать ваш телефон, на него придет СМС с паролем
r = requests.post('https://proverkacheka.nalog.ru:9999/v1/mobile/users/signup', json = {&amp;quot;email&amp;quot;:&amp;quot;email@email.com&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;USERNAME&amp;quot;,&amp;quot;phone&amp;quot;:your_phone})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результате выполнения POST-запроса мы получим пароль в виде SMS на указанный мобильный телефон. Далее, мы будем использовать его в переменной &lt;i&gt;&lt;b&gt;pwd&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;Теперь распарсим нашу строку со значениями из QR-кода:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import re
qr_string='t=20190320T2303&amp;amp;s=5803.00&amp;amp;fn=9251440300007971&amp;amp;i=141637&amp;amp;fp=4087570038&amp;amp;n=1'
t=re.findall(r't=(\w+)', qr_string)[0]
s=re.findall(r's=(\w+)', qr_string)[0]
fn=re.findall(r'fn=(\w+)', qr_string)[0]
i=re.findall(r'i=(\w+)', qr_string)[0]
fp=re.findall(r'fp=(\w+)', qr_string)[0]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Будем использовать полученные переменные для извлечения данных.&lt;br /&gt;
В &lt;a href="https://habr.com/ru/post/358966/"&gt;посте на Хабре&lt;/a&gt; довольно подробно изучены статусы ошибок при формировании запроса к API, не буду повторять эту информацию.&lt;/p&gt;
&lt;p&gt;В начале необходимо проверить, что по данному чеку есть данные, формируем GET-запрос.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;headers = {'Device-Id':'', 'Device-OS':''}
payload = {'fiscalSign': fp, 'date': t,'sum':s}
check_request=requests.get('https://proverkacheka.nalog.ru:9999/v1/ofds/*/inns/*/fss/'+fn+'/operations/1/tickets/'+i,params=payload, headers=headers,auth=(your_phone, pwd))
print(check_request.status_code)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В запросе необходимо указать headers, хотя бы пустые. В моем случае GET-запрос возвращает ошибку 406, из чего я понимаю, что такой чек находится (почему GET-запрос возвращает 406 для меня загадка, буду рад подсказкам в комментариях). Если не указать сумму или дату, то GET-запрос вернет ошибку 400 — bad request.&lt;/p&gt;
&lt;p&gt;Переходим к самому интересному, получаем данные чека:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;request_info=requests.get('https://proverkacheka.nalog.ru:9999/v1/inns/*/kkts/*/fss/'+fn+'/tickets/'+i+'?fiscalSign='+fp+'&amp;amp;sendToEmail=no',headers=headers,auth=(your_phone, pwd))
print(request_info.status_code)
products=request_info.json()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Должны получить код 200 (успешное выполнение GET-запроса), а в переменной products — все, что относится к нашему чеку.&lt;/p&gt;
&lt;p&gt;Чтобы работать с этими данными воспользуемся &lt;i&gt;pandas&lt;/i&gt; и преобразуем все в dataframe.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
from datetime import datetime
my_products=pd.DataFrame(products['document']['receipt']['items'])
my_products['price']=my_products['price']/100
my_products['sum']=my_products['sum']/100
datetime_check = datetime.strptime(t, '%Y%m%dT%H%M') #((https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior отформатируем дату))
my_products['date']=datetime_check
my_products.set_index('date',inplace=True)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь мы имеем рабочий pandas.dataframe с чеками, визуально это выглядит так:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2019-03-22_17-14-33@2x.png" width="679" height="374" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;«Шапка» чековых данных&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Можно построить гистограмму покупок или посмотреть на все в виде «ящика с усами»:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import matplotlib.pyplot as plt
%matplotlib inline
my_products['sum'].plot(kind='hist', bins=20)
plt.show()
my_products['sum'].plot(kind='box')
plt.show()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="386" data-ratio="1.531746031746"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/hist_cheques.png" width="386" height="252" alt="" /&gt;
&lt;img src="http://test.leftjoin.ru/pictures/boxplot_cheques.png" width="378" height="252" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;В завершение элементарно получим описательные статистики в текстовом виде командой .describe():&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;my_products.describe()&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2019-03-22_17-27-06@2x.png" width="362" height="268" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Данные удобно записать в .csv-файл, чтобы в следующий раз дополнить статистику:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with open('hyper_receipts.csv', 'a') as f:
             my_products.to_csv(f, header=True)&lt;/code&gt;&lt;/pre&gt;</description>
<pubDate>Fri, 22 Mar 2019 17:41:37 +0300</pubDate>
</item>


</channel>
</rss>