Русский
Русский
English
Статистика
Реклама

Data mining

Изучаем YELP с помощью Neo4j, python

12.05.2021 16:20:09 | Автор: admin

YELP зарубежная сеть, которая помогает людям находить местные предприятия и услуги, основываясь на отзывах, предпочтениях и рекомендациях. В текущей статей будет проведен определенный ее анализ с использованием платформы Neo4j, относящаяся к графовым СУБД, а также язык python.
Что посмотрим:
как работать с Neo4j и объемными датасетами на примере YELP;
чем может быть полезен YELP dataset;
частично: какие особенности в новых версиях Neo4j и почему книга Графовые алгоритмы 2019 года от O'REILLY уже устарела.


Что такое YELP и yelp dataset.


Сеть YELP на текущий момент охватывает 30 стран, РФ пока не входит в их число. Русский язык сетью не поддерживается. Сама сеть содержит достаточно объемное количество сведений о различного рода предприятиях, а также отзывах о них. Также yelp можно смело назвать социальной сетью, так как в ней имеются данные о пользователях, оставлявших отзывы. Никаких персональных данных там нет, только имена. Тем не менее пользователи образуют сообщества, группы или же могут быть в дальнейшем в эти группы и сообщества объединены по различным признакам. Например по количеству звезд (stars), которые поставили той точке (ресторану, заправке и т.п.), которую посетили.
Сама себя YELP описывает следующим образом:
-8,635,403 отзывов
-160,585 предприятий
-200,000 картинок
-8 мегаполисов
1,162,119 рекомендаций от 2,189,457 пользователей
Более 1.2 миллиона бизнес-атрибутики: часы работы, парковка, доступность и т.п.

С 2013 года Yelp регулярно проводит конкурс Yelp Dataset, призывая всех желающих
исследовать и изучать открытый набор данных Yelp.
Сам датасет доступен по ссылке
Датасет достаточно объемный и после распаковки представляет из себя 5 файлов формата json:


Все бы ничего, да вот только YELP выкладывает сырые (raw), необработанные данные и, чтобы начать с ними работать, потребуется предобработка.

Установка и быстрая настройка Neo4j.


Для анализа будет использоваться Neo4j, используем возможности графовой СУБД и их незамысловатый язык cypher для работы с датасетом.
О Neo4j как графовой СУБД неоднократно писали на Habrе (здесь и здесь статьи для начинающих), поэтому повторно представлять ее нет смысла.
Для того, чтобы начать работу с платформой, необходимо скачать desktop версию (около 500Mb) либо поработать в online песочнице. На момент написания статьи доступна Neo4j Enterprise 4.2.6 for Developers, а также иные, более ранние версии для установки.
Далее будет использоваться вариант работа в desktop версии в среде Windows (версии 4.2.5, 4.2.1).
Не смотря на то, что самая свежая версия 4.2.6, лучше ее пока не устанавливать, так как для нее еще не актуализированы все плагины, использующиеся в neo4j. Достаточно будет предыдущей версии 4.2.5.
После установки скачанного пакета, необходимо будет:
создать новую локальную БД, указав пользователя neo4j и пароль 123 (почему именно их, объясню ниже),
картинка


установить плагины, которые понадобятся APOC, Graph Data Science Library.
картинка


проверить, запускается ли БД и открывается ли браузер при нажатии на кнопку старт.
картинка




*- включить offline режим, чтобы БД истово не пыталась предлагать новые версии.
картинка



Загружаем данные в Neo4j.


Если с установкой Neo4j все прошло гладко, можно двигаться дальше и тут есть два пути.

Путь первый пройти долгий путь от импорта данных в БД с нуля, включающий их первичную очистку и трансформацию.
Путь второй загрузить готовую БД и начать с ней работать.
В итоге в обоих случаях должна получиться БД со следующими параметрами:

и итоговой схемой:


Чтобы пройти первый путь, лучше ознакомиться сперва со статьей на medium.
*Большое человеческое спасибо за это TRAN Ngoc Thach.
И воспользоваться готовым jupyter notebookом (адаптирован мною под windows) ссылка.
Процесс импорта не из простых и занимает достаточно продолжительное время

Проблем с памятью при этом не возникает даже при наличии всего лишь 8Гб Ram, так как используется пакетный импорт.
Однако потребуется создать swap файл размером на 10Гб, так как при проверке импортированных данных jupyter крашится, об этом моменте есть упоминание в вышеуказанной тетрадке jupyter.

Второй путь самый быстрый и был обнаружен случайно. Он подразумевает копирование уже готовой БД neo4j в существующую БД neo4j напрямую. Из минусов (пока обнаруженных) нельзя произвести backup БД средствами Neo4j (neo4j-admin dump --database=neo4j --to=D:\neo4j\neo4j.dump). Однако, это может быть связано с различиями в версиях в версии 4.2.1 была скопирована БД от версии 4.2.5.
Как реализуется этот метод:
открыть вкладку Manage БД, куда будет произведен импорт
картинка




перейти в папку с БД и скопировать туда папку data, перезаписав возможные совпадения.
картинка


При этом сама БД, куда произведено копирование не должна быть запущена.
перезапустить Neo4j.
И вот здесь пригодятся логин-пароль, которые ранее были использованы (neo4j,123) для избежания конфликтов.
После старта скопированной БД будет доступна БД c yelp-датасетом:


Смотрим YELP.


Изучать YELP можно как из Neo4j браузера, так и отправляя запросы в БД из того же jupyter notebook.
Благодаря тому, что БД графовая, в браузере будет сопровождать приятная наглядная картинка, на которой эти графы и будут отображаться.
Приступая к ознакомлению с YELP необходимо оговориться, что в БД будут только 3 страны US,KG и CA:

Посмотреть схему БД можно написав запрос на языке cypher в браузере neo4j:
CALL db.schema.visualization()

И вот здесь, если мы пошли по пути импорта БД путем прямого копирования (второй путь) нас ждет совсем иная картинка:

На работоспособность БД это не влияет.
Однако будем ориентироваться на оригинальную схему


Как читать эту схему? Выглядит все следующим образом. Вершина User имеет связь сама с собой типа FRIENDS, а также связь WROTE с вершиной Review. Rewiew в свою очередь имеет связь REVIEWS с Business и так далее. Посмотреть на это можно наглядно после нажатия на одной из вершин (node labels), например на User:

БД выберет любых 25 пользователей и покажет их:

Если нажать на соответствующий значок прямо на пользователе, то будут показаны все идущие от него прямые связи, а так как связи у User двух типов FRIENDS и REVIEW, то все они появятся:

Это удобно и неудобно одновременно. С одной стороны о пользователе можно посмотреть всю информацию одним кликом, но в то же время этим кликом нельзя убрать лишнее.
Но здесь нет ничего страшного, можно по id найти этого пользователя и только всех его друзей:
MATCH p=(:User {user_id:"u-CFWELen3aWMSiLAa_9VANw"}) -[r:FRIENDS]->() RETURN p LIMIT 25


Точно так же можно посмотреть какие отзывы написал данный человек:

YELP хранит отзывы аж от 2010 года! Сомнительная полезность, но тем не менее.
Чтобы почитать эти отзывы необходимо переключиться в вид текста, нажав на А

Посмотрим на место, о котором писала Sandy 10 лет назад и найдем его на yelp.com
Такое место действительно существует www.yelp.com/biz/cafe-sushi-cambridge,
а вот и сама Sandy co своим отзывом www.yelp.com/biz/cafe-sushi-cambridge?q=I%20was%20really%20excited
картинка



Запросы на python из jupyter notebook.


Здесь будут частично использованы сведения из упомянутой свободно распространяемой книги Графовые алгоритмы 2019 года от O'REILLY. Частично, потому как синтаксис из книги во многих местах устарел.
База, с которой мы будем работать должна быть запущена, при этом сам neo4j браузер запускать нет необходимости.
Импорт библиотек:
from neo4j import GraphDatabaseimport pandas as pdfrom tabulate import tabulateimport matplotlibmatplotlib.use('TkAgg')import matplotlib.pyplot as plt

Подключение к БД:
driver = GraphDatabase.driver("bolt://localhost", auth=("neo4j", "123"))

Подсчитаем количество вершин для каждой метки в БД:
result = {"label": [], "count": []}with driver.session() as session:    labels = [row["label"] for row in session.run("CALL db.labels()")]    for label in labels:        query = f"MATCH (:`{label}`) RETURN count(*) as count"        count = session.run(query).single()["count"]        result["label"].append(label)        result["count"].append(count)df = pd.DataFrame(data=result)print(tabulate(df.sort_values("count"), headers='keys',tablefmt='psql', showindex=False))

На выходе:
+----------+---------+
| label | count |
|----------+---------|
| Country | 3 |
| Area | 15 |
| City | 355 |
| Category | 1330 |
| Business | 160585 |
| User | 2189457 |
| Review | 8635403 |
+----------+---------+
Похоже на правду, в нашей базе 3 страны, как мы увидели ранее через neo4j браузер.
А этот код подсчитает количество связей (ребер):
result = {"relType": [], "count": []}with driver.session() as session:    rel_types = [row["relationshipType"] for row in session.run    ("CALL db.relationshipTypes()")]    for rel_type in rel_types:        query = f"MATCH ()-[:`{rel_type}`]->() RETURN count(*) as count"        count = session.run(query).single()["count"]        result["relType"].append(rel_type)        result["count"].append(count)df = pd.DataFrame(data=result)print(tabulate(df.sort_values("count"), headers='keys',tablefmt='psql', showindex=False))

Выход:
+-------------+---------+
| relType | count |
|-------------+---------|
| IN_COUNTRY | 15 |
| IN_AREA | 355 |
| IN_CITY | 160585 |
| IN_CATEGORY | 708884 |
| REVIEWS | 8635403 |
| WROTE | 8635403 |
| FRIENDS | 8985774 |
+-------------+---------+
Думаю, принцип понятен. В завершение напишем запрос и визуализируем его.
Top 10 отелей Ванкувера с наибольшим количеством отзывов
# Find the 10 hotels with the most reviewsquery = """MATCH (review:Review)-[:REVIEWS]->(business:Business),      (business)-[:IN_CATEGORY]->(category:Category {category_id: $category}),      (business)-[:IN_CITY]->(:City {name: $city})RETURN business.name AS business, collect(review.stars) AS allReviewsORDER BY size(allReviews) DESCLIMIT 10"""#MATCH (review:Review)-[:REVIEWS]->(business:Business),#(business)-[:IN_CATEGORY]->(category:Category {category_id: "Hotels"}),#(business)-[:IN_CITY]->(:City {name: "Vancouver"})#RETURN business.name AS business, collect(review.stars) AS allReviews#ORDER BY size(allReviews) DESC#LIMIT 10fig = plt.figure()fig.set_size_inches(10.5, 14.5)fig.subplots_adjust(hspace=0.4, wspace=0.4)with driver.session() as session:    params = { "city": "Vancouver", "category": "Hotels"}    result = session.run(query, params)    for index, row in enumerate(result):                business = row["business"]        stars = pd.Series(row["allReviews"])        #print(dir(stars))        total = stars.count()        #s = pd.concat([pd.Series(x['A']) for x in data]).astype(float)        s = pd.concat([pd.Series(row['allReviews'])]).astype(float)        average_stars = s.mean().round(2)        # Calculate the star distribution        stars_histogram = stars.value_counts().sort_index()        stars_histogram /= float(stars_histogram.sum())        # Plot a bar chart showing the distribution of star ratings        ax = fig.add_subplot(5, 2, index+1)        stars_histogram.plot(kind="bar", legend=None, color="darkblue",                             title=f"{business}\nAve:{average_stars}, Total: {total}")                                    #print(business)        #print(stars)plt.tight_layout()plt.show()


Результат должен получиться следующий

Ось X представляет рейтинг отеля в звездах, а ось Y общий процент каждого рейтинга.

Чем может быть полезен YELP dataset

.
Из плюсов можно выделить следующие:
достаточно богатое информационное поле по содержательной составляющей. В частности можно просто насобирать отзывы со звездами 1.0 или 5.0 и заспамить какой-либо бизнес. Гм. Немного не в ту сторону, но вектор понятен;
датасет объемен, что создает дополнительные приятные трудности в плане тестирования производительности различных платформ по анализу данных;
представленные данные имеют определенную ретроспективу и в принципе возможно понять, как менялось предприятие, исходя из отзывов о нем;
данные можно использовать как ориентиры по предприятиям, учитывая, что имеются адреса;
пользователи в датасете зачастую образуют интересные взаимосвязанные структуры, которые можно брать как есть, не формируя пользователей в искусственную соц. сеть и не собирая данную сеть из иных существующих соц. сетей.
Из минусов:
всего лишь три страны представлены из 30-ти и есть подозрение, что и то не полностью,
отзывы хранятся по 10 лет, что может искажать и зачастую портить характеристику существующего бизнеса,
о пользователях мало данных, они обезличены, поэтому, рекомендательные системы на базе датасета будут явно хромать,
в связях FRIENDS используются направленные графы, то есть Аня дружит -> Петей. Получается, что Петя не дружит с Аней. Это решается программно, но все равно это неудобно.
датасет выкладывается сырой и требуется значительные усилия для его предобработки.

Несколько слов об особенностях новых версий Neo4j


Neo4j динамично обновляется и новая версия интерфейса, используемого в 4.2.6 не совсем удобна, на мой взгляд. В частности не хватает наглядности в части сведений о количестве нод и связей в БД, что было в предыдущих версиях. Кроме того, интерфейс перемещения по вкладкам при работе с БД был изменен и к нему тоже необходимо привыкнуть.
Главная неприятность в обновлениях интеграция графовых алгоритмов в плагин Graph Data Science Library. Ранее они именовались neo4j-graph-algorithms
После интеграции многие алгоритмы значительно изменили синтаксис. По этой причине, изучение книги Графовые алгоритмы 2019 года от O'REILLY может быть затруднено.

Обработанная БД yelp для neo4j для прямого копирования и последующего анализа будет выложена позднее.
Подробнее..

Python, наука о данных и выборы часть 1

05.05.2021 20:22:59 | Автор: admin

Серия из 5 постов для начинающих представляет собой ремикс первой главы книги 2015 года под названием Clojure для науки о данных (Clojure for Data Science). Автор книги, Генри Гарнер, любезно дал согласие на использование материалов книги для данного ремикса с использованием языка Python.

Книга была написана как приглашение в так называемую науку о данных, которая в последние годы получила сильный импульс к развитию в связи с потребностью в быстрой и своевременной обработке больших наборов данных локально и в распределенной среде.

Материал книги излагается живым языком и подается в задачно-ориентированном стиле, главным образом с упором на анализе данных с использованием соответствующих алгоритмов и вычислительных платформ с предоставлением коротких и прямых объяснений по ходу изложения.

Несправедливо, когда превосходный учебный материал пылится невостребованным просто из-за того, что имплементирован на достаточно академичном, если не сказать элитарном языке, каким является язык функционального программирования Clojure. Поэтому возникло желание внести свои пять копеек, чтобы сделать материал книги доступным для более широкой публики.

Три главы книги были адаптированы под язык Python в течение следующего года после издания книги, т.е. в 2016 году. Публикация ремикса книги в РФ не получилась по разным причинам, но главная из них станет понятной в конце этой серии постов. В конце заключительного поста можно будет проголосовать за или против размещения следующей серии постов. А пока же

Пост 1 посвящен подготовке среды и данных.

Статистика

Важно не кто голосует, а кто подсчитывает голоса

Иосиф Сталин

Как только перед нами возникает задача проанализировать данные, которые состоят из двух и более чисел, становится содержательным вопрос о том, каким образом эти числа распределены. Вы, наверное, уже слыхали такие выражения, как длинный хвост и правило 80/20. Они касаются разброса чисел по диапазону. В этой главе мы продемонстрируем смысл распределений и познакомим с наиболее полезным из них: нормальным распределением.

При изучении распределений чрезвычайную важность играет наглядная и удобная визуализация данных, и для этого мы воспользуемся Python-овской библиотекой pandas. Мы покажем, как пользоваться ею для загрузки, преобразования и разведывательного анализа реальных данных, а также начнем работать с фундаментальной библиотекой numpy для научных вычислений. Мы проведем сопоставительный анализ результатов двух общенациональных выборов всеобщих выборов в Великобритании 2010 г. и российских выборов депутатов Государственной Думы Федерального Собрания РФ шестого созыва 2011 г. и увидим, каким образом даже элементарный анализ может предъявить подтверждающие данные о потенциальных фальсификациях.

Примеры исходного кода для этого поста находится в моем репо на Github.

В этом посте мы будем пользоваться тремя главными библиотеками экосистемы SciPy: одноименной библиотекой SciPy для выполнения сложных математико-статистических расчетов, библиотекой pandas для загрузки данных из разнообразных источников, управления ими и их визуализации, а также библиотекой NumPy в основном для работы с массивами и матрицами.

Кроме того, мы будем пользоваться встроенными в Python модулями. Так, например, модуль random позволяет генерировать случайные числа и извлекать выборки, и модуль collections содержит дополнительные структуры данных, из которых мы воспользуемся специальным словарем Counter.

В основе библиотеки pandas лежит понятие кадра данных, DataFrame, т.е. структуры, состоящей из строк и столбцов, или записей и полей. Если у вас есть опыт работы с реляционными базами данных, то таблицы pandas можно представить, как таблицы базы данных. Каждый столбец в кадре данных поименован, а каждая строка имеет одинаковое число столбцов, как и любая другая. Загрузить данные в кадр данных pandas можно несколькими способами, и тот, которым мы воспользуемся, будет зависеть от того, в каком виде наши данные хранятся:

  • Если данные представлены текстовым файлом с разделением полей данных запятыми (.csv) или символами табуляции (.tsv), то мы будем использовать функцию чтения данных read_csv

  • Если данные представлены файлом Excel (например, файл .xls или .xlsx), то мы воспользуемся функцией чтения данных read_excel

  • Для любого другого источника данных (внешняя база данных, веб-сайт, буфер обмена данными, JSON-файлы, HTML-файлы и т. д.) предусмотрен ряд других функций

Помимо кадров данных мы будем пользоваться еще одной популярной структурой Series, т.е. рядом данных. Это одномерный массив данных, необязательно числовых, которую мы тоже будем использовать.

В этой серии постов в качестве источника данных используется файл Excel, поэтому для чтения данных мы воспользуемся функцией read_excel. Эта функция принимает один обязательный аргумент файл для загрузки и ряд необязательных аргументов, в т. ч. номер либо название листа в виде именованного аргумента. Все наши примеры имеют всего один лист, и поэтому мы будем предоставлять лишь один файловый аргумент в виде следующей ниже строки исходного кода:

pd.read_excel('data/ch01/UK2010.xls')

Мы будем загружать данные из нескольких источников, поэтому мы создадим несколько вариантов загрузки данных. В приведенном ниже фрагменте кода мы определяем функцию загрузки данных load_uk:

def load_uk(): '''Загрузить данные по Великобритании''' return pd.read_excel('data/ch01/UK2010.xls') 

Эта функция вернет кадр данных DataFrame библиотеки pandas, содержащий данные по Великобритании. Далее в этой главе, мы определим дополнительные имплементации загрузки этого же и еще одного набора данных.

Первая строка электронной таблицы UK2010.xls содержит имена столбцов. Функция библиотеки pandas read_excel резервирует их в качестве имен столбцов возвращаемого кадра данных. Начнем обследование данных с их проверки атрибут кадра данных columns возвращает имена столбцов в виде списка, при этом адресация атрибутов осуществляется при помощи оператора точки (.):

def ex_1_1(): '''Получить имена полей кадра данных''' return load_uk().columns

Результатом выполнения приведенной выше функции должен быть следующий ниже список полей кадра данных pandas:

Index(['Press Association Reference', 'Constituency Name', 'Region', 'Election Year', 'Electorate', 'Votes', 'AC', 'AD', 'AGS', 'APNI', ... 'UKIP', 'UPS', 'UV', 'VCCA', 'Vote', 'Wessex Reg', 'WRP', 'You', 'Youth', 'YRDPL'], dtype='object', length=144)

Это очень широкий набор данных, состоящий из 144 полей. Первые шесть столбцов в файле данных описываются следующим образом; последующие столбцы лишь распределяют число голосов по партиям:

  • Информация для Ассоциации прессы: число, идентифицирующее избирательный округ (представленный одним депутатом)

  • Название избирательного округа: стандартное название, данное избирательному округу

  • Регион: географический район Великобритании, где округ расположен

  • Год выборов: год, в котором выборы состоялись

  • Электорат: общее число граждан, имеющих право голоса в избирательном округе

  • Голосование: общее число проголосовавших

Всякий раз, когда мы сталкиваемся с новыми данными, важно потратить некоторое время на то, чтобы в них разобраться. В отсутствии подробного описания данных лучше всего начать с подтверждения наших предположений по поводу данных. Например, мы ожидаем, что этот набор данных содержит информацию о выборах 2010 г., поэтому проверим содержимое столбца года выборов Election Year.

В pandas предусмотрена обширная функциональность для горизонтального (построчного) и вертикального (постолбцового) отбора и фильтрации данных. В элементарном случае нужный столбец можно выбрать, указав его номер или имя. В этой главе мы часто будем таким способом отбирать столбцы данных из различных представлений данных:

def ex_1_2(): '''Получить значения поля "Год выборов"''' return load_uk()['Election Year']

В результате будет выведен следующий список:

0 2010.01 2010.02 2010.0...646 2010.0647 2010.0648 2010.0649 2010.0650 NaNName: Election Year, dtype: float64

Столбец года выборов возвращается в виде последовательности значений. Полученный результат бывает трудно интерпретировать, поскольку кадр данных содержит слишком много строк. Учитывая, что мы хотели бы узнать, какие уникальные значения есть в этом столбце, можно воспользоваться методом unique кадра данных. Одно из преимуществ использования библиотеки pandas состоит в том, что ее утилитные функции управления данными дополняют те, которые уже встроены в Python. Следующий ниже пример это показывает:

def ex_1_3(): '''Получить значения в поле "Год выборов" без дубликатов''' return load_uk()['Election Year'].unique()
[ 2010. nan]

Значение 2010 еще больше подкрепляет наши ожидания в отношении того, что эти данные относятся к 2010 году. Впрочем, наличие специального значения nan, от англ. not a number, т.е. не число, которое сигнализирует о пропущенных данных, является неожиданным и может свидетельствовать о проблеме с данными.

Мы еще не знаем, в скольких элементах набора данных пропущены значения, и установив их число, мы смогли бы решить, что делать дальше. Простой способ подсчитать такие пустые элементы состоит в использовании подкласса словарей Counter языка Python из модуля collections. Этот словарь трансформирует последовательность значений в коллекцию, где ключам поставлены в соответствие количества появлений элементов данных, т.е. их частоты:

def ex_1_4(): '''Рассчитать частоты в поле "Год выборов"  (количества появлений разных значений)''' return Counter( load_uk()['Election Year'] )
Counter({nan: 1, 2010.0: 650})

Нам не потребуется много времени, чтобы получить подтверждение, что в 2010 г. в Великобритании было 650 избирательных округов. Знание предметной области, как в этом случае, имеет неоценимое значение при проверке достоверности новых данных. Таким образом, весьма вероятно, что значение nan является посторонним, и его можно удалить. Мы увидим, как это сделать, в следующем разделе.

Исправление данных

Согласно неоднократно подтвержденной статистике, как минимум 80% рабочего времени исследователь данных тратит на исправление данных. Эта процедура заключается в выявлении потенциально поврежденных или некорректных данных и их корректировке либо фильтрации.

Специальное значение nan в конце столбца года выборов может сигнализировать о грязных данных, которые требуется удалить. Мы уже убедились, что нужные столбцы данных в pandas можно отобрать, указав их номера или имена. Для фильтрации записей данных можно воспользоваться одним из предикативных методов библиотеки pandas.

Мы сообщаем библиотеке pandas, какие записи мы хотим отфильтровать, передавая в кадр данных как аргумент логическое выражение с использованием предикативных функций. В результате будут сохранены только те записи, для которых все предикаты возвращают истину. Например, чтобы отобрать только пустые значения из нашего набора данных, нужно следующее:

def ex_1_5(): '''Вернуть отфильтрованную по полю "Год выборов"  запись в кадре данных (в виде словаря)''' df = load_uk() return df[ df['Election Year'].isnull() ]

Press Association Reference

Constituency Name

Region

Election Year

Electorate

Votes

AC

AD

AGS

...

650

NaN

NaN

NaN

NaN

NaN

29687604

NaN

NaN

NaN

...

Выражение dt['Election Year'].isnull() вернет булеву последовательность, в которой все элементы, кроме последнего, равны False, в результате чего будет возвращена последняя запись кадра данных. Если Вы знаете язык запросов SQL, то отметите, что этот метод очень похож на условный оператор WHERE.

Присмотревшись к результатам примера ex_1_5, можно заметить, что в полученной записи все поля (кроме одного) имеют значение NaN. Дальнейший анализ данных подтверждает, что строка с непустым полем на самом деле является строкой итоговой суммы в листе файла Excel. Эту строку следует из набора данных удалить. Мы можем удалять проблемные строки путем обновления коллекции предикативной функцией notnull(), которая в данном случае вернет только те строки, в которых год выборов не равен NaN:

 df = load_uk() return df[ df[ 'Election Year' ].notnull() ]

Приведенные выше строки нам почти всегда придется вызывать непосредственно перед использованием данных. Лучше всего это сделать, добавив еще одну имплементацию функции загрузки данных по Великобритании load_uk_scrubbed с этим этапом фильтрации:

def load_uk_scrubbed(): '''Загрузить и отфильтровать данные по Великобритании''' df = load_uk() return df[ df[ 'Election Year' ].notnull() ]

Теперь при написании любого фрагмента кода с описанием доступа к набору данных в файле, можно выбирать вариант загрузки: обычный при помощи функции load_uk либо его аналог с очисткой данных load_uk_scrubbed. Приведенный выше пример должен вернуть список из 650 чисел, обозначающих количество избирателей в каждом избирательном округе Великобритании.

Внося исправления поверх загружаемых данных, мы сохраняем возможность проследить характер выполненных преобразований. В результате нам самим и будущим читателям нашего исходного кода становится понятнее, какие были внесены изменения в источник данных. Это также означает, что, как только потребуется снова запустить наш анализ, но уже с данными из нового источника, мы можем попросту загрузить новый файл, указав его вместо существующего.

Следующая часть, часть 2, серии постов Python, наука о данных и выборы посвящена описательным статистикам, группированию данных и нормальному распределению. Все эти сведения заложат основу для дальнейшего анализа электоральных данных.

Подробнее..

Python, наука о данных и выборы часть 3

06.05.2021 06:16:23 | Автор: admin

Пост 3 для начинающих посвящен генерированию распределений, их свойствам, а также графикам для их сопоставительного анализа.

Булочник и Пуанкаре

Существует легенда, почти наверняка апокрифическая, которая дает возможность детальнее рассмотреть вопрос о том, каким образом центральная предельная теорема позволяет рассуждать о принципе формирования статистических распределений. Она касается прославленного французского эрудита XIX-ого века Анри Пуанкаре, который, как гласит легенда, в течение одного года каждый день занимался тем, что взвешивал свежую буханку хлеба.

В те времена хлебопекарное ремесло регламентировалось государством, и Пуанкаре обнаружил, что, хотя результаты взвешивания буханок хлеба подчинялись нормальному распределению, пик находился не на публично афишируемом 1 кг, а на 950 г. Он сообщил властям о булочнике, у которого он регулярно покупал хлеб, и тот был оштрафован. Такова легенда ;-).

В следующем году Пуанкаре продолжил взвешивать буханки хлеба того же булочника. Он обнаружил, что среднее значение теперь было равно 1 кг, но это распределение больше не было симметричным вокруг среднего значения. Теперь оно было смещено вправо. А это соответствовало тому, что булочник теперь давал Пуанкаре только самые тяжелые из своих буханок хлеба. Пуанкаре снова сообщил о булочнике властям, и булочник был оштрафован во второй раз.

Было ли это на самом деле или нет здесь не суть важно; этот пример всего лишь служит для того, чтобы проиллюстрировать ключевой момент статистическое распределение последовательности чисел может сообщить нам нечто важное о процессе, который ее создал.

Генерирование распределений

В целях развития нашего интуитивного понимания относительно нормального распределения и дисперсии, давайте смоделируем честного и нечестного булочников, и для этого воспользуемся функцией генерирования нормально распределенных случайных величин stats.norm.rvs. (rvs от англ. normal variates, т.е. случайные величины). Честного булочника можно смоделировать в виде нормального распределения со средним значением 1000, что соответствует справедливой буханке хлеба весом 1 кг. При этом мы допустим наличие дисперсии в процессе выпекания, которая приводит к стандартному отклонению в 30г.

def honest_baker(mu, sigma): '''Модель честного булочника''' return pd.Series( stats.norm.rvs(loc, scale, size=10000) )def ex_1_18(): '''Смоделировать честного булочника на гистограмме''' honest_baker(1000, 30).hist(bins=25) plt.xlabel('Честный булочник')  plt.ylabel('Частота') plt.show()

Приведенный выше пример построит гистограмму, аналогичную следующей:

Теперь смоделируем булочника, который продает только самые тяжелые буханки хлеба. Мы разобьем последовательность на группы по тринадцать элементов (на "чертовы дюжины") и отберем максимальное значение в каждой:

def dishonest_baker(mu, sigma): '''Модель нечестного булочника''' xs = stats.norm.rvs(loc, scale, size=10000)  return pd.Series( map(max, bootstrap(xs, 13)) ) def ex_1_19(): '''Смоделировать нечестного булочника на гистограмме''' dishonest_baker(950, 30).hist(bins=25) plt.xlabel('Нечестный булочник')  plt.ylabel('Частота') plt.show()

Приведенный выше пример создаст гистограмму, аналогичную следующей:

Совершенно очевидно, что эта гистограмма выглядит не совсем так, как другие, которые мы видели. Среднее значение по-прежнему равно 1 кг, но разброс значений вокруг среднего больше не является симметричным. Мы говорим, что эта гистограмма показывает смещенное нормальное распределение.

Асимметрия

Асимметрией называется смещение распределения относительно ее моды. Отрицательная асимметрия, или левое смещение кривой, указывает на то, что площадь под графиком больше на левой стороне моды. Положительная асимметрия, или правое смещение кривой, указывает на то, что площадь под графиком больше на правой стороне моды.

Положительная и отрицательная асимметрииПоложительная и отрицательная асимметрии

Библиотека pandas располагает функцией skew для измерения асимметрии:

def ex_1_20(): '''Получить коэффициент асимметрии нормального распределения''' s = dishonest_baker(950, 30) return { 'среднее' : s.mean(),  'медиана' : s.median(),  'асимметрия': s.skew() }
{'асимметрия': 0.4202176889083849, 'медиана': 998.7670301469957, 'среднее': 1000.059263920949}

Приведенный выше пример показывает, что коэффициент асимметрии в выпечке от нечестного булочника составляет порядка 0.4. Этот коэффициент количественно определяет степень скошенности, которая видна на гистограмме.

Графики нормального распределения

Ранее в этой главе мы познакомились с квантилями как средством описания статистического распределения данных. Напомним, что функция quantile принимает число между 0 и 1 и возвращает значение последовательности в этой точке. 0.5-квантиль соответствует значению медианы.

Изображение квантилей данных относительно квантилей нормального распределения позволяет увидеть, каким образом наши измеренные данные соотносятся с теоретическим распределением. Подобные графики называются квантильными графиками, или диаграммами квантиль-квантиль, графиками Q-Q, от англ. Q-Q plot. Они предоставляют быстрый и интуитивно понятный способ определить степень нормальности статистического распределения. Для данных, которые близки к нормальному распределению, квантильный график покажет прямую линию. Отклонения от прямой линии показывают, каким образом данные отклоняются от идеализированного нормального распределения.

Теперь построим квантильные графики для честного и нечестного булочников. Функция qqplot принимает список точек данных и формирует график выборочных квантилей, отображаемых относительно квантилей из теоретического нормального распределения:

def qqplot( xs ): '''Квантильный график (график квантиль-квантиль, Q-Q plot)''' d = {0:sorted(stats.norm.rvs(loc=0, scale=1, size=len(xs))), 1:sorted(xs)} pd.DataFrame(d).plot.scatter(0, 1, s=5, grid=True) df.plot.scatter(0, 1, s=5, grid=True) plt.xlabel('Квантили теоретического нормального распределения') plt.ylabel('Квантили данных') plt.title ('Квантильный график', fontweight='semibold')def ex_1_21(): '''Показать квантильные графики  для честного и нечестного булочников''' qqplot( honest_baker(1000, 30) ) plt.show() qqplot( dishonest_baker(950, 30) ) plt.show()

Приведенный выше пример создаст следующие ниже графики:

Выше показан квантильный график для честного булочника. Далее идет квантильный график для нечестного булочника:

Тот факт, что линия имеет изогнутую форму, показывает, что данные положительно асимметричны; наклон в другую сторону будет означать отрицательную асимметрию. Квантильный график в сущности позволяет легко различить целый ряд отклонений от стандартного нормального распределения, как показано на следующем ниже рисунке:

Надписи: нормально распределенные, тяжелые хвосты, легкие хвосты, скошенность влево, скошенность вправо, раздельные кластеры

Квантильные графики сопоставляют статистическое распределение честного и нечестного булочника с теоретическим нормальным распределением. В следующем разделе мы сравним несколько альтернативных способов визуального сопоставления двух (или более) измеренных последовательностей значений.

Технические приемы сопоставительной визуализации

Квантильные графики дают замечательную возможность сопоставить измеренное эмпирическое (выборочное) распределение с теоретическим нормальным распределением. Однако если мы хотим сопоставить друг другу два или более эмпирических распределения, то графики нормального распределения для этого не подойдут. Впрочем, у нас есть несколько других вариантов, как показано в следующих двух разделах.

Коробчатые диаграммы

Коробчатые диаграммы, или диаграммы типа "ящик с усами", это способ визуализации таких описательных статистик, как медиана и дисперсия. Мы можем сгенерировать их с помощью следующего исходного кода:

def ex_1_22(): '''Показать коробчатую диаграмму с данными честного и нечестного булочников''' d = {'Честный булочник' :honest_baker(1000, 30), 'Нечестный булочник':dishonest_baker(950, 30)}  pd.DataFrame(d).boxplot(sym='o', whis=1.95, showmeans=True) plt.ylabel('Вес буханки (гр.)') plt.show()

Этот пример создаст следующую диаграмму:

Ящики в центре диаграммы представляют интерквартильный размах. Линия поперек ящика это медиана. Большая точка это среднее. Для честного булочника линия медианы проходит через центр окружности, показывая, что среднее и медиана примерно одинаковые. Для нечестного булочника среднее отодвинуто от медианы, что указывает на смещение.

Усы показывают на диапазон данных. Выбросы представлены полыми кругами. Всего одна диаграмма позволяет яснее увидеть расхождение между двумя статистическими распределениями, чем рассматривать их отдельно на гистограмме или квантильном графике.

Интегральные функции распределения

Интегральные функции распределения (ИФР), также именуемые кумулятивными функциями распределения, от англ. Cumulative Distribution Function (CDF), описывают вероятность, что значение, взятое из распределения, будет меньше x. Как и все распределения вероятностей, их значения лежат в диапазоне между 0 и 1, где 0 это невозможность, а 1 полная определенность. Например, представьте, что я собираюсь бросить шестигранный кубик. Какова вероятность, что выпадет значение меньше 6?

Для уравновешенного кубика вероятность выпадения пятерки или меньшего значения равна 5/6. И наоборот, вероятность, что выпадет единица, равна всего1/6. Тройка или меньше соответствуют равным шансам то есть вероятности 50%.

ИФР выпадения чисел на кубике следует той же схеме, что и все ИФР для чисел на нижнем краю диапазона ИФР близка к нулю, что соответствует низкой вероятности выбора чисел в этом диапазоне или ниже. На верхнем краю диапазона ИФР близка к единице, поскольку большинство значений, взятых из последовательности, будет ниже.

ИФР и квантили тесно друг с другом связаны ИФР является инверсией квантильной функции. Если 0.5-квантиль соответствует значению 1000, тогда ИФР для 1000 составляет 0.5.

Подобно тому, как функция pandas quantile позволяет нам отбирать значения из распределения в конкретных точках, эмпирическая ИФР empirical_cdf позволяет нам внести значение из последовательности и вернуть значение в диапазоне между 0 и 1. Это функция более высокого порядка, т.е. она принимает значение (в данном случае последовательность значений) и возвращает функцию, которую потом можно вызывать, сколько угодно, с различными значениями на входе, и возвращая ИФР для каждого из них.

Функции более высокого порядка это функции, которые принимают или возвращают функции.

Построим график ИФР одновременно для честного и нечестного булочников. Для этих целей можно воспользоваться функцией библиотеки pandas построения двумерного графика plot для визуализации ИФР, изобразив на графике исходные данные то есть выборки из распределений честного и нечестного булочников в сопоставлении с вероятностями, вычисленными относительно эмпирической ИФР. Функция plot ожидает, что значения x и значения y будут переданы в виде двух раздельных последовательностей значений. Для этих целей мы воспользуемся конструктором кадра данных pandas DataFrame.

Чтобы изобразить оба распределения на одном графике, мы должны передать функции plot несколько серий. Для многих своих графиков pandas предоставляет функции, которые позволяют добавлять дополнительные серии. В случае с функцией plot мы можем присвоить указатель на создаваемый график, присвоив временной переменной (ax) результат первого вызова функции plot, и затем при повторных вызовах указывать эту переменную в именованном аргументе функции (ax=ax). Можно также передать необязательную метку серии. Мы выполним это в следующем ниже примере, чтобы на готовом графике отличить две серии друг от друга. Сначала определим универсальную функцию построения эмпирической ИФР против теоретической, которая получает на вход кортеж из двух серий (tp[1] и tp[3]) и их названий и метки осей, и затем вызовем ее:

def empirical_cdf(x): """Вернуть эмпирическую ИФР для x""" sx = sorted(x) return pd.DataFrame( {0: sx, 1:sp.arange(len(sx))/len(sx)} )def ex_1_23(): '''Показать графики эмпирической ИФР честного булочника в сопоставлении с нечестным''' df = empirical_cdf(honest_baker(1000, 30)) df2 = empirical_cdf(dishonest_baker(950, 30)) ax = df.plot(0, 1, label='Честный булочник')  df2.plot(0, 1, label='Нечестный булочник', grid=True, ax=ax)  plt.xlabel('Вес буханки') plt.ylabel('Вероятность') plt.legend(loc='best') plt.show()

Приведенный выше пример сгенерирует следующий график:

Несмотря на то, что этот график выглядит совсем по-другому, он в сущности показывает ту же самую информацию, что и коробчатая диаграмма. Мы видим, что две линии пересекаются примерно в медиане 0.5, соответствующей 1000 гр. Линия нечестного булочника обрезается в нижнем хвосте и удлиняется на верхнем хвосте, что соответствует асимметричному распределению.

Следующая часть, часть 4, серии постов Python, наука о данных и выборы посвящена техническим приемам визуализации данных.

Подробнее..

Python, наука о данных и выборы часть 2

06.05.2021 06:16:23 | Автор: admin

Пост 2 для начинающих посвящен описательным статистикам, группированию данных и нормальному распределению. Все эти сведения заложат основу для дальнейшего анализа электоральных данных.

Описательные статистики

Описательные статистические величины, или статистики, это числа, которые используются для обобщения и описания данных. В целях демонстрации того, что мы имеем в виду, посмотрим на столбец с данными об электорате Electorate. Он показывает суммарное число зарегистрированных избирателей в каждом избирательном округе:

def ex_1_6(): '''Число значений в поле "Электорат"''' return load_uk_scrubbed()['Electorate'].count()
650

Мы уже очистили столбец, отфильтровав пустые значения (nan) из набора данных, и поэтому предыдущий пример должен вернуть суммарное число избирательных округов.

Описательные статистики, которые также называются сводными, представляют собой способы измерения свойств последовательностей чисел. Они помогают охарактеризовать последовательность и способны выступать в качестве ориентира для дальнейшего анализа. Начнем с двух самых базовых статистик, которые мы можем вычислить из последовательности чисел ее среднее значение и дисперсию.

  • Среднее значение

Наиболее распространенный способ усреднить набор данных взять его среднее значение. Среднее значение на самом деле представляет собой один из нескольких способов измерения центра распределения данных. Среднее значение числового ряда вычисляется на Python следующим образом:

def mean(xs):  '''Среднее значение числового ряда''' return sum(xs) / len(xs) 

Мы можем воспользоваться нашей новой функцией mean для вычисления среднего числа избирателей в Великобритании:

def ex_1_7(): '''Вернуть среднее значение поля "Электорат"''' return mean( load_uk_scrubbed()['Electorate'] )
70149.94

На самом деле, библиотека pandas уже содержит функцию mean, которая гораздо эффективнее вычисляет среднее значение последовательности. В нашем случае ее можно применить следующим образом:

load_uk_scrubbed()['Electorate'].mean()
  • Медиана

Медиана это еще одна распространенная описательная статистика для измерения центра распределения последовательности. Если Вы упорядочили все данные от меньшего до наибольшего, то медиана это значение, которое находится ровно по середине. Если в последовательности число точек данных четное, то медиана определяется, как полусумма двух срединных значений.

def median(xs): '''Медиана числового ряда''' n = len(xs) mid = n // 2 if n % 2 == 1: return sorted(xs)[mid] else: return mean( sorted(xs)[mid-1:][:2] )

Медианное значение электората Великобритании составляет:

def ex_1_8(): '''Вернуть медиану поля "Электорат"''' return median( load_uk_scrubbed()['Electorate'] )
70813.5

Библиотека pandas тоже располагает встроенной функцией для вычисления медианного значения, которая так и называется median.

  • Дисперсия

Среднее арифметическое и медиана являются двумя альтернативными способами описания среднего значения последовательности, но сами по себе они мало что говорят о содержащихся в ней значениях. Например, если известно, что среднее последовательности из девяноста девяти значений равно 50, то мы почти ничего не скажем о том, какого рода значения последовательность содержит.

Она может содержать целые числа от 1 до 99 или два ряда чисел, состоящих из 49 нулей и 50 девяносто-девяток, а может быть и так, что она содержит ряд из 98 чисел, равных -1 и одно единственное значение 5048, или же вообще все значения могут быть равны 50.

Дисперсия последовательности чисел показывает "разброс" данных вокруг среднего значения. К примеру, данные, приведенные выше, имели бы разную дисперсию. На языке математики дисперсия обозначается следующим образом:

где s2 это математический символ, который часто используют для обозначения дисперсии.

Выражение

можно преобразовать в функцию квадрата отклонения, square_deviation, которую можно применить к каждому элементу последовательности xs. При этом мы также воспользуемся созданной ранее функцией вычисления среднего значения mean, которая суммирует значения последовательности и делит полученную сумму на их количество.

def variance(xs): '''Дисперсия числового ряда, несмещенная дисперсия при n <= 30''' mu = mean(xs) n = len(xs) n = n-1 if n in range(1, 30) else n  square_deviation = lambda x : (x - mu) ** 2  return sum( map(square_deviation, xs) ) / n

Для вычисления квадрата выражения используется оператор языка Python возведения в степень **.

Поскольку мы взяли средний квадрат отклонения, т.е. получили квадрат отклонения и затем его среднее, то единицы измерения дисперсии тоже будут в квадрате, т.е. дисперсия электората Великобритании будет измеряться "людьми в квадрате". Несколько неестественно рассуждать об избирателях в таком виде. Единицу измерения можно привести к более естественному виду, снова обозначающему "людей", путем извлечения квадратного корня из дисперсии. В результате получим так называемое стандартное отклонение, или среднеквадратичное отклонение:

def standard_deviation(xs): '''Стандартное отклонение числового ряда''' return sp.sqrt( variance(xs) ) def ex_1_9(): '''Стандартное отклонение поля "Электорат"''' return standard_deviation( load_uk_scrubbed()['Electorate'] )
7672.77

В библиотеке pandas функции для вычисления дисперсии и стандартного отклонения имплементированы соответственно, как var и std. При этом последняя по умолчанию вычисляет несмещенное значение, поэтому, чтобы получить тот же самый результат, нужно применить именованный аргумент ddof=0, который сообщает, что требуется вычислить смещенное значение стандартного отклонения:

load_uk_scrubbed()['Electorate'].std( ddof=0 )
  • Квантили

Медиана это один из способов вычислить из списка срединное значение, т.е. находящееся ровно по середине, дисперсия же предоставляет способ измерить разброс данных вокруг среднего значения. Если весь разброс данных представить на шкале от 0 до 1, то значение 0.5 будет медианным.

Для примера рассмотрим следующую ниже последовательность чисел:

[10 11 15 21 22.5 28 30]

Отсортированная последовательность состоит из семи чисел, поэтому медианой является число 21 четвертое в ряду. Его также называют 0.5-квантилем. Мы можем получить более полную картину последовательности чисел, взглянув на 0.0 (нулевой), 0.25, 0.5, 0.75 и 1.0 квантили. Все вместе эти цифры не только показывают медиану, но также обобщают диапазон данных и сообщат о характере распределения чисел внутри него. Они иногда упоминаются в связи с пятичисловой сводкой.

Один из способов составления пятичисловой сводки для данных об электорате Великобритании показан ниже. Квантили можно вычислить непосредственно в pandas при помощи функции quantile. Последовательность требующихся квантилей передается в виде списка.

def ex_1_10(): '''Вычислить квантили: возвращает значение в последовательности xs,  соответствующее p-ому проценту''' q = [0, 1/4, 1/2, 3/4, 1] return load_uk_scrubbed()['Electorate'].quantile(q=q)
0.00 21780.000.25 65929.250.50 70813.500.75 74948.501.00 109922.00Name: Electorate, dtype: float64

Когда квантили делят диапазон на четыре равных диапазона, как показано выше, то они называются квартилями. Разница между нижним (0.25) и верхним (0.75) квартилями называется межквартильным размахом, или иногда сокращенно МКР. Аналогично дисперсии вокруг среднего значения, межквартильный размах измеряет разброс данных вокруг медианы.

Группирование данных в корзины

В целях развития интуитивного понимания в отношении того, что именно все эти расчеты разброса значений измеряют, мы можем применить метод под названием группировка в частотные корзины (binning). Когда данные имеют непрерывный характер, использование специального словаря для подсчета частот Counter (подобно тому, как он использовался при подсчете количества пустых значений в наборе данных об электорате) становится нецелесообразным, поскольку никакие два значения не могут быть одинаковыми. Между тем, общее представление о структуре данных можно все-равно получить, сгруппировав для этого данные в частотные корзины (bins).

Процедура образования корзин заключается в разбиении диапазона значений на ряд последовательных, равноразмерных и меньших интервалов. Каждое значение в исходном ряду попадает строго в одну корзину. Подсчитав количества точек, попадающих в каждую корзину, мы можем получить представление о разбросе данных:

На приведенном выше рисунке показано 15 значений x, разбитых на 5 равноразмерных корзин. Подсчитав количество точек, попадающих в каждую корзину, мы можем четко увидеть, что большинство точек попадают в корзину по середине, а меньшинство в корзины по краям. Следующая ниже функция Python nbin позволяет добиться того же самого результата:

def nbin(n, xs):  '''Разбивка данных на частотные корзины''' min_x, max_x = min(xs), max(xs) range_x = max_x - min_x fn = lambda x: min( int((abs(x) - min_x) / range_x * n), n-1 ) return map(fn, xs)

Например, мы можем разбить диапазон 0-14 на 5 корзин следующим образом:

list( nbin(5, range(15)) )
[0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]

После того, как мы разбили значения на корзины, мы можем в очередной раз воспользоваться словарем Counter, чтобы подсчитать количество точек в каждой корзине. В следующем ниже примере мы воспользуемся этим словарем для разбиения данных об электорате Великобритании на пять корзин:

def ex_1_11(): '''Разбиmь электорат Великобритании на 5 корзин''' series = load_uk_scrubbed()['Electorate'] return Counter( nbin(5, series) )
Counter({2: 450, 3: 171, 1: 26, 0: 2, 4: 1})

Количество точек в крайних корзинах (0 и 4) значительно ниже, чем в корзинах в середине количества, судя по всему, растут по направлению к медиане, а затем снова снижаются. В следующем разделе мы займемся визуализацией формы этих количеств.

Гистограммы

Гистограмма это один из способов визуализации распределения одной последовательности значений. Гистограммы попросту берут непрерывное распределение, разбивают его на корзины, и изображают частоты точек, попадающих в каждую корзину, в виде столбцов. Высота каждого столбца гистограммы показывает количество точек данных, которые содержатся в этой корзине.

Мы уже увидели, каким образом можно выполнить разбиение данных на корзины самостоятельно, однако в библиотеке pandas уже содержится функция hist, которая разбивает данные и визуализирует их в виде гистограммы.

def ex_1_12(): '''Построить гистограмму частотных корзин        электората Великобритании''' load_uk_scrubbed()['Electorate'].hist() plt.xlabel('Электорат Великобритании') plt.ylabel('Частота') plt.show()

Приведенный выше пример сгенерирует следующий ниже график:

Число корзин, на которые данные разбиваются, можно сконфигурировать, передав в функцию при построении гистограммы именованный аргумент bins:

def ex_1_13(): '''Построить гистограмму частотных корзин  электората Великобритании с 200 корзинами''' load_uk_scrubbed()['Electorate'].hist(bins=200) plt.xlabel('Электорат Великобритании') plt.ylabel('Частота') plt.show()

Приведенный выше график показывает единственный высокий пик, однако он выражает форму данных довольно грубо. Следующий ниже график показывает мелкие детали, но величина столбцов делает неясной форму распределения, в особенности в хвостах:

При выборе количества корзин для представления данных следует найти точку равновесия с малым количеством корзин форма данных будет представлена лишь приблизительно, а слишком большое их число приведет к тому, что шумовые признаки могут заслонить лежащую в основании структуру.

def ex_1_14(): '''Построить гистограмму частотных корзин  электората Великобритании с 20 корзинами''' load_uk_scrubbed()['Electorate'].hist(bins=20) plt.xlabel('Электорат Великобритании') plt.ylabel('Частота') plt.show()

Ниже показана гистограмма теперь уже из 20 корзин:

Окончательный график, состоящий из 20 корзин, судя по всему, пока лучше всего представляет эти данные.

Наряду со средним значением и медианой, есть еще один способ измерить среднюю величину последовательности. Это мода. Мода это значение, встречающееся в последовательности наиболее часто. Она определена исключительно только для последовательностей, имеющих по меньшей мере одно дублирующееся значение; во многих статистических распределениях это не так, и поэтому для них мода не определена. Тем не менее, пик гистограммы часто называют модой, поскольку он соответствует наиболее распространенной корзине.

Из графика ясно видно, что распределение вполне симметрично относительно моды, и его значения резко падают по обе стороны от нее вдоль тонких хвостов. Эти данные приближенно подчиняются нормальному распределению.

Нормальное распределение

Гистограмма дает приблизительное представление о том, каким образом данные распределены по всему диапазону, и является визуальным средством, которое позволяет квалифицировать данные как относящиеся к одному из немногих популярных распределений. В анализе данных многие распределения встречаются часто, но ни одно не встречается также часто, как нормальное распределение, именуемое также гауссовым распределением.

Распределение названо нормальным распределением из-за того, что оно очень часто встречается в природе. Галилей заметил, что ошибки в его астрономических измерениях подчинялись распределению, где малые отклонения от среднего значения встречались чаще, чем большие. Вклад великого математика Гаусса в описание математической формы этих ошибок привел к тому, что это распределение стали называть в его честь распределением Гаусса.

Любое распределение похоже на алгоритм сжатия: оно позволяет очень эффективно резюмировать потенциально большой объем данных. Нормальное распределение требует только два параметра, исходя из которых можно аппроксимировать остальные данные. Это среднее значение и стандартное отклонение.

Центральная предельная теорема

Высокая встречаемость нормального распределения отчасти объясняется центральной предельной теоремой. Дело в том, что значения, полученные из разнообразных статистических распределений, при определенных обстоятельствах имеют тенденцию сходиться к нормальному распределению, и мы это покажем далее.

В программировании типичным распределением является равномерное распределение. Оно представлено распределением чисел, генерируемых функцией библиотеки SciPy stats.uniform.rvs: в справедливом генераторе случайных чисел все числа имеют равные шансы быть сгенерированными. Мы можем увидеть это на гистограмме, многократно генерируя серию случайных чисел между 0 и 1 и затем построив график с результатами.

def ex_1_15(): '''Показать гистограмму равномерного распределения  синтетического набора данных''' xs = stats.uniform.rvs(0, 1, 10000) pd.Series(xs).hist(bins=20) plt.xlabel('Равномерное распределение') plt.ylabel('Частота') plt.show()

Обратите внимание, что в этом примере мы впервые использовали тип Series библиотеки pandas для числового ряда данных.

Приведенный выше пример создаст следующую гистограмму:

Каждый столбец гистограммы имеет примерно одинаковую высоту, что соответствует равновероятности генерирования числа, которое попадает в каждую корзину. Столбцы имеют не совсем одинаковую высоту, потому что равномерное распределение описывает теоретический результат, который наша случайная выборка не может отразить в точности. В следующих нескольких главах мы будем изучать способы точной количественной оценки расхождения между теорией и практикой, чтобы определить, являются ли расхождения достаточно большими, чтобы обратить на это внимание. В данном случае они таковыми не являются.

Если напротив сгенерировать гистограмму средних значений последовательностей чисел, то в результате получится распределение, которое выглядит совсем непохоже.

def bootstrap(xs, n, replace=True):  '''Вернуть список массивов меньших размеров  по n элементов каждый''' return np.random.choice(xs, (len(xs), n), replace=replace) def ex_1_16(): '''Построить гистограмму средних значений''' xs = stats.uniform.rvs(loc=0, scale=1, size=10000) pd.Series( map(sp.mean, bootstrap(xs, 10)) ).hist(bins=20) plt.xlabel('Распределение средних значений')  plt.ylabel('Частота') plt.show()

Приведенный выше пример сгенерирует результат, аналогичный следующей ниже гистограмме:

Хотя величина среднего значения близкая к 0 или 1 не является невозможной, она является чрезвычайно невероятной и становится менее вероятной по мере роста числа усредненных чисел и числа выборочных средних. Фактически, на выходе получается результат очень близкий к нормальному распределению.

Этот результат, когда средний эффект множества мелких случайных колебаний в итоге приводит к нормальному распределению, называется центральной предельной теоремой, иногда сокращенно ЦПТ, и играет важную роль для объяснения, почему нормальное распределение встречается так часто в природных явлениях.

До 20-ого века самого термина еще не существовало, хотя этот эффект был зафиксирован еще в 1733 г. французским математиком Абрахамом де Mуавром (Abraham de Moivre), который использовал нормальное распределение, чтобы аппроксимировать число орлов в результате бросания уравновешенной монеты. Исход бросков монеты лучше всего моделировать при помощи биномиального распределения, с которым мы познакомимся в главе 4, Классификация. В отличие от центральной предельной теоремы, которая позволяет получать выборки из приближенно нормального распределения, библиотека ScyPy содержит функции для эффективного генерирования выборок из самых разнообразных статистических распределений, включая нормальное:

def ex_1_17(): '''Показать гистограмму нормального распределения  синтетического набора данных''' xs = stats.norm.rvs(loc=0, scale=1, size=10000) pd.Series(xs).hist(bins=20) plt.xlabel('Нормальное распределение') plt.ylabel('Частота') plt.show()

Отметим, что в функции sp.random.normal параметр loc это среднее значение, scale дисперсия и size размер выборки. Приведенный выше пример сгенерирует следующую гистограмму нормального распределения:

По умолчанию среднее значение и стандартное отклонение для получения нормального распределения равны соответственно 0 и 1.

Следующая часть, часть 3, серии постов Python, наука о данных и выборы посвящена генерированию распределений, их свойствам, а также графикам для их сопоставительного анализа

Подробнее..

Python, наука о данных и выборы часть 5

06.05.2021 08:19:11 | Автор: admin

Заключительный пост 5 для начинающих посвящен сопоставительной визуализации электоральных данных.

Сопоставительная визуализация электоральных данных

Теперь рассмотрим набор данных других всеобщих выборов, на этот раз Российских, проходивших в 2011 г. Россия гораздо более крупная страна, и поэтому данные о проголосовавших на выборах там гораздо объемнее. Для этого мы загрузим в оперативную память один большой TSV-файл с разделением полей данных символом табуляции.

def load_ru(): '''Загрузить данные по России''' return pd.read_csv('data/ch01/Russia2011.tsv', '\t')

Посмотрим, какие имена столбцов имеются в российских данных:

def ex_1_29(): '''Показать список полей электоральных  данных по России''' return load_ru().columns

Будет выведен следующий список столбцов:

Index(['Код ОИК', 'ОИК ', 'Имя участка','Число избирателей, внесенных в список избирателей',...'Политическая партия СПРАВЕДЛИВАЯ РОССИЯ','Политическая партия ЛДПР - Либерально-демократическая партия России','Политическая партия "ПАТРИОТ РОССИИ"','Политическая партия КОММУНИСТИЧЕСКАЯ ПАРТИЯ КОММУНИСТ РОССИИ','Политическая партия "Российская объединенная демократическая партия "ЯБЛОКО"','Политическая партия "ЕДИНАЯ РОССИЯ"','Всероссийская политическая партия "ПАРТИЯ РОСТА"'],dtype='object')

Имена столбцов в российском наборе данных очень описательны, но, пожалуй, длиннее, чем нужно. Также, было бы удобно, если столбцы, представляющие те же самые атрибуты, которые мы уже видели в данных по выборам в Великобритании (к примеру, доля победителя и явка на выборы) были промаркированы одинаковым образом в обоих наборах данных. Переименуем их надлежащим образом.

Наряду с набором данных функция библиотеки pandas rename ожидает словарь, в котором ключам с текущими именами столбцов поставлены в соответствие значения с новыми именами. Если объединить ее с данными, которые мы уже рассматривали, то мы получим следующее:

def load_ru_victors(): '''Загрузить данные по России,  выбрать, переименовать и вычислить поля''' new_cols_dict = { 'Число избирателей, внесенных в список избирателей':'Электорат', 'Число действительных избирательных бюллетеней': 'Действительные бюллетени', 'Политическая партия "ЕДИНАЯ РОССИЯ"':'Победитель'  } newcols = list(new_cols_dict.values())  df = load_ru().rename( columns=new_cols_dict )[newcols]  df['Доля победителя'] = df['Победитель'] / df['Действительные бюллетени']  df['Явка'] = df['Действительные бюллетени'] / df['Электорат']  return df

Библиотека pandas располагает функцией безопасного деления divide, которая идентична операции /, но защищает от деления на ноль. Она вместо пропущенного значения (nan) в одном из полей подставляет значение, передаваемое в именованном аргументе fill_value. Если же оба значения поля равны nan, то результат будет отсутствовать. Поэтому операцию деления можно было бы переписать следующим образом:

 df[ 'Доля победителя' ] = \ df[ 'Победитель' ].divide( df[ 'Действительные бюллетени' ], \ fill_value=1 )

Визуализация электоральных данных РФ

Мы ранее видели, что гистограмма явки на выборы в Великобритании была приближенно нормально распределенной (хотя и с легкими хвостами). Теперь, когда мы загрузили и преобразовали данные о выборах в России, посмотрим, насколько они сопоставимы:

def ex_1_30(): '''Показать гистограмму  электоральных данных по России''' load_ru_victors()['Явка'].hist(bins=20) plt.xlabel('Явка в России')  plt.ylabel('Частота') plt.show()

Приведенный выше пример сгенерирует следующую гистограмму:

Эта гистограмма совсем не похожа на классические колоколообразные кривые, которые мы видели до сих пор. Имеется явно выраженная положительная асимметрия, и явка избирателей в действительности увеличивается с 80% в сторону 100% совсем не то, что мы ожидали бы от нормально распределенных данных.

Учитывая ожидания, заданные данными из Британии и центральной предельной теоремой (ЦПТ), такой результат любопытен. Для начала покажем данные на квантильном графике:

def ex_1_31(): '''Показать квантильный график  победителя на выборах в РФ''' qqplot( load_ru_victors()['Доля победителя'].dropna() ) plt.show()

Этот пример вернет следующий график:

На квантильном графике показана линия, которая не является ни прямой, ни одной из S-образных кривых. По существу, квантильный график говорит о наличии легкого хвоста в верхнем конце распределения и тяжелого хвоста в нижнем. Это почти противоположно тому, что мы видим на гистограмме, которая четко указывает на крайне тяжелый правый хвост.

На самом деле, этот квантильный график дезориентирует, и происходит этот именно потому, что хвост очень тяжелый: плотность точек между 0.5 и 1.0 на гистограмме говорит о том, что пик должен составлять порядка 0.7 с последующим правым хвостом за пределами 1.0. Наличие значения, превышающего 100% явно выходит за рамки логики, но квантильный график не объясняет это (он не учитывает, что речь идет о процентах), так что внезапное отсутствие данных за пределами 1.0 интерпретируется как подрезанный правый хвост.

С учетом центральной предельной теоремы и того, что мы наблюдали в данных выборов в Великобритании, тенденция к 100% явке избирателей на выборы выглядит очень любопытно. Давайте выполним параллельный сопоставительный анализ наборов данных по Великобритании и России.

Сравнительная визуализация

Предположим, мы хотели бы сравнить распределение электоральных данных между Великобританией и Россией. Мы уже видели в этой главе, как использовать ИФР и коробчатые диаграммы, поэтому теперь займемся исследованием альтернативного варианта, который аналогичен гистограмме.

Мы могли бы попытаться изобразить оба набора данных на гистограмме, но это будет безуспешной затеей, поскольку результаты не поддаются интерпретации по двум следующим причинам:

  • Размеры избирательных округов, и, следовательно, средних значений распределений сильно отличаются

  • Абсолютные количества избирательных округов настолько отличаются, что столбцы гистограмм будут иметь разную высоту

Вместо гистограммы альтернативным вариантом, позволяющим решить обе эти задачи, является функция массы вероятности.

Функции массы вероятности

Функция массы вероятности (ФМВ), от англ. Probability Mass Function (PMF), чаще именуемая функцией вероятности дискретной случайной величины, имеет много общего с гистограммой. Однако, вместо того, чтобы показывать количества значений, попадающих в группы, она показывает вероятность, что взятое из распределения число будет в точности равно заданному значению. Поскольку функция закрепляет вероятность за каждым значением, которое может быть возвращено распределением, и поскольку вероятности измеряются по шкале от 0 до 1, (где 1 соответствует полной определенности), то площадь под функцией массы вероятности равна 1.

Таким образом функция массы вероятности гарантирует, что площадь под нашими графиками будет между наборами данных сопоставима. Однако у нас все еще имеется одно затруднение, которое заключается в том, что размеры избирательных округов и поэтому средних значений распределений несопоставимы. Это затруднение решается отдельно при помощи нормализации.

Существует неисчислимое количество способов нормализации данных, однако один из самых основных обеспечивает, чтобы каждый числовой ряд находился в диапазоне от 0 до 1. Ни одно наше значение не находится в отрицательном диапазоне, поэтому мы можем выполнить нормализацию, попросту разделив каждое индивидуальное значение на самое большое:

def plot_as_pmf(dt, label, ax): '''График функции вероятности дискретной случайной величины (или функции массы вероятности)''' s = pd.cut(dt, bins=40, labels=False) # разбить на 40 корзин pmf = s.value_counts().sort_index() / len(s) # подсчитать кво в корзинах newax = pmf.plot(label=label, grid=True, ax=ax)  return newax

Имея в распоряжении приведенную выше функцию, мы теперь можем нормализовать данные по Великобритании и России и изобразить их рядом на тех же осях:

def ex_1_32(): '''Сопоставление данных явки по Великобритании и РФ, данные нормализованы на основе функции массы вероятностей''' ax = plot_as_pmf(load_uk_victors()['Явка'], 'Великобритания', None) plot_as_pmf(load_ru_victors()['Явка'], 'Россия', ax) plt.xlabel('Интервальные группы явки') # Частотные корзины plt.ylabel('Вероятность') plt.legend(loc='best') plt.show()

Приведенный выше пример сгенерирует следующий график:

После нормализации эти два распределения вполне готовы для проведения сопоставительного анализа. Теперь становится совершенно очевидным, каким образом несмотря на более низкую среднюю явку, чем в Великобритании (0.6366 против 0.6523) на российских выборах произошел массивный подъем явки близкий к 100%. Поскольку результаты голосования представляют собой объединенный эффект многих независимых волеизъявлений, они ожидаемо будут соответствовать центральной предельной теореме и будут приближенно нормально распределенными. В сущности, за редким исключением, как в Канаде, например, где население имеет гетерогенный характер (там французскоговорящая и англоговорящая группы населения в результате дают бимодальную кривую), результаты выборов по всему миру такому ожиданию обычно соответствуют.

Данные российских выборов показывают чрезвычайно аномальный результат, хотя и не настолько высокий, как модальный пик в центре распределения, который приблизительно соответствует 50% явке. Исследователь Питер Климек (Peter Klimek) и его коллеги в Венском медицинском университете пошли дальше и предположили, что этот результат является явным признаком подтасовки результатов голосования.

Диаграммы рассеяния

Мы обнаружили любопытные результаты, связанные с явкой на российских выборах и установили, что они имеют сигнатуру, отличающуюся от британских выборов. Теперь посмотрим, каким образом доля голосов за побеждающего кандидата связана с явкой. В конце концов, если неожиданно высокая явка действительно является признаком нечестной игры в пользу действующего президента и правительства, то можно ожидать, что они будут голосовать за себя, а не за кого-либо еще. Таким образом, большинство, если не все, из этих дополнительных голосов ожидаемо будут отданы за итоговых победителей выборов.

Раздел статистики посвященный корреляции довольно подробно рассматривает величины, лежащие в основе взаимосвязи двух переменных, однако на данный момент было бы интересно попросту визуализировать связь между явкой и долей голосов за побеждающую партию.

Заключительный технический прием визуализации, с которым мы познакомим в этой главе, представлен диаграммой рассеяния. Диаграммы рассеяния очень хорошо подходят для визуализации взаимосвязей между двумя переменными: там, где существует линейная взаимосвязь, на графике она будет видна, как диагональная направленность. Библиотека pandas содержит для этого вида графиков функцию scatter с такими же аргументами, что и для функции двумерных графиков plot.

def ex_1_33(): '''Показать диаграмму рассеяния  выборов в Великобритании''' df = load_uk_victors()[ ['Явка', 'Доля победителей'] ] df.plot.scatter(0, 1, s=3) plt.xlabel('Явка') plt.ylabel('Доля победителя') plt.show()

Приведенный выше пример сгенерирует следующую ниже диаграмму:

Хотя точки широко разбросаны в виде нечеткого эллипса, четко видна общая диагональная направленность к верхнему правому углу графика рассеяния. Она указывает на интересный результат явка на выборы коррелирует с долей голосов, отданных за окончательных победителей на выборах, в то время, как ожидалось получить обратный результат: наличия так называемого самодовольства избирателей, которое приводит к более низкой явке в случае, когда победитель гонки очевиден.

Как отмечалось ранее, британские выборы 2010 г. были далеко необычными: они привели к "подвисшему" парламенту и коалиционному правительству. Фактически, "победители" в данном случае представлены обеими сторонами, которые были противниками, вплоть до дня выборов. И поэтому голосование за любую из партий считается как голосование за победителя.

Затем, мы создадим такую же диаграмму рассеяния для выборов в России:

def ex_1_34(): '''Показать диаграмму рассеяния выборов в РФ''' df = load_ru_victors()[ ['Явка', 'Доля победителя'] ] df.plot.scatter(0, 1, s=3) plt.xlabel('Явка') plt.ylabel('Доля победителя') plt.show()

Этот пример сгенерирует следующую диаграмму:

Хотя из расположения точек в данных по России четко прослеживается диагональная направленность, сплошной объем данных затеняет внутреннюю структуру. В последнем разделе этой главы мы покажем простой метод, который позволяет с помощью настройки прозрачности графического изображения вычленять структуру из графиков, подобных этому.

Настройка прозрачности рассеяния

В ситуациях, аналогичных приведенной выше, когда диаграмма рассеяния переполнена точками, можно улучшить визуализацию внутренней структуры данных путем настройки уровня прозрачности. Поскольку наложение полупрозрачных точек будет менее прозрачным, а области с меньшим количеством точек будут более прозрачными, то диаграмма рассеяния с полупрозрачными точками может изобразить плотность данных намного лучше, чем сплошные точки.

Выполнить настройку альфа-канала, регулирующего прозрачность изображаемых на графике pandas точек можно при помощи именованного аргумента alpha в функции scatter в виде числа между 0 и 1, где 1 означает полную непрозрачность, 0 полную прозрачность.

def ex_1_35(): '''Показать диаграмму рассеяния (с прозрачностью) выборов в РФ''' df = load_ru_victors()[ ['Явка', 'Доля победителя'] ] rows = sp.random.choice(df.index.values, 10000) df.loc[rows].plot.scatter(0, 1, s=3, alpha=0.1) plt.xlabel('Явка') plt.ylabel('Доля победителя') plt.axis([0, 1.05, 0, 1.05]) plt.show()

Приведенный выше пример сгенерирует следующую диаграмму:

Приведенная выше диаграмма рассеяния показывает общую направленность совместного изменения доли победителя и явки на выборы. Мы видим корреляцию между двумя значениями и "горячую точку" в правом верхнем углу графика, которая соответствует явке близкой к 100% и 100%-ому голосованию в пользу побеждающей стороны. Как раз эта особенность в частности является признаком того, что исследователи из Венского медицинского университета обозначили как сигнатура фальсификации выборов. Этот факт также подтверждается результатами других спорных выборов по всему миру, например, таких как президентские выборы 2011 г. в Уганде.

Результаты многих других выборов по всему миру на уровне округов доступны здесь. На указанном веб-сайте можно получить ссылку на научно-исследовательскую работу и скачать другие наборы данных, на которых можно попрактиковать то, что Вы узнали в этой главе об исправлении и преобразовании реальных данных.

Примеры исходного кода для этого поста находится в моем репо на Github.

Выводы

Эта серия постов была посвящена сводным статистическим величинам и значениям статистических распределений. Мы увидели, каким образом даже простой анализ может предъявить свидетельства о потенциальной фальсификационной активности.

В частности, мы познакомились с центральной предельной теоремой и причиной, почему она играет такую важную роль в объяснении широкого применения нормального распределения в науке о данных. Подходящее статистическое распределение способно всего в нескольких статистиках выразить сущность большой последовательности чисел, некоторые из которых были имплементированы тут на основе встроенных функций языка Python и функций библиотеки scipy. Кроме того, были показаны возможности библиотеки pandas, которая была задействована для загрузки, преобразования и визуального сопоставления нескольких наборов данных. В ходе анализа было обнаружено любопытное расхождение между двумя статистическими распределениями.

Внизу поста можно проголосовать за или против размещения следующей серии постов. Их тема - инференциальная статистика, которая позволяет давать количественную оценку измеренному расхождению между двумя или несколькими статистическими распределениями и принимать решение о статистической значимости этого расхождения. Указанная серия постов также посвящена методам проверки статистических гипотез, которые лежат в основе проведения робастных статистических экспериментов, позволяющих делать выводы на основе имеющихся данных.

Подробнее..

Python, наука о данных и выборы часть 4

06.05.2021 08:19:11 | Автор: admin

Пост 4 для начинающих посвящен техническим приемам визуализации данных.

Важность визуализации

Простые приемы визуализации, подобные тем, которые были показаны ранее, позволяют лаконично передать большое количество информации. Они дополняют сводные статистики, которые мы рассчитали ранее в этой главе, и поэтому очень важно уметь ими пользоваться. Такие статистики, как среднее значение и стандартное отклонение, неизбежно скрывают много информации по той причине, что сворачивают последовательность в одно единственное число.

Английский математик Фрэнсис Энскомб составил коллекцию из четырех точечных графиков, ныне известную как квартет Энскомба, которые обладают практически идентичными статистическими свойствами (включая среднее, дисперсию и стандартное отклонение). Несмотря на это, они четко показывают, что распределение значений последовательностей и сильно расходится:

При этом наборы данных не должны быть подстроенными, потому как ценные аналитические выводы будут непременно выявлены при построении графиков. Возьмем для примера гистограмму оценок, полученных выпускниками по национальному экзамену на аттестат зрелости в Польше в 2013 г.:

Способности учащихся вполне ожидаемо должны быть нормально распределены, и действительно за исключением крутого всплеска вокруг 30% так и есть. То, что мы ясно наблюдаем это результат по-человечески понятного натягивания экзаменаторами оценок учащегося на проходной балл.

На самом деле статистические распределения для последовательностей, взятых из крупных выборок, могут быть настолько надежными, что даже незначительное отклонение от них может являться свидетельством противоправной деятельности. Закон Бенфорда, или закон первой цифры, показывает любопытную особенность случайных чисел, которые генерируются в широком диапазоне. Единица появляется в качестве ведущей цифры примерно в 30% случаев, в то время как цифры крупнее появляется все реже и реже. Например, девятка появляется в виде первой цифры менее чем в 5% случаев.

Закон Бенфорда назван в честь физика Фрэнка Бенфорда (Frank Benford), который сформулировал его в 1938 г., показав его состоятельность на различных источниках данных. Проявление этого закона было ранее отмечено американским астрономом Саймоном Ньюкомом (Simon Newcomb), который еще более 50 лет назад до него обратил внимание на страницы своих логарифмических справочников: страницы с номерами, начинавшихся с цифры 1, имели более потрепанный вид.

Бенфорд показал, что его закон применяется к самым разнообразным данным, таким как счета за электричество, адреса домов, цены на акции, численность населения, уровень смертности и длины рек. Для наборов данных, которые охватывают большие диапазоны значений, закон настолько состоятелен, что случаи отклонения от него стали принимать в качестве подтверждающих данных в судебной практике по финансовым махинациям.

Визуализация данных об электорате

Вернемся к данным выборов и сравним электоральную последовательность, которую мы создали ранее, относительно теоретической нормальной ИФР. Для создания нормальной ИФР из последовательности значений можно воспользоваться функцией sp.random.normal библиотеки SciPy, как уже было показано выше. Среднее значение и стандартное отклонение по умолчанию равны соответственно 0 и 1, поэтому нам нужно предоставить измеренные среднее значение и стандартное отклонение, взятые из электоральных данных. Эти значения для наших электоральных данных составляют соответственно 70150 и 7679.

Ранее в этой главе мы уже генерировали эмпирическую ИФР. Следующий ниже пример просто сгенерирует обе ИФР и выведет их на одном двумерном графике:

def ex_1_24(): '''Показать эмпирическую и подогнанную ИФР  электората Великобритании''' emp = load_uk_scrubbed()['Electorate'] fitted = stats.norm.rvs(emp.mean(), emp.std(ddof=0), len(emp)) df = empirical_cdf(emp) df2 = empirical_cdf(fitted) ax = df.plot(0, 1, label='эмпирическая')  df2.plot(0, 1, label='подогнанная', grid=True, ax=ax)  plt.xlabel('Электорат') plt.ylabel('Вероятность') plt.legend(loc='best') plt.show()

Приведенный выше пример создаст следующий график:

Несмотря на наличие незначительной асимметрии, близкая расположенность двух кривых друг к другу говорит о нормальности исходных данных. Асимметрия выражена в противоположном направлении относительно построенной ранее кривой ИФР нечестного булочника, то есть наши данные об электорате слегка смещены влево.

Поскольку мы сравниваем наше распределение с теоретическим нормальным распределением, то можно воспользоваться квантильным графиком, который делает это по умолчанию:

def ex_1_25(): '''Показать квантильный график  электората Великобритании''' qqplot( load_uk_scrubbed()['Electorate'] ) plt.show()

Следующий ниже квантильный график еще лучше показывает левую асимметрию, с очевидностью присутствующую в данных:

Как мы и ожидали, кривая изгибается в противоположном направлении по отношению к построенному ранее в этой главе квантильному графику нечестного булочника. Это свидетельствует о том, что число более мелких избирательных округов на самом деле больше, чем можно было бы ожидать, если бы данные были более тесно нормально распределены.

Добавление производных столбцов

В целях выяснения процента электората, который проголосовал за одну из двух партий, требуется вычислить сумму голосов, отданных за каждую из них. Для этого нам понадобится создать новое поле данных Victors (Победители) из данных, которые соответствуют Консервативной (Con) и Либерально-демократической (LD) партиям и заодно проверим, имеются ли пропущенные значения.

def ex_1_26(): '''Вычислить производное поле данных "Победители" и  число имеющихся в нем пропущенных значений''' df = load_uk_scrubbed() df['Победители'] = df['Con'] + df['LD'] freq = Counter(df['Con'].apply( lambda x: x > 0 )) print('Поле "Победители": %d, в т.ч. пропущено %d'  % (freq[True], freq[False]))
Поле "Победители": 631, в т.ч. пропущено 19

Результат показывает, что в 19 случаях данные отсутствуют. Очевидно, что в каком-то из столбцов: столбце Con либо столбце LD (либо обоих), данные отсутствуют, но в каком именно? Снова воспользуемся словарем Counter, чтобы увидеть масштаб проблемы:

'''Проверить пропущенные значения в полях "Консервативная партия" (Con) и   "Либерально-демократическая партия" (LD)'''df = load_uk_scrubbed()Counter(df['Con'].apply(lambda x: x > 0)),  Counter(df['LD'].apply(lambda x: x > 0))
(Counter({False: 19, True: 631}), Counter({False: 19, True: 631}))

В обоих случаях будет выведено одинаковое число строк, в которых значения есть, и в которых они отсутствуют. Воспользуемся предикативной функцией isnull, которую мы уже встречали ранее в этой главе, чтобы узнать, какие строки не содержат значений:

def ex_1_27(): '''Выборка полей данных по условию, что поля "Консервативная партия" (Con) и  "Либерально-демократическая" (LD) не пустые''' df = load_uk_scrubbed() rule = df['Con'].isnull() & df['LD'].isnull() return df[rule][['Region', 'Electorate', 'Con', 'LD']]

Region

Electorate

Con

LD

12

Northern Ireland

60204.0

NaN

NaN

13

Northern Ireland

73338.0

NaN

NaN

14

Northern Ireland

63054.0

NaN

NaN

584

Northern Ireland

64594.0

NaN

NaN

585

Northern Ireland

74732.0

NaN

NaN

Небольшое обследование данных должно определить причину, почему эти поля оказались пустыми. Как оказалось, кандидаты в соответствующих избирательных округах не выдвигались. Следует ли эти строки отфильтровать или же оставить как есть, равными нулю? Это интересный вопрос. Давайте их отфильтруем, поскольку вообще-то невозможно, чтобы в этих округах избиратели выбрали какого-либо кандидата, неважно от либеральных демократов или консерваторов. Если же мы напротив допустили, что они равны нулю, то среднее количество людей, которое при заданных вариантах выбора проголосовало за одну из этих партий, было бы искусственно занижено.

Зная, как фильтровать проблемные строки, теперь добавим производные столбцы, которые будут представлять победителя, долю голосов за победителя и явку на выборы. Отфильтруем строки так, чтобы показать только те, где были выдвинуты кандидаты от обеих партий:

def load_uk_victors(): '''Загрузить данные по Великобритании,  выбрать поля и отфильтровать''' df = load_uk_scrubbed() rule = df['Con'].notnull() df = df[rule][['Con', 'LD', 'Votes', 'Electorate']]  df['Победители'] = df['Con'] + df['LD']  df['Доля победителей'] = df['Победители'] / df['Votes']  df['Явка'] = df['Votes'] / df['Electorate'] return df

В результате в нашем наборе данных теперь имеется три дополнительных столбца: Victors, Victors Share и Turnout, т.е. победители, доля победителей и явка на выборы. Покажем на квантильном графике долю голосов за победителя, чтобы увидеть, как она соотносится с теоретическим нормальным распределением:

def ex_1_28(): '''Показать квантильный график победителей  на выборах в Великобритании''' qqplot( load_uk_victors()['Доля победителей'] ) plt.show()

Приведенный выше пример создаст следующий ниже график:

Основываясь на сводном графике разных форм кривых квантильных графиков, показанном ранее в этой главе, можно заключить, что доля голосов, отданных за победителя, имеет "легкие хвосты" по сравнению с нормальным распределением. Это означает, что ближе к среднему значению расположено больше данных, чем можно было бы ожидать, исходя из действительно нормально распределенных данных.

Примеры исходного кода для этого поста находится в моем репо на Github.

Следующая заключительная часть, часть 5, серии постов Python, наука о данных и выборы посвящена сопоставительной визуализации электоральных данных.

Подробнее..

Python и статистический вывод часть 2

11.05.2021 18:04:27 | Автор: admin

Предыдущий пост см. здесь.

Выборки и популяции

В статистической науке термины выборка и популяция имеют особое значение. Популяция, или генеральная совокупность, это все множество объектов, которые исследователь хочет понять или в отношении которых сделать выводы. Например, во второй половине 19-го века основоположник генетики Грегор Йохан Мендель) записывал наблюдения о растениях гороха. Несмотря на то, что он изучал в лабораторных условиях вполне конкретные сорта растения, его задача состояла в том, чтобы понять базовые механизмы, лежащие в основе наследственности абсолютно всех возможных сортов гороха.

В статистической науке о группе объектов, из которых извлекается выборка, говорят, как о популяции, независимо от того, являются изучаемые объекты живыми существами или нет.

Поскольку популяция может быть крупной или бесконечной, как в случае растений гороха Менделя мы должны изучать репрезентативные выборки, и на их основе делать выводы о всей популяции в целом. В целях проведения четкого различия между поддающимися измерению атрибутами выборок и недоступными атрибутами популяции, мы используем термин статистики, имея при этом в виду атрибуты выборки, и говорим о параметрах, имея в виду атрибуты популяции.

Статистики это атрибуты, которые мы можем измерить на основе выборок. Параметры это атрибуты популяции, которые мы пытаемся вывести статистически.

В действительности, статистики и параметры различаются в силу применения разных символов в математических формулах:

Мера

Выборочная статистика

Популяционный параметр

Объем

n

N

Среднее значение

x

x

Стандартное отклонение

Sx

x

Стандартная ошибка

Sx

Если вы вернетесь к уравнению стандартной ошибки, то заметите, что она вычисляется не из выборочного стандартного отклонения Sx, а из популяционного стандартного отклонения x. Это создает парадоксальную ситуацию мы не можем вычислить выборочную статистику, используя для этого популяционные параметры, которые мы пытаемся вывести. На практике, однако, предполагается, что выборочное и популяционное стандартные отклонения одинаковы при размере выборки порядка n 30.

Теперь вычислим стандартную ошибку средних значений за определенный день. Например, возьмем конкретный день, скажем, 1 мая:

def ex_2_8():  '''Вычислить стандартную ошибку  средних значений за определенный день''' may_1 = '2015-05-01' df = with_parsed_date( load_data('dwell-times.tsv') )  filtered = df.set_index( ['date'] )[may_1] se = standard_error( filtered['dwell-time'] ) print('Стандартная ошибка:', se)
Стандартная ошибка: 3.627340273094217

Хотя мы взяли выборку всего из одного дня, вычисляемая нами стандартная ошибка очень близка к стандартному отклонению всех выборочных средних 3.6 сек. против 3.7 сек. Это, как если бы, подобно клетке, содержащей ДНК, в каждой выборке была закодирована информация обо всей находящейся внутри нее популяции.

Интервалы уверенности

Поскольку стандартная ошибка выборки измеряет степень близости, с которой, по нашим ожиданиям, выборочное среднее соответствует среднему популяционному, то мы можем также рассмотреть обратное стандартная ошибка измеряет степень близосто, с которой, по нашим ожиданиям, популяционное среднее соответствует измеренному среднему выборочному. Другими словами, на основе стандартной ошибки мы можем вывести, что популяционное среднее находится в пределах некого ожидаемого диапазона выборочного среднего с некоторой степенью уверенности.

Понятия степень уверенности и ожидаемый диапазон, взятые вместе, дают определение термину интервал уверенности.

Примечание. В большинстве языков под термином confidence в контексте инференциальной статистики понимается именно уверенность в отличие от отечественной статистики, где принято говорить о доверительном интервале. На самом деле речь идет не о доверии (trust), а об уверенности исследователя в полученных результатах. Это яркий пример мягкой подмены понятия. Подобного рода ошибки вполне объяснимы - первые переводы появились еще в доколумбову доинтернетовскую эпоху, когда источников было мало, и приходилось домысливать в силу своего понимания. Сегодня же, когда существует масса профильных глоссариев, словарей и источников, такого рода искажения не оправданы. ИМХО.

При установлении интервалов уверенности обычной практикой является задание интервала размером 95% мы на 95% уверены, что популяционный параметр находится внутри интервала. Разумеется, еще остается 5%-я возможность, что он там не находится.

Какой бы ни была стандартная ошибка, 95% популяционного среднего значения будет находиться между -1.96 и 1.96 стандартных отклонений от выборочного среднего. И, следовательно, число 1.96 является критическим значением для 95%-ого интервала уверенности. Это критическое значение носит название z-значения.

Название z-значение вызвано тем, что нормальное распределение называется z-распределением. Впрочем, иногда z-значение так и называют гауссовым значением.

Число 1.96 используется так широко, что его стоит запомнить. Впрочем, критическое значение мы можем вычислить сами, воспользовавшись функцией scipy stats.norm.ppf. Приведенная ниже функция confidence_interval ожидает значение для p между 0 и 1. Для нашего 95%-ого интервала уверенности оно будет равно 0.95. В целях вычисления положения каждого из двух хвостов нам нужно вычесть это число из единицы и разделить на 2 (2.5% для интервала уверенности шириной 95%):

def confidence_interval(p, xs): '''Интервал уверенности''' mu = xs.mean() se = standard_error(xs) z_crit = stats.norm.ppf(1 - (1-p) / 2)  return [mu - z_crit * se, mu + z_crit * se]def ex_2_9(): '''Вычислить интервал уверенности для данных за определенный день''' may_1 = '2015-05-01' df = with_parsed_date( load_data('dwell-times.tsv') )  filtered = df.set_index( ['date'] )[may_1] ci = confidence_interval(0.95, filtered['dwell-time']) print('Интервал уверенности: ', ci)
Интервал уверенности: [83.53415272762004, 97.753065317492741]

Полученный результат говорит о том, что можно на 95% быть уверенным в том, что популяционное среднее находится между 83.53 и 97.75 сек. И действительно, популяционное среднее, которое мы вычислили ранее, вполне укладывается в этот диапазон.

Сравнение выборок

После вирусной маркетинговой кампании веб-команда в AcmeContent извлекает для нас однодневную выборку времени пребывания посетителей на веб-сайте для проведения анализа. Они хотели бы узнать, не привлекла ли их недавняя кампания более активных посетителей веб-сайта. Интервалы уверенности предоставляют нам интуитивно понятный подход к сравнению двух выборок.

Точно так же, как мы делали ранее, мы загружаем значения времени пребывания, полученные в результате маркетинговой кампании, и их резюмируем:

def ex_2_10():    '''Сводные статистики данных, полученных        в результате вирусной кампании'''    ts = load_data('campaign-sample.tsv')['dwell-time']         print('n:                      ', ts.count())    print('Среднее:                ', ts.mean())    print('Медиана:                ', ts.median())    print('Стандартное отклонение: ', ts.std())    print('Стандартная ошибка:     ', standard_error(ts))    ex_2_10()
n:                       300Среднее:                 130.22Медиана:                 84.0Стандартное отклонение:  136.13370714388034Стандартная ошибка:      7.846572839994115

Среднее значение выглядит намного больше, чем то, которое мы видели ранее 130 сек. по сравнению с 90 сек. Вполне возможно, здесь имеется некое значимое расхождение, хотя стандартная ошибка более чем в 2 раза больше той, которая была в предыдущей однодневной выборке, в силу меньшего размера выборки и большего стандартного отклонения. Основываясь на этих данных, можно вычислить 95%-й интервал уверенности для популяционного среднего, воспользовавшись для этого той же самой функцией confidence_interval, что и прежде:

def ex_2_11(): '''Интервал уверенности для данных, полученных в результате вирусной кампании''' ts = load_data('campaign-sample.tsv')['dwell-time']  print('Интервал уверенности:', confidence_interval(0.95, ts))
Интервал уверенности: [114.84099983154137, 145.59900016845864]

95%-ый интервал уверенности для популяционного среднего лежит между 114.8 и 145.6 сек. Он вообще не пересекается с вычисленным нами ранее популяционным средним в размере 90 сек. Похоже, имеется какое-то крупное расхождение с опорной популяцией, которое едва бы произошло по причине одной лишь ошибки выборочного обследования. Наша задача теперь состоит в том, чтобы выяснить почему это происходит.

Ошибка выборочного обследования, также систематическая ошибка при взятии выборки, возникает, когда статистические характеристики популяции оцениваются исходя из подмножества этой популяции.

Искаженность

Выборка должна быть репрезентативной, то есть представлять популяцию, из которой она взята. Другими словами, при взятии выборки необходимо избегать искажения, которое происходит в результате того, что отдельно взятые члены популяции систематически из нее исключаются (либо в нее включаются) по сравнению с другими.

Широко известным примером искажения при взятии выборки является опрос населения, проведенный в США еженедельным журналом Литературный Дайджест (Literary Digest) по поводу президентских выборов 1936 г. Это был один из самых больших и самых дорогостоящих когда-либо проводившихся опросов: тогда по почте было опрошено 2.4 млн. человек. Результаты были однозначными губернатор-республиканец от шт. Канзас Альфред Лэндон должен был победить Франклина Д. Рузвельта с 57% голосов. Как известно, в конечном счете на выборах победил Рузвельт с 62% голосов.

Первопричина допущенной журналом огромной ошибки выборочного обследования состояла в искажении при отборе. В своем стремлении собрать как можно больше адресов избирателей журнал Литературный Дайджест буквально выскреб все телефонные справочники, подписные перечни журнала и списки членов клубов. В эру, когда телефоны все еще во многом оставались предметом роскоши, такая процедура гарантировано имела избыточный вес в пользу избирателей, принадлежавших к верхнему и среднему классам, и не был представительным для электората в целом. Вторичной причиной искажения стала искаженность в результате неответов в опросе фактически согласились участвовать всего менее четверти тех, к кому обратились. В этом виде искаженности при отборе предпочтение отдается только тем респондентам, которые действительно желают принять участие в голосовании.

Распространенный способ избежать искаженности при отборе состоит в том, чтобы каким-либо образом рандомизировать выборку. Введение в процедуру случайности делает вряд ли возможным, что экспериментальные факторы окажут неправомерное влияние на качество выборки. Опрос населения еженедельником Литературный Дайджест был сосредоточен на получении максимально возможной выборки, однако неискаженная малая выборка была бы намного полезнее, чем плохо отобранная большая выборка.

Если мы откроем файл campaign_sample.tsv, то обнаружим, что наша выборка приходится исключительно на 6 июня 2015 года. Это был выходной день, и этот факт мы можем легко подтвердить при помощи функции pandas:

'''Проверка даты''' d = pd.to_datetime('2015 6 6') d.weekday() in [5,6]
True

Все наши сводные статистики до сих пор основывались на данных, которые мы отфильтровывали для получения только рабочих дней. Искаженность в нашей выборке вызвана именно этим фактом, и, если окажется, что поведение посетителей в выходные отличается от поведения в будние дни вполне возможный сценарий тогда мы скажем, что выборки представляют две разные популяции.

Визуализация разных популяций

Теперь снимем фильтр для рабочих дней и построим график среднесуточного времени пребывания для всех дней недели рабочих и выходных:

def ex_2_12():  '''Построить график времени ожидания  по всем дням, без фильтра''' df = load_data('dwell-times.tsv') means = mean_dwell_times_by_date(df)['dwell-time'] means.hist(bins=20) plt.xlabel('Ежедневное время ожидания неотфильтрованное, сек.') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

Распределение уже не является нормальным. Оно фактически является бимодальным, поскольку у него два пика. Второй меньший пик соответствует вновь добавленным выходным дням, и он ниже потому что количество выходных дней гораздо меньше количества рабочих дней, а также потому что стандартная ошибка распределения больше.

Распределения более чем с одним пиком обычно называются мультимодальными. Они могут указывать на совмещение двух или более нормальных распределений, и, следовательно, возможно, на совмещение двух или более популяций. Классическим примером бимодальности является распределение показателей роста людей, поскольку модальный рост мужчин выше, чем у женщин.

Данные выходных дней имеют другие характеристики в отличие от данных будних дней. Мы должны удостовериться, что мы сравниваем подобное с подобным. Отфильтруем наш первоначальный набор данных только по выходным дням:

def ex_2_13():    '''Сводные статистики данных,       отфильтрованных только по выходным дням'''    df = with_parsed_date( load_data('dwell-times.tsv') )    df.index = df['date']    df = df[df['date'].index.dayofweek > 4]   # суббота-воскресенье    weekend_times = df['dwell-time']      print('n:                      ', weekend_times.count())    print('Среднее:                ', weekend_times.mean())    print('Медиана:                ', weekend_times.median())    print('Стандартное отклонение: ', weekend_times.std())    print('Стандартная ошибка:     ', standard_error(weekend_times))        
n:                       5860Среднее:                 117.78686006825939Медиана:                 81.0Стандартное отклонение:  120.65234077179436Стандартная ошибка:      1.5759770362547678

Итоговое среднее значение в выходные дни (на основе 6-ти месячных данных) составляет 117.8 сек. и попадает в пределы 95%-ого интервала уверенности для маркетинговой выборки. Другими словами, хотя среднее значение времени пребывания в размере 130 сек. является высоким даже для выходных, расхождение не настолько большое, что его нельзя было бы приписать простой случайной изменчивости в выборке.

Мы только что применили подход к установлению подлинного расхождения в популяциях (между посетителями веб-сайта в выходные по сравнению с посетителями в будние дни), который при проведении проверки обычно не используется. Более традиционный подход начинается с выдвижения гипотетического предположения, после чего это предположение сверяется с данными. Для этих целей статистический метод анализа определяет строгий подход, который называется проверкой статистических гипотез.

Это и будет темой следующего поста, поста 3.

Примеры исходного кода для этого поста находятся в моемрепона Github. Все исходные данные взяты врепозиторииавтора книги.

Подробнее..

Python и статистический вывод часть 4

12.05.2021 06:16:40 | Автор: admin

Этот заключительный пост посвящен анализу дисперсии. Предыдущий пост см. здесь.

Анализ дисперсии

Анализ дисперсии (варианса), который в специальной литературе также обозначается как ANOVA от англ. ANalysis Of VAriance, это ряд статистических методов, используемых для измерения статистической значимости расхождений между группами. Он был разработан чрезвычайно одаренным статистиком Рональдом Фишером, который также популяризировал процедуру проверки статистической значимости в своих исследовательских работах по биологическому тестированию.

Примечание. В предыдущей и этой серии постов для термина variance использовался принятый у нас термин дисперсия и в скобках местами указывался термин варианс. Это не случайно. За рубежом существуют парные термины variance и covariance, и они по идее должны переводиться с одним корнем, например, как варианс и коварианс, однако на деле у нас парная связь разорвана, и они переводятся как совершенно разные дисперсия и ковариация. Но это еще не все. Dispersion (статистическая дисперсия) за рубежом является отдельным родовым понятием разбросанности, т.е. степени, с которой распределение растягивается или сжимается, а мерами статистической дисперсии являются варианс, стандартное отклонение и межквартильный размах. Dispersion, как родовое понятие разбросанности, и variance, как одна из ее мер, измеряющая расстояние от среднего значения - это два разных понятия. Далее в тексте для variance везде будет использоваться общепринятый термин дисперсия. Однако данное расхождение в терминологии следует учитывать.

Наши тесты на основе z-статистики и t-статистики были сосредоточены на выборочных средних значениях как первостепенном механизме проведения разграничения между двумя выборками. В каждом случае мы искали расхождение в средних значениях, деленных на уровень расхождения, который мы могли обоснованно ожидать, и количественно выражали стандартной ошибкой.

Среднее значение не является единственным выборочным индикатором, который может указывать на расхождение между выборками. На самом деле, в качестве индикатора статистического расхождения можно также использовать выборочную дисперсию.

Длительности (сек), постранично и совмещенноДлительности (сек), постранично и совмещенно

В целях иллюстрации того, как это могло бы работать, рассмотрим приведенную выше диаграмму. Каждая из трех групп слева может представлять выборки времени пребывания на конкретном веб-сайте с его собственным средним значением и стандартным отклонением. Если время пребывания для всех трех групп объединить в одну, то дисперсия будет больше средней дисперсии для групп, взятых отдельно.

Статистическая значимость на основе анализа дисперсии вытекает из соотношения двух дисперсий дисперсии между исследуемыми группами и дисперсии внутри исследуемых групп. Если существует значимое межгрупповое расхождение, которое не отражено внутри групп, то эти группы помогают объяснить часть дисперсии между группами. И напротив, если внутригрупповая дисперсия идентична межгрупповой дисперсии, то группы статистически от друг друга неразличимы.

F-распределение

F-распределение параметризуется двумя степенями свободы степенями свободы размера выборки и числа групп.

Первая степень свободы это количество групп минус 1, и вторая степень свободы размер выборки минус число групп. Если k представляет число групп, и n объем выборки, то получаем:

df_1=k-1df_2=n-k

Мы можем визуализировать разные F-распределения на графике при помощи функции библиотеки pandas plot:

def ex_2_Fisher(): '''Визуализация разных F-распределений на графике''' mu = 0 d1_values, d2_values = [4, 9, 49], [95, 90, 50] linestyles = ['-', '--', ':', '-.'] x = sp.linspace(0, 5, 101)[1:]  ax = None for (d1, d2, ls) in zip(d1_values, d2_values, linestyles): dist = stats.f(d1, d2, mu) df = pd.DataFrame( {0:x, 1:dist.pdf(x)} )  ax = df.plot(0, 1, ls=ls,  label=r'$d_1=%i,\ d_2=%i$' % (d1,d2), ax=ax) plt.xlabel('$x$\nF-статистика') plt.ylabel('Плотность вероятности \n$p(x|d_1, d_2)$') plt.show()

Кривые приведенного выше графика показывают разные F-распределения для выборки, состоящей из 100 точек, разбитых на 5, 10 и 50 групп.

F-статистика

Тестовая выборочная величина, которая представляет соотношение дисперсии внутри и между группами, называется F-статистикой. Чем ближе значение F-статистики к единице, тем более похожи обе дисперсии. F-статистика вычисляется очень просто следующим образом:

где S2b это межгрупповая дисперсия, и S2w внутригрупповая дисперсия.

По мере увеличения соотношения F межгрупповая дисперсия увеличивается по сравнению с внутригрупповой. Это означает, что это разбиение на группы хорошо справляется с задачей объяснения дисперсии, наблюдавшейся по всей выборке в целом. Там, где это соотношение превышает критический порог, мы можем сказать, что расхождение является статистически значимым.

F-тест всегда является односторонним, потому что любая дисперсия среди групп демонстрирует тенденцию увеличивать F. При этом F не может уменьшаться ниже нуля.

Внутригрупповая дисперсия для F-теста вычисляется как среднеквадратичное отклонение от среднего значения. Мы вычисляем ее как сумму квадратов отклонений от среднего значения, деленную на первую степень свободы. Например, если имеется kгрупп, каждая со средним значением xk, то мы можем вычислить внутригрупповую дисперсию следующим образом:

где SSW это внутригрупповая сумма квадратов, и xjk это значение j-ого элемента в группе .

Приведенная выше формула для вычисления SSW имеет грозный вид, но на деле довольно легко имплементируется на Python, как сумма квадратичных отклонений от среднего значения ssdev, делающая вычисление внутригрупповой суммы квадратов тривиальным:

def ssdev( xs ): '''Сумма квадратов отклонений между  каждым элементом и средним по выборке''' mu = xs.mean()  square_deviation = lambda x : (x - mu) ** 2  return sum( map(square_deviation, xs) )

Межгрупповая дисперсия для F-теста имеет похожую формулу:

где SST это полная сумма квадратов отклонений, и SSW значение, которое мы только что вычислили. Полная сумма квадратов является суммой квадратичных расхождений от итогового среднего значения, которую можно вычислить следующим образом:

Отсюда, SST это попросту полная сумма квадратов без какого-либо разбиения на группы. На языке Python значения SST и SSW вычисляются элементарно, как будет показано ниже.

ssw = sum( groups.apply( lambda g: ssdev(g) ) ) # внутригрупповая сумма # квадратов отклонений sst = ssdev( df['dwell-time'] ) # полная сумма квадратов по всему наборуssb = sst  ssw # межгрупповая сумма квадратов отклонений

F-статистика вычисляется как отношение межгрупповой дисперсии к внутригрупповой. Объединив определенные ранее функции ssb и ssw и две степени свободы, мы можем вычислить F-статистика.

На языке Python F-статистика из групп и двух степеней свободы вычисляется следующим образом:

msb = ssb / df1 # усредненная межгрупповаяmsw = ssw / df2 # усредненная внутригрупповаяf_stat = msb / msw

Имея возможность вычислить F-статистику из групп, мы теперь готовы использовать его в соответствующем F-тесте.

F-тест

Как и в случае со всеми проверками статистических гипотез, которые мы рассмотрели в этой серии постов, как только имеется выборочная величина (статистика) и распределение, нам попросту нужно подобрать значение уровня значимости и посмотреть, не превысили ли наши данные критическое для теста значение.

Библиотека scipy предлагает функцию stats.f.sf, но она измеряет дисперсию между и внутри всего двух групп. В целях выполнения F-теста на наших 20 разных группах, нам придется имплементировать для нее нашу собственную функцию. К счастью, мы уже проделали всю тяжелую работу в предыдущих разделах, вычислив надлежащую F-статистику. Мы можем выполнить F-тест, отыскав F-статистику в F-распределении, параметризованном правильными степенями свободы. В следующем ниже примере мы напишем функцию f_test, которая все это использует для выполнения теста на произвольном числе групп:

def f_test(groups): m, n = len(groups), sum(groups.count()) df1, df2 = m - 1, n - m  ssw = sum( groups.apply(lambda g: ssdev(g)) )  sst = ssdev( df['dwell-time'] )  ssb = sst - ssw  msb = ssb / df1  msw = ssw / df2  f_stat = msb / msw return stats.f.sf(f_stat, df1, df2)    def ex_2_24(): '''Проверка вариантов дизайна веб-сайта на основе F-теста''' df = load_data('multiple-sites.tsv') groups = df.groupby('site')['dwell-time'] return f_test(groups)
0.014031745203658217

В последней строке приведенной выше функции мы преобразуем значение F-статистики в p-значение, пользуясь функцией scipy stats.f.sf, параметризованной правильными степенями свободы. P-значение является мерой всей модели, т.е. насколько хорошо разные веб-сайты объясняют дисперсию времени пребывания в целом. Нам остается только выбрать уровень значимости и выполнить проверку. Будем придерживаться 5%-ого уровня значимости.

Проверка возвращает p-значение, равное 0.014, т.е. значимый результат. Разные варианты веб-сайта действительно имеют разные дисперсии, которые нельзя просто объяснить одной лишь случайной ошибкой в выборке.

F-распределение со степенями свободы 19 и 980F-распределение со степенями свободы 19 и 980

Для визуализации распределений всех вариантов дизайна веб-сайта на одном графике мы можем воспользоваться коробчатой диаграммой, разместив распределения для сопоставления рядом друг с другом:

def ex_2_25(): '''Визуализация распределений всех вариантов  дизайна веб-сайта на одной коробчатой диаграмме''' df = load_data('multiple-sites.tsv') df.boxplot(by='site', showmeans=True) plt.xlabel('Номер дизайна веб-сайта') plt.ylabel('Время пребывания, сек.') plt.title('') plt.suptitle('') plt.show()

В приведенном выше примере показана работа функции boxplot, которая вычисляет свертку на группах и сортирует группы по номеру варианта дизайна веб-сайта. Соответственно наш изначальный веб-сайт с номером 0, на графике расположен крайним слева.

Может создастся впечатление, что вариант дизайна веб-сайта с номером 10 имеет самое длительное время пребывания, поскольку его межквартильный размах простирается вверх выше других. Однако, если вы присмотритесь повнимательнее, то увидите, что его среднее значение меньше, чем у варианта дизайна с номером 6, имеющего среднее время пребывания более 144 сек.:

def ex_2_26(): '''T-проверка вариантов 0 и 10 дизайна веб-сайта''' df = load_data('multiple-sites.tsv') groups = df.groupby('site')['dwell-time'] site_0 = groups.get_group(0)  site_10 = groups.get_group(10) _, p_val = stats.ttest_ind(site_0, site_10, equal_var=False) return p_val
0.0068811940138903786

Подтвердив статистически значимый эффект при помощи F-теста, теперь мы вправе утверждать, что вариант дизайна веб-сайта с номером 6 статистически отличается от изначального значения:

def ex_2_27(): '''t-тест вариантов 0 и 6 дизайна веб-сайта''' df = load_data('multiple-sites.tsv') groups = df.groupby('site')['dwell-time'] site_0 = groups.get_group(0)  site_6 = groups.get_group(6) _, p_val = stats.ttest_ind(site_0, site_6, equal_var=False) return p_val
0.005534181712508717

Наконец, у нас есть подтверждающие данные, из которых вытекает, что веб-сайт с номером 6 является подлинным улучшением существующего веб-сайта. В результате нашего анализа исполнительный директор AcmeContent санкционирует запуск обновленного дизайна веб-сайта. Веб-команда - в восторге!

Размер эффекта

В этой серии постов мы сосредоточили наше внимание на широко используемых в статистической науке методах проверки статистической значимости, которые обеспечивают обнаружение статистического расхождения, которое не может быть легко объяснено случайной изменчивостью. Мы должны всегда помнить, что выявление значимого эффекта это не одно и то же, что и выявление большого эффекта. В случае очень больших выборок значимым будет считаться даже крошечное расхождение в выборочных средних. В целях более глубокого понимания того, является ли наше открытие значимым и важным, мы так же должны констатировать величину эффекта.

Интервальный индекс d Коэна

Индекс dКоэна это поправка, которую применяется для того, чтобы увидеть, не является ли наблюдавшееся расхождение не просто статистически значимым, но и действительно большим. Как и поправка Бонферрони, она проста:

Здесь Sab это объединенное стандартное отклонение (не объединенная стандартная ошибка) выборок. Она вычисляется аналогично вычислению объединенной стандартной ошибки:

def pooled_standard_deviation(a, b): '''Объединенное стандартное отклонение  (не объединенная стандартная ошибка)''' return sp.sqrt( standard_deviation(a) ** 2 + standard_deviation(b) ** 2)

Так, для варианта под номером 6 дизайна нашего веб-сайта мы можем вычислить индекс dКоэна следующим образом:

def ex_2_28(): '''Вычисление интервального индекса d Коэна  для варианта дизайна веб-сайта под номером 6''' df = load_data('multiple-sites.tsv') groups = df.groupby('site')['dwell-time'] a = groups.get_group(0) b = groups.get_group(6) return (b.mean() - a.mean()) / pooled_standard_deviation(a, b)
0.38913648705499848

В отличие от p-значений, абсолютный порог для индекса d Коэна отсутствует. Считать ли эффект большим или нет частично зависит от контекста, однако этот индекс действительно предоставляет полезную, нормализованную меру величины эффекта. Значения выше 0.5, как правило, считаются большими, поэтому значение 0.38 это умеренный эффект. Он определенно говорит о значительном увеличении времени пребывания на нашем веб-сайте и что усилия, потраченные на обновление веб-сайта, определенно не были бесполезными.

Примеры исходного кода для этого поста находятся в моемрепона Github. Все исходные данные взяты врепозиторииавтора книги.

Резюме

В этой серии постов мы узнали о разнице между описательной и инференциальной статистикой, занимающейся методами статистического вывода. Мы еще раз убедились в важности нормального распределения и центральной предельной теоремы, и научились квантифицировать расхождение с популяциями, используя проверку статистических гипотез на основе z-теста, t-теста и F-теста.

Мы узнали о том, каким именно образом методология инференциальной статистики анализирует непосредственно сами выборки, чтобы выдвигать утверждения о всей популяции в целом, из которой они были отобраны. Мы познакомились с целым рядом методов интервалами уверенности, размножением выборок путем бутстрапирования и проверкой статистической значимости которые помогают разглядеть глубинные параметры популяции. Симулируя многократные испытания, мы также получили представление о трудности проверки статистической значимости в условиях многократных сравнений и увидели, как F-тест помогает решать эту задачу и устанавливать равновесие между ошибками 1-го и 2-го рода.

Мы также коснулись терминологических болевых точек и выяснили некоторые нюансы смыслового дрейфа в отечественной статистике.

В следующей серии постов, если читатели пожелают, мы применим полученные знания о дисперсии и F-тесте к одиночным выборкам. Мы представим метод регрессионного анализа и воспользуемся им для обнаружения корреляции между переменными в выборке из спортсменов-олимпийцев.

Подробнее..

Python и статистический вывод часть 3

12.05.2021 06:16:40 | Автор: admin

Предыдущий пост см. здесь.

Проверка статистических гипотез

Для статистиков и исследователей данных проверка статистической гипотезы представляет собой формальную процедуру. Стандартный подход к проверке статистической гипотезы подразумевает определение области исследования, принятие решения в отношении того, какие переменные необходимы для измерения предмета изучения, и затем выдвижение двух конкурирующих гипотез. Во избежание рассмотрения только тех данных, которые подтверждают наши субъективные оценки, исследователи четко констатируют свою гипотезу заранее. Затем, основываясь на данных, они применяют выборочные статистики с целью подтвердить либо отклонить эту гипотезу.

Проверка статистической гипотезы подразумевает использование тестовой статистики, т.е. выборочной величины, как функции от результатов наблюдений. Тестовая статистика (test statistic) - это вычисленная из выборочных данных величина, которая используется для оценивания прочности данных, подтверждающих нулевую статистическую гипотезу и служит для выявления меры расхождения между эмпирическими и гипотетическими значениями. Конкретные методы проверки называются тестами, например, z-тест, t-тест (соответственно z-тест Фишера, t-тест Студента) и т.д. в зависимости от применяемых в них тестовых статистик.

Примечание. В отечественной статистической науке используется туманный термин статистика критерия. Туманный потому здесь мы снова наблюдаем мягкую подмену: вместо теста возникает критерий. Если уж на то пошло, то критерий - это принцип или правило. Например, выполняя z-тест, t-тест и т.д., мы соответственно используем z-статистику, t-статистику и т.д. в правиле отклонения гипотезы. Это хорошо резюмируется следующей ниже таблицей:

Тестирование гипотезы

Тестовая статистика

Правило, или критерий, отклонения гипотезы

z-тесты

z-статистика

Если тестовая статистика z или -z, то отклонить нулевую гипотезу H0.

t-тесты

t-статистика

Если тестовая статистика t или -t, то отклонить нулевую гипотезу H0.

Анализ дисперсии (ANOVA)

F-статистика

Если тестовая статистика F, то отклонить нулевую гипотезу H0.

Тесты хи-квадрат

Статистика хи-квадрат

Если тестовая статистика , то отклонить нулевую гипотезу H0.

Для того, чтобы помочь сохранить поток посетителей веб-сайта, дизайнеры приступают к работе над вариантом веб-сайта с использованием всех новейших методов по поддержанию внимания аудитории. Мы хотели бы удостовериться, что наши усилия не напрасны, и поэтому стараемся увеличить время пребывания посетителей на обновленном веб-сайте.

Отсюда главный вопрос нашего исследования состоит в том, приводит ли обновленный вид веб-сайта к увеличению времени пребывания на нем посетителей? Мы принимаем решение проверить его относительно среднего значения времени пребывания. Теперь, мы должны изложить две наши гипотезы. По традиции считается, что изучаемые данные не содержат того, что исследователь ищет. Таким образом, консервативное мнение заключается в том, что данные не покажут ничего необычного. Все это называется нулевой гипотезой и обычно обозначается как H0.

При тестировании статистической гипотезы исходят из того, что нулевая гипотеза является истинной до тех пор, пока вес представленных данных, подтверждающих обратное, не сделает ее неправдоподобной. Этот подход к поиску доказательств в обратную сторону частично вытекает из простого психологического факта, что, когда люди пускаются на поиски чего-либо, они, как правило, это находят.

Затем исследователь формулирует альтернативную гипотезу, обозначаемую как H1. Она может попросту заключаться в том, что популяционное среднее отличается от базового уровня. Или же, что популяционное среднее больше или меньше базового уровня, либо больше или меньше на некоторую указанную величину. Мы хотели бы проверить, не увеличивает ли обновленный дизайн веб-сайта время пребывания, и поэтому нашей нулевой и альтернативной гипотезами будут следующие:

  • H0: Время пребывания для обновленного веб-сайта не отличается от времени пребывания для существующего веб-сайта

  • H1: Время пребывания для обновленного веб-сайта больше по сравнению с временем пребывания для существующего веб-сайта

Наше консервативное допущение состоит в том, что обновленный веб-сайт никак не влияет на время пребывания посетителей на веб-сайте. Нулевая гипотеза не обязательно должна быть нулевой (т.е. эффект отсутствует), но в данном случае, у нас нет никакого разумного оправдания, чтобы считать иначе. Если выборочные данные не поддержат нулевую гипотезу (т.е. если данные расходятся с ее допущением на слишком большую величину, чтобы носить случайный характер), то мы отклоним нулевую гипотезу и предложим альтернативную в качестве наилучшего альтернативного объяснения.

Указав нулевую и альтернативную гипотезы, мы должны установить уровень значимости, на котором мы ищем эффект.

Статистическая значимость

Проверка статистической значимости изначально разрабатывалась независимо от проверки статистических гипотез, однако сегодня оба подхода очень часто используются во взаимодействии друг с другом. Задача проверки статистической значимости состоит в том, чтобы установить порог, за пределами которого мы решаем, что наблюдаемые данные больше не поддерживают нулевую гипотезу.

Следовательно, существует два риска:

  • Мы можем принять расхождение как значимое, когда на самом деле оно возникло случайным образом

  • Мы можем приписать расхождение случайности, когда на самом деле оно показывает истинное расхождение с популяцией

Эти две возможности обозначаются соответственно, как ошибки 1-го и 2-го рода:

H0ложная

H0истинная

Отклонить H0

Истинноотрицательный исход

Ошибка 1-го рода (ложноположительный исход)

Принять H0

Ошибка 2-го рода (ложноотрицательный исход)

Истинноположительный исход

Чем больше мы уменьшаем риск совершения ошибок 1-го рода, тем больше мы увеличиваем риск совершения ошибок 2-го рода. Другими словами, с чем большей уверенностью мы хотим не заявлять о наличии расхождения, когда его нет, тем большее расхождение между выборками нам потребуется, чтобы заявить о статистической значимости. Эта ситуация увеличивает вероятность того, что мы проигнорируем подлинное расхождение, когда мы с ним столкнемся.

В статистической науке обычно используются два порога значимости. Это уровни в 5% и 1%. Расхождение в 5% обычно называют значимым, а расхождение в 1% крайне значимым. В формулах этот порог часто обозначается греческой буквой (альфа) и называется уровнем значимости. Поскольку, отсутствие эффекта по результатам эксперимента может рассматриваться как неуспех (эксперимента либо обновленного веб-сайта, как в нашем случае), то может возникнуть желание корректировать уровень значимости до тех пор, пока эффект не будет найден. По этой причине классический подход к проверке статистической значимости требует, чтобы мы устанавливали уровень значимости до того, как обратимся к нашим данным. Часто выбирается уровень в 5%, и поэтому мы на нем и остановимся.

Проверка обновленного дизайна веб-сайта

Веб-команда в AcmeContent была поглощена работой, конструируя обновленный веб-сайт, который будет стимулировать посетителей оставаться на нем в течение более длительного времени. Она употребила все новейшие методы и, в результате мы вполне уверены, что веб-сайт покажет заметное улучшение показателя времени пребывания.

Вместо того, чтобы запустить его для всех пользователей сразу, в AcmeContent хотели бы сначала проверить веб-сайт на небольшой выборке посетителей. Мы познакомили веб-команду с понятием искаженности выборки, и в результате там решили в течение одного дня перенаправлять случайные 5% трафика на обновленный веб-сайт. Результат с дневным трафиком был нам предоставлен одним текстовым файлом. Каждая строка показывает время пребывания посетителей. При этом, если посетитель пользовался исходным дизайном, ему присваивалось значение "0", и если он пользовался обновленным (и надеемся, улучшенным) дизайном, то ему присваивалось значение "1".

Выполнение z-теста

Ранее при тестировании с интервалами уверенности мы располагали лишь одним популяционным средним, с которым и выполнялось сравнение.

При тестировании нулевой гипотезы с помощью z-теста мы имеем возможность сравнивать две выборки. Посетители, которые видели обновленный веб-сайт, отбирались случайно, и данные для обеих групп были собраны в тот же день, чтобы исключить другие факторы с временной зависимостью.

Поскольку в нашем распоряжении имеется две выборки, то и стандартных ошибок у нас тоже две. Z-тест выполняется относительно объединенной стандартной ошибки, т.е. квадратного корня суммы дисперсий (вариансов), деленных на размеры выборок. Она будет такой же, что и результат, который мы получим, если взять стандартную ошибку обеих выборок вместе:

Здесь 2a это дисперсия выборки a, 2b дисперсия выборки bи соответственно na и nb размеры выборок a и b. На Python объединенная стандартная ошибка вычисляется следующим образом:

def pooled_standard_error(a, b, unbias=False): '''Объединенная стандартная ошибка''' std1 = a.std(ddof=0) if unbias==False else a.std()  std2 = b.std(ddof=0) if unbias==False else b.std() x = std1 ** 2 / a.count() y = std2 ** 2 / b.count() return sp.sqrt(x + y)

С целью выявления того, является ли видимое нами расхождение неожиданно большим, можно взять наблюдавшиеся расхождения между средними значениями на объединенной стандартной ошибке. Эту статистическую величину принято обозначать переменной z:

Используя функции pooled_standard_error, которая вычисляет объединенную стандартную ошибку, z-статистику можно получить следующим образом:

def z_stat(a, b, unbias=False): return (a.mean() - b.mean()) / pooled_standard_error(a, b, unbias)

Соотношение z объясняет, насколько средние значения отличаются относительно величины, которую мы ожидаем при заданной стандартной ошибке. Следовательно, z-статистика сообщает нам о том, на какое количество стандартных ошибок расходятся средние значения. Поскольку стандартная ошибка имеет нормальное распределение вероятностей, мы можем связать это расхождение с вероятностью, отыскав z-статистику в нормальной ИФР:

def z_test(a, b):  return stats.norm.cdf([ z_stat(a, b) ])

В следующем ниже примере z-тест используется для сравнения результативность двух веб-сайтов. Это делается путем группировки строк по номеру веб-сайта, в результате чего возвращается коллекция, в которой конкретному веб-сайту соответствует набор строк. Мы вызываем groupby('site')['dwell-time'] для конвертирования набора строк в набор значений времени пребывания. Затем вызываем функцию get_group с номером группы, соответствующей номеру веб-сайта:

def ex_2_14():    '''Сравнение результативности двух вариантов       дизайна веб-сайта на основе z-теста'''    groups = load_data('new-site.tsv').groupby('site')['dwell-time']    a = groups.get_group(0)    b = groups.get_group(1)         print('a n:         ', a.count())    print('b n:         ', b.count())    print('z-статистика:', z_stat(a, b))    print('p-значение:  ', z_test(a, b))
a n:          284b n:          16z-статистика: -1.6467438180091214p-значение:   [0.04980536]

Установление уровня значимости в размере 5% во многом аналогично установлению интервала уверенности шириной 95%. В сущности, мы надеемся убедиться, что наблюдавшееся расхождение попадает за пределы 95%-го интервала уверенности. Если это так, то мы можем утверждать, что нашли результат с 5%-ым уровнем значимости.

P-значение это вероятность совершения ошибки 1-го рода в результате неправильного отклонения нулевой гипотезы, которая в действительности является истинной. Чем меньше p-значение, тем больше определенность в том, что нулевая гипотеза является ложной, и что мы нашли подлинный эффект.

Этот пример возвращает значение 0.0498, или 4.98%. Поскольку оно немногим меньше нашего 5% порога значимости, мы можем утверждать, что нашли нечто значимое.

Приведем еще раз нулевую и альтернативную гипотезы:

  • H0: Время пребывания на обновленном веб-сайте не отличается от времени пребывания на существующем веб-сайте

  • H1: Время пребывания на обновленном веб-сайте превышает время пребывания на существующем веб-сайте.

Наша альтернативная гипотеза состоит в том, что время пребывания на обновленном веб-сайте больше.

Мы готовы заявить о статистической значимости, и что время пребывания на обновленном веб-сайте больше по сравнению с существующим веб-сайтом, но тут есть одна трудность чем меньше размер выборки, тем больше неопределенность в том, что выборочное стандартное отклонение совпадет с популяционным. Как показано в результатах предыдущего примера, наша выборка из обновленного веб-сайта содержит всего 16 посетителей. Столь малые выборки делают невалидным допущение о том, что стандартная ошибка нормально распределена.

К счастью, существует тест и связанное с ним распределение, которое моделирует увеличенную неопределенность стандартных ошибок для выборок меньших размеров.

t-распределение Студента

Популяризатором t-распределения был химик, работавший на пивоварню Гиннес в Ирландии, Уилльям Госсетт, который включил его в свой анализ темного пива Стаут.

В 1908 Уильям Госсет опубликовал статью об этой проверке в журнале Биометрика, но при этом по распоряжению своего работодателя, который рассматривал использованную Госсеттом статистику как коммерческую тайну, был вынужден использовать псевдоним. Госсет выбрал псевдоним Студент.

В то время как нормальное распределение полностью описывается двумя параметрами средним значением и стандартным отклонением, t-распределение описывается лишь одним параметром, так называемыми степенями свободы. Чем больше степеней свободы, тем больше t-распределение похоже на нормальное распределение с нулевым средним и стандартным отклонением, равным 1. По мере уменьшения степеней свободы, это распределение становится более широким с более толстыми чем у нормального распределения, хвостами.

Нормальное распределение, t-распределение со степенью свободы df = 20 и степенью свободы df = 5Нормальное распределение, t-распределение со степенью свободы df = 20 и степенью свободы df = 5

Приведенный выше рисунок показывает, как t-распределение изменяется относительно нормального распределения при наличии разных степеней свободы. Более толстые хвосты для выборок меньших размеров соответствуют увеличенной возможности наблюдать более крупные отклонения от среднего значения.

Степени свободы

Степени свободы, часто обозначаемые сокращенно df от англ. degrees of freedom, тесно связаны с размером выборки. Это полезная статистика и интуитивно понятное свойство числового ряда, которое можно легко продемонстрировать на примере.

Если бы вам сказали, что среднее, состоящее из двух значений, равно 10 и что одно из значений равно 8, то Вам бы не потребовалась никакая дополнительная информация для того, чтобы суметь заключить, что другое значение равно 12. Другими словами, для размера выборки, равного двум, и заданного среднего значения одно из значений ограничивается, если другое известно.

Если напротив вам говорят, что среднее, состоящее из трех значений, равно 10, и первое значение тоже равно 10, то Вы были бы не в состоянии вывести оставшиеся два значения. Поскольку число множеств из трех чисел, начинающихся с 10, и чье среднее равно 10, является бесконечным, то прежде чем вы сможете вывести значение третьего, второе тоже должно быть указано.

Для любого множества из трех чисел ограничение простое: вы можете свободно выбрать первые два числа, но заключительное число ограничено. Степени свободы могут таким образом быть обобщены следующим образом: количество степеней свободы любой отдельной выборки на единицу меньше размера выборки.

При сопоставлении двух выборок степени свободы на две единицы меньше суммы размеров этих выборок, что равно сумме их индивидуальных степеней свободы.

t-статистика

При использовании t-распределения мы обращаемся к t-статистике. Как и z-статистика, эта величина количественно выражает степень маловероятности отдельно взятого наблюдавшегося отклонения. Для двухвыборочного t-теста соответствующая t-статистика вычисляется следующим образом:

Здесь Sab это объединенная стандартная ошибка. Объединенная стандартная ошибка вычисляется таким же образом, как и раньше:

Однако это уравнение допускает наличие информации о популяционных параметрах aи b, которые можно аппроксимировать только на основе крупных выборок. t-тест предназначен для малых выборок и не требует от нас принимать допущения о поплуляционной дисперсии (вариансе).

Как следствие, объединенная стандартная ошибка для t-теста записывается как квадратный корень суммы стандартных ошибок:

На практике оба приведенных выше уравнения для объединенной стандартной ошибки дают идентичные результаты при заданных одинаковых входных последовательностях. Разница в математической записи всего лишь служит для иллюстрации того, что в условиях t-теста мы на входе зависим только от выборочных статистик. Объединенная стандартная ошибка может быть вычислена следующим образом:

def pooled_standard_error_t(a, b):  '''Объединенная стандартная ошибка для t-теста''' return sp.sqrt(standard_error(a) ** 2 +  standard_error(b) ** 2)

Хотя в математическом плане t-статистика и z-статистика представлены по-разному, на практике процедура вычисления обоих идентичная:

t_stat = z_statdef ex_2_15():    '''Вычисление t-статистики        двух вариантов дизайна веб-сайта'''    groups = load_data('new-site.tsv').groupby('site')['dwell-time']    a = groups.get_group(0)    b = groups.get_group(1)        return t_stat(a, b)
-1.6467438180091214

Различие между двумя выборочными показателями является не алгоритмическим, а концептуальным z-статистика применима только тогда, когда выборки подчинены нормальному распределению.

t-тест

Разница в характере работы t-теста вытекает из распределения вероятностей, из которого вычисляется наше p-значение. Вычислив t-статистику, мы должны отыскать ее значение в t-распределении, параметризованном степенями свободы наших данных:

def t_test(a, b): df = len(a) + len(b) - 2 return stats.t.sf([ abs(t_stat(a, b)) ], df)

Значение степени свободы обеих выборок на две единицы меньше их размеров, и для наших выборок составляет 298.

t-распределение, степень свободы = 298t-распределение, степень свободы = 298

Напомним, что мы выполняем проверку статистической гипотезы. Поэтому выдвинем нашу нулевую и альтернативную гипотезы:

  • H0: Эта выборка взята из популяции с предоставленным средним значением

  • H1: Эта выборка взята из популяции со средним значением большего размера

Выполним следующий ниже пример:

def ex_2_16(): '''Сравнение результативности двух вариантов  дизайна веб-сайта на основе t-теста''' groups = load_data('new-site.tsv').groupby('site')['dwell-time'] a = groups.get_group(0) b = groups.get_group(1)  return t_test(a, b)
array([ 0.05033241])

Этот пример вернет p-значение, составляющее более 0.05. Поскольку оно больше , равного 5%, который мы установили для проверки нулевой гипотезы, то мы не можем ее отклонить. Наша проверка с использованием t-теста значимого расхождения между средними значениями не обнаружила. Следовательно, наш едва значимый результат z-теста отчасти объясняется наличием слишком малой выборки.

Двухсторонние тесты

В нашей альтернативной гипотезе было принято неявное допущение, что обновленный веб-сайт будет работать лучше существующего. В процедуре проверки нулевой статистической гипотезы предпринимаются особые усилия для обеспечения того, чтобы при поиске статистической значимости мы не делали никаких скрытых допущений.

Проверки, при выполнении которых мы ищем только значимое количественное увеличение или уменьшение, называются односторонними и обычно не приветствуются, кроме случая, когда изменение в противоположном направлении было бы невозможным. Название термина односторонний обусловлено тем, что односторонняя проверка размещает всю в одном хвосте распределения. Не делая проверок в другом направлении, проверка имеет больше мощности отклонить нулевую гипотезу в отдельно взятом направлении и, в сущности, понижает порог, по которому мы судим о результате как значимом.

Статистическая мощность это вероятность правильного принятия альтернативной гипотезы. Она может рассматриваться как способность проверки обнаруживать эффект там, где имеется искомый эффект.

Хотя более высокая статистическая мощность выглядит желательной, она получается за счет наличия большей вероятности совершить ошибку 1-го рода. Правильнее было бы допустить возможность того, что обновленный веб-сайт может в действительности оказаться хуже существующего. Этот подход распределяет нашу одинаково по обоим хвостам распределения и обеспечивает значимый результат, не искаженный под воздействием априорного допущения об улучшении работы обновленного веб-сайта.

Надписи: t-распределение, степень свободы = 298Надписи: t-распределение, степень свободы = 298

В действительности в модуле stats библиотеки scipy уже предусмотрены функции для выполнения двухвыборочных t-проверок. Это функция stats.ttest_ind. В качестве первого аргумента мы предоставляем выборку данных и в качестве второго - выборку для сопоставления. Если именованный аргумент equal_var равен True, то выполняется стандартная независимая проверка двух выборок, которая предполагает равные популяционные дисперсии, в противном случае выполняется проверка Уэлша (обратите внимание на служебную функцию t_test_verbose, (которую можно найти среди примеров исходного кода в репо):

def ex_2_17(): '''Двухсторонний t-тест''' groups = load_data('new-site.tsv').groupby('site')['dwell-time'] a = groups.get_group(0) b = groups.get_group(1)  return t_test_verbose(a, sample2=b, fn=stats.ttest_ind) #t-тест Уэлша
{'p-значение': 0.12756432502462475, 'степени свободы     ': 17.761382349686098, 'интервал уверенности': (76.00263198799597, 99.89877646270826), 'n1          ': 284, 'n2          ': 16, 'среднее x   ': 87.95070422535211, 'среднее y   ': 122.0, 'дисперсия x ': 10463.941024237296, 'дисперсия y ': 6669.866666666667, 't-статистика': -1.5985205593851322}

По результатам t-теста служебная функция t_test_verbose возвращает много информации и в том числе p-значение. P-значение примерно в 2 раза больше того, которое мы вычислили для односторонней проверки. На деле, единственная причина, почему оно не совсем в два раза больше, состоит в том, что в модуле stats имплементирован легкий вариант t-теста, именуемый t-тестом Уэлша, который немного более робастен, когда две выборки имеют разные стандартные отклонения. Поскольку мы знаем, что для экспоненциальных распределений среднее значение и дисперсия тесно связаны, то этот тест немного более строг в применении и даже возвращает более низкую значимость.

Одновыборочный t-тест

Независимые выборки в рамках t-тестов являются наиболее распространенным видом статистического анализа, который обеспечивает очень гибкий и обобщенный способ установления, что две выборки представляют одинаковую либо разную популяцию. Однако в случаях, когда популяционное среднее уже известно, существует еще более простая проверка, представленная функцией библиотеки sciзy stats.ttest_1samp.

Мы передаем выборку и популяционное среднее относительно которого выполняется проверка. Так, если мы просто хотим узнать, не отличается ли обновленный веб-сайт значимо от существующего популяционного среднего времени пребывания, равного 90 сек., то подобную проверку можно выполнить следующим образом:

def ex_2_18(): groups = load_data('new-site.tsv').groupby('site')['dwell-time'] b = groups.get_group(1)  return t_test_verbose(b, mean=90, fn=stats.ttest_1samp) 
{'p-значение          ': 0.13789520958229415, 'степени свободы df  ': 15.0, 'интервал уверенности': (78.4815276659039, 165.5184723340961), 'n1                  ': 16, 'среднее x           ': 122.0, 'дисперсия x         ': 6669.866666666667, 't-статистика        ': 1.5672973291495713}

Служебная функция t_test_verbose не только возвращает p-значение для выполненной проверки, но и интервал уверенности для популяционного среднего. Интервал имеет широкий диапазон между 78.5 и 165.5 сек., и, разумеется, перекрывается 90 сек. нашего теста. Как раз он и объясняет, почему мы не смогли отклонить нулевую гипотезу.

Многократные выборки

В целях развития интуитивного понимания относительно того, каким образом t-тест способен подтвердить и вычислить эти статистики из столь малых данных, мы можем применить подход, который связан с многократными выборками, от англ. resampling. Извлечение многократных выборок основывается на допущении о том, что каждая выборка является лишь одной из бесконечного числа возможных выборок из популяции. Мы можем лучше понять природу того, какими могли бы быть эти другие выборки, и, следовательно, добиться лучшего понимания опорной популяции, путем извлечения большого числа новых выборок из нашей существующей выборки.

На самом деле существует несколько методов взятия многократных выборок, и мы обсудим один из самых простых бутстрапирование. При бустрапировании мы генерируем новую выборку, неоднократно извлекая из исходной выборки случайное значение с возвратом до тех пор, пока не сгенерируем выборку, имеющую тот же размер, что и оригинал. Поскольку выбранные значения возвращаются назад после каждого случайного отбора, то в новой выборке то же самое исходное значение может появляться многократно. Это как если бы мы неоднократно вынимали случайную карту из колоды игральных карт и каждый раз возвращали вынутую карту назад в колоду. В результате время от времени мы будем иметь карту, которую мы уже вынимали.

Бутстраповская выборка, или бутстрап, синтетический набор данных, полученный в результате генерирования повторных выборок (с возвратом) из исследуемой выборки, используемой в качестве суррогатной популяции, в целях аппроксимации выборочного распределения статистики (такой как, среднее, медиана и др.).

В библиотеке pandas при помощи функции sample можно легко извлекать бутстраповские выборки и генерировать большое число многократных выборок. Эта функция принимает ряд опциональных аргументов, в т.ч. n (число элементов, которые нужно вернуть из числового ряда), axis (ось, из которой извлекать выборку) и replace (выборка с возвратом или без), по умолчанию равный False. После этой функции можно задать метод агрегирования, вычисляющий сводную статистику в отношении бутстраповских выборок:

def ex_2_19(): '''Построение графика синтетических времен пребывания  путем извлечения бутстраповских выборок''' groups = load_data('new-site.tsv').groupby('site')['dwell-time'] b = groups.get_group(1)  xs = [b.sample(len(b), replace=True).mean() for _ in range(1000)]  pd.Series(xs).hist(bins=20) plt.xlabel('Бутстрапированные средние значения времени пребывания, сек.') plt.ylabel('Частота')  plt.show()

Приведенный выше пример наглядно показывает результаты на гистограмме:

Гистограмма демонстрирует то, как средние значения изменялись вместе с многократными выборками, взятыми из времени пребывания на обновленном веб-сайте. Хотя на входе имелась лишь одна выборка, состоящая из 16 посетителей, бутстрапированные выборки очень четко просимулировали стандартную ошибку изначальной выборки и позволили визуализировать интервал уверенности (между 78 и 165 сек.), вычисленный ранее в результате одновыборочного t-теста.

Благодаря бутстрапированию мы просимулировали взятие многократных выборок, при том, что у нас на входе имелась всего одна выборка. Этот метод обычно применяется для оценивания параметров, которые мы не способны или не знаем, как вычислить аналитически.

Проверка многочисленных вариантов дизайна

Было разочарованием обнаружить отсутствие статистической значимости на фоне увеличенного времени пребывания пользователей на обновленном веб-сайте. Хотя хорошо, что мы обнаружили это на малой выборке пользователей, прежде чем выкладывать его на всеобщее обозрение.

Не позволяя себя обескуражить, веб-команда AcmeContent берется за сверхурочную работу и создает комплект альтернативных вариантов дизайна веб-сайта. Беря лучшие элементы из других проектов, они разрабатывают 19 вариантов для проверки. Вместе с нашим изначальным веб-сайтом, который будет действовать в качестве контрольного, всего имеется 20 разных вариантов дизайна веб-сайта, куда посетители будут перенаправляться.

Вычисление выборочных средних

Веб-команда разворачивает 19 вариантов дизайна обновленного веб-сайта наряду с изначальным. Как отмечалось ранее, каждый вариант дизайна получает случайные 5% посетителей, и при этом наше испытание проводится в течение 24 часов.

На следующий день мы получаем файл, показывающий значения времени пребывания посетителей на каждом варианте веб-сайта. Все они были промаркированы числами, при этом число 0 соответствовало веб-сайту с исходным дизайном, а числа от 1 до 19 представляли другие варианты дизайна:

def ex_2_20(): df = load_data('multiple-sites.tsv') return df.groupby('site').aggregate(sp.mean)

Этот пример сгенерирует следующую ниже таблицу:

site

dwell-time

0

79.851064

1

106.000000

2

88.229167

3

97.479167

4

94.333333

5

102.333333

6

144.192982

7

123.367347

8

94.346939

9

89.820000

10

129.952381

11

96.982143

12

80.950820

13

90.737705

14

74.764706

15

119.347826

16

86.744186

17

77.891304

18

94.814815

19

89.280702

Мы хотели бы проверить каждый вариант дизайна веб-сайта, чтобы увидеть, не генерирует ли какой-либо из них статистически значимый результат. Для этого можно сравнить варианты дизайна веб-сайта друг с другом следующим образом, причем нам потребуется вспомогательный модуль Python itertools, который содержит набор функций, создающих итераторы для эффективной циклической обработки:

import itertoolsdef ex_2_21(): '''Проверка вариантов дизайна веб-сайта на основе t-теста по принципу "каждый с каждым"''' groups = load_data('multiple-sites.tsv').groupby('site') alpha = 0.05 pairs = [list(x) # найти сочетания из n по k for x in itertools.combinations(range(len(groups)), 2)]  for pair in pairs: gr, gr2 = groups.get_group( pair[0] ), groups.get_group( pair[1] ) site_a, site_b = pair[0], pair[1] a, b = gr['dwell-time'], gr2['dwell-time']  p_val = stats.ttest_ind(a, b, equal_var = False).pvalue  if p_val < alpha:  print('Варианты веб-сайта %i и %i значимо различаются: %f'  % (site_a, site_b, p_val))

Однако это было бы неправильно. Мы скорее всего увидим статистическое расхождение между вариантами дизайна, показавшими себя в особенности хорошо по сравнению с вариантами, показавшими себя в особенности плохо, даже если эти расхождения носили случайный характер. Если вы выполните приведенный выше пример, то увидите, что многие варианты дизайна веб-сайта статистически друг от друга отличаются.

С другой стороны, мы можем сравнить каждый вариант дизайна веб-сайта с нашим текущим изначальным значением средним значением времени пребывания, равным 90 сек., измеренным на данный момент для существующего веб-сайта:

def ex_2_22(): groups = load_data('multiple-sites.tsv').groupby('site') alpha = 0.05  baseline = groups.get_group(0)['dwell-time'] for site_a in range(1, len(groups)): a = groups.get_group( site_a )['dwell-time'] p_val = stats.ttest_ind(a, baseline, equal_var = False).pvalue  if p_val < alpha:  print('Вариант %i веб-сайта значимо отличается: %f'  % (site_a, p_val))

В результате этой проверки будут идентифицированы два варианта дизайна веб-сайта, которые существенно отличаются:

Вариант 6 веб-сайта значимо отличается: 0.005534Вариант 10 веб-сайта 10 значимо отличается: 0.006881

Малые p-значения (меньше 1%) указывают на то, что существует статистически очень значимые расхождения. Этот результат представляется весьма многообещающим, однако тут есть одна проблема. Мы выполнили t-тест по 20 выборкам данных с уровнем значимости , равным 0.05. Уровень значимости определяется, как вероятность неправильного отказа от нулевой гипотезы. На самом деле после 20-кратного выполнения t-теста становится вероятным, что мы неправильно отклоним нулевую гипотезу по крайней мере для одного варианта веб-сайта из 20.

Сравнивая таким одновременным образом многочисленные страницы, мы делаем результаты t-теста невалидными. Существует целый ряд альтернативных технических приемов решения проблемы выполнения многократных сравнений в статистических тестах. Эти методы будут рассмотрены в следующем разделе.

Поправка Бонферрони

Для проведения многократных проверок используется подход, который объясняет увеличенную вероятность обнаружить значимый эффект в силу многократных испытаний. Поправка Бонферрони это очень простая корректировка, которая обеспечивает, чтобы мы вряд ли совершили ошибки 1-го рода. Она выполняется путем настройки значения уровня значимости для тестов.

Настройка очень простая поправка Бонферрони попросту делит требуемое значение на число тестов. Например, если для теста имелось kвариантов дизайна веб-сайта, и эксперимента равно 0.05, то поправка Бонферрони выражается следующим образом:

=\frac{0.05}{k}

Она представляет собой безопасный способ смягчить увеличение вероятности совершения ошибки 1-го рода при многократной проверке. Следующий пример идентичен примеру ex-2-22, за исключением того, что значение разделено на число групп:

def ex_2_23(): '''Проверка вариантов дизайна веб-сайта на основе t-теста против исходного (0) с поправкой Бонферрони''' groups = load_data('multiple-sites.tsv').groupby('site') alpha = 0.05 / len(groups) baseline = groups.get_group(0)['dwell-time'] for site_a in range(1, len(groups)): a = groups.get_group(site_a)['dwell-time'] p_val = stats.ttest_ind(a, baseline, equal_var = False).pvalue  if p_val < alpha:  print('Вариант %i веб-сайта значимо отличается от исходного: %f'  % (site_a, p_val))

Если вы выполните приведенный выше пример, то увидите, что при использовании поправки Бонферрони ни один из веб-сайтов больше не считается статистически значимым.

Метод проверки статистической значимости связан с поддержанием равновесия чем меньше шансы совершения ошибки 1-го рода, тем больше риск совершения ошибки 2-го рода. Поправка Бонферрони очень консервативна, и весьма возможно, что из-за излишней осторожности мы пропускаем подлинное расхождение.

Примеры исходного кода для этого поста находятся в моемрепона Github. Все исходные данные взяты врепозиторииавтора книги.

В заключительном посте, посте 4, этой серии постов мы проведем исследование альтернативного подхода к проверке статистической значимости, который позволяет устанавливать равновесие между совершением ошибок 1-го и 2-го рода, давая нам возможность проверить все 20 вариантов веб-сайта одновременно.

Подробнее..

Python, корреляция и регрессия часть 1

18.05.2021 14:13:42 | Автор: admin

Чем больше я узнаю людей, тем больше мне нравится моя собака.

Марк Твен

В предыдущих сериях постов из ремикса книги Генри Гарнера Clojure для исследования данных (Clojure for Data Science) на языке Python мы рассмотрели методы описания выборок с точки зрения сводных статистик и методов статистического вывода из них параметров популяции. Такой анализ сообщает нам нечто о популяции в целом и о выборке в частности, но он не позволяет нам делать очень точные утверждения об их отдельных элементах. Это связано с тем, что в результате сведения данных всего к двум статистикам - среднему значению и стандартному отклонению - теряется огромный объем информации.

Нам часто требуется пойти дальше и установить связь между двумя или несколькими переменными либо предсказать одну переменную при наличии другой. И это подводит нас к теме данной серии из 5 постов - исследованию корреляции и регрессии. Корреляция имеет дело с силой и направленностью связи между двумя или более переменными. Регрессия определяет природу этой связи и позволяет делать предсказания на ее основе.

В этой серии постов будет рассмотрена линейная регрессия. При наличии выборки данных наша модель усвоит линейное уравнение, позволяющее ей делать предсказания о новых, не встречавшихся ранее данных. Для этого мы снова обратимся к библиотеке pandas и изучим связь между ростом и весом спортсменов-олимпийцев. Мы введем понятие матриц и покажем способы управления ими с использованием библиотеки pandas.

О данных

В этой серии постов используются данные, любезно предоставленные компанией Guardian News and Media Ltd., о спортсменах, принимавших участие в Олимпийских Играх 2012 г. в Лондоне. Эти данные изначально были взяты из блога газеты Гардиан.

Обследование данных

Когда вы сталкиваетесь с новым набором данных, первая задача состоит в том, чтобы его обследовать с целью понять, что именно он содержит.

Файл all-london-2012-athletes.tsv достаточно небольшой. Мы можем обследовать данные при помощи pandas, как мы делали в первой серии постов Python, исследование данных и выборы, воспользовавшись функцией read_csv:

def load_data(): return pd.read_csv('data/ch03/all-london-2012-athletes-ru.tsv', '\t') def ex_3_1(): '''Загрузка данных об участниках  олимпийских игр в Лондоне 2012 г.''' return load_data()

Если выполнить этот пример в консоли интерпретатора Python либо в блокноте Jupyter, то вы должны увидеть следующий ниже результат:

Столбцы данных (нам повезло, что они ясно озаглавлены) содержат следующую информацию:

  • ФИО атлета

  • страна, за которую он выступает

  • возраст, лет

  • рост, см.

  • вес, кг.

  • пол "М" или "Ж"

  • дата рождения в виде строки

  • место рождения в виде строки (со страной)

  • число выигранных золотых медалей

  • число выигранных серебряных медалей

  • число выигранных бронзовых медалей

  • всего выигранных золотых, серебряных и бронзовых медалей

  • вид спорта, в котором он соревновался

  • состязание в виде списка, разделенного запятыми

Даже с учетом того, что данные четко озаглавлены, очевидно присутствие пустых мест в столбцах с ростом, весом и местом рождения. При наличии таких данных следует проявлять осторожность, чтобы они не сбили с толку.

Визуализация данных

В первую очередь мы рассмотрим разброс роста спортсменов на Олимпийских играх 2012 г. в Лондоне. Изобразим эти значения роста в виде гистограммы, чтобы увидеть характер распределения данных, не забыв сначала отфильтровать пропущенные значения:

def ex_3_2(): '''Визуализация разброса значений  роста спортсменов на гистограмме''' df = load_data() df['Рост, см'].hist(bins=20) plt.xlabel('Рост, см.') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

Как мы и ожидали, данные приближенно нормально распределены. Средний рост спортсменов составляет примерно 177 см. Теперь посмотрим на распределение веса олимпийских спортсменов:

def ex_3_3(): '''Визуализация разброса значений веса спортсменов''' df = load_data() df['Вес'].hist(bins=20) plt.xlabel('Вес') plt.ylabel('Частота') plt.show()

Приведенный выше пример сгенерирует следующую ниже гистограмму:

Данные показывают четко выраженную асимметрию. Хвост с правой стороны намного длиннее, чем с левой, и поэтому мы говорим, что асимметрия - положительная. Мы можем оценить асимметрию данных количественно при помощи функции библиотеки pandas skew:

def ex_3_4(): '''Вычисление асимметрии веса спортсменов''' df = load_data() swimmers = df[ df['Вид спорта'] == 'Swimming'] return swimmers['Вес'].skew()
0.23441459903001483

К счастью, эта асимметрия может быть эффективным образом смягчена путем взятия логарифма веса при помощи функции библиотеки numpy np.log:

def ex_3_5(): '''Визуализация разброса значений веса спортсменов на полулогарифмической гистограмме с целью удаления  асимметрии''' df = load_data() df['Вес'].apply(np.log).hist(bins=20) plt.xlabel('Логарифмический вес') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

Теперь данные намного ближе к нормальному распределению. Из этого следует, что вес распределяется согласно логнормальному распределению.

Логнормальное распределение

Логнормальное распределение это распределение набора значений, чей логарифм нормально распределен. Основание логарифма может быть любым положительным числом за исключением единицы. Как и нормальное распределение, логнормальное распределение играет важную роль для описания многих естественных явлений.

Логарифм показывает степень, в которую должно быть возведено фиксированное число (основание) для получения данного числа. Изобразив логарифмы на графике в виде гистограммы, мы показали, что эти степени приближенно нормально распределены. Логарифмы обычно берутся по основанию 10 или основанию e, трансцендентному числу, приближенно равному 2.718. В функции библиотеки numpy np.log и ее инверсии np.exp используется основание e. Выражение loge также называется натуральным логарифмом, или ln, из-за свойств, делающих его особенно удобным в исчислении.

Логнормальное распределение обычно имеет место в процессах роста, где темп роста не зависит от размера. Этот феномен известен как закон Джибрэта, который был cформулирован в 1931 г. Робертом Джибрэтом, заметившим, что он применим к росту фирм. Поскольку темп роста пропорционален размеру, более крупные фирмы демонстрируют тенденцию расти быстрее, чем фирмы меньшего размера.

Нормальное распределение случается в ситуациях, где много мелких колебаний, или вариаций, носит суммирующий эффект, тогда как логнормальное распределение происходит там, где много мелких вариаций имеет мультипликативный эффект.

С тех пор выяснилось, что закон Джибрэта применим к большому числу ситуаций, включая размеры городов и, согласно обширному математическому ресурсу Wolfram MathWorld, к количеству слов в предложениях шотландского писателя Джорджа Бернарда Шоу.

В остальной части этой серии постов мы будем использовать натуральный логарифм веса спортсменов, чтобы наши данные были приближенно нормально распределены. Мы выберем популяцию спортсменов примерно с одинаковыми типами телосложения, к примеру, олимпийских пловцов.

Визуализация корреляции

Один из самых быстрых и самых простых способов определить наличие корреляции между двумя переменными состоит в том, чтобы рассмотреть их на графике рассеяния. Мы отфильтруем данные, выбрав только пловцов, и затем построим график роста относительно веса спортсменов:

def swimmer_data(): '''Загрузка данных роста и веса только олимпийских пловцов''' df = load_data() return df[df['Вид спорта'] == 'Swimming'].dropna()def ex_3_6(): '''Визуализация корреляции между ростом и весом''' df = swimmer_data() xs = df['Рост, см'] ys = df['Вес'].apply( np.log ) pd.DataFrame(np.array([xs,ys]).T).plot.scatter(0, 1, s=12, grid=True) plt.xlabel('Рост, см.') plt.ylabel('Логарифмический вес') plt.show()

Этот пример сгенерирует следующий ниже график:

Результат ясно показывает, что между этими двумя переменными имеется связь. График имеет характерно смещенную эллиптическую форму двух коррелируемых, нормально распределенных переменных с центром вокруг среднего значения. Следующая ниже диаграмма сравнивает график рассеяния с распределениями вероятностей роста и логарифма веса:

Точки, близко расположенные к хвосту одного распределения, также демонстрируют тенденцию близко располагаться к тому же хвосту другого распределения, и наоборот. Таким образом, между двумя распределениями существует связь, которую в ближайших нескольких разделах мы покажем, как определять количественно. Впрочем, если мы внимательно посмотрим на предыдущий график рассеяния, то увидим, что из-за округления измерений точки уложены в столбцы и строки (в см. и кг. соответственно для роста и веса). Там, где это происходит, иногда желательно внести в данные искажения, которые также называются сдвигом или джиттером с тем, чтобы яснее показать силу связи. Без генерирования джиттера (в виде случайных отклонений) может оказаться, что, то, что по внешнему виду составляет одну точку, фактически представляет много точек, которые обозначены одинаковой парой значений. Внесение нескольких случайных помех делает эту ситуацию вряд ли возможной.

Генерирование джиттера

Поскольку каждое значение округлено до ближайшего сантиметра или килограмма, то значение, записанное как 180 см, на самом деле может быть каким угодно между 179.5 и 180.5 см, тогда как значение 80 кг на самом деле может быть каким угодно между 79.5 и 80.5 кг. Для создания случайных искажений, мы можем добавить случайные помехи в каждую точку данных роста в диапазоне между -0.5 и 0.5 и в том же самом диапазоне проделать с точками данных веса (разумеется, это нужно cделать до того, как мы возьмем логарифм значений веса):

def jitter(limit): '''Генератор джиттера (произвольного сдвига точек данных)''' return lambda x: random.uniform(-limit, limit) + xdef ex_3_7(): '''Визуализация корреляции между ростом и весом с джиттером''' df = swimmer_data() xs = df['Рост, см'].apply(jitter(0.5)) ys = df['Вес'].apply(jitter(0.5)).apply(np.log) pd.DataFrame(np.array([xs,ys]).T).plot.scatter(0, 1, s=12, grid=True) plt.xlabel('Рост, см.') plt.ylabel('Логарифмический вес') plt.show()

График с джиттером выглядит следующим образом:

Как и в случае с внесением прозрачности в график рассеяния в первой серии постов об описательной статистике, генерирование джиттера это механизм, который обеспечивает исключение несущественных факторов, таких как объем данных или артефакты округления, которые могут заслонить от нас возможность увидеть закономерности в данных.

Ковариация

Одним из способов количественного определения силы связи между двумя переменными является их ковариация. Она измеряет тенденцию двух переменных изменяться вместе.

Если у нас имеется два ряда чисел, X и Y, то их отклонения от среднего значения составляют:

dx_i= x_i-x dy_i=y_i-y

Здесь xi это значение X с индексом i, yi значение Y с индексом i, x среднее значение X, и y среднее значение Y. Если X и Y проявляют тенденцию изменяться вместе, то их отклонения от среднего будет иметь одинаковый знак: отрицательный, если они меньше среднего, положительный, если они больше среднего. Если мы их перемножим, то произведение будет положительным, когда у них одинаковый знак, и отрицательным, когда у них разные знаки. Сложение произведений дает меру тенденции этих двух переменных отклоняться от среднего значения в одинаковом направлении для каждой заданной выборки.

Ковариация определяется как среднее этих произведений:

На чистом Python ковариация вычисляется следующим образом:

def covariance(xs, ys): '''Вычисление ковариации (несмещенная, т.е. n-1)''' dx = xs - xs.mean()  dy = ys - ys.mean() return (dx * dy).sum() / (dx.count() - 1)

В качестве альтернативы, мы можем воспользоваться функцией pandas cov:

df['Рост, см'].cov(df['Вес'])
1.3559273321696459

Ковариация роста и логарифма веса для наших олимпийских пловцов равна 1.356, однако это число сложно интерпретировать. Единицы измерения здесь представлены произведением единиц на входе.

По этой причине о ковариации редко сообщают как об отдельной сводной статистике. Сделать число более понятным можно, разделив отклонения на произведение стандартных отклонений. Это позволяет трансформировать единицы измерения в стандартные оценки и ограничить выход числом в диапазоне между -1 и +1. Этот результат называется корреляцией Пирсона.

Стандартная оценка, англ. standard score, также z-оценка это относительное число стандартных отклонений, на которые значение переменной отстоит от среднего значения. Положительная оценка показывает, что переменная находится выше среднего, отрицательная ниже среднего. Это безразмерная величина, получаемая при вычитании популяционного среднего из индивидуальных значений и деления разности на популяционное стандартное отклонение.

Корреляция Пирсона

Корреляция Пирсона часто обозначается переменной rи вычисляется следующим образом, где отклонения от среднего dxiи dyiвычисляются как и прежде:

Поскольку для переменных X и Y стандартные отклонения являются константными, уравнение может быть упрощено до следующего, где xи y это стандартные отклонения соответственно X и Y:

В таком виде формула иногда упоминается как коэффициент корреляции смешанных моментов Пирсона или попросту коэффициент корреляции и, как правило, обозначается буквой r.

Ранее мы уже написали функции для вычисления стандартного отклонения. В сочетании с нашей функцией с вычислением ковариации получится следующая реализация корреляции Пирсона:

def variance(xs): '''Вычисление корреляции, несмещенная дисперсия при n <= 30''' x_hat = xs.mean() n = xs.count() n = n - 1 if n in range( 1, 30 ) else n  return sum((xs - x_hat) ** 2) / ndef standard_deviation(xs): '''Вычисление стандартного отклонения''' return np.sqrt(variance(xs))def correlation(xs, ys):  '''Вычисление корреляции''' return covariance(xs, ys) / (standard_deviation(xs) *  standard_deviation(ys))

В качестве альтернативы мы можем воспользоваться функцией pandas corr:

df['Рост, см'].corr(df['Вес'])

Поскольку стандартные оценки безразмерны, то и коэффициент корреляции rтоже безразмерен. Если rравен -1.0 либо 1.0, то переменные идеально антикоррелируют либо идеально коррелируют.

Правда, если r = 0, то с необходимостью вовсе не следует, что переменные не коррелируют. Корреляция Пирсона измеряет лишь линейные связи. Как продемонстрировано на следующих графиках, между переменными может существовать еще некая нелинейная связь, которую rне объясняет:

Отметим, что корреляция центрального примера не определена, потому что стандартное отклонение y = 0. Поскольку наше уравнение для rсодержало бы деление ковариации на 0, то результат получается бессмысленным. В этом случае между переменными не может быть никакой корреляции; yвсегда будет иметь среднее значение. Простое обследование стандартных отклонений это подтвердит.

Мы можем вычислить коэффициент корреляции для данных роста и логарифма веса наших пловцов следующим образом:

def ex_3_8(): '''Вычисление корреляции средствами pandas на примере данных роста и веса''' df = swimmer_data() return df['Рост, см'].corr( df['Вес'].apply(np.log))
0.86748249283924894

В результате получим ответ 0.867, который количественно выражает сильную, положительную корреляцию, уже наблюдавшуюся нами на точечном графике.

Выборочный rи популяционный

Аналогично среднему значению и стандартному отклонению, коэффициент корреляции является сводной статистикой. Он описывает выборку; в данном случае, выборку спаренных значений: роста и веса. Коэффициент корреляции известной выборки обозначается буквой r, тогда как коэффициент корреляции неизвестной популяции обозначается греческой буквой (рхо).

Как мы убедились в предыдущей серии постов о тестировании гипотез, мы не должны исходить из того, что результаты, полученные в ходе измерения нашей выборки, применимы к популяции в целом. К примеру, наша популяция может состоять из всех пловцов всех недавних Олимпийских игр. И будет совершенно недопустимо обобщать, например, на другие олимпийские виды спорта, такие как тяжелая атлетика или фитнес-плавание.

Даже в допустимой популяции такой как пловцы, выступавшие на недавних Олимпийских играх, наша выборка коэффициента корреляции является всего лишь одной из многих потенциально возможных. То, насколько мы можем доверять нашему r, как оценке параметра , зависит от двух факторов:

  • Размера выборки

  • Величины r

Безусловно, чем больше выборка, тем больше мы ей доверяем в том, что она представляет всю совокупность в целом. Возможно, не совсем интуитивно очевидно, но величина тоже оказывает влияние на степень нашей уверенности в том, что выборка представляет параметр . Это вызвано тем, что большие коэффициенты вряд ли возникли случайным образом или вследствие случайной ошибки при отборе.

Проверка статистических гипотез

В предыдущей серии постов мы познакомились с проверкой статистических гипотез, как средством количественной оценки вероятности, что конкретная гипотеза (как, например, что две выборки взяты из одной и той же популяции) истинная. Чтобы количественно оценить вероятность, что корреляция существует в более широкой популяции, мы воспользуемся той же самой процедурой.

В первую очередь, мы должны сформулировать две гипотезы, нулевую гипотезу и альтернативную:

H_0=0H_1\ne 0

H0 - это гипотеза, что корреляция в популяции нулевая. Другими словами, наше консервативное представление состоит в том, что измеренная корреляция целиком вызвана случайной ошибкой при отборе.

H1 - это альтернативная возможность, что корреляция в популяции не нулевая. Отметим, что мы не определяем направление корреляции, а только что она существует. Это означает, что мы выполняем двустороннюю проверку.

Стандартная ошибка коэффициента корреляции rпо выборке задается следующей формулой:

Эта формула точна, только когда rнаходится близко к нулю (напомним, что величина влияет на нашу уверенность), но к счастью, это именно то, что мы допускаем согласно нашей нулевой гипотезы.

Мы можем снова воспользоваться t-распределением и вычислить t-статистику:

В приведенной формуле df это степень свободы наших данных. Для проверки корреляции степень свободы равна n - 2, где n это размер выборки. Подставив это значение в формулу, получим:

В итоге получим t-значение 102.21. В целях его преобразования в p-значение мы должны обратиться к t-распределению. Библиотека scipy предоставляет интегральную функцию распределения (ИФР) для t-распределения в виде функции stats.t.cdf, и комплементарной ей (1-cdf) функции выживания stats.t.sf. Значение функции выживания соответствует p-значению для односторонней проверки. Мы умножаем его на 2, потому что выполняем двустороннюю проверку:

def t_statistic(xs, ys): '''Вычисление t-статистики''' r = xs.corr(ys) # как вариант, correlation(xs, ys) df = xs.count() - 2 return r * np.sqrt(df / 1 - r ** 2)def ex_3_9(): '''Выполнение двухстороннего t-теста''' df = swimmer_data() xs = df['Рост, см'] ys = df['Вес'].apply(np.log) t_value = t_statistic(xs, ys) df = xs.count() - 2  p = 2 * stats.t.sf(t_value, df) # функция выживания  return {'t-значение':t_value, 'p-значение':p}
{'p-значение': 1.8980236317815443e-106, 't-значение': 25.384018200627057}

P-значение настолько мало, что в сущности равно 0, означая, что шанс, что нулевая гипотеза является истинной, фактически не существует. Мы вынуждены принять альтернативную гипотезу о существовании корреляции.

Интервалы уверенности

Установив, что в более широкой популяции, безусловно, существует корреляция, мы, возможно, захотим количественно выразить диапазон значений, внутри которого, как мы ожидаем, будет лежать параметр , вычислив для этого интервал уверенности. Как и в случае со средним значением в предыдущей серии постов, интервал уверенности для rвыражает вероятность (выраженную в %), что параметр популяции находится между двумя конкретными значениями.

Однако при попытке вычислить стандартную ошибку коэффициента корреляции возникает сложность, которой не было в случае со средним значением. Поскольку абсолютное значение коэффициента корреляции rне может превышать 1, распределение возможных выборок коэффициентов корреляции rсмещается по мере приближения rк пределу своего диапазона.

Приведенный выше график показывает отрицательно скошенное распределение r-выборок для параметра , равного 0.6.

К счастью, трансформация под названием z-преобразование Фишера стабилизирует дисперсию r по своему диапазону. Она аналогична тому, как наши данные о весе спортсменов стали нормально распределенными, когда мы взяли их логарифм.

Уравнение для z-преобразования следующее:

Стандартная ошибка z равна:

Таким образом, процедура вычисления интервалов уверенности состоит в преобразовании rв z с использованием z-преобразования, вычислении интервала уверенности в терминах стандартной ошибки SEzи затем преобразовании интервала уверенности в r.

В целях вычисления интервала уверенности в терминах SEz, мы можем взять число стандартных отклонений от среднего, которое дает нам требуемый уровень доверия. Обычно используют число 1.96, так как оно является числом стандартных отклонений от среднего, которое содержит 95% площади под кривой. Другими словами, 1.96 стандартных ошибок от среднего значения выборочного rсодержит истинную популяционную корреляцию с 95%-ой определенностью.

Мы можем убедиться в этом, воспользовавшись функцией scipy stats.norm.ppf. Она вернет стандартную оценку, связанную с заданной интегральной вероятностью в условиях односторонней проверки.

Однако, как показано на приведенном выше графике, мы хотели бы вычесть ту же самую величину, т.е. 2.5%, из каждого хвоста с тем, чтобы 95%-й интервал уверенности был центрирован на нуле. Для этого при выполнении двусторонней проверки нужно просто уменьшить разность наполовину и вычесть результат из 100%. Так что, требуемый уровень доверия в 95% означает, что мы обращаемся к критическому значению 97.5%:

def critical_value(confidence, ntails): # ДИ и число хвостов '''Расчет критического значения путем вычисления квантиля и получения  для него нормального значения''' lookup = 1 - ((1 - confidence) / ntails)  return stats.norm.ppf(lookup, 0, 1) # mu=0, sigma=1critical_value(0.95, 2)
1.959963984540054

Поэтому наш 95%-й интервал уверенности в z-пространстве для задается следующей формулой:

Подставив в нашу формулу zrи SEz, получим:

Для r=0.867и n=859она даст нижнюю и верхнюю границу соответственно 1.137 и 1.722. В целях их преобразования из z-оценок в r-значения, мы используем следующее обратное уравнение z-преобразования:

Преобразования и интервал уверенности можно вычислить при помощи следующего исходного кода:

def z_to_r(z): '''Преобразование z-оценки обратно в r-значение''' return (np.exp(z*2) - 1) / (np.exp(z*2) + 1)def r_confidence_interval(crit, xs, ys):  '''Расчет интервала уверенности для критического значения и данных''' r = xs.corr(ys) n = xs.count() zr = 0.5 * np.log((1 + r) / (1 - r))  sez = 1 / np.sqrt(n - 3) return (z_to_r(zr - (crit * sez))), (z_to_r(zr + (crit * sez)))def ex_3_10(): '''Расчет интервала уверенности на примере данных роста и веса''' df = swimmer_data() X = df['Рост, см'] y = df['Вес'].apply(np.log) interval = r_confidence_interval(1.96, X, y)  print('Интервал уверенности (95%):', interval)
Интервал уверенности (95%): (0.8499088588880347, 0.8831284878884087)

В результате получаем 95%-й интервал уверенности для , расположенный между 0.850 и 0.883. Мы можем быть абсолютно уверены в том, что в более широкой популяции олимпийских пловцов существует сильная положительная корреляция между ростом и весом.

В следующем посте, посте 2, будет рассмотрена сама тема серии постов - регрессия и приемы оценивания ее качества.

Подробнее..

Python, корреляция и регрессия часть 2

18.05.2021 20:09:49 | Автор: admin

Предыдущий пост см. здесь.

Регрессия

Хотя, возможно, и полезно знать, что две переменные коррелируют, мы не можем использовать лишь одну эту информацию для предсказания веса олимпийских пловцов при наличии данных об их росте или наоборот. При установлении корреляции мы измерили силу и знак связи, но не наклон, т.е. угловой коэффициент. Для генерирования предсказания необходимо знать ожидаемый темп изменения одной переменной при заданном единичном изменении в другой.

Мы хотели бы вывести уравнение, связывающее конкретную величину одной переменной, так называемой независимой переменной, с ожидаемым значением другой, зависимой переменной. Например, если наше линейное уравнение предсказывает вес при заданном росте, то рост является нашей независимой переменной, а вес зависимой.

Описываемые этими уравнениями линии называются линиями регрессии. Этот Термин был введен британским эрудитом 19-ого века сэром Фрэнсисом Гэлтоном. Он и его студент Карл Пирсон, который вывел коэффициент корреляции, в 19-ом веке разработали большое количество методов, применяемых для изучения линейных связей, которые коллективно стали известны как методы регрессионного анализа.

Вспомним, что из корреляции не следует причинная обусловленность, причем термины зависимый и независимый не означают никакой неявной причинной обусловленности. Они представляют собой всего лишь имена для входных и выходных математических значений. Классическим примером является крайне положительная корреляция между числом отправленных на тушение пожара пожарных машин и нанесенным пожаром ущербом. Безусловно, отправка пожарных машин на тушение пожара сама по себе не наносит ущерб. Никто не будет советовать сократить число машин, отправляемых на тушение пожара, как способ уменьшения ущерба. В подобных ситуациях мы должны искать дополнительную переменную, которая была бы связана с другими переменными причинной связью и объясняла корреляцию между ними. В данном примере это может быть размер пожара. Такие скрытые причины называются спутывающими переменными, потому что они искажают нашу возможность определять связь между зависимыми переменными.

Линейные уравнения

Две переменные, которые мы можем обозначить как xи y, могут быть связаны друг с другом строго или нестрого. Самая простая связь между независимой переменной xи зависимой переменной yявляется прямолинейной, которая выражается следующей формулой:

y=a+bx

Здесь значения параметров aи bопределяют соответственно точную высоту и крутизну прямой. Параметр aназывается пересечением с вертикальной осью или константой, а b градиентом, наклоном линии или угловым коэффициентом. Например, в соотнесенности между температурными шкалами по Цельсию и по Фаренгейту a = 32и b = 1.8. Подставив в наше уравнение значения aи b, получим:

y=32+1.8x

Для вычисления 10С по Фаренгейту мы вместо xподставляем 10:

y=32+1.8(10)=50

Таким образом, наше уравнение сообщает, что 10С равно 50F, и это действительно так. Используя Python и возможности визуализации pandas, мы можем легко написать функцию, которая переводит градусы из Цельсия в градусы Фаренгейта и выводит результат на график:

'''Функция перевода из градусов Цельсия в градусы Фаренгейта'''celsius_to_fahrenheit = lambda x: 32 + (x * 1.8)def ex_3_11(): '''График линейной зависимости температурных шкал''' df = pd.DataFrame({'C':s, 'F':s.map(celsius_to_fahrenheit)}) df.plot('C', 'F', legend=False, grid=True) plt.xlabel('Градусы Цельсия') plt.ylabel('Градусы Фаренгейта') plt.show()

Этот пример сгенерирует следующий ниже линейный график:

Обратите внимание, как синяя линия пересекает 0 на шкале Цельсия при величине 32 на шкале Фаренгейта. Пересечение a это значение y, при котором значение xравно 0.

Наклон линии с неким угловым коэффициентом определяется параметром b; в этом уравнении его значение близко к 2. Как видно, диапазон шкалы Фаренгейта почти вдвое шире диапазона шкалы Цельсия. Другими словами, прямая устремляется вверх по вертикали почти вдвое быстрее, чем по горизонтали.

Остатки

К сожалению, немногие связи столь же чистые, как перевод между градусами Цельсия и Фаренгейта. Прямолинейное уравнение редко позволяет нам определять yстрого в терминах x. Как правило, будет иметься ошибка, и, таким образом, уравнение примет следующий вид:

y=a+bx+

Здесь, это ошибка или остаточный член, обозначающий расхождение между значением, вычисленным параметрами aи bдля данного значения xи фактическим значением y. Если предсказанное значение y это y, то ошибка это разность между обоими:

=y-y

Такая ошибка называется остатком. Остаток может возникать из-за случайных факторов, таких как погрешность измерения, либо неслучайных факторов, которые неизвестны. Например, если мы пытаемся предсказать вес как функцию роста, то неизвестные факторы могут состоять из диеты, уровня физической подготовки и типа телосложения (либо просто эффекта округления до самого близкого килограмма).

Если для aи bмы выберем неидеальные параметры, то остаток для каждого xбудет больше, чем нужно. Из этого следует, что параметры, которые мы бы хотели найти, должны минимизировать остатки во всех значениях xи y.

Обычные наименьшие квадраты

Для того, чтобы оптимизировать параметры линейной модели, мы бы хотели создать функцию стоимости, так называемую функцией потери, которая количественно выражает то, насколько близко наши предсказания укладывается в данные. Мы не можем просто взять и просуммировать положительные и отрицательные остатки, потому что даже самые большие остатки обнулят друг друга, если их знаки противоположны.

Прежде, чем вычислить сумму, мы можем возвести значения в квадрат, чтобы положительные и отрицательные остатки учитывались в стоимости. Возведение в квадрат также создает эффект наложения большего штрафа на большие ошибки, чем на меньшие ошибки, но не настолько много, чтобы самый большой остаток всегда доминировал.

Выражаясь в терминах задачи оптимизации, мы стремимся выявить коэффициенты, которые минимизируют сумму квадратов остатков. Этот метод называется обычными наименьшими квадратами, от англ. Ordinary Least Squares (OLS), и формула для вычисления наклона линии регрессии по указанному методу выглядит так:

Хотя она выглядит сложнее предыдущих уравнений, на самом деле, эта формула представляет собой всего лишь сумму квадратов остатков, деленную на сумму квадратов отклонений от среднего значения. В данном уравнении используется несколько членов из других уравнений, которые уже рассматривались, и мы можем его упростить, приведя к следующему виду:

Пересечение (a) это член, позволяющий прямой с заданным наклоном проходить через среднее значение X и Y:

a=y -bx

Значения aи b это коэффициенты, получаемые в результате оценки методом обычных наименьших квадратов.

Наклон и пересечение

Мы уже рассматривали функции covariance, variance и mean, которые нужны для вычисления наклона прямой и точки пересечения для данных роста и веса пловцов. Поэтому вычисление наклона и пересечения имеют тривиальный вид:

def slope(xs, ys): '''Вычисление наклона линии (углового коэффициента)''' return xs.cov(ys) / xs.var()def intercept(xs, ys):  '''Вычисление точки пересечения (с осью Y)''' return ys.mean() - (xs.mean() * slope(xs, ys))def ex_3_12(): '''Вычисление пересечения и наклона (углового коэффициента)  на примере данных роста и веса''' df = swimmer_data() X = df['Рост, см'] y = df['Вес'].apply(np.log) a = intercept(X, y) b = slope(X, y)  print('Пересечение: %f, наклон: %f' % (a,b))
Пересечение: 1.691033, наклон: 0.014296

В результате будет получен наклон приблизительно 0.0143 и пересечение приблизительно 1.6910.

Интерпретация

Величина пересечения это значение зависимой переменной (логарифмический вес), когда независимая переменная (рост) равна нулю. Для получения этого значения в килограммах мы можем воспользоваться функцией np.exp, обратной для функции np.log. Наша модель дает основания предполагать, что вероятнее всего вес олимпийского пловца с нулевым ростом будет 5.42 кг. Разумеется, такое предположение лишено всякого смысла, к тому же экстраполяция за пределы границ тренировочных данных является не самым разумным решением.

Величина наклона показывает, насколько yизменяется для каждой единицы изменения в x. Модель исходит из того, что каждый дополнительный сантиметр роста прибавляет в среднем 1.014 кг. веса олимпийских пловцов. Поскольку наша модель основывается на данных о всех олимпийских пловцах, она представляет собой усредненный эффект от увеличения в росте на единицу без учета любого другого фактора, такого как возраст, пол или тип телосложения.

Визуализация

Результат линейного уравнения можно визуализировать при помощи имплементированной ранее функции regression_line и простой функции от x, которая вычисляет yна основе коэффициентов aи b.

'''Функция линии регрессии'''regression_line = lambda a, b: lambda x: a + (b * x) # вызовы fn(a,b)(x)def ex_3_13(): '''Визуализация линейного уравнения на примере данных роста и веса''' df = swimmer_data() X = df['Рост, см'].apply( jitter(0.5) ) y = df['Вес'].apply(np.log) a, b = intercept(X, y), slope(X, y)  ax = pd.DataFrame(np.array([X, y]).T).plot.scatter(0, 1, s=7) s = pd.Series(range(150,210)) df = pd.DataFrame( {0:s, 1:s.map(regression_line(a, b))} )  df.plot(0, 1, legend=False, grid=True, ax=ax) plt.xlabel('Рост, см.') plt.ylabel('Логарифмический вес') plt.show()

Функция regression_line возвращает функцию от x, которая вычисляет a + bx.

Указанная функция может также использоваться для вычисления каждого остатка, показывая степень, с которой наша оценка yотклоняется от каждого измеренного значения y.

def residuals(a, b, xs, ys): '''Вычисление остатков''' estimate = regression_line(a, b) # частичное применение return pd.Series( map(lambda x, y: y - estimate(x), xs, ys) )constantly = lambda x: 0def ex_3_14(): '''Построение графика остатков на примере данных роста и веса''' df = swimmer_data() X = df['Рост, см'].apply( jitter(0.5) ) y = df['Вес'].apply(np.log) a, b = intercept(X, y), slope(X, y)  y = residuals(a, b, X, y) ax = pd.DataFrame(np.array([X, y]).T).plot.scatter(0, 1, s=12) s = pd.Series(range(150,210)) df = pd.DataFrame( {0:s, 1:s.map(constantly)} )  df.plot(0, 1, legend=False, grid=True, ax=ax) plt.xlabel('Рост, см.') plt.ylabel('Остатки') plt.show()

График остатков это график, который показывает остатки на оси Y и независимую переменную на оси X. Если точки на графике остатков разбросаны произвольно по обе стороны от горизонтальной оси, то линейная модель хорошо подогнана к нашим данным:

За исключением нескольких выбросов на левой стороне графика, график остатков, по-видимому, показывает, что линейная модель хорошо подогнана к данным. Построение графика остатков имеет важное значение для получения подтверждения, что линейная модель применима. В линейной модели используются некоторые допущения относительно данных, которые при их нарушении делают не валидными модели, которые вы строите.

Допущения

Первостепенное допущение линейной регрессии состоит в том, что, безусловно, существует линейная зависимость между зависимой и независимой переменной. Кроме того, остатки не должны коррелировать друг с другом либо с независимой переменной. Другими словами, мы ожидаем, что ошибки будут иметь нулевое среднее и постоянную дисперсию по отношению к зависимой и независимой переменной. График остатков позволяет быстро устанавливать, является ли это действительно так.

Левая сторона нашего графика имеет более крупные значения остатков, чем правая сторона. Это соответствует большей дисперсии веса среди более низкорослых спортсменов. Когда дисперсия одной переменной изменяется относительно другой, говорят, что переменные гетероскедастичны, т.е. их дисперсия неоднородна. Этот факт представляет в регрессионном анализе проблему, потому что делает не валидным допущение в том, что модельные ошибки не коррелируют и нормально распределены, и что их дисперсии не варьируются вместе с моделируемыми эффектами.

Гетероскедастичность остатков здесь довольно мала и особо не должна повлиять на качество нашей модели. Если дисперсия на левой стороне графика была бы более выраженной, то она привела бы к неправильной оценке дисперсии методом наименьших квадратов, что в свою очередь повлияло бы на выводы, которые мы делаем, основываясь на стандартной ошибке.

Качество подгонки и R-квадрат

Хотя из графика остатков видно, что линейная модель хорошо вписывается в данные, т.е. хорошо к ним подогнана, было бы желательно количественно измерить качество этой подгонки. R2, или R-квадрат, варьируется в интервале между 0 и 1 и обозначает объяснительную мощность линейной регрессионной модели. Он вычисляет объясненную долю изменчивости в зависимой переменной.

Обычно, чем ближе R2к 1, тем лучше линия регрессии подогнана к точкам данных и больше изменчивости в Y объясняется независимой переменной X. R2 можно вычислить с помощью следующей ниже формулы:

Здесь var() это дисперсия остатков и var(Y) дисперсия в Y. В целях понимания смысла этой формулы допустим, что вы пытаетесь угадать чей-то вес. Если вам больше ничего неизвестно об испытуемых, то наилучшей стратегией будет угадывать среднее значение весовых данных внутри популяции в целом. Таким путем средневзвешенная квадратичная ошибка вашей догадки в сравнении с истинным весом будет var(Y), т.е. дисперсией данных веса в популяции.

Но если бы я сообщил вам их рост, то в соответствии с регрессионной моделью вы бы предположили, что a + bx. В этом случае вашей средневзвешенной квадратичной ошибкой было бы или дисперсия остатков модели.

Компонент формулы var()/var(Y) это соотношение средневзвешенной квадратичной ошибки с объяснительной переменной и без нее, т. е. доля изменчивости, оставленная моделью без объяснения. Дополнение R2до единицы это доля изменчивости, объясненная моделью.

Как и в случае с r, низкий R2не означает, что две переменные не коррелированы. Просто может оказаться, что их связь не является линейной.

Значение R2 описывает качество подгонки линии регрессии к данным. Оптимально подогнанная линия это линия, которая минимизирует значение R2. По мере удаления либо приближения от своих оптимальных значений R2 всегда будет расти.

Левый график показывает дисперсию модели, которая всегда угадывает среднее значение для , правый же показывает меньшие по размеру квадраты, связанные с остатками, которые остались необъясненными моделью f. С чисто геометрической точки зрения можно увидеть, как модель объяснила большинство дисперсии в y. Приведенный ниже пример вычисляет R2путем деления дисперсии остатков на дисперсию значений y:

def r_squared(a, b, xs, ys): '''Рассчитать коэффициент детерминации (R-квадрат)''' r_var = residuals(a, b, xs, ys).var()  y_var = ys.var() return 1 - (r_var / y_var)def ex_3_15(): '''Рассчитать коэффициент R-квадрат  на примере данных роста и веса''' df = swimmer_data() X = df['Рост, см'].apply( jitter(0.5) ) y = df['Вес'].apply(np.log) a, b = intercept(X, y), slope(X, y) return r_squared(a, b, X, y)
0.75268223613272323

В результате получим значение 0.753. Другими словами, более 75% дисперсии веса пловцов, выступавших на Олимпийских играх 2012 г., можно объяснить ростом.

В случае простой регрессионной модели (с одной независимой переменной), связь между коэффициентом детерминации R2 и коэффициентом корреляции rявляется прямолинейной:

Коэффициент корреляции rможет означать, что половина изменчивости в переменной Y объясняется переменной X, но фактически R2 составит 0.52, т.е. 0.25.

Множественная линейная регрессия

Пока что в этой серии постов мы видели, как строится линия регрессии с одной независимой переменной. Однако, нередко желательно построить модель с несколькими независимыми переменными. Такая модель называется множественной линейной регрессией.

Каждой независимой переменной потребуется свой собственный коэффициент. Вместо того, чтобы для каждой из них пытаться подобрать букву в алфавите, зададим новую переменную (бета), которая будет содержать все наши коэффициенты:

y=_1x_1+_2x_2

Такая модель эквивалентна двухфакторной линейно-регрессионной модели, где 1= a и 2= b при условии, что x1всегда гарантированно равен 1, вследствие чего 1 это всегда константная составляющая, которая представляет наше пересечение, при этом x1называется постоянным смещением уравнения регрессии, или членом смещения.

Обобщив линейное уравнение в терминах , его легко расширить на столько коэффициентов, насколько нам нужно:

y=_1x_1+_2x_2++_nx_n

Каждое значение от x1до xnсоответствует независимой переменной, которая могла бы объяснить значение y. Каждое значение от 1до nсоответствует коэффициенту, который устанавливает относительный вклад независимой переменной.

Простая линейная регрессия преследовала цель объяснить вес исключительно с точки зрения роста, однако объяснить вес людей помогает много других факторов: их возраст, пол, питание, тип телосложения. Мы располагаем сведениями о возрасте олимпийских пловцов, поэтому мы смогли бы построить модель, которая учитывает и эти дополнительные данные.

До настоящего момента мы предоставляли независимую переменную в виде одной последовательности значений, однако при наличии двух и более параметров нам нужно предоставлять несколько значений для каждого x. Мы можем воспользоваться функциональностью библиотеки pandas, чтобы выбрать два и более столбцов и управлять каждым как списком, но есть способ получше: матрицы.

Темой следующего поста, поста 3, будут матричные операции, нормальное уравнение и коллинеарность.

Подробнее..

Python, корреляция и регрессия часть 4

19.05.2021 12:19:31 | Автор: admin

Предыдущий пост см. здесь.

Предсказание

В заключении, мы подходим к одному из наиболее важных применений линейной регрессии: предсказанию. Мы натренировали модель способную предсказывать вес олимпийских пловцов при наличии данных об их росте, половой принадлежности и годе рождения.

9-кратный олимпийский чемпион по плаванию Марк Шпитц завоевал 7 золотых медалей на Олимпийских играх 1972 г. Он родился в 1950 г. и, согласно веб-страницы Википедии, имеет рост 183 см. и вес 73 кг. Посмотрим, что наша модель предсказывает в отношении его веса.

Наша множественная регрессионная модель требует предоставить эти значения в матричной форме. Каждый параметр нужно передать в том порядке, в котором модель усвоила признаки, чтобы применить правильный коэффициент. После смещения вектор признаков должен содержать рост, пол и год рождения в тех же единицах измерения, в которых модель была натренирована:

Матрица содержит коэффициенты для каждого из этих признаков:

Предсказанием модели будет сумма произведений коэффициентов и признаков xв каждой строке:

Поскольку матричное умножение производит каждый элемент в результате соответствующего сложения произведений строк и столбцов каждой матрицы, получение результата попросту сводится к перемножению транспонированной матрицы с вектором xspitz.

Напомним, что размерность итоговой матрицы будет числом строк из первой матрицы и числом столбцов из второй матрицы:

Здесь Tx это произведение матрицы размера 1 nи матрицы размера n 1. Результатом является матрица размера 1 1:

Исходный код вычислений очень прост:

def predict(coefs, x):     '''функция предсказания'''    return np.matmul(coefs, x.values) 
def ex_3_29():    '''Вычисление ожидаемого веса спортсмена'''    df = swimmer_data()    df['бин_Пол'] = df['Пол'].map({'М': 1, 'Ж': 0}).astype(int)     df['Год рождения'] = df['Дата рождения'].map(str_to_year)    X = df[['Рост, см', 'бин_Пол', 'Год рождения']]     X.insert(0, 'константа', 1.0)    y = df['Вес'].apply(np.log)     beta = linear_model(X, y)    xspitz = pd.Series([1.0, 183, 1, 1950]) # параметры Марка Шпитца    return np.exp( predict(beta, xspitz) )  
84.20713139038605

Этот пример вернет число 84.21, которое соответствует ожидаемому весу 84.21 кг. Это намного тяжелее зарегистрированного веса Марка Шпитца 73 кг. Наша модель, похоже, не сработала как надо.

Интервал уверенности для конкретного предсказания

Ранее мы рассчитали интервалы уверенности для параметров популяции. Мы можем также построить интервал уверенности для отдельно взятого предсказания, именуемый интервалом предсказания, или интервалом изменений предсказанной величины. Интервал предсказания количественно определяет размер неопределенности в предсказании, предлагая минимальное и максимальное значение, между которыми истинное значение ожидаемо попадает с определенной вероятностью. Интервал предсказания для yшире интервала уверенности для параметра популяции, такого как например, среднее значение . Это объясняется тем, что в интервале уверенности учитывается только неопределенность при оценке среднего значения, в то время как интервал предсказания также должен принимать в расчет дисперсию в yот среднего значения.

Линии интервала предсказания находятся дальше от оптимально подогнанной линии, чем линии интервала уверенности, и тем дальше, чем больше имеется точек данных. 95%-ый интервал предсказания это площадь, в которую ожидаемо попадают 95% всех точек данных. В отличие от него, 95%-й интервал уверенности это площадь, которая с 95%-й вероятностью содержит истинную линию регрессии.

Приведенный выше график показывает связь между внешним интервалом предсказания и внутренним интервалом уверенности. Мы можем вычислить интервал предсказания, используя следующую ниже формулу:

Здесь yp это предсказание, плюс или минус интервал. Мы пользуемся t-распределением, где степень свободы равна n - p, т.е. размер выборки минус число параметров. Это та же самая формула, которая ранее применялась при вычислении F-тестов. Хотя указанная формула, возможно, пугает своей сложностью, она относительно прямолинейно транслируется в исходный код, показанный в следующем ниже примере, который вычисляет 95%-ый интервал предсказания.

def prediction_interval(x, y, xp):    '''Вычисление интервала предсказания'''    xtx    = np.matmul(x.T, np.asarray(x))    xtxi   = np.linalg.inv(xtx)      xty    = np.matmul(x.T, np.asarray(y))     coefs  = linear_model(x, y)     fitted = np.matmul(x, coefs)    resid  = y - fitted    rss    = resid.dot(resid)      n      = y.shape[0]  # строки    p      = x.shape[1]  # столбцы    dfe    = n - p     mse    = rss / dfe    se_y   = np.matmul(np.matmul(xp.T, xtxi), xp)    t_stat = np.sqrt(mse * (1 + se_y))         # t-статистика    intl   = stats.t.ppf(0.975, dfe) * t_stat       yp     = np.matmul(coefs.T, xp)    return np.array([yp - intl, yp + intl])

Поскольку t-статистика параметризуется степенью свободы ошибки, она принимает в расчет присутствующую в модели неопределенность.

Если вместо интервала предсказания потребуется рассчитать интервал уверенности для среднего значения, мы попросту можем опустить прибавление единицы к se_y при вычислении t-статистики t_stat.

Приведенный выше исходный код можно использовать для генерирования следующего ниже графика, который показывает, как интервал предсказания изменяется вместе со значением независимой переменной:

В приведенном выше графике модель тренируется на выборке из 5 элементов и показывает, как 95%-й интервал предсказания увеличивается по мере нашего движения дальше от среднего роста. Применив приведенную выше формулу к Марку Шпитцу, в результате получим следующее:

def ex_3_30():    '''Интервал предсказания       применительно к данным о Марке Шпитце'''    df = swimmer_data()    df['бин_Пол'] = df['Пол'].map({'М': 1, 'Ж': 0}).astype(int)     df['Год рождения'] = df['Дата рождения'].map(str_to_year)    X = df[['Рост, см', 'бин_Пол', 'Год рождения']]     X.insert(0, 'константа', 1.0)    y = df['Вес'].apply(np.log)     xspitz = pd.Series([1.0, 183, 1, 1950])  # данные М.Шпитца    return np.exp( prediction_interval(X, y, xspitz) )
array([72.74964444, 97.46908087])

Этот пример возвращает диапазон между 72.7 и 97.4 кг., который как раз включает в себя вес Марка 73 кг., поэтому наше предсказание находится в пределах 95%-ого интервала предсказания. Правда оно лежит неудобно близко к границам диапазона.

Границы действия модели

Марк Шпитц родился в 1950 г., за несколько десятилетий до рождения самых возрастных пловцов на Олимпийских играх 2012 г. Пытаясь предсказать вес Марка, используя его год рождения, мы виноваты в том, что пытаемся экстраполировать слишком далеко за пределы наших тренировочных данных. Мы превысили границы действия нашей модели.

Есть и вторая причина, по которой этот результат сопряжен с проблемами. Наши данные полностью основаны на пловцах, которые соревнуются в наши дни на уровне международных стандартов, тогда как Марк не участвовал в соревнованиях уже много лет. Другими словами, Марк теперь не является частью популяции, на которой мы натренировали нашу модель. В целях исправления обеих этих проблем нам нужно поискать подробную информацию о Марке в 1979 г., когда он еще участвовал в соревнованиях по плаванию.

Согласно данным, в 1972 г. 22-летний Марк Шпитц имел рост 185 см. и весил 79 кг.

Отбор верных признаков это одна из самых важных предпосылок для получения хороших результатов от любого алгоритма предсказания.

Следует отбирать признаки не только, опираясь на их предсказательную силу, но и на их актуальность для моделируемой области.

Окончательная модель

Хотя наша модель имеет чуть более низкий R2, давайте натренируем ее повторно, взяв в качестве признака возраст вместо года рождения. Это позволит нам легко предсказывать веса для прошлых и будущих ранее не встречавшихся данных, т.к. она ближе моделирует переменную, которая, как мы полагаем, имеет причинно-следственную связь с весом.

Модель произведет приблизительно с такими значениями:

Наши признаки для Марка на играх 1972 г. таковы:

Эти признаки можно использовать для предсказания его соревновательного веса при помощи приведенного ниже примера:

def ex_3_32():    '''Окончательная модель для предсказания        соревновательного веса'''    df = swimmer_data()    df['бин_Пол'] = df['Пол'].map({'М': 1, 'Ж': 0}).astype(int)     X = df[['Рост, см', 'бин_Пол', 'Возраст']]     X.insert(0, 'константа', 1.0)    y = df['Вес'].apply(np.log)     beta = linear_model(X, y)    # предсказать вес Марка Шпитца    xspitz = pd.Series([1.0, 185, 1, 22])     return np.exp( predict(beta, xspitz) )
78.46882772630318

Пример возвращает число 78.47, т.е. предсказывает вес 78.47 кг. Теперь результат находится очень близко к истинному соревновательному весу Марка, равному 79 кг.

Примеры исходного кода для этого поста находятся в моемрепона Github. Все исходные данные взяты врепозиторииавтора книги.

Резюме

В этой серии постов мы узнали о том, как определять наличие общей линейной связи между двумя и более переменными. Мы увидели, как выражать силу их корреляции при помощи корреляции Пирсона rи насколько хорошо линейная модель объясняет вариацию при помощи коэффициентов детерминации R2и R2. Мы также выполнили проверку статистических гипотез и рассчитали интервалы уверенности, чтобы вывести диапазон истинного популяционного параметра для корреляции.

Определив корреляцию между переменными, мы смогли построить предсказательную модель с использованием регрессии методом обычных наименьших квадратов и простых функций Python. Затем мы обобщили наш подход, воспользовавшись для этого матричной функциональностью библиотек pandas и numpy и нормальным уравнением. Эта простая модель продемонстрировала принципы машинного усвоения закономерностей путем определения модельных параметров , извлеченных из выборочных данных, которые могут использоваться для выполнения предсказаний. Наша модель смогла предсказать ожидаемый вес нового атлета, который полностью попадал в пределы интервала предсказания для истинного значения.

Подробнее..

Маленький и быстрый BERT для русского языка

10.06.2021 02:22:28 | Автор: admin

BERT нейросеть, способная весьма неплохо понимать смысл текстов на человеческом языке. Впервые появивишись в 2018 году, эта модель совершила переворот в компьютерной лингвистике. Базовая версия модели долго предобучается, читая миллионы текстов и постепенно осваивая язык, а потом её можно дообучить на собственной прикладной задаче, например, классификации комментариев или выделении в тексте имён, названий и адресов. Стандартная версия BERT довольно большая: весит больше 600 мегабайт, обрабатывает предложение около 120 миллисекунд (на CPU). В этом посте я предлагаю уменьшенную версию BERT для русского языка 45 мегабайт, 6 мс на предложение. Уже есть tinybert для английского от Хуавея, есть моя уменьшалка FastText'а, а вот маленький (англо-)русский BERT, кажется, появился впервые. Но насколько он хорош?

Дистилляция путь к маленькости

Чтобы создать маленький BERT, я решил обучить свою нейросеть, используя при этом уже готовые модели в качестве учителей. Сейчас объясню подробнее.

Если очень коротко, то BERT работает так: сначала токенизатор разбивает текст на токены (кусочки размером от одной буквы до целого слова), от них берутся эмбеддинги из таблицы, и эти эмбеддинги несколько раз обновляются, используя механизм self-attention для учёта контекста (соседних токенов). При предобучении классический BERT выполняет две задачи: угадывает, какие токены в предложении были заменены на специальный токен [MASK], и шли ли два предложения следом друг за другом в тексте. Как потом показали, вторая задача не очень нужна. Но токен [CLS], который ставится перед началом текста и эмбеддинг которого использовался для этой второй задаче, употреблять продолжают, и я тоже сделал на него ставку.

Дистилляция способ перекладывания знаний из одной модели в другую. Это быстрее, чем учить модель только на текстах. Например, в тексте [CLS] Ехал Грека [MASK] реку "верное" решение поставить на место маски токен через, но большая модель знает, что токены на, в, за в этом контексте тоже уместны, и это знание полезно для обучения маленькой модели. Его можно передать, заставляя маленькую модель не только предсказывать высокую вероятность правильного токена через, а воспроизводить всё вероятностное распределение возможных замаскированных токенов в данном тексте.

В качестве основы для модели я взял классический bert-multilingual (веса), ибо хочу, чтобы модель понимала и русский, и английский, и его же использую на ранних стадиях дистилляции как учителя по распределению токенов. Словарь этой модели содержит 120К токенов, но я отобрал только те, которые часто встречаются в русском и английском языках, оставив 30К. Размер эмбеддинга я сократил с 768 до 312, число слоёв с 12 до 3. Эмбеддинги я инициализировал из bert-multilingual, все остальные веса случайным образом.

Поскольку я собираюсь использовать маленький BERT в первую очередь для классификации коротких текстов, мне надо, чтобы он мог построить хорошее векторное представление предложения. Поэтому в качестве учителей для дистилляции я выбрал модели, которые с этим здорово справляются: RuBERT (статья, веса), LaBSE (статья, веса), Laser (статья, пакет) и USE (статья, код). А именно, я требую, чтобы [CLS] эмбеддинг моей модели позволял предсказать эмбеддинги предложений, полученные из этих трёх моделей. Дополнительно я обучаю модель на задачу translation ranking (как LaBSE). Наконец, я решил, что неплохо было бы уметь полностью расшифровывать предложение назад из CLS-эмбеддингов, причём делать это одинаково для русских и английских предложений как в Laser. Для этих целей я примотал изолентой к своей модели декодер от уменьшенного русского T5. Таким образом, у меня получилась многозадачная модель о восьми лоссах:

  • Обычное предсказание замаскированных токенов (я использую full word masks).

  • Translation ranking по рецепту LaBSE: эмбеддинг фразы на русском должен быть ближе к эмбеддингу её перевода на английский, чем к эмбеддингу остальных примеров в батче. Пробовал добавлять наивные hard negatives, но заметной пользы они не дали.

  • Дистилляция распределения всех токенов из bert-base-multilingual-cased (через несколько эпох я отключил её, т.к. она начала мешать).

  • Приближение CLS-эмбеддингов (после линейной проекции) к эмбеддингам DeepPavlov/rubert-base-cased-sentence (усреднённым по токенам).

  • Приближение CLS-эмбеддингов (после другой линейной проекции) к CLS-эмбеддингам LaBSE.

  • Приближение CLS-эмбеддингов (после третьей проекции) к эмбеддингам LASER.

  • Приближение CLS-эмбеддингов (после ещё одной проекции) к эмбеддингам USE.

  • Расшифровка декодером от T5 предложения (на русском) из последней проекции CLS-эмбеддинга.

Скорее всего, из этих лоссов больше половины можно было безболезненно выкинуть, но ресурсов на ablation study я пока не нашёл. Обучал я это всё в течении нескольких дней на Colab, по пути нащупывая learning rate и другие параметры. В общем, не очень научно, но дешево и результативно. В качестве обучающей выборки я взял три параллельных корпуса англо-русских предложений: от Яндекс.Переводчика, OPUS-100 и Tatoeba, суммарно 2.5 млн коротких текстов. Весь процесс создания модели, включая некоторые неудачные эксперименты, содержится в блокноте. Сама модель, названная мною rubert-tiny (или просто Энкодечка), выложена в репозитории Huggingface.

И как этим пользоваться?

Если у вас есть Python и установлены пакет transformers и sentencepiece, скачать и запустить модель просто. Например, вот так вы можете получить 312-мерный CLS-эмбеддинг предложения.

Этот эмбеддинг вы можете использовать как признак для любой модели классификации или кластеризации текстов. Как (рекомендуемый) вариант, вы можете дообучить мою модель на собственной задаче. Про это будет отдельный пост, а пока отсылаю вас к обучающи материалам от Huggingface (раз, два).

Насколько быстр и мал мой Энкодечка? Я сравил его с другими BERT'ами, понимающими русский язык. Скорость указана в расчёте на одно предложение из Лейпцигского веб-корпуса русского языка.

Модель

Скорость (CPU)

Скорость (GPU)

Вес на диске

cointegrated/rubert-tiny

6 мс

3 мс

45 мб

bert-base-multilingual-cased

125 мс

8 мс

680 мб

DeepPavlov/rubert-base-cased-sentence

110 мс

8 мс

680 мб

sentence-transformers/LaBSE

120 мс

8 мс

1.8 гб

sberbank-ai/sbert_large_nlu_ru

420 мс

16 мс

1.6 гб

Все расчёты я выполнял на Colab (Intel(R) Xeon(R) CPU @ 2.00GHz и Tesla P100-PCIE c батчом размера 1 (если использовать крупные батчи, то ускорение на GPU ещё заметнее, т.к. с маленькой моделью можно собрать более большой батч).

Как видим, rubert-tiny на CPU работает раз в 20 быстрее своих ближайших соседей, и легко помещается на бюджетные хостинги типа Heroku (и даже, наверное, на мобильные устройства). Надеюсь, эта модель сделает предобученные нейросети для русского языка в целом более доступными для прикладных применений. Но надо ещё убедиться, что модель хоть чему-то научилась.

Оценка качества эмбеддингов

Рекомендованный и проверенный временем рецепт использования BERT дообучать его на конечную задачу. Но дообучение процесс небыстрый и наукоёмкий, а гипотезу об осмысленности выученных эмбеддингов хочется проверить побыстрее и попроще. Поэтому я не дообучаю модели, а использую их как готовые feature extractors, и обучаю поверх их эмбеддингов простые модели логистическую регрессию и KNN.

Для оценки русских моделей есть бенчмарк RussianSuperGLUE, но задачи там сложные, требующие подбора параметров и дообучения моделей, так что к нему я тоже перейду позже. Ещё есть бенчмарк RuSentEval, там задачи более простые, но они слишком абстрактные, ориентированные больше на лингвистические свойства, а хочется начать с чего-то прикладного. Поэтому я собрал свой маленький бенчмарк. Пока что он тоже живёт в блокноте, но надо будет допилить его и выложить в более удобном виде. И вот какие задачи туда вошли:

STS: бенчмарк по семантической близости предложений (переведённый с английского). Например, фразы "Кошка спит на фиолетовой простыне" и "Черно-белый кот спит на фиолетовом одеяле" из этого датасета оценены на 4 из 5 баллов сходства. Качество моделей на нём я мерял ранговой корреляций этих баллов с косинусной близостью эмбеддингов предложений. Для наилучшей модели, LaBSE, корреляция оказалась 77%, для моей 65%, на одном уровне с моделью от Сбера, которая в 40 раз больше моей.

Paraphraser: похожий бенчмарк по семантической близости, но с чисто русскими новостными заголовками и трёхбалльной шкалой. Эту задачу я решил так: обучил поверх косинусных близостей логистическую регрессию, и ей предсказывал, к какому из трёх классов пара заголовков относится. На этом бенчмарке моя модель выдала точность 43% и победила все остальные (впрочем, с небольшим отрывом).

XNLI: предсказание, следует ли одно предложение из другого. Например, из "это стреляющее пластиковое автоматическое оружие" не следует "это более надежно, чем металлическое оружие", но и не противоречит. Оценивал её я как предыдущую логрегом поверх косинусных близостей. Тут на первом месте оказалась модель от DeepPavlov (которая дообучалась ровно на этой задаче), а моя заняла второе.

SentiRuEval2016: классификация тональности твитов на три класса. Тут я оценивал модели по точности логистической регрессии, обученной поверх эмбеддингов, и моя модель заняла третье место из пяти. В этой и некоторых последующих задачах я сэмплировал обучающую и тестовые выборки, уменьшая их размер до 5К, скорости ради.

OKMLCup: детекция токсичных комментариев из Одноклассников. Тут моя модель заняла четвёртое место по ROC AUC, обогнав только bert-base-cased-multilingual.

Inappropriateness: детекция сообщений, неприятных для собеседника или вредящих репутации. Тут моя модель оказалась на последнем месте, но таки набрала 68% AUC (у самой лучшей, Сберовской, вышло 79%).

Классификация интентов: накраудсоршенные обращения к голосовому помощнику, покрывающие 18 доменов и 68 интентов. Они собирались на английском языке, но я перевёл их на русский простой моделькой. Часть переводов получились странными, но для бенчмарка сойдёт. Оценивал я по точности логистической регрессии или KNN (что лучше). LaBSE набрала точность 75%, модель от Сбера 68%, от DeepPavlov 60%, моя 58%, мультиязычная 56%. Есть, куда расти.

Перенос классификации интентов с английского: та же классификация интентов, но обучающая выбора на английском языке, а тестовая на русском. Тут моя модель заняла второе место после LaBSE, а качество всех остальных сильно просело (что ожидаемо, ибо на параллельных корпусах они не обучались).

factRuEval-2016: задача распознавания классических именованных сущностей (адреса, организации, личности). Я обучал логистическую регрессию поверх эмбеддингов токенов, а качество мерял макро F1 скором (относительно токенов же, что не вполне корректно). Оказалось, что на таком NER моя модель работает откровенно плохо: она набрала скор 43%, остальные 67-69%.

RuDReC: распознавание медицинских именованных сущностей. Тут моя модель тоже проиграла остальным, но с меньшим отрывом: 58% против 62-67%.

Для всех задач, кроме NER, я пробовал для каждой модели два вида эмбеддингов: из CLS токена и усреднённые по всем токенам. Для меня было неожиданностью, что на многих задачах и моделях эмбеддинг СLS-токена оказывался даже лучше, чем средний (в целом, у него так себе репутация). Поэтому рекомендую не лениться и пробовать оба варианта. Код и результаты моей оценки можно взять из этого блокнота, но лучше подождать, пока я ещё причешу этот бенчмарк.

По итогам оценки оказалось, что модель LaBSE очень крутая: она заняла первое место на 6 из 10 задач. Поэтому я решил выложить LaBSE-en-ru, у которой я отрезал эмбеддинги всех 99 языков, кроме русского и английского. Модель похудела с 1.8 до 0.5 гигабайт, и, надеюсь, таким образом стала чуть более удобной для практического применения. Ну а rubert-tiny оказался по качеству в целом близок к моделям от DeepPavlov и Sber, будучи при этом на порядок меньше и быстрее.

Заключение

Я обещал сделать компактную модель для эмбеддингов русских предложений, и я это наконец сделал. Процесс дистилляции, скорее всего, я настроил неоптимально, и его ещё можно сильно улучшать, но уже сейчас маленькая модель на некоторых задачах приближается к уровню своих учителей и даже иногда обходит его. Так что если вам нужен маленький и быстрый BERT для русского языка, то пользуйтесь: https://huggingface.co/cointegrated/rubert-tiny.

Впереди работы много: с одной стороны, хочется обучить маленький BERT решать задачи из RussianSuperGLUE (и не только), с другой затащить в русский язык хорошие небольшие модели для контролируемой генерации текста (я уже начал делать это для T5). Посему лайкайте данный пост, подписывайтесь на мой канал про NLP, подкидывайте в комментариях и в личке интересные задачи, и, если у вас доведутся руки попробовать rubert-tiny, то обязательно оставляйте обратную связь!
Мне и самому интересно, что будет дальше.

Подробнее..

Гугл финанс перестал транслировать данные российских акций что делать?

15.06.2021 06:21:37 | Автор: admin

С 5 июня 2021 года сайт гугла, и самое главное гугл таблицы - перестали отдавать данные с Московской биржи.

При попытке получить котировки с префиксом MCX, например для Сбербанка, формулой из гугл таблиц =GOOGLEFINANCE("MCX:SBER") теперь всегда возвращается результат #N/A.

А при поиске любой российской бумаги на сайте Google находятся все рынки, кроме Московской биржи:

Попытка поиска котировки Sberbank of Russia на сайте https://www.google.com/finance/quote/MCX:SBER Попытка поиска котировки Sberbank of Russia на сайте https://www.google.com/finance/quote/MCX:SBER

В этой статье я рассказываю как получать в гугл таблицу биржевые данные с помощью API Московской биржи.

Копирование формул из таблицы-примера в ваши собственные таблицы

Вы можете свободно копировать формулы из моей таблицы с примером для использования в собственных таблицах, потому что на формулах нет и не может стоять никакой защиты.

Если после копирования из таблицы-примера в вашу таблицу формула не работает, то проверьте региональные настройки вашей таблицы.

Моя таблица с примером получения данных с Московской биржиМоя таблица с примером получения данных с Московской биржи

Я использую регион Соединенные Штаты, а если по умолчанию ваш регион Россия, то формулы корректно НЕ копируются!

Вот подробная инструкция как проверить региональные настройки конкретной таблицы:

  • Откройте файл в Google Таблицах на компьютере.

  • Нажмите Файл затем Настройки таблицы.

  • Выберите нужные варианты в разделах "Региональные настройки".

  • Нажмите Сохранить настройки.

    Как изменить региональные настройки и параметры расчетовКак изменить региональные настройки и параметры расчетов

Если вы не хотите менять регион в своей собственной таблице, то можно просто изменить разделитель в формуле. Для корректной работы надо изменить разделитель с запятой на точку с запятой.

Получение названий акций и облигаций

Гугл таблица с примерами автоматического получения имени для разных классов активовГугл таблица с примерами автоматического получения имени для разных классов активов

Можно получать названия акций и облигаций используя сервера Московской Биржи вместо переставшего работать гугл финанса. Сама формула при этом выглядит следующим образом:

=IMPORTxml(    "https://iss.moex.com/iss/engines/stock/markets/" &      IFS(                 or(            B3 = "TQOB",            B3 = "EQOB",            B3 = "TQOD",            B3 = "TQCB",            B3 = "EQQI",            B3 = "TQIR"        ),        "bonds",                 or(            B3 = "TQTF",            B3 = "TQBR",            B3 = "SNDX",            B3 = "TQIF"        ),        "shares"    )  & "/boards/" & B3 & "/securities.xml?iss.meta=off&iss.only=securities&securities.columns=SECID,SECNAME",      "//row[@SECID='" & A3 & "']/@SECNAME")

Получение цен акций и облигаций

Гугл таблица с примерами автоматического получения цен акций и облигацийГугл таблица с примерами автоматического получения цен акций и облигаций

Можно получать цены акций и облигаций используя сервера Московской Биржи вместо переставшего работать гугл финанса. Сама формула при этом выглядит следующим образом:

=IMPORTxml(    "https://iss.moex.com/iss/engines/stock/markets/" &      IFS(                 or(            B10 = "TQOB",            B10 = "EQOB",            B10 = "TQOD",            B10 = "TQCB",            B10 = "EQQI",            B10 = "TQIR"        ),        "bonds",                 or(            B10 = "TQTF",            B10 = "TQBR",            B10 = "SNDX",            B10 = "TQIF"        ),        "shares"    )  & "/boards/" & B10 & "/securities.xml?iss.meta=off&iss.only=securities&securities.columns=SECID,PREVADMITTEDQUOTE",      "//row[@SECID='" & A10 & "']/@PREVADMITTEDQUOTE")

Получение даты и значения дивиденда для акций

Гугл таблица с примерами автоматического получения дат и значений дивидендов для акций Гугл таблица с примерами автоматического получения дат и значений дивидендов для акций

Можно получать даты и значения дивидендов для акций, используя сервера Московской Биржи. У гугл финанса такого функционала никогда не было для российских акций. Сама формула при этом выглядит следующим образом:

=iferror(     INDEX(         IMPORTxml(            "http://iss.moex.com/iss/securities/" & A22 & "/dividends.xml?iss.meta=off",            "//row[@secid='" & A22 & "']/@value"        )  ,         ROWS(            IMPORTxml(                "http://iss.moex.com/iss/securities/" & A22 & "/dividends.xml?iss.meta=off",                "//row[@secid='" & A22 & "']/@value"            )        )  ,        1    )  ,    "нет")

Получение даты купона и значения для облигаций

Гугл таблица с примерами автоматического получения дат купонов и значений для облигацийГугл таблица с примерами автоматического получения дат купонов и значений для облигаций

Можно получать даты и значения купонов для облигаций, используя сервера Московской Биржи. У гугл финанса такого функционала никогда не было для российских облигаций и вообще облигаций в сервисе нет. Сама формула при этом выглядит следующим образом:

=IMPORTxml(    "https://iss.moex.com/iss/engines/stock/markets/" &      IFS(                 or(            B12 = "TQOB",            B12 = "EQOB",            B12 = "TQOD",            B12 = "TQCB",            B12 = "EQQI",            B12 = "TQIR"        ),        "bonds",                 or(            B12 = "TQTF",            B12 = "TQBR",            B12 = "SNDX",            B12 = "TQIF"        ),        "shares"    )  & "/boards/" & B12 & "/securities.xml?iss.meta=off&iss.only=securities&securities.columns=SECID,NEXTCOUPON,COUPONVALUE",      "//row[@SECID='" & A17 & "']/@COUPONVALUE")

Получение даты оферты

Гугл таблица с примерами автоматического получения дат оферт для облигацийГугл таблица с примерами автоматического получения дат оферт для облигаций

Можно получать оферты облигаций, используя сервера Московской Биржи. У гугл финанса такого функционала никогда не было для российских облигаций и вообще облигаций в сервисе нет. Сама формула при этом выглядит следующим образом:

=IFNA(     IMPORTxml(        "https://iss.moex.com/iss/engines/stock/markets/" &          IFS(                         or(                B27 = "TQOB",                B27 = "EQOB",                B27 = "TQOD",                B27 = "TQCB",                B27 = "EQQI",                B27 = "TQIR"            ),            "bonds",                         or(                B27 = "TQTF",                B27 = "TQBR",                B27 = "SNDX",                B27 = "TQIF"            ),            "shares"        )  & "/boards/" & B27 & "/securities.xml?iss.meta=off&iss.only=securities&securities.columns=SECID,OFFERDATE",          "//row[@SECID='" & A27 & "']/@OFFERDATE"    )  ,    "нет")

Источник данных

Если вы хотите разобраться во всех нюансах работы - откуда берутся данные, то вы, также как и я можете обратиться к официальной документации к информационно-статистическому серверу Московской Биржи (ИСС / ISS). Правда, изучая этот документ, вы можете обнаружить что большая часть интересных функций, приведенная в этой статье, в документе никак не отображена.

Итоги

Король умер, да здравствует король! Считалось, что трон никогда не должен пустовать, поэтому после смерти короля сразу же объявлялся следующий правитель.

Как и в случае c сервисом Google Финансы, который перестал выдавать российские результаты мы видим что можно использовать API Московской биржи, которое предоставляет широкие возможности.

Эти формулы работают только за счет API Московской биржи, с которой я никак не связан. Использую ИСС Мосбиржи только в личных информационных интересах.

Автор: Михаил Шардин,

15 июня 2021 г.

Подробнее..

Storytelling отчет против BI, прагматичный подход

08.05.2021 10:22:37 | Автор: admin

Проблематика


Когда говорят про отчеты к данным (неважно, какая тема) все хотят гибкие дашборды, МНОГО дашбордов, играют конкурсы про BI, выдумывают разные сложные требования и кейсы, отсматривают массу вендоров и решений, разбиваются на непримиримые лагеря и на 100% уверены, что это то, без чего жизнь на работе тяжела, уныла и печальна.


Так ли это? По описанию очень сомнительно (похоже на серебряную пулю), а практика дает подтверждение отнюдь не так.


Является продолжением серии предыдущих публикаций.


Что в реальности


В реальности все оказывается совсем по другому. Сразу исключим из рассмотрения настоящих аналитиков, которым этот инструмент может раскрыться в полной мере (остановимся чуть позже). В подавляющем большинстве случаев не нужны менеджерам эти дашборды с сотней ручек. Им нужны ограниченные срезы для решения повседневных задач. Основным драйвером за BI и дашборды являются страх и лень. Страх остаться с данными один на один и невозможностью ответить на какой-либо вопрос. Страх пойти по пути, который не отлит в корпоративных стандартах как бетонный монолит. Лень задуматься о истинных потребностях обычных пользователей и альтернативных способах решениях.


Откуда такие утверждения? Дело в том, что отчетные квесты больше похожи на сеансы психотерапии. Каждый раз приходится медленно пробираться через наслоение индуцированных утверждений и убеждений, чтобы добраться до истинной потребности и после этого объяснить ее компьютеру. Последний точно не умеет и не понимает общих слов и смутных ощущений. У него только 0 и 1.


Итак, какие есть существенные выгоды у storytelling отчетов, выполненных на базе технологии RMarkdown. Для ознакомления с возможностями RMarkdown можно проглядеть галерею. Далее будем по умолчанию подразумевать HTML форму отчета, как наиболее удобную для динамичной работы.


Выгода 1. Предоставление пользователю информации в готовом к употреблению виде


Storytelling отчет строится под потребности конкретной группы потребителей. Содержание документа, набор визуальных представлений, порядок подачи материала, поясняющий и связующий текст все предназначено для того, чтобы пользователь мог читать отчет как книгу. Материал раскрывается подробно, от общего к частному. Можно быстро перейти к нужной главе. Если структура отчета давно знакома, то можно быстро просматривать ключевые точки в известных местах. При этом подача материала устроена таким образом, что нет нужды задавать фильтры, листать менюшки, загружать настройки (как это делают в дашбордах). Машина уже это выполнила на этапе подготовки. Все собрано сразу в готовом к употреблению виде, достаточно лишь скроллить вверх/вниз и брать информацию в работу.


Выгода 2. Полная отвязка от инфраструктурных ограничений


В большинстве случаев storytelling отчет нужен по закрытому дню и генерировать его надо один раз в сутки. Оптимальный вариант ночная offline генерация всех необходимых отчетов для текущего рабочего дня. Классическое окно для генерации 2:00-7:00. Когда предыдущий день закрыт и переходные процессы закрытия во всех внутренних ИС завершились. Отсюда четыре существенных плюса:


  • offline генерация на серверной стороне позволяет не заморачиваться на скорость исполнения. В случае интерактивного анализа отклик должен измеряться десятками миллисекунд, иначе пользователи начинают жаловаться на торможение системы. Здесь же мы можем считать секунды. Снижение к требованиям по железу колоссальное.
  • можно использовать любой сложности алгоритмы, в т.ч. весь спектр ML инструментов.
  • нет понятие лицензий на доступ, нет непредсказуемой конкурентности. планируете задания последовательно-параллельно, опираясь на доступные вам аппаратные средства.

Выгода 3. Полная отвязка отчета от источников данных


Будучи собранным, storytelling отчет является самодостаточной и законченной сущностью. Это единый html контейнер, который содержит в себе всю необходимую информацию. Отсюда три существенных плюса:


  • готовый отчет можно как рассылать пользователям по почте, так и предоставлять доступ через корпоративные порталы. пользователь сможет работать с ним везде и за пределом корп.сети, и даже в бункере в режиме полного оффлайна.
  • отчет это html файл. тривальными средствами можно обеспечить историческое хранение на любую требуемую глубину со стоимостью, близкой к 0.
  • в исторической части хорошо известная проблема, когда отлаженный неизменный отчет нельзя собрать на исторических данных так, как он собирался бы тогда, когда эта дата была. Связано это с изменчивостью данных, изменчивостью ландшафта, эволюцией ИС и систем, динамичностью справочников. В тривиальном случае оргструктура любой компании динамична и она сегодня не такова, какой была 3 года назад. Обычному отчету никогда не вернуться в прошлое. А storytelling отчет это след папоротника в известняке.

Выгода 4. Динамическая генерация, основанная на предоставленных данных


Классический отчет жестко зафиксированный состав элементов и их форма. В случае со storytelling отчетом можно использовать подходы динамической генерации, когда сама ткань отчета генерируется на основе структуры полученных данных. Тем самым возможна почти любая вариативность, при этом текст отчета остается связным и без пустых мест. Чуть детальнее с кодом можно посмотреть в публикации R Markdown. Как сделать отчет в условиях неопределенности?.


Также можно параметризировать RMarkdown отчет и получать различные представления на различных данных, используя один и тот же код.


Выгода 5. Динамический контент


Представление html позволяет добавить такой объем динамичности, что пользователь может забыть, что это offline. Включение js виджетов позволяет обеспечить динамичные таблицы, графики. Средствами html и js можно располагать части отчета на закладках, обеспечивать кросснавигацию и оглавление, управлять видимостью объектов. Использование механизмов кросскоммуникации js виджетов позволяет делать связанные обновления элементов. Более конкретно:



Выгода 6. Безопасный доступ к данным


Очень часто возникают требования по управлению доступом к данным. Та или иная группа пользователей должна иметь возможность строить отчет только по своему подмножеству. Но весь парадок в том, что все равно остается ряд показателей, которые считаются по всей компании. И если это делать через self-service bi, то потребуется продумывать как это предоставить. Витрины, кубы, выгрузки или еще что-либо.


В случае с Rmarkdown отчетами пользователи вообще не имеют доступа к данным. Все собирается на сервере с контролируемым полным доступом. А в html фиксируются посчитанные агрегаты в соответствии с ролевой моделью.


Выгода 7. Один источник масса представлений


RMarkdown позволяет из единого источника делать совершенно различные финальные представления. html, pdf, doc, pptx.


Также, технология RMarkdown позволяет делать статические сайты (blogdown) и книги (bookdown).


Выгода 8. Вся мощь devops для гарантий корректности


Поскольку Rmarkdown является набором R инструкций, то управление жизненным циклом отчетов получается идентичным управлениею жизненным циклом ПО. Репозиторий, пакетирование, документирование, автотесты, continious integration, code coverage, профилировка узких мест.


Также, RMarkdown отчеты могут самостоятельно на этапе сборки подключаться к любым источникам данных (не обязательно выверенным SQL таблицам) и строить отчет на актуальных в моменте данных.


Выгода 9, 10, X. ......


Выберите из списка...


Заключение


Еще один важный комментарий. Для того, чтобы корректно использовать дашборды или self-analytics bi нужно очень-очень хорошо знать структуру данных, которые используются в процессе рассчета и визуализации и иметь к ним полный доступ. Данные должны быть чистые, консистентые и рафинированные. Аналитик должен обладать навыками проведения расчетов и проверки результатов. Только так можно получить показатели и картинки, которым как-то можно доверять. В противном случае имеем сплошные жареные факты и желтую прессу. Затраты на такого аналитика никак не середнячковые.


P.S.


По сути, storytelling отчеты являются реализацией давным-давно предложенной Дональдом Кнутом концепции литературного программирования. И вся вычислительная мощь только через 30 лет смогла доехать до практической и удобной реализации.


Алармисты уже не дремлют. Gartner вовсю начал предрекать закат BI в том виде как он есть сейчас. Нет поводов не задуматься:



Предыдущая публикация R в руках маркетолога. Делаем когортный анализ своими руками.

Подробнее..

Оценка кредитного портфеля на R

19.05.2021 18:19:31 | Автор: admin

В ходе обсуждений возникла маленькая задачка построить динамику структуры кредитного портфеля (динамика кредитной карты, например). В качестве важной специфики необходимо применять метод FIFO для погашения займов. Т.е. при погашении первыми должны гаситься самые ранние займы. Это накладывает определенные требования на расчет статуса каждого отдельного займа и определения его даты погашения.


Ниже приведен код на R с прототипом подхода. Не более одного экрана кода на прототип и никаких циклов (закладные для производительности и читаемости).


Декомпозиция


Поскольку мы делаем все с чистого листа, то задачу разбиваем на три шага:


  1. Формирование тестовых данных.
  2. Расчет даты погашения каждого займа.
  3. Расчет и визуализация динамики для заданного временнОго окна.

Допущения и положения для прототипа:


  1. Гранулярность до даты. В одну дату только одна транзакция. Если в один день будет несколько транзакций, то надо будет их порядок устанавливать (для соблюдения принципа FIFO). Можно использовать доп. индексы, можно использовать unixtimestamp, можно еще что-либо придумывать. Для прототипа это несущественно.
  2. Явных циклов for быть не должно. Лишних копирований быть не должно. Фокус на минимальное потребление памяти и максимальную производительность.
  3. Будем рассматривать следующие группы задержек: "< 0", "0-30", "31-60", "61-90", "90+".

Шаг 1.


Генерируем датасет. Просто тестовый датасет. Для каждого пользователя сформируем ~ по 10 записей. Для расчетов полагаем, что займ положительное значение, погашение отрицательное. И весь жизненный цикл для каждого пользователя должен начинаться с займа.


Генерация датасета
library(tidyverse)library(lubridate)library(magrittr)library(tictoc)library(data.table)total_users <- 100events_dt <- tibble(  date = sample(    seq.Date(as.Date("2021-01-01"), as.Date("2021-04-30"), by = "1 day"),    total_users * 10,    replace = TRUE)  ) %>%  # сделаем суммы кратными 50 р.  mutate(amount = (runif(n(), -2000, 1000)) %/% 50 * 50) %>%  # нашпигуем идентификаторами пользователей  mutate(user_id = sample(!!total_users, n(), replace = TRUE)) %>%  setDT(key = "date") %>%  # первая запись должна быть займом  .[.[, .I[1L], by = user_id]$V1, amount := abs(amount)] %>%  # для простоты оставим только одну операцию в день,   # иначе нельзя порядок определить и гранулярность до секунд надо спускать  # либо вводить порядковый номер займа и погашения  unique(by = c("user_id", "date"))

Шаг 2. Расчитываем даты погашения каждого займа


data.table позволяет изменять объекты по ссылке даже внутри функций, будем этим активно пользоваться.


Расчет даты погашения
# инициализируем аккумуляторaccu_dt <- events_dt[amount < 0, .(accu = cumsum(amount), date), by = user_id]ff <- function(dt){  # на вход получаем матрицу пользователей и их платежей на заданную дату  # затягиваем суммы займов  accu_dt[dt, amount := i.amount, on = "user_id"]  accu_dt[is.na(amount) == FALSE, accu := accu + amount][accu > 0, accu := NA, by = user_id]  calc_dt <- accu_dt[!is.na(accu), head(date, 1), by = user_id]  # нанизываем обратно на входной data.frame, сохраняя порядок следования  calc_dt[dt, on = "user_id"]$V1}repay_dt <- events_dt[amount > 0] %>%  .[, repayment_date := ff(.SD), by = date] %>%  .[order(user_id, date)]

Шаг 3. Считаем динамику задолженности за период


Расчет динамики
calcDebt <- function(report_date){  as_tibble(repay_dt) %>%    # выкидываем все, что уже погашено на дату отчета    filter(is.na(repayment_date) | repayment_date > !! report_date) %>%    mutate(delay = as.numeric(!!report_date - date)) %>%    # размечаем просрочки    mutate(tag = santoku::chop(delay, breaks = c(0, 31, 61, 90),                               labels = c("< 0", "0-30", "31-60", "61-90", "90+"),                               extend = TRUE, drop = FALSE)) %>%    # делаем сводку    group_by(tag) %>%    summarise(amount = sum(amount)) %>%    mutate_at("tag", as.character)}# Устанавливаем окно наблюденияdf <- seq.Date(as.Date("2021-04-01"), as.Date("2021-04-30"), by = "1 day") %>%  tibble(date = ., tbl = purrr::map(., calcDebt)) %>%  unnest(tbl)# строим графикggplot(df, aes(date, amount, colour = tag)) +  geom_point(alpha = 0.5, size = 3) +  geom_line() +  ggthemes::scale_colour_tableau("Tableau 10") +  theme_minimal()

Можем получить примерно такую картинку.


Один экран кода, как и требовалось.


Предыдущая публикация Storytelling R отчет против BI, прагматичный подход.

Подробнее..

Проблемы мониторинга дата-пайплайнов и как я их решал

16.06.2021 00:20:01 | Автор: admin

Последние несколько лет я занимаюсь дата-инженерингом: строю пайплайны разного уровня сложности, добываю данные, нужные бизнесу, преобразую их и сохраняю, в общем, строю классические ETL.

В этом деле проблем можно ждать откуда угодно и на каждом шаге: источник данных прилег, данные пришли битые, источник без объявления войны поменял формат данных или доступ к ним, хранилище тупит, данных внезапно стало меньше или больше и много других приколюх.

Чтобы понять, как все это мониторить, давайте разберемся кто вообще работает с данными, которые мы с таким трудом намайнили:

Бизнес - это все те люди, которые не особо разбираются в инженерии, но принимают важные решения, основываясь на добытых нами данных: увеличить ли расходы на рекламу, как быстро увеличивать размер аудитории для раскатывания апдейта, как работают различные партрнеры и т.п.

Технари - это мы, инженеры, можем запустить руки в логи, доработать логику, следим, чтобы ничего не падало без острой необходимости.

Код - запускается следующий в цепочке пайплайн, происходят преобразования, рисуются графики и т.п.

Дата-пайплайны могут слегка отличаться от классического бэкенда - если упал какой-то из пайплайнов, это не значит, что все плохо и нужно бежать немедленно чинить, данные могут определенное время продолжать оставаться актуальными. Вы тут скажете, что бэкенд тоже может успешно падать частями, так-то оно так, но я в данном контексте рассматриваю бэкенд и дата-пайплайн как одну логическую сущность, а не набор дублирующих микросервисов и баз с репликациями.

Вот примеры:

  • Пайплайн отрабатывает каждые 10 минут, а люди смотрят в отчеты, в среднем, раз в час. Падение одной из джоб не критично, да и если источник данных прилег, тут все равно ничего не поделаешь.

  • Пайплайн стучится в источник с надеждой получить свежие данные, которые появляются в API источника с неопределенной задержкой (привет Apple), пайплан может успешно завершиться как скачав отчет, так и получив сообщение о том, что отчета еще нет. Тут, конечно, можно делать бесконечные ретраи внутри, либо просто ронять пайплайн, но тогда не особо очевидно будет, по какой причине он упал - что-то пошло не так, или данные в источнике еще не подтянулись.

  • Пайплайн работает слишком долго - это может быть как разовой проблемой из-за тормозов в сети или в источнике, так и постоянной проблемой: что-то сломалось в трансформации, или ретраев стало больше чем нужно, в любом случае требуется вмешательство человека.

ETL как он естьETL как он есть

И тут вы скажете, но в том же Airflow есть замечательная система мониторинга, которая показывает и время работы джобы, и количество упавших или требующих перезапуска задач, и т.п. И я соглашусь, отчасти...

Чего не хватает во встроенных мониторингах систем работы с данными:

  • Бизнес не может просто посмотреть в модный мониторинг типа того же Airflow или ELK и понять, можно или нельзя доверять данным, актуальность состояния данных непрозрачна.

  • Единичные сбои, которые самоустраняются при каждом запуске, отвлекают инженеров, потому что следующий успешный запуск просто восстановит пробелы в данных, если пайплайн имеет правильную архитектуру. Нужно избегать фальшивых алертов, кстати, частично это можно сделать той же графаной.

  • Нет возможности собрать реальную статистику и проанализировать, где самое слабое место. Просто считать инциденты не вариант, т.к. важнее не количество инцидентов, а время, в течение которого состояние данных было неактуальным. Знаю, что такие проблемы некоторые компании решают самописными парсерами логов или подсчетом пиков-просадок на графиках (напишите, как это реализовано у вас).

Все это превращается в такие вот проблемы:

  1. Обычно мониторинг заточен на отслеживаение проблем в технической части, в то время как куда важнее обнаруживать проблемы в бизнес части.

  2. Люди из бизнеса хотят более наглядного отображения состояния данных и системы, чем оно представлено в технических мониторингах.

  3. Статистика, если и собирается, то собирается по техническим проблемам и нельзя понять, насколько эти технические проблемы повлияли на бизнес.

Концепция

Чтобы решить некоторые из проблем, я написал простую систему мониторинга, прикинув, что идеально было бы получать от процессов события, а затем анализировать их количество, отправленное значение, продолжительность и разные временные интервалы между началом и концом событий. Этот мониторинг, после некоторого тестирования локально, я решил выкатить в паблик, назвав его Sensorpad.

Смотрите, сколько всего можно мерять, получая простые запросы в вебхук.Смотрите, сколько всего можно мерять, получая простые запросы в вебхук.

Почему вообще вебхуки?

Ну тут все просто: никто не любит ставить на сервер дополнительный софт, никто не хочет выставлять метрики наружу и настраивать фаерволы, а слать http-запросы могут все. Секьюрити и приватность данных важны всем, а с вебхуками можно скрыть или анонимизировать за кодовым именем даже суть того, как называется ваш процессинг.

После того, как мы начали отсылать из процессов события, нужно их проанализировать и получить ответы на всякие важные вопросы, например (все числа абстрактны):

  • запустилась ли наша задача 10 раз за последний день?

  • не превышает ли количество падений (определяем падение, если полученное значение > 0, например) 15% от всех запусков за сегодня?

  • нет ли процессов, которые длятся больше 20 минут?

  • не прошло ли больше часа с момента последнего успешного завершения?

  • стартовало ли событие по планировщику в нужное время?

    тут у каждого свои параметры стабильности и после того, как мы проверим их все, останется только визуализировать их и отображать на дашборде, обновляя в режиме реального времени.

Реализация

Я начал с дашборда, дизайн - не моя профессия, так что просто взял за основу дашборд, показывающий состояние крон-джобов на сайте Nomadlist, у меня получилось как-то так:

Дашборд состояния серверов Sensorpad средствами SensorpadДашборд состояния серверов Sensorpad средствами Sensorpad

Каждый монитор на этом дашборде - это комбинация метрик, которые должны быть понятны и бизнесу, и инженерам.

Для примера попробуем настроить мониторинг для чего-то, что понятно даже тем, кто не знаком с дата-инженерингом, например, мониторингом свободного места на диске. Допустим, нам надо показывать дашборд сразу и отделу поддержки, чтобы они знали, когда чистить старые логи, и отделу закупок, чтобы не надеялись на поддержку и сразу заказывали новые диски.


Для инженера тут все понятно:

  • скрипт отрабатывает быстро (еще бы, простая крон-джоба);

  • монитор вполне живой, 25 минут назад обновился;

  • места еще с запасом (цифра 53 в левом нижнем углу - это последнее принятое значение);

Для людей из бизнеса тут тоже все просто:

  • монитор зеленый;

  • статус прописан в первой же строчке;

  • никакой лишней информации;

Вместо размера диска, разумеется, может быть что угодно, начиная от импорта данных из гугл-аналитики, заканчивая мониторингом ревенью компании - суть не меняется.

И насколько просто такое настроить?

  1. Создаем вебхук в самом сервисе, они там называются сенсорами, по аналогии со всякими штуками из физического мира.

  2. Настраиваем крон-джобу на сервере, которая будет отсылать события со значением свободного места в процентах:

    df -h |grep vda1 | awk  '{ print $5 }'| sed 's/.$//' | xargs -I '{}' curl -G "https://sensorpad.link/<уникальный ID>?value={}" > /dev/null 2>&1
    
  3. Присоединяем к этому вебхуку монитор, называем его: количество свободного места (но можно еще и другие, например, то, что события уходят по графику означает, что сервер не упал)

  4. Настраиваем правила, по которым монитор меняет свой статус.

  5. Присоединяем каналы для отправки уведомлений.

  6. Добавляем монитор на один или несколько дашбордов.

А можно поподробнее?

Для вебхуков я пока что сделал саму простую имплементацию:

  • базовый вебхук, который будет нужен для 80% проектов;

  • cron-вебхук, который ожидает события в заданное через cron-синтаксис время;

  • chain-вебхук, который умеет отслеживать события от процессов, соединенных в цепочки;

главное в нашем деле - не усложнять интерфейсыглавное в нашем деле - не усложнять интерфейсы

После создания попадаем на страницу сенсора, тут автоматически появляются полученные события (повозился в js) и кнопочкой можно отсылать тестовые события, для тех, у кого не установлен Curl или кому лень делать это из браузера:

Догфудинг в действииДогфудинг в действии

Дальше создаем тот самый монитор - квадратик, меняющий статус и цвет.

Можно даже иконку выбратьМожно даже иконку выбрать

Есть две стратегии для статуса по умолчанию: сказать, что монитор должен быть в статусе "все хорошо", а потом правилами менять его на "что-то пошло не так", или можно сразу сделать ему статус "все плохо", а правилами проверять, что все действительно хорошо - обе стратегии имеют право на жизнь.

Теперь, собственно то, из-за чего я и написал эту балалайку: правила и гибкая логика.

К каждому монитору можно прицепить определенное количество правил, дальше движок мониторинга проверяет все правила раз в пару секунд. Правила проверяются поочередно, как только одно из правил вернет значение True, статус монитора изменится и остальные правила проверяться не будут. Если никакое из правил не сработало, статус монитора примет значение по умолчанию - то самое, которое задавалось при создании монитора.

Правила для мониторинга места на дискеПравила для мониторинга места на диске

На скриншоте выше видно уже созданные правила, но я покажу как они создаются.

Например правило, которое можно сформулировать так: "установи статус Warning, если за последний день было больше 5 джоб, которые работали дольше 10 секунд".

А вот какие вообще можно выбирать проверки в каждом из пунктов:

И какие реальные кейсы можно покрыть этими правилами?

У каждого свои кейсы. Дата-инженерия вообще весьма специфичное для каждой компании направление. Если у вас есть дата-пайплайны или cron jobs, сервис оповестит вас, если (все цифры, разумеется, конфигурируемы):

  • Cron job, Airflow DAG или любой другой процесс не запустился по расписанию;

  • 20% задач одного и того же пайплайна за день не отработали как надо;

  • связанная задача в пайплайне не запустилась через 2 минуты после окончания родительской задачи;

  • интервал между запусками двух задач меньше 1 минуты (похоже, у нас две конкурентные джобы);

  • с момента последнего успешного завершения пайплайна прошло 2 часа (а данные должны считаться каждый час);

  • время работы пайплайна уже целых 20 минут (а должен был отработать за 5, что-то подвисло).

Это те идеи, которые могли бы использоваться в практически любой, полагаю, компании.

А теперь - статистика!

Красивые графики любят все. Крутость sensorpad в том, что он показывает в статистике реальные состояния систем, а не просто количество уведомлений (их, кстати, тоже показывает). А уже с этими данными можно разгуляться: и самому поковырять, чтобы понять во что вложить силы и что нужно оптимизировать, да и стейкхолдерам не грех показать идеально зеленый график.

Немного полезных и не очень графиковНемного полезных и не очень графиков

Я подумываю допилить возможность шарить эти странички по секретной ссылке за пределы аккаунта, тогда такую страницу можно будет использовать не хуже любой status page.

Вот такой концепт. Чего не хватает?


Sensorpad - проект выходного дня, который делался в свободное от основной работы время, без тонны инвестиций да еще и в одно лицо, именно поэтому мне так важно мнение комьюнити: как вам? Что добавить, что улучшить, продолжать ли развивать?

Потыкайте его вживую, заодно зацените, какой я у мамы дизайнер лендингов: https://sensorpad.io

Подробнее..

Простыми словами о простых линейных функциях

06.06.2021 14:21:06 | Автор: admin
Случайный лес (в буквальном смысле, сфотографировал с телефона)

Эта маленькая заметка предназначена для начинающих разработчиков, которые хотят понять как линейные функции устроены под капотом. Для опытных специалистов в моей заметки нет ничего нового. И так. Линейные функции применяются очень часто в самых разнообразных проектах, например, в сложных компонентах для создания отчётов или в задачах ранжирования результатов поиска. Предлагаю вместе посмотреть на то, как это работает.


Пусть существует некая функция, которая используется на портале для ранжирования популярных заметок. На вход она принимает количество просмотров документа, а возвращает некий score, по которому и происходит сортировка. При этом, у вас нет доступа к исходному коду всё работает по принципу чёрного ящика. Необходимо вычислить формулу ранжирования. Проведём эксперимент: многократно передадим данные и запишем ответ.


Для начала есть смысл взглянуть на сами данные. Если вы отобразите массив данных с несколькими миллионами объектов, то человек не сможет всё это осмысленно прочитать. Придётся придумывать способ понятно описать его. Частая ошибка не изучив набор данных использовать среднее значение или медиану. Подобный подход чреват серьёзным искажением реальной картины.


Попробуем преобразовать большой массив в специальную структуру данных, которая содержит описательную статистику (среднеквадратическое отклонение, максимум, минимум, среднее, медиана, нижний и верхний квартиль). Но не будем торопиться с таким решением. Есть даже известный набор данных (квартет Энскомба), который хорошо иллюстрирует эту проблему. Очень советую про него прочитать.


В такой ситуации на помощь разработчику приходит гистограмма распределения. Она является удобным способом наглядно отобразить массив из гигантского количества измерений случайной величины. Суть очень простая: разбиваем массив на интервалы и отображаем количество элементов в каждом интервале. Визуально это выглядит следующим образом:


Это гистограмма распределения

В итоге мы видим распределение случайной величины. Регулируя число интервалов можно добиться адекватного представления. Теперь нам нужен способ увидеть взаимосвязь этой случайной величины с другой. Отобразим наблюдения в виде точек на плоскости, где по оси X будет score, а по Y наш единственный предиктор (признак). И вот перед вами появляется график:


Взаимосвязь

На нём показано, что обе переменных сильно коррелируют между собой, следовательно, делаем предположение о линейной зависимости (score=a+bx). А теперь самая суть, но очень кратко: простая линейная регрессия это произведение коэффициента и признака, к которым добавляется смещение. Вот и всё. Степень линейной взаимосвязи вычисляют следующим образом:


Взаимосвязь

Не все точки выстроились на одну линию, что говорит нам о небольшом влиянии неизвестного фактора. Это я специально сделал, для красоты. Подобрать коэффициенты можно с помощью готовых решений, допустим, scikit-learn. Так, например, класс LinearRegression после обучения (fit) позволяет получить смещение (intercept) и массив коэффициентов (coef). Подставляем значения в формулу и проверяем результат с помощью метрик mean_absolute_error и median_absolute_error. Собственно, это и есть решение нашей задачи. В случае множества признаков суть не меняется: под капотом всё устроено аналогичным образом (смещение + скалярное произведение вектора признаков и соответствующих коэффициентов).


А теперь превратим эту функцию в классификатор, который называется логистическая регрессия. Под капотом это обычная линейная регрессия, только результат её работы передают в сигмоиду. Далее выполняется проверка если результат больше 0.5, то класс 1, иначе класс 0. По сути, это формула гиперплоскости, разделяющей классы.


Обе упомянутые линейные модели это очень быстрые и простые алгоритмы, которые достаточно легко внедрить в любой проект. Надеюсь, теперь стало чуть проще понять, как это устроено под капотом. Сталкиваясь с этими алгоритмами, начинающему разработчику будет проще понять их принципы работы.

Подробнее..

Несколько мыслей про ранжирование

06.06.2021 08:10:54 | Автор: admin
Случайный лес

1. Вступление


В самых различных проектах разработчику приходится сталкиваться с задачами ранжирования. Список подобных задач весьма обширен: от банальной сортировки новостей по дате публикации до сложных рейтингов на основе поведенческих факторов.


Эта заметка представляет собой личное мнение автора по поводу применения некоторых универсальных алгоритмов в задачах ранжирования. Цель подсказать разработчику возможные пути решения соответствующих задач и уберечь от наиболее типичных ошибок.


Пара слов про управление ожиданиями. Это не учебный курс и не инструкция, а рассказ о возможных подходах. Предпочтение отдавалось наиболее универсальным из них. Обращаю ваше внимание, что всё изложенное далее публикуется на правах личного мнения.


2. Первый взгляд


Скажите, пожалуйста, а сколько всего может быть способов перестановки результатов ранжирования? Для массивов из двух элементов интуиция легко поможет найти правильный ответ: [Первый, Второй] и [Второй, Первый]. А для массива из 30 элементов? Применим формулу комбинаторики: n! (факториал мощности множества). Для множества из 30 элементов количество возможных перестановок будет равно 265252859812191058636308480000000. Даже для рейтинга ТОП-10 есть 3628800 вариантов перестановки.


Представим, что порядок элементов задаётся случайно. Какая вероятность появления нужного элемента на первом месте? Пусть всего 10 элементов. Нас интересует один конкретный элемент. Порядок всех остальных уже не имеет значения. По сути, это аналогично случайному выбору из мешка синего шарика, где 9 красных и 1 синий шарик. Тогда вероятность составляет 1/10. Другими словами, при многократном повторении эксперимента примерно в 10% случаев мы увидим этот элемент на первом месте.


Кстати, а с какой вероятностью один и тот же элемент окажется на первом месте два раза подряд? Запуски алгоритмов мы считаем независимыми событиями, так как один запуск никак не влияет на другой. Следовательно, это произведение вероятностей независимых событий: 1/10 * 1/10. Это касается и всех последующих попыток, но тут красивее возводить вероятность в соответствующую степень.


Я дважды упоминал про случайную сортировку. А вдруг там другой алгоритм ранжирования? Можно ли вычислить формулу ранжирования, если код закрыт? Оказывается, что при определённых условиях это возможно. Нам потребуется многократно проводить эксперимент: передавать аргументы функции (факторы ранжирования) и фиксировать её ответ. Предлагаю посмотреть на пример с линейной регрессией.


3. Открываем чёрный ящик


Пусть существует неизвестный алгоритм ранжирования. На вход он принимает количество просмотров документа, а возвращает некий score, по которому и происходит сортировка. При этом, у вас нет доступа к исходному коду всё работает по принципу чёрного ящика. Необходимо вычислить формулу ранжирования. Проведём эксперимент: многократно передадим данные и запишем ответ.


Для начала есть смысл взглянуть на сами данные. Если вы отобразите массив данных с несколькими миллионами объектов, то человек не сможет всё это осмысленно прочитать. Придётся придумывать способ понятно описать его. Частая ошибка не изучив набор данных использовать среднее значение или медиану. Подобный подход чреват серьёзным искажением реальной картины.


Попробуем преобразовать большой массив в специальную структуру данных, которая содержит описательную статистику (среднеквадратическое отклонение, максимум, минимум, среднее, медиана, нижний и верхний квартиль). Но не будем торопиться с таким решением. Есть даже известный набор данных (квартет Энскомба), который хорошо иллюстрирует эту проблему. Очень советую про него прочитать.


В такой ситуации на помощь разработчику приходит гистограмма распределения. Она является удобным способом наглядно отобразить массив из гигантского количества измерений случайной величины. Суть очень простая: разбиваем массив на интервалы и отображаем количество элементов в каждом интервале. Визуально это выглядит следующим образом:


Это гистограмма распределения

В итоге мы видим распределение случайной величины. Регулируя число интервалов можно добиться адекватного представления. Теперь нам нужен способ увидеть взаимосвязь этой случайной величины с другой. Отобразим наблюдения в виде точек на плоскости, где по оси X будет score, а по Y наш единственный предиктор (признак). И вот перед вами появляется график:


Взаимосвязь

На нём показано, что обе переменных сильно коррелируют между собой, следовательно, делаем предположение о линейной зависимости (score=a+bx). Не все точки выстроились на одну линию, что говорит нам о небольшом влиянии неизвестного фактора. Это я специально сделал, для красоты.


Подобрать коэффициенты в нашем случае труда не составит. Так, например, класс LinearRegression (из scikit-learn) после обучения (fit) позволяет получить смещение (intercept) и массив коэффициентов (coef). Подставляем значения в формулу и проверяем результат с помощью метрик mean_absolute_error и median_absolute_error. Собственно, это и есть решение нашей задачи. В случае множества признаков суть не меняется: под капотом всё устроено аналогичным образом (смещение + скалярное произведение вектора признаков и соответствующих коэффициентов).


Предупреждение о подводных камнях: обязательно внимательно изучайте данные, а при необходимости выполняйте подготовку, включая масштабирование и преобразование категориальных признаков (one-hot). Кроме этого, увеличение количества признаков сильно затрудняет визуальный анализ даже специальные алгоритмы уменьшения размерности (PCA, Isomap, TSNE) не всегда помогут отобразить информацию в удобном для человека виде.


4. Простая формула ранжирования


А что мы будем ранжировать? Список задач весьма обширный: рекламные блоки, рейтинги товаров, результаты анализа общественного мнения, интересные новости, анкеты на сайтах, группы из социальных сетей, баннеры, рекомендации, результаты парсинга сайтов, блоки персонализации. И это далеко не полный список. Получается, что под каждый частный случай нужно придумывать свою формулу ранжирования?


Или придумать формулу только для определённого типа задач? Уверен, что вы сталкивались с полнотекстовом поиском в реляционных базах данных (PostgreSQL, MySQL, SQLite) или в специальных системах (Elasticsearch, Sphinx, Solr). Например, в Elasticsearch формула ранжирования видна в режиме explain, а явно задать её в запросе можно через script_score. Не исключено, что вы сами пробовали написать BM25 (TF-IDF-подобный алгоритм).


Даже когда вы сортируете новости по дате публикации это тоже можно назвать экспертной формулой ранжирования. Но сейчас интереснее попробовать универсальный алгоритм, который может быть применён в большом количестве разнообразных задач.


Мы уже говорили про линейную регрессию. Действительно, это вполне универсальный алгоритм. Несмотря на всю простоту его применяют в серьёзных проектах. Благодаря своей скорости работы это замечательное решение в тех задачах, где критически важна производительность. Линейные алгоритмы могут быть частью иерархических формул ранжирования ненужные объекты отбрасываются простыми моделями, чтобы не расходовать драгоценные ресурсы действительно сложных алгоритмов.


Ярким примером фильтра выступает логистическая регрессия. Это быстрая и лёгкая в реализации модель классификации. Под капотом это обычная линейная регрессия, только результат её работы передают в сигмоиду. Далее выполняется проверка если результат больше 0.5, то класс 1, иначе класс 0. По сути, это формула гиперплоскости, разделяющей классы. На вид это простые алгоритмы, но их применяют даже в сложных задачах классификации текстов. Разумеется, тексты предварительно подготавливаются и преобразуются в вектор признаков (примеры алгоритмов: CountVectorizer, TfidfVectorizer, Word2Vec).


Замечание: в коммерческих проектах важно учитывать соотношение затраченных ресурсов и полученной выгоды. Самые минимальные увеличения могут затребовать такие ресурсы, что они просто не окупятся. Либо проблема может заключаться в сроках: нужно сегодня к вечеру получить хотя бы слабую модель, а завтра уже нет смысла даже в самой идеальной.


5. Более сложное ранжирование


Сложными задачами ранжирования будем называть такие, которые нельзя решить написанием хорошего запроса на SQL или доработкой компонента для подсчёта рейтинга. Другими словами, нет конкретной формулы или алгоритма. Самый банальный пример: как бы разработчик не старался, но простая функция не сможет классифицировать фотографии наравне с ResNet50 из Keras. Даже для создания хорошего алгоритма учёта поведенческих факторов может потребоваться использовать K-means для кластеризации контента и метрик поведения пользователей.


По этой причине разработчику пригодятся знания алгоритмов машинного обучения. В современных проектах это нужно всё чаще. Обратите внимание, что мы сейчас не говорим про тонкости настройки или проверки алгоритмов машинного обучения. Это явно выходит за пределы моей заметки. Речь о том, что алгоритмы машинного обучения сами могут создать формулу ранжирования, при условии, что вы располагаете подходящим размеченном набором данных. Теперь задумаемся на тем, как задействовать сильную модель в своём проекте.


Идея состоит в том, чтобы получить возможность быстро добавлять готовые модели в любой проект. Без подключения дополнительных зависимостей. И проекты могут быть на разных языках программирования. Или это вообще специальные устройства. Когда мы говорили про линейные модели, то вы могли заметить, что такие алгоритмы очень легко добавить в любой проект. Достаточно просто скопировать массив с коэффициентами, который будет использован в соответствующем методе. А вот с комитетами решающих деревьев не всё так просто. В качестве конкретных реализаций будут выступать Catboost и Random forest.


Хочу вам предложить одно из решений, которое мне давилось делать для переноса модели на специальные устройства. Идея следующая: получим из обученной и проверенной модели её деревья, а в циклах (по шаблонам) сформируем исходный код. Собственно, код генерируется по шаблонам (получается простыня условий) и добавляется в проект в виде отдельного файла на нужном языке программирования.


Так, например, в случае Random forest мы получим много относительно глубоких деревьев. Каждое дерево будет получать на вход набор данных (отдельные деревья могут работать только со своими признаками) и возвращать тот результат, который предскажет его лист (терминальный узел). В итоге будет массив предсказаний, где каждый элемент является ответом дерева. В задачах регрессии следует вычислить среднее значение, а для классификации тот класс, за который проголосовало большинство деревьев (либо распределение голосов, которое можно рассматривать как вероятность принадлежности к классу).


Повторюсь, этот подход использовался для специальных устройств. Однако, в ряде случаев так можно делать для любых проектов не будет лишних зависимостей и производительность может быть выше, а так же есть возможность самому более тонко настраивать работу деревьев. Если с Random forest такое упражнение доводилось делать для реальных проектов, то с Catboost я просто побаловался в субботу на своём домашнем компьютере.


У ранее упомянутого Catboost есть возможность сохранения модели в формате JSON. В массиве oblivious_trees будут решающие деревья (обычно пни зависит от гиперпараметров), которые содержат индексы признаков (split_index) и границу разделения (border). Схема экспорта модели аналогичная, только специфика работы деревьев отличается, а именно, они друг за другом последовательно снижают энтропию.


6. Выводы


Лучше использовать принцип KISS, однако, если задача требует сложного решения, то можно вспомнить про предложенные подходы. При этом, всегда следует осознавать смысл (финальную цель) и учитывать соотношение затрат, рисков и ожидаемой пользы.


7. Послесловие


Очень надеюсь, что некоторые изложенные мной мысли подскажут вам способы решения задач в ваших проектах. Повторюсь, что заметка отражает моё личное мнение и может содержать неточности банальный человеческий фактор, особенно при написании текстов в выходные дни.

Подробнее..

Как победить букмекеров с помощью ИИ опыт студентов магистратуры Наука о данных

06.05.2021 14:05:14 | Автор: admin

Привет, Хабр! Сегодня хотим представить вам проект студентов магистратуры Наука о данных НИТУ МИСиС и Zavtra.Online (подразделении SkillFactory по работе с университетами) созданный на внутреннем хакатоне, который прошел в марте. Команда поделится решением выбранной задачи предсказание победителя-бойца турнира UFC. Задача отличалась от прочих тем, что после написания модели из неё можно сделать целый продукт, оформив модель в приложение, готовое к использованию конечными пользователями, например теми, кто захочет обыграть букмекеров.


Гипотеза и её проверка

Для нашего проекта мы сформулировали гипотезу, что на победу бойца влияют:

  • физические параметры бойца (его рост, вес, размах рук и ног);

  • возраст бойца (всё-таки со временем физически мы слабеем);

  • разница в опыте соперников (вряд ли какой-то новичок без опыта одолеет Хабиба);

  • характер поведения бойца на ринге (чаще обороняется или чаще нападает);

  • географические параметры (вдруг наш боец не успел адаптироваться к другому часовому поясу).

Основа всех методов проверки модели машинного обучения разделение данных при обучении модели на тренировочную и валидационную выборки. Валидационная выборка потребуется, чтобы понять, что произойдёт, если наша модель столкнётся с данными, которых она раньше не видела.

Принцип разделения прост: мы случайным образом разбиваем свои данные примерно на 70 %, используемых для обучения модели, и 30 % для тестирования модели. Однако есть нюанс... Для предсказания результата боя UFC важно, чтобы тренировочная выборка была раньше тестовой во временном периоде.

Иначе модель будет смотреть в будущее, и на реальных данных модель будет работать некорректно. Этот нюанс мы учли и разбили выборку не случайным образом, а в зависимости от временных периодов.

Разработка проекта

В ходе работы над проектом мы просидели в зуме, а затем в дискорде более 10 часов. Углубились во все тонкости предобработки данных, сделали немного занимательной визуализации и обучили нашу модель.

Примерно 90 % времени мы потратили на обработку данных и остальные 10 % на обучение модели. Пайплайн подготовки данных верхнеуровнево выглядел следующим образом: мы очистили датасет от пропусков и выбросов и обогатили его новыми признаками.

Например, на основе физических характеристик рассчитали другие, а также посчитали кумулятивную статистику для каждого боя и каждого бойца. А ещё для увеличения качества модели вычислили разницу между физическими характеристиками бойцов.

Сбор данных и статистика

Ментор предоставил нам спарсенные данные по истории Боёв UFC и статистику по бойцам. Данные и Jupyter Notebook с бейзлайном модели можно найти по этой ссылке на Kaggle. В связи с тем что сроки у нас были ограничены, мы решили не заниматься дополнительным парсингом фичей, а уделить большое внимание обработке имеющихся данных и генерации новых признаков.

Также в процессе обработки мы собрали немного занимательной статистики по боям: например, на этом бублике можно увидеть распределение боев по категориям, из которой можно увидеть, что мужских боёв гораздо больше, чем женских (увы).

А на этом бублике видим, что бои заканчиваются с равной вероятностью либо нокаутом, либо признанием своего поражения, либо решением судьи.

Предобработка данных

Основная идея обработки данных заключалась в создании единого датасета, содержащего накопительную статистику по каждому из бойцов за все его предыдущие бои и его физические характеристики на момент начала боя. Необходимо было избежать лика в данных, при котором модель могла переобучиться на данных из ещё не состоявшегося боя.

После очистки датасета от пропусков и выбросов в первую очередь мы сгенерировали фичи по каждому из бойцов, связанные с его физическими характеристиками. Поскольку мы имеем информацию по физическим характеристикам бойца на настоящий момент, для корректного обучения модели на данных по боям из прошлого мы использовали фичи, которые не заглядывают в будущее.

Вторым шагом был сбор фичей с кумулятивной статистикой по всем предыдущим боям для каждого из соперников и генерации из получившейся накопленной суммы новых важных признаков, таких как серия побед, сумма побед нокаутами, сумма чистых побед, точность ударов, среднее время боя, KO/TKO и т. д. Это перечень важных показателей по уже проведённым боям, которые обычно публикуются на сайте UFC до начала боя. Затем мы посчитали разницу по физическим характеристикам бойцов, удалили коррелированные между собой величины и законсервировали данные в pkl-файл.

Случайный лес, стекинг, бэггинг и итоговая модель

В качестве первой модели мы решили взять случайный лес, так как эта модель неплохо справляется с ненормализованными данными. Результат ошеломил по сравнению с букмекерскими конторами, процент точности был очень высок, однако не всё так просто

Изначально мы не учли наличие временного ряда, и наша модель тренировалась на данных из будущего перемешанного с прошлым, то есть мы взяли временной ряд и случайным образом поделили данные на тренировочные и тестовые (80/20). Тренировочные для тренировки модели, тестовые для проверки правильности уже тренированной модели.

Процент точности прогноза победы варьировался от 75 до 82%! Но какая разница, если обучение было неправильным?.. Модель не должна смотреть в будущее, как это было у нас. Решить эту проблему удалось достаточно просто: мы вернули даты для нашего датасета и поделили на тренировочные и тестовые по датам: данные до 2018 года взяли за тренировочные, данные после 2018 года за тестовые, и вуаля, точность упала в среднем на 5 %, однако теперь мы в ней хотя бы уверены.

Потюнить модели в достаточной мере не получилось, так как было мало времени. Но получилось попробовать стекинг, бэггинг и другие разновидности моделей с параметрами по умолчанию. Они, к сожалению, не дали результатов лучше, чем стандартный случайный лес, поэтому в качестве итоговой модели мы оставили именно этот алгоритм. Возможно, в будущем после подбора параметров для всех моделей найдётся та, которая окажется лучше.

Посмотреть на модель можно на GitHub.

Для того чтобы модель предсказала победителя на новых данных, нужно эти данные обработать таким же образом, как мы это делали в нашем ноутбуке (Jupyter Notebook) DeepOverfitting-DataPreparing, после этого просто подать эти строчки данных для двух бойцов в predict функцию нашей модели и получить предсказание, либо 0, либо 1, 0 победил 2 боец, 1 победил первый боец.

Итоги

Самое главное мы все выявили свои слабые и сильные стороны, поняли, какие пробелы в понимании работы с временными рядами нам нужно заполнить, и научились распределять нагрузку в команде. После всего проделанного нами пути мы решили, что не остановимся на достигнутом и продолжим развивать наш продукт.

Поэтому мы наметили следующий план:

  1. Улучшим точность предсказаний путем более кропотливой настройки модели.

  2. Проверим нашу текущую модель на предстоящих боях.

  3. Попробуем применить нейронные сети и глубокое обучение.

  4. Разработаем приложение, которое будет в удобном виде показывать, сколько и куда ставить.

  5. Станем кем-то кроме букмекера, кто заработает на ставках. И, конечно, будем писать на Хабре про дальнейшее развитие проекта. Букмекеры, берегитесь, мы идём за вами.

Состав команды работавшей над проектов:

  • Евгения Шикина (г. Видное)

  • Оксана Евсеева (г. Барселона)

  • Максим Щиколодков (г. Москва)

  • Михаил Стриженов (г. Москва)

  • Лев Неганов (г. Москва)

  • Кирилл Плотников (г. Екатеринбург)

Узнать больше про магистратуру можно на сайтеdata.misis.ruи вTelegram канале.

Ну и конечно не магистратурой единой! Если вы хотите узнать больше про data science, машинное и глубокое обучение заглядывайте к нам на соответствующий курсы, будет непросто, но увлекательно.

Узнайте, как прокачаться и в других специальностях и навыках или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru