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

Pandas

Аналитика алкогольной продукции сети магазинов Лента

27.03.2021 18:23:22 | Автор: admin

Сегодня вашему вниманию представлена аналитика (исследование) алкогольной продукции сети магазинов Лента (далее - Лента), находящаяся в каталоге на официальном сайте компании.

Планирование, подготовка, выборка

Почему выбор пал на алкогольную продукцию и на эту компанию? Да очень всё просто - первое, что пришло в голову (да, такое бывает) и большой ассортимент данного сегмента. Думаю в дальнейшем будет аналитика продукции других компаний. Но вернёмся к нашим бутылкам элементам исследования. Предварительно изучив структуру самого сайта компании приходим к выводу, что каталог продукции динамичен в зависимости от места расположения объекта продаж и изменением ассортимента. Выборку было решено взять по г. Москва, в гипермаркетах. Конечно же не обладая точными данными ассортимента алкогольной продукции компании нельзя сказать, что она репрезентативная, но всё же полагаем, что всё ок. Изучив количество единиц продукции в каталогах на сайте по точкам продаж получаем, что в среднем одинаково, вотЪ.

Инструменты для аналитики

Как любой себя уважающий senior data analyst джун в нашем исследовании мы будем применять стандартные инструменты для этого - язык программирования python 3+, библиотеки pandas для анализа и обработки данных, библиотеки BeautifulSoup, request, csv, lxml для парсинга, библиотеки seaborn, matplotlib, plotly для визуализации результата, всё это находится в нашей любимой Anaconda, в которой JupyterLab и Jupyternotebook, плюс родные и добрые таблицы google sheets(возможно понадобятся) и конечно же не забудем про свой brain.

Получение данных для исследования

Данные мы будем получать конечно же с помощью нашего любимого автоматизированного процесса сбора данных (о как !) или более проще - парсинга (скрапинга). Парсинг будет осуществляться посредством скрипта на питОне (python). Для написания скрипта пришлось где-то почитать, где-то посмотреть, где-то прихватить (да простите меня товарищи). Код скрипта можно посмотреть тут.

Процесс парсинга

Итак, заходим на сайт подопытного объекта исследования и включаем в браузере режим разработчика, находим нужные нам классы и категории и прописываем (подставляем) их в коде.Кстати, у нас получилось 101 страница в каталоге. Запускаем процесс парсинга в Jupyter и ждём когда файл наполнится данными.

Процесс подготовки и обработки данных

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

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

Пробел в числах в поле price был идентифицирован как символ \xa0 - неразрывный пробел, элемент компьютерной кодировки текстов (подробнее тут). Далее с помощью метода replace() удаляем его и с помощью astype(float) меняем формат столбцов в float и у нас получается нормальный формат цены товара.

Процесс анализа данных

Далее с помощью describe() узнаем краткие описательные статистики. Как мы видим среднее значение равное 986.78 руб. в цене без скидки, медиана равна 631.59 руб.

Построим гистограмму c помощью библиотеки matplotlib и увидим распределение, в параметрах прологарифмируем переменную. Как мы видим на гистограмме основная цена на алкогольную продукцию в пределах до 2400 руб, есть много выбросов.

Для углубленного анализа построим график boxplot от библиотеки plotly. Теперь мы видим, что подавляющее значение выбросов начинается от 2420 руб. А самым большим выбросом оказался коньяк Hennessy XO за 16209 руб. С помощью метода sort_value() узнаем пятерку самого дорогого алкоголя в Ленте.

Напомню, что цены и ассортимент динамичны, поэтому всё может изменяться каждый день.

Благодарю за внимание, всем всего наилучшего, ваш konstatic.

Подробнее..

Как мы собираем общие сведения о парке из Kubernetes-кластеров

16.06.2021 10:13:29 | Автор: admin

Имея в обслуживании большой (более 150) парк Kubernetes-кластеров, всегда хотелось иметь удобное представление их общего состояния, в том числе и для того, чтобы поддерживать их гомогенными. В первую очередь нас интересовали следующие данные:

  • версия Kubernetes чтобы все кластеры были on the edge;

  • версия Deckhouse (наша Kubernetes-платформа) для лучшего планирования релизных циклов;

  • количество узлов с разбивкой по типам (управляющие, виртуальные и статические) для отдела продаж;

  • количество ресурсов (CPU, memory) на управляющих узлах;

  • на какой инфраструктуре запущен кластер (виртуальные облачные ресурсы, bare metal или гибридная конфигурация);

  • какой облачный провайдер используется.

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

Истоки и проверка концепции

В какой-то момент времени мы стали использовать Terraform для раскатки инфраструктуры в облака и вопрос отслеживания соответствия желаемых конфигураций реальности встал еще острее. Мы храним Terraform state в самих кластерах и проверку соответствия их с реальностью проверяет отдельно написанный Prometheus exporter. Хотя ранее у нас уже была информация для реагирования на изменения (через соответствующие алерты в системе управления инцидентами), хотелось ещё иметь полное представление о ситуации в отдельной аналитической системе.

Итак, изначально в качестве PoC был несложный Bash-скрипт, которым мы вручную время от времени собирали интересующие данные с K8s-кластеров по SSH. Он выглядел примерно так:

((kubectl -n d8-system get deploy/deckhouse -o json | jq .spec.template.spec.containers[0].image -r | cut -d: -f2 | tr "\n" ";") &&(kubectl get nodes -l node-role.kubernetes.io/master="" -o name | wc -l | tr "\n" ";") &&(kubectl get nodes -l node-role.kubernetes.io/master="" -o json | jq "if .items | length > 0 then .items[].status.capacity.cpu else 0 end" -r | sort -n | head -n 1 | tr "\n" ";") &&(kubectl get nodes -l node-role.kubernetes.io/master="" -o json | jq "if .items | length > 0 then .items[].status.capacity.memory else \"0Ki\" end | rtrimstr(\"Ki\") | tonumber/1000000 | floor" | sort -n | head -n 1 | tr "\n" ";") &&(kubectl version -o json | jq .serverVersion.gitVersion -r | tr "\n" ";") &&(kubectl get nodes -o wide | grep -v VERSION | awk "{print \$5}" | sort -n | head -n 1 | tr "\n" ";") &&echo "") | tee res.csvsed -i '1ideckhouse_version;mastersCount;masterMinCPU;masterMinRAM;controlPlaneVersion;minimalKubeletVersion' res.csv

(Здесь приведен лишь фрагмент для демонстрации общей идеи.)

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

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

  • собирал желаемую информацию,

  • агрегировал ее,

  • отправлял в какое-то централизованное хранилище.

а заодно соответствовал каноном высокой доступности и cloud native.

Этот путь дал начало истории модуля в Kubernetes-платформе Deckhouse, развёрнутой на всех наших кластерах, и сопутствующего ему хранилища.

Реализация

Хуки на shell-operator

В первой итерации источником данных в клиентских кластерах служили Kubernetes-ресурсы, параметры из ConfigMap/Deckhouse, версия образа Deckhouse и версия control-plane из вывода kubectl version. Для соответствующей реализации лучше всего подходил shell-operator.

Были написаны хуки (да, снова на Bash) с подписками на ресурсы и организована передача внутренних values. По результатам работы этих хуков мы получали список желаемых Prometheus-метрик (их экспорт поддерживается в shell-operator из коробки).

Вот пример хука, генерирующего метрики из переменных окружения, он прост и понятен:

#!/bin/bash -efor f in $(find /frameworks/shell/ -type f -iname "*.sh"); do  source $fdonefunction __config__() {  cat << EOF    configVersion: v1    onStartup: 20EOF}function __main__() {  echo '  {    "name": "metrics_prefix_cluster_info",    "set": '$(date +%s)',    "labels": {      "project": "'$PROJECT'",      "cluster": "'$CLUSTER'",      "release_channel": "'$RELEASE_CHANNEL'",      "cloud_provider": "'$CLOUD_PROVIDER'",      "control_plane_version": "'$CONTROL_PLANE_VERSION'",      "deckhouse_version": "'$DECKHOUSE_VERSION'"    }  }' | jq -rc >> $METRICS_PATH}hook::run "$@"

Отдельно хочу обратить ваше внимание на значение метрики (параметр set). Изначально мы писали туда просто 1, но возник резонный вопрос: Как потом получить через PromQL именно последние, свежие labels, включая те series, которые уже две недели не отправлялась? Например, в том же MetricsQL от VictoriaMetrics для этого есть специальная функция last_over_time. Оказалось, достаточно в значение метрики отправлять текущий timestamp число, которое постоянно инкрементируется во времени. Вуаля! Теперь стандартная функция агрегации max_over_time из Prometheus выдаст нам самые последние значения labels по всем series, которые приходили хоть раз в запрошенном периоде.

Чуть позже к источникам данных добавились метрики из Prometheus в кластерах. Для их получения был написан еще один хук, который через curl ходил в кластерный Prometheus, подготавливал полученные данные и экспортировал их в виде метрик.

Чтобы вписаться в парадигму cloud-native и обеспечить HA агента, мы запустили его в несколько реплик на управляющих узлах кластера.

Grafana Agent

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

Выбор пал на разработку Grafana Labs, а именно Grafana Agent. Он умеет делать scrape метрик с endpointов, отправлять их по протоколу Prometheus remote write, а также (что немаловажно!) ведет свой WAL на случай недоступности принимающей стороны.

Задумано сделано: и вот приложение из shell-operator и sidecarом с grafana-agent уже способно собирать необходимые данные и гарантировать их поступление в центральное хранилище.

Конфигурация агента делается довольно просто благо, все параметры подробно описаны в документации. Вот пример нашего итогового конфига:

server:  log_level: info  http_listen_port: 8080prometheus:  wal_directory: /data/agent/wal  global:    scrape_interval: 5m  configs:  - name: agent    host_filter: false    max_wal_time: 360h    scrape_configs:    - job_name: 'agent'      params:        module: [http_2xx]      static_configs:      - targets:        - 127.0.0.1:9115      metric_relabel_configs:      - source_labels: [__name__]        regex: 'metrics_prefix_.+'      - source_labels: [job]        action: keep        target_label: cluster_uuid        replacement: {{ .Values.clusterUUID }}      - regex: hook|instance        action: labeldrop    remote_write:    - url: {{ .Values.promscale.url }}      basic_auth:        username: {{ .Values.promscale.basic_auth.username }}        password: {{ .Values.promscale.basic_auth.password }}

Пояснения:

  • Директория /data это volumeMount для хранения WAL-файлов;

  • Values.clusterUUID уникальный идентификатор кластера, по которому мы его идентифицируем при формировании отчетов;

  • Values.promscale содержит информацию об endpoint и параметрах авторизации для remote_write.

Хранилище

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

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

NB. Справедливости ради, хочется отметить, что на данный момент Cortex выглядит уже вполне жизнеспособным, оформленным как конечный продукт. Очень вероятно, что через какое-то время вернемся к нему и будем использовать. Уж очень сладко при мысли о generic S3 как хранилище для БД: никаких плясок с репликами, бэкапами и растущим количеством данных

К тому времени у нас была достаточная экспертиза по PostgreSQL и мы выбрали Promscale как бэкенд. Он поддерживает получение данных по протоколу remote-write, а нам казалось, что получать данные используя pure SQL это просто, быстро и незатратно: сделал VIEWхи и обновляй их, да выгружай в CSV.

Разработчики Promscale предоставляют готовый Docker-образ, включающий в себя PostgreSQL со всеми необходимыми extensions. Promscale использует расширение TimescaleDB, которое, судя по отзывам, хорошо справляется как с большим количеством данных, так и позволяет скейлиться горизонтально. Воспользовались этим образом, задеплоили connector данные полетели!

Далее был написан скрипт, создающий необходимые views, обновляющий их время от времени и выдающий на выход желаемый CSV-файл. На тестовом парке dev-кластеров всё работало отлично: мы обрадовались и выкатили отправку данных со всех кластеров.

Но с хранилищем всё не так просто

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

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

Было решено перестать ковыряться в потрохах данных и положиться на мощь и наработки разработчиков Promscale. Ведь их connector может не только складывать данные в базу через remote-write, но и позволяет получать их привычным для Prometheus способом через PromQL.

Одним Bashем уже было не обойтись мы окунулись в мир аналитики данных с Python. К нашему счастью, в сообществе уже были готовы необходимые инструменты и для походов с PromQL! Речь про замечательный модуль prometheus-api-client, который поддерживает представление полученных данных в формате Pandas DataFrame.

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

Изначально мы выбрали период скрейпинга данных grafana-agentом равным одной минуте, что отразилось на огромных аппетитах конечной БД в диске: ~800 мегабайт данных в день. Это, конечно, не так много в масштабах одного кластера (~5 мегабайт), но когда кластеров много суммарный объём начинает пугать. Решение оказалось простым: увеличили период scrapeа в конфигах grafana-agentов до одного раза в 5 минут. Прогнозируемый суммарный объем хранимых данных с retentionом в 5 лет уменьшился с 1,5 Тб до 300 Гб, что, согласитесь, уже выглядит не так ужасающе.

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

Получившаяся архитектура выглядит так:

Итоги и перспективы

Мы смотрим в будущее и планируем отказаться от отчётов в формате CSV в пользу красивого интерфейса собственной разработки. А по мере разработки собственной биллинговой системы начнём отгружать данные и туда для нужд отдела продаж и развития бизнеса. Но даже те CSV, что мы получили сейчас, уже сильно упрощают рабочие процессы всего Фланта.

А пока не дошли руки до фронтенда, мы сделали dashboard для Grafana (почему бы и нет, раз всё в стандартах Prometheus?..). Вот как это выглядит:

Общая сводная таблица по кластерам с Terraform-состояниямиОбщая сводная таблица по кластерам с Terraform-состояниямиРаспределение кластеров по облачным провайдерамРаспределение кластеров по облачным провайдерамРазбивка по используемым Inlet в Nginx Ingress-контроллерахРазбивка по используемым Inlet в Nginx Ingress-контроллерахКоличество podов Nginx Ingress-контроллеров с разбивкой по версиямКоличество podов Nginx Ingress-контроллеров с разбивкой по версиям

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

P.S.

Читайте также в нашем блоге:

Подробнее..

Перевод Система рекомендаций фильмов с GUI на Python

14.10.2020 18:14:52 | Автор: admin

Без опыта я никому не нужен! Где взять опыт? часто думают люди, осваивающие новую для себя сферу или изучающие новый язык программирования. Решение есть делать пет-проекты. Представленный под катом проект системы рекомендации фильмов не претендует на сложность и точность аналогичных систем от энтерпрайз-контор, но может стать практическим стартом для новичка, которому интересны системы рекомендации в целом. Этот пост также подойдет для демонстрации как использовать Python-библиотеку EasyGUI на практике.

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



Начало работы


В этой статье я расскажу, как создать базовую систему рекомендаций фильмов со встроенным графическим пользовательским интерфейсом. Прежде всего нам нужны данные. Чтобы получить хорошее представление о том, насколько хорошо система рекомендаций работает на самом деле, нам понадобится довольно большой набор данных. Используем MovieLens на 25M, который вы можете скачать здесь. Набор данных состоит из шести файлов .csv и файла readme, объясняющих набор данных. Не стесняйтесь взглянуть на него, если хотите. Мы будем использовать только эти три файла:

movies.csv; ratings.csv; tags.csv

Также потребуется несколько библиотек Python:

  1. NumPy
  2. Pandas
  3. Progress (pip install progress)
  4. Fuzzywuzzy (pip install fuzzywuzzy & pip install python-Levenshtein)
  5. EasyGUI

Вероятно, все они могут быть установлены через pip. Точные команды будут зависеть от ОС. Кроме того, должна работать любая IDE Python (см. материал по ним вот тут). Я пользуюсь Geany, легкой IDE для Raspbian. Посмотрим на набор данных:


movies.csv

Выше показан файл movies.csv с тремя столбцами данных, а именно: movieId, title и genres идентификатор фильма, название и жанр. Все очень удобно и просто. Будем работать со всеми тремя.

Ниже мы видим tags.csv. Здесь используются только столбцы movieId и tag, связывающие тег со столбцом movieId, которые также есть в файлах movies.csv и rating.csv.


tags.csv

И последний, но не менее важный файл: rating.csv. От этого парня мы возьмем столбцы movieId и rating.


ratings.csv

Отлично, теперь давайте запустим IDE и начнем. Импортируем библиотеки, как показано ниже. Pandas и NumPy хорошо известны в области Data Science. Fuzzywuzzy, EasyGUI и библиотека Progress менее известны^ судя по тому, что мне удалось собрать, однако вы, возможно, знакомы с ними. Я добавлю в код много комментариев, чтобы все было понятно. Посмотрите:



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

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

Строки 16, 17, 33 и 35 это, в основном, индикатор прогресса из библиотеки Progress. Цикл выполняется только один раз. Загрузка набора данных в мою систему занимает около 30 секунд, поэтому мы используем индикатор прогресса, чтобы показать, что набор данных загружается после запуска программы, как показано ниже. После этого нам не придется загружать его снова во время навигации по графическому интерфейсу.



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

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

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


which_way()

Внутри функции мы определяем параметры для EasyGUI chiocebox, который отображается пользователю. Строка параметра, которую вводит пользователь, возвращается и сохраняется в переменной fieldValues. После условный оператор направляет пользователя к следующей функции или окну на основе выбора.

Если пользователь нажимает отмену, программа завершается. Но если пользователь нажимает поиск фильмов по жанру или поиск фильмов по тегу, то будут вызваны функции genre_entry() или tag_entry() и появится что-то, известное как EasyGUI multenterbox.



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



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



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



Как только пользователь что-то вводит, текст (строка) сохраняется в переменной fieldValues, а код работает со строки 114 в функции tag_entry. Пользовательский ввод из поля EasyGUI multenter возвращается в виде списка. При нажатии кнопки отмены возвращается Нет. Чтобы использовать этот ввод и в других функциях, нам нужно объявить переменную как глобальную.

Теперь мы нарезаем список пользовательского ввода из multenterbox, и сохраняем как переменную user_input_2. Нас интересует только возвращаемый текст, если пользователь не нажимает кнопку отмены, отсюда и условный оператор:

if fieldValues != None:

Если пользователь был достаточно любезен, чтобы ввести какой-то текст, мы переходим к функции Similarity_test2(), которая в основном содержит базовые аспекты этой системы рекомендаций, в качестве альтернативы пользователь возвращается в главное меню. Similarity_test1() и Similarity_test2() очень похожи, так же как genre_entry и tag_entry. Я буду рассматривать здесь только Similarity_test2(). Давайте посмотрим на это:



Как мы видим, она принимает один параметр, переменнаую user_input_2 из функции tag_entry(). Помните тот фрейм данных, который мы создали в строке 19 из файла tags.csv? Сначала мы хотим собрать все уникальные теги из столбца тегов и сохранить их в переменной.

Существует множество способов применения библиотеки Fuzzywuzzy в зависимости от ваших задач. Мы будем работать так: output = process.extract(query, choices)

Можно передать дополнительный параметр тип счетчика. Мы просто будем использовать синтаксис по умолчанию. По сути Fuzzywuzzy работает с расстоянием Левенштейна для вычисления различий между последовательностями и возвращает оценку в пределах 100%.

Функция process.extract(query, choices) возвращает список оценок, где каждая строка и ее оценка заключена в скобки, например (строка 95) в качестве элементов списка.

После перебора всего списка тегов и поиска совпадений с переменной user_input_2, мы перебираем список оценок и вырезаем только совпадения выше 90% и сохраняем их в переменной final_2. Мы объявляем его глобальным, чтобы использовать в следующей функции. Если fuzzywuzzy не нашел для нас соответствия, вернется []. Если совпадение по условию не найдено, просим пользователя повторить попытку, вернувшись к функции tag_entry(). В качестве альтернативы, когда у нас есть совпадения более чем на 90%, мы можем использовать их в функции tag(), как показано ниже:



tag()

Теперь, когда у нас есть совпадения более 90%, перебираем их в цикле и просматриваем каждую строку столбца tag фрейма данных df_tags, чтобы увидеть, какие теги соответствуют строкам из Fuzzywuzzy. Теперь сохраняем все совпадения тегов вместе с идентификатором movieId в переменной final_1. Чтобы очистить добавленные данные, мы отрезаем первый элемент и сбрасываем индекс фрейма данных. Теперь можно удалить столбец с именем index и все дубликаты из столбца movieId. Чтобы фильмы с наивысшим рейтингом отображались первыми в порядке убывания, отсортируем фрейм данных и удалим фильмы с рейтингом меньше 2,5/5,0.

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

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



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

Исходный код проекта
# импорт библиотекfrom fuzzywuzzy import fuzz from fuzzywuzzy import processfrom progress.bar import IncrementalBarfrom easygui import *import easygui as guiimport pandas as pdimport numpy as npimport sys# максимальное увеличение размера массива отображения numpy для отображения easygui np.set_printoptions(threshold=sys.maxsize)# фрейм данных относительно большой, начальная загрузка займет около 30 секунд# в зависимости от вашего компьютера, поэтому здесь уместна индикация загрузкиprogress_bar = IncrementalBar('Loading Movie Database...', max=1)for i in range(1):    # чтение файлов csv    df_tags = pd.read_csv("tags.csv", usecols = [1,2])    df_movies = pd.read_csv("movies.csv")    df_ratings = pd.read_csv("ratings.csv", usecols = [1,2])        # объединение столбцов из отдельных фреймов данных в новый фрейм    df_1 = pd.merge(df_movies ,df_ratings, on='movieId', how='outer')    # заполнение значений NaN средним рейтингом    df_1['rating'] = df_1['rating'].fillna(df_1['rating'].mean())     # группирование строк df по среднему рейтингу фильма    df_1 = pd.DataFrame(df_1.groupby('movieId')['rating'].mean().reset_index().round(1))    # добавление столбцов title и genres в df    df_1['title'] = df_movies['title']    df_1['genres'] = df_movies['genres']        progress_bar.next()    # заполнение индикатора загрузки при успешной загрузке progress_bar.finish()def which_way():    '''    Эта функция, которая выполняется при запуске программы.     Работает как перекресток, вы выбираете поиск фильмов по    тегу или по жанру. По выбору пользователь переходит к следующему окну.    '''    # определение параметров easygui choicebox    msg = "Choose an option:"    title = "Main Menu"    choices = ["Search recommended movies by genre:","Search recommended movies by tag:"]    fieldValues = choicebox(msg,title, choices)        # переменная fieldValues - это пользовательский ввод, который возвращается из графического интерфейса    # условный оператор, направляющий пользователя к следующему интерфейсу на основе ввода    if fieldValues == "Search recommended movies by genre:":        genre_entry()        elif fieldValues == "Search recommended movies by tag:":        tag_entry()def field_check(msg, title, fieldNames):    '''    Эта функция проверяет отсутствие вводимых пользователем значений в multenterbox    и возвращает пользовательский ввод как переменную fieldValues.        Параметры:        msg, title и fieldnames графического интерфейса multienterbox        '''        fieldValues = multenterbox(msg, title, fieldNames)        # Цикл с условием, чтобы проверить,    # что поля ввода не пусты    while 1:        if fieldValues is None: break        errmsg = ""        for i in range(len(fieldNames)):            if fieldValues[i].strip() == "":                errmsg += ('"%s" is a required field.\n\n' % fieldNames[i])        if errmsg == "":            break # если пустых полей не найдено, перейти к следующему блоку кода        # cохранить пользовательский ввода в виде списка в переменной fieldValues        fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues)        return fieldValuesdef tag_entry():    '''     Эта функция определяет параметры easygui multenterbox и вызывает    field_check, если пользователь вводил значнеие,    вызывает тест на подобие; если совпадение не найдено, пользователь возвращается    в окно ввода    '''        # определение параметров easygui multenterbox    msg = "Enter movie tag for example: world war 2 | brad pitt | documentary \nIf tag not found you will be returned to this window"    title = 'Search by tag'                            fieldNames = ["Tag"]        # вызов field_check() для проверки отсутствия пользовательского ввода и    # сохранения вода как переменной fieldValues    fieldValues = field_check(msg, title, fieldNames)        # Если пользователь ввел значение, сохраняем его в fieldValues[0]    if fieldValues != None:        global user_input_2        user_input_2 = fieldValues[0]                # здесь мы вызываем функцию, которая в основном проверяет строку        # на схожесть с другими строками. Когда пользователь нажимает кнопку отмены, он возвращается в главное меню         similarity_test2(user_input_2)    else:        which_way()def tag():    '''    Эта функция добавляет все совпадающие по тегам фильмы во фрейм данных pandas,    изменяет фрейм данных для правильного отображения easygui, отбросив некоторые    столбцы, сбрасывая индекс df, объединяя фреймы и сортируя элементы так,    чтобы показывались фильмы с рейтингом >= 2.5. Она также преобразует столбцы df в списки    и приводит их в порядок в массиве numpy для отображения easygui.      '''        # добавление тегов найденных фильмов как объекта фрейма    final_1 = []    for i in final_2:        final_1.append(df_tags.loc[df_tags['tag'].isin(i)])        # сброс индекса df, удаление столбца индекса, а также повторяющихся записей    lst = final_1[0]    lst = lst.reset_index()    lst.drop('index', axis=1, inplace=True)    lst = lst.drop_duplicates(subset='movieId')# слияние movieId с названиями и жанрами + удаление тега и идентификатора фильма    df = pd.merge(lst, df_1, on='movieId', how='left')    df.drop('tag', axis=1, inplace=True)    df.drop('movieId', axis=1, inplace=True)# сортировка фильмов по рейтингам, отображение только фильмов с рейтингом выше или равным 2,5    data = df.sort_values(by='rating', ascending=False)    data = data[data['rating'] >= 2.5]    heading = [] # добавление названий столбцов как первой строки фрейма данных для отображения easygui    heading.insert(0, {'rating': 'Rating', 'title': '----------Title',     'genres': '----------Genre'})    data = pd.concat([pd.DataFrame(heading), data], ignore_index=True, sort=True)        # преобразование столбцов фрейма данных в списки    rating = data['rating'].tolist()    title = data['title'].tolist()    genres = data['genres'].tolist()        # составление массива numpy из списков столбцов dataframe для отображения easygui    data = np.concatenate([np.array(i)[:,None] for i in [rating,title,genres]], axis=1)    data = str(data).replace('[','').replace(']','')        # отображение фильмов пользователю    gui.codebox(msg='Movies filtered by tag returned from database:',    text=(data),title='Movies')        which_way()def genre_entry():    '''     Эта функция определяет параметры easygui multenterbox    и вызывает field_check, если пользователь что-то вводил,    вызывается тест на подобие. Если совпадение не найдено, пользователь возвращается    в то же окно      '''    # определение параметров easygui multenterbox    msg = "Enter movie genre for example: mystery | action comedy | war \nIf genre not found you will be returned to this window"    title = "Search by genre"    fieldNames = ["Genre"]        # вызов field_check() для проверки отсутствия пользовательского ввода и    # сохранения ввода в fieldValues.    fieldValues = field_check(msg, title, fieldNames)        # Если пользовательский ввод не пуст, сохраняет его в переменной user_input    if fieldValues != None:        global user_input        user_input = fieldValues[0]            # здесь мы вызываем функцию, которая в основном проверяет строку    # на подобие с другими строками. Если пользователь нажмет кнопку отмена, то он вернется в главное меню         similarity_test1(user_input)    else:        which_way()def genre():    '''    Эта функция добавляет все соответствующие жанру фильмы во фрейм pandas,    изменяет фрейм для правильного отображения easygui, отбросив некоторые    столбцы, сбрасывает индекс df, объединеняет фреймы и сортирует фильмы для отображения    только фильмов с рейтингом >= 2.5. Она также преобразует столбцы конечного df в списки    и приводит их в порядок в массиве numpy для отображения easygui.    '''        # добавление соответствующих жанру фильмов во фрейм.    final_1 = []    for i in final:        final_1.append(df_movies.loc[df_movies['genres'].isin(i)])        # сброс индекса df, удаление индекса столбцов и дубликатов записей    lst = final_1[0]    lst = lst.reset_index()    lst.drop('index', axis=1, inplace=True)    lst.drop('title', axis=1, inplace=True)    lst.drop('genres', axis=1, inplace=True)    lst = lst.drop_duplicates(subset='movieId')        # объединение идентификатора фильма с названием, рейтингом и жанром + удаление индекса, названия и жанра    df = pd.merge(lst, df_1, on='movieId', how='left')        # сортировка по рейтингу, отображение только фильмов с рейтингом выше или равным 2,5    data = df.sort_values(by='rating', ascending=False)    data.drop('movieId', axis=1, inplace=True)    data = data[data['rating'] >= 2.5]    heading = [] # add column names as first dataframe row for easygui display    heading.insert(0, {'rating': 'Rating', 'title': '----------Title',     'genres': '----------Genre'})    data = pd.concat([pd.DataFrame(heading), data], ignore_index=True, sort=True)        # преобразование столбцов фрейма данных в списки    rating = data['rating'].tolist()    title = data['title'].tolist()    genres = data['genres'].tolist()        # составление массива numpy из списков столбцов фрейма для отображения easygui    data = np.concatenate([np.array(i)[:,None] for i in [rating,title,genres]], axis=1)    data = str(data).replace('[','').replace(']','')        # отображение фильмов пользователю    gui.codebox(msg='Movies filtered by genre returned from database:',    text=(data),title='Movies')        which_way()def similarity_test1(user_input):    '''    Эта функция проверяет схожесть строк путем сопоставления пользовательского ввода    для жанров фильмов, совпадения > 90% сохраняется в переменной, которая    затем передается функции жанра для сопоставления с базой данных и    возврата в окно ввода, если совпадение не найдено    '''    # сохранение жанров фильмов в качестве тестовой базы и пользовательского ввода для тестирования     genre_list = df_movies['genres'].unique()    query = user_input    choices = genre_list     # here fuzzywuzzy does its magic to test for similarity    output = process.extract(query, choices)        # сохранение совпадений в переменной и их передача следующей функции    global final    final = [i for i in output if i[1] > 90]        # если совпадений > 90%  не найдено, вернуть пользователя в окно жанра    if final == []:        genre_entry()    else:        genre()def similarity_test2(user_input_2):    '''    Эта функция проверяет схожесть строк путем сопоставления пользовательского ввода    в теги фильмов, совпадение > 90% сохраняется в переменной, которая    затем передается в функцию тега для сопоставления базы данных и    возврата в окно ввода, если совпадение не найдено    '''    # сохранение тега фильма в качестве тестовой базы и пользовательского ввода для тестирования    tag_list = df_tags['tag'].unique()    query = user_input_2    choices = tag_list     # here fuzzywuzzy does its magic to test for similarity    output = process.extract(query, choices)        # сохранение возвращенных совпадений в переменной и их передача следующей функции    global final_2    final_2 = [i for i in output if i[1] > 90]        #если совпадение> 90% не найдено, возврат в окно ввода    if final_2 == []:        tag_entry()    else:        tag()if __name__ == '__main__':    which_way()



image

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



Читать еще


Подробнее..

Использование статистических методов для анализа временных рядов

04.02.2021 14:12:29 | Автор: admin

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

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

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

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

Но для чего может использоваться временной ряд в аудите? Для всего!

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

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

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

Возьмем простой пример: рассмотрим покупки подарков к праздничному мероприятию для десяти коллег.

Здесь все более-менее понятно:

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

А если коллег и знакомых не десять, а 200? И закупаться нужно не за 10 дней, а в течение целого месяца?

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

  1. Самым близким коллегам и знакомым мы готовим дорогие подарки;

  2. Сразу найти нужный подарок не всегда удается и процесс покупки подарков растягивается на месяц.

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

Наличие таких одинаковых периодов говорит о том, что ряд стационарен. То есть существует какая-то часть, которая всегда повторяется. Но как понять, есть ли этот период?

Для этого используется целая группа тестов:

  1. Тест Дики Фуллера,

  2. Тест Филипса Перрона,

  3. Тест Лейбурна,

  4. Тест Шмидта Филлипса,

  5. Тест Квятковского Филлипса Шмидта Шина,

  6. Тест DF GLS,

  7. Тест Кохрейн.

В этом примере мы будем использовать тест Дики Фуллера, который реализован в модуле statsmodels на языке python. Для этого нам надо будет всего лишь выбрать нужные модули (statsmodels и pandas), загрузить данные, и вывести результат. Ниже представлен пример скрипта:

При этом сами данные выглядят следующим образом:

Основная идея теста заключается в подтверждении или отклонении двух гипотез:

  • Нулевая гипотеза (H0): предполагает, что временной ряд имеет единичный корень, то есть он нестационарный (покупаем подарки в случайном порядке).

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

Для подтверждения или опровержения этих гипотез используются p -values значения.

p -values это наименьшее значение уровня значимости (то есть вероятности отказа от справедливой гипотезы).

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

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

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

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

Подробнее..

Популярность BPM в разных жанрах музыки. Анализ скорости исполнения 500 лучших песен

02.03.2021 02:04:15 | Автор: admin

Несколько лет назад, занимался изучением теории музыки, продавал и писал аудио-инструментал для аренды или заказов. Изначально, процесс явно творческий, но вскоре, мой интерес к коммерческой части превысил и возник вопрос: В каком же темпе создавать ритм музыки?.

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

BPM [в музыке] показатель, для определения скорости исполнения композиции, путём измерения количества тактовых долей в минуту.


1: Пролог

Устанавливаем Matplotlibи Pandas с необходимыми зависимостями через pip-менеджер в консоли/терминале.

python -m pip install -V matplotlib и pip install pandaspython -m pip install -V matplotlib и pip install pandas

Создаём директорию, а потом виртуальное окружение для проекта. После, подключаем библиотеки в IDE [в моём случае: PyCharm].

File Settings Project: [...] Python InterpreterFile Settings Project: [...] Python Interpreter

2: BPM

BPM будем вычислять через функцию Detect tempo в FL Studio и через сайт tunebat.com

ПКМ по верхней левой иконке на звуковой дорожке Detect tempo Выбрать диапазонПКМ по верхней левой иконке на звуковой дорожке Detect tempo Выбрать диапазон

3: DataSet

Начинаем создание DataSetа [выборки-коллекции данных] в Excel, для каждого жанра. Экспортируем в CSV-формат с настройками разделителя запятой. Следующие CSV-файлы создавал в IDE, так удобнее. Выборки перемещаем в директорию, где находится файл самой программы.

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

Параметры: name название трека; bpm темп; year год релизаПараметры: name название трека; bpm темп; year год релиза

4: Rap построение точечной диаграммы и гистограммы

Выборка взята здесь: rollingstone.com/100-greatest-hip-hop-songs-of-all-time
Сам CSV-DataSet: github.com/Rap.csv

На основе информации DataSet'а, создаём точечную диаграмму [Scatter Plots] для изучения взаимосвязи между BPM и годом выпуска, а также для отображения концентраций при ранжировании данных.

Видно, что с 1980 по 2005 гг. основным темпом был диапазон в 90-105 BPMВидно, что с 1980 по 2005 гг. основным темпом был диапазон в 90-105 BPMКод точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Rap.csv')                               # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)   # Подпись шкалыplt.title('Популярность скорости '                                  # Заголовок графика  'исполнения в Rap\'е ', fontsize=25)plt.xlabel('BPM', fontsize=18)                                         # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                               # Ось ординатplt.tight_layout()                                                              # Настройка параметров подзаголовков в области отображенияplt.show()                                                                        # Вывод на экран

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

Самый популярный диапазон: 80-100 BPMСамый популярный диапазон: 80-100 BPMКод гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Rap end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в rap\'е', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

5: Рок

Выборка взята здесь: rockfm.ru/top100
Сам CSV-DataSet: github.com/Rock.csv

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

Код точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Rock.csv')                             # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точкиalpha=.7                                                                      # Прозрачность точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)    # Подпись шкалыplt.title('Популярность скорости '                                   # Заголовок графика  'исполнения в роке', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                                # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                                         # Вывод на экран
Самые популярные диапазоны: 120-140 и 100-120 BPMСамые популярные диапазоны: 120-140 и 100-120 BPMКод гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Rock end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в роке', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

6: Блюз

Выборка взята здесь: digitaldreamdoor.com/best_bluesong
Сам CSV-DataSet: github.com/Blues.csv

Видно высокую концентрацию использования темпа около 100 BPM в 90-хВидно высокую концентрацию использования темпа около 100 BPM в 90-хКод точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Blues.csv')                            # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)    # Подпись шкалыplt.title('Популярность скорости '                                   # Заголовок графика  'исполнения в блюзе', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                                # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                                         # Вывод на экран
Самый популярный диапазон: 100-120 BPMСамый популярный диапазон: 100-120 BPMКод гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Blues end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в блюзе', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

7: Chillout

Выборка взята здесь: open.spotify.com
Сам CSV-DataSet: github.com/Chillout.csv

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

Код точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Chillout.csv')                         # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точкиalpha=.5                                                                      # Прозрачность точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)   # Подпись шкалыplt.title('Популярность скорости '                                  # Заголовок графика  'исполнения в Chillout', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                               # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                        # Вывод на экран
Самый популярный диапазон: 80-100Самый популярный диапазон: 80-100Код гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Chillout end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в Chillout', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

8: EDM

Выборка взята здесь: edmcharts.net
Сам CSV-DataSet: github.com/EDM.csv

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

Довольно однозначно вышло...Довольно однозначно вышло...Код точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('EDM.csv')                             # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точкиalpha=.2                                                                      # Прозрачность точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)   # Подпись шкалыplt.title('Популярность скорости '                                  # Заголовок графика  'исполнения в EDM', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                               # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                        # Вывод на экран
Самый популярный диапазон: 120-140Самый популярный диапазон: 120-140Код гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('EDM end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в EDM', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

9: Заключение

Самым простым графиком сравним количество попаданий в каждый диапазон, композиций, из всех проанализированных ранее жанров*.

* такие жанры как ethnic, ambient, folk, dubstep, reggae и др, не удалось к сожалению разобрать из-за отсутствия качественной выборки...

BPM/Кол-во треков

<60

60-80

80-100

100-120

120-140

140-160

160-180

Blues

2

9

25

35

15

6

8

Chillout

11

35

18

19

12

5

EDM

1

3

21

67

6

2

Rap

5

61

20

7

4

3

Rock

6

20

25

27

11

11

Итог:

2

32

144

119

135

39

29

Простой код, простого графика
from matplotlib import pyplot as pltplt.style.use('fivethirtyeight')x = ['<60', '60-80', '80-100', '100-120', '120-140', '140-160', '160-180']y = [2, 32, 144, 119, 135, 39, 29]plt.plot(x, y, label='BPM', c='#e85b45')plt.legend()plt.title('Сравнение всех диапазонов BPM во всех жанрах', fontsize=25)plt.xlabel('Диапазон BPM', fontsize=18)plt.ylabel('Количество треков', fontsize=18)plt.tight_layout()plt.show()
Подробнее..

Вычислительная геология и визуализация пример Python 3 Jupyter Notebook

13.03.2021 08:12:27 | Автор: admin

Сегодня вместо обсуждения геологических моделей мы посмотрим пример их программирования в среде Jupyter Notebook на языке Python 3 и с библиотеками Pandas, NumPy, SciPy, XArray, Dask Distributed, Numba, VTK, PyVista, Matplotlib. Это довольно простой ноутбук с поддержкой многопоточной работы и возможностью запуска локально и в кластере для обработки больших данных, отложенными вычислениями (ленивыми) и наглядной трехмерной визуализацией результатов. В самом деле, я постарался собрать разом целый набор сложных технических концепций и сделать их простыми. Для создания кластера на Amazon AWS смотрите скрипт AWS Init script for Jupyter Python GIS processing, предназначенный для единовременного создания набора инстансов и запуска планировщика ресурсов на главном инстансе.

Визуализация с помощью Visualization Toolkit(VTK) и PyVista это уже далеко не Matplotlib


Идея сделать такой пример возникла у меня давно, поскольку я регулярно занимаюсь разнообразными вычислительными задачами, в том числе для различных университетов и для геологоразведочной индустрии, и знаком очень близко с проблемами переносимости и поддерживаемости программ, а также проблемами работы с так называемыми большими данными (сотни гигабайт и терабайты) и визуализацией результатов. Так что само собой появилось желание сделать ноутбук-пример, в котором коротко и просто показать и красивую визуализацию и распараллеливание и ускорение кода Python и чтобы этот ноутбук можно было без изменений запустить как локально, так и на кластере. Все использованные библиотеки доступны уже много лет, но мало известны, или, как говорится, они остаются широко известными в узких кругах. Оставалось лишь найти подходящую задачку, на которой все это можно показать и это было, пожалуй, самым сложным ведь мне хотелось, чтобы пример получился достаточно осмысленным и полезным. И вот такая задача нашлась рассмотреть моделирование гравитационного поля на поверхности для заданной (синтетической в данном случае) модели плотности недр и некоторые последующие преобразования с вычислением фрактального индекса по компонентам пространственного спектра и кольцевого преобразования Радона, как его называют математики, или Хафа, согласно компьютерным наукам. Замечательно то, что с популярными библиотеками Python эти преобразования делаются буквально в несколько строчек кода, что особенно ценно для примера. Поскольку моделирование поля в каждой точке поверхности требует вычисления для всего трехмерного объема, мы будем обрабатывать гигантский объем данных. Для визуализации используем человеколюбивую обертку PyVista для библиотеки VTK Visualization Toolkit, потому что писать код для последней это путь истинных джедаев кто хочет лично в том убедиться, смотрите мой модуль к ParaView N-Cube ParaView plugin for 3D/4D GIS Data Visualization, написанный как раз на Python + VTK.


Теперь предлагаю проследовать по ссылке на страницу GitHub репозитория или сразу открыть ноутбук basic.ipynb Надеюсь, код достаточно просто читается, остановлюсь лишь на нескольких особенностях. Запускаемый в ноутбуке локальный кластер dask предназначен для работы на многоядерных компьютерах, а вот для работы в кластере потребуется настроить подключение к его планировщику. В упомянутом выше скрипте AWS Init script for Jupyter Python GIS processing есть соответствующие комментарии и ссылки. В коде мы используем векторизацию NumPy, то есть передаем сразу массивы, а не скаляры, при этом пользуемся тем, что XArray объекты предоставляют доступ к внутренним NumPy объектам (object.values). Код NumPy ускорить непросто, но с помощью Numba и для такого кода можно получить некоторый выигрыш в скорости исполнения (возможно, даже около 15%):


from numba import jit@jit(nopython=True, parallel=True)def delta_grav_vertical(delta_mass, x, y, z):    G=6.67408*1e-11    return -np.sum((100.*1000)*G*delta_mass*z/np.power(x**2 + y**2 + z**2, 1.5))

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


def forward_gravity(da):    (da_y, da_x, da_z) = xr.broadcast(da.y, da.x, da.z)    deltagrav = lambda x0, y0: delta_grav_vertical(da.values.ravel(), (da_x.values.ravel()-x0), (da_y.values.ravel()-y0), (da_z.values.ravel()-0))    gravity = xr.apply_ufunc(deltagrav, da_x.isel(z=0).chunk(50), da_y.isel(z=0).chunk(50), vectorize=True, dask='parallelized')    ...

Здесь xarray.broadcast с линеаризацией массивов функцией ravel() позволяют из трех одномерных координат x, y, z получить триплеты координат для каждой точки куба. Выражения da_x.isel(z=0) и da_y.isel(z=0) извлекают x, y координаты верхней поверхности куба, на которой и вычисляется гравитационное поле (точнее, его вертикальную компоненту, т.к. именно она измеряется при практических исследованиях и такие данные доступны для анализа). Функция xarray.apply_ufunc() весьма универсальная и одновременно обеспечивает векторизацию и поддержку параллельных ленивых вычислений dask для указанной коллбэк функции deltagrav. Хитрость заключается в том, что для выполнения вычислений на кубе для каждой точки поверхности нужно координаты поверхности передать в виде XArray массивов, а для использования dask они также должны быть dask массивами, что мы и обеспечиваем конструкциями da_x.isel(z=0).chunk(50) и da_y.isel(z=0).chunk(50), где 50 это размер блока по координатам x, y (подбирается в зависимости от размера массивов и количества доступных вычислительных потоков). Да, такая вот магия достаточно лишь использовать вызов chunk() для XArray массива, чтобы автоматически превратить его в dask массив.


Обратим внимание, что dask-вычисления по умолчанию являются ленивыми (отложенными), то есть вызов функции forward_gravity() завершается почти мгновенно, но возвращаемый результат является лишь оберткой, которая инициирует вычисления только при непосредственном обращении к данным или вызовом load(). При интерактивной работе это очень удобно, так как мы можем написать сложный пайплайн с большими наборами данных и для проверки и визуализации выбрать лишь маленький его кусочек, а при необходимости и запустить вычисления на полном наборе данных. К примеру, мне часто приходится работать с NetCDF датасетами глобального рельефа планеты и прочими в сотни гигабайт на своем ноутбуке визуализируя малую часть данных, а потом запускать уже готовый ноутбук в облаке для обработки всех данных. Таким образом, код для локальной работы и его продакшен версия ничем не отличаются. Главное, правильно настроить размеры dask блоков, иначе вся магия "сломается".


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


В заключение, приглашаю всех посетить GitHub репозитории с множеством геологических моделей и их визуализацией в Blender и ParaView, а также примерами различного анализа. Также смотрите готовые визуализации на YouTube канале.

Подробнее..

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

29.04.2021 16:13:12 | Автор: admin
Геолокационные данные могут применяться в различных сценарияхГеолокационные данные могут применяться в различных сценариях

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


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

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

Данные о предметной области приложения (включают основную информацию о местоположении)

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

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

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

Спутниковые изображенияСпутниковые изображения

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

Геопространственные данные (используются как дополнение к информации о местоположении)

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

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

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

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

Форматы геопространственных данных

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

Самые часто используемые форматы:

  • Векторный формат (самый древний и самый распространённый стандарт. Файл в векторном формате фактически представляет собой набор файлов: в одном файле хранятся геометрические данные, в другом специальные атрибуты данных и т. п.).

  • GeoPackage (более новый стандарт, набирающий популярность. Данные хранятся в одном небольшом по размеру файле, реализованном в виде контейнера базы данных SQLLite).

  • GeoJSON (использует стандартный текстовый формат JSON).

Геометрические геоданные хранятся в виде векторных объектов:

  • точка: например местоположения зданий, домов, ресторанов, стоянок такси;

  • ломаная: например улицы, реки, железные дороги;

  • полигон: определяет зоны, например регионы, районы, озера, штаты, страны;

  • мультиполигон: набор полигонов.

В геоинформационных данных используются структуры данных Точка, Ломаная, Полигон и пр.В геоинформационных данных используются структуры данных Точка, Ломаная, Полигон и пр.

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

  • дуга: аналогично ломаной;

  • узел: точка пересечения различных дуг или полигонов;

  • вершины: излом ломаной.

Географические объекты представляют географические особенности и отношения между ними Географические объекты представляют географические особенности и отношения между ними

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

  • Какие объекты находятся рядом друг с другом?

  • Какие дуги соединяются друг с другом?

  • Какие объекты находятся внутри других полигонов?

Загрузка геоданных

К счастью, нам не нужно вникать в тонкости структуры таких форматов и работать с низкоуровневыми структурами данных.

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

Она работает с объектами GeoDataFrame и GeoSeries, представляющими собой "пространственно ориентированные" версии объектов DataFrame и Series в Pandas. В надстройке реализуется ряд дополнительных методов и атрибутов, которые можно использовать для работы с геоданными в DataFrame.

GeoDataFrame это обычный объект в Pandas DataFrame с дополнительным "геометрическим" столбцом в каждой строке, в который заносятся данные о местоположении.

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

import geopandas as gpd# Load the Shape map of New York City as a GeoDataFrameshape_df = gpd.read_file(shape_data_dir/'ny.shp')

Предварительная обработка геоданных (базовые системы координат)

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

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

Базовая система координат привязывает координаты широты/долготы к реальной точке на ЗемлеБазовая система координат привязывает координаты широты/долготы к реальной точке на Земле

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

Предварительная обработка геоданных (картографические проекции)

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

Картографическая проекция выводит изображение 3D-сферы на 2D-поверхностьКартографическая проекция выводит изображение 3D-сферы на 2D-поверхность

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

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

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

В Geopandas реализована довольно мощная встроенная функциональность прорисовки. Кроме того, для визуализации геоданных можно использовать другие отличные библиотеки Python, например Folium и Plotly.

Загрузите данные приложения в Pandas Dataframe.

Переведите данные в GeoDataFrame посредством преобразования информации о местоположении в её геометрический формат.

import pandas as pdimport geopandas as gpdfrom shapely.geometry import Point# Load your application data with Pandasapp_df = pd.read_csv(app_data_dir/'app.csv')# Convert it to a GeoDataFrame by transforming the Latitude/Longitude coordinates loc_crs = {'init': 'epsg:4326'}loc_geom = [Point(xy) for xy in zip(app_df['longitude'], app_df['latitude'])]geo_df = gpd.GeoDataFrame(app_df, crs=loc_crs, geometry=loc_geom)# Plot the GeoDataFramegeo_df.plot()

Затем выведите изображение GeoDataFrame.

Изображение данных о местоположенииИзображение данных о местоположении

Сами по себе точки данных не несут осмысленного контекста. Поскольку эти точки представляют собой места в Нью-Йорке, вы должны наложить их на базовую карту Нью-Йорка (которую мы загрузили из Shapefile), и только тогда такой набор точек приобретёт значимость.

Базовая карта Нью-ЙоркаБазовая карта Нью-Йорка
import matplotlib.pyplot as pltfig, ax = plt.subplots(figsize=(10,10))# Plot the base mapshape_df.plot(ax=ax, color='lightgrey', zorder=1)# Overlay the data locationsgeo_df.plot(ax=ax, alpha=0.5, zorder=2)
Для получения контекста наложите данные о местоположении на базовую картуДля получения контекста наложите данные о местоположении на базовую карту

Добавление функциональных возможностей

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

Геокодирование и обратное геокодирование

Геокодирование это способ привязки адреса, записанного в текстовом виде (например адреса дома, который вы собираетесь оценить), к координатам (широте/долготе). И, наоборот, обратное геокодирование позволяет сопоставить с координатами адрес (по улице, городу, штату и почтовому индексу). Такую функциональность обеспечивает Geopy популярная Python-библиотека.

Расстояние между двумя точками

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

  • эвклидово расстояние простое расстояние по прямой между координатами (x, y) двух точек. Это расстояние измеряется на плоской 2D-поверхности;

  • геодезическое расстояние измеряется на сферической Земле, то есть на трёхмерной поверхности. Например, кратчайшим расстоянием будет расстояние между двумя точками на сфере. Расстояние Haversine это примерно то же, что и дуга большого круга, но для его расчёта используется формула Haversine;

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

Манхэттенское расстояниеМанхэттенское расстояние

Определение направления из одной точки к другой

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

Направление между Кейптауном и МельбурномНаправление между Кейптауном и Мельбурном

Расстояние от точки до ломаной

Это ещё одна полезная информация. Например, вы хотите узнать, как далеко отстоит дом от шоссе, железнодорожного пути или автобусного маршрута? Или какое расстояние от фермы до реки?

Локализация

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

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

Перекрытие регионов

Перекрытие появляется при пересечении или объединении двух регионов. Например, перекрывает ли зона наводнения (или лесная зона) границы округа или штата.

Географическая кластеризация

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

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

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

Встраивание географических областей

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

Модели машинного обучения

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

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

Обратите внимание, что значения широты/долготы часто могут использоваться в чистом виде с древовидными моделями, такими как Random Forest или Gradient Boost, не требующими нормализации данных. Другие модели, например нейросетевые, обычно требуют нормализации значений координат.

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

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, наука о данных и выборы посвящена сопоставительной визуализации электоральных данных.

Подробнее..

Black Olives Matter раса, криминал и огонь на поражение в США. Часть 1

04.09.2020 04:18:51 | Автор: admin
Дисклеймер

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

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

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

  • Цель публикации - статистический анализ данных из открытых источников и выявление взаимосвязей и закономерностей; широкие выводы предоставляется сделать читателям.

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

  • Я не считаю себя профессиональным Data Scientist и использую самые базовые инструменты анализа данных (при этом, наверное, не всегда наиболее оптимальным способом). Буду благодарен каждому за подсказки, как можно сделать то или иное более эффективно или углубить исследование!

Во времена Советского Союза нашим с вами, уважаемые читатели, папам и мамам, дедушкам и бабушкам неустанно и отовсюду напоминали о том, как "империалисты" притесняли и угнетали представителей иных рас, как уже после отмены крепостного права в Российской Империи американские капиталисты продолжали использовать рабский труд африканцев и их потомков, как и в нынешнем (на то время) двадцатом веке издевательства не прекращаются даже после формального упразднения рабства, выражаясь в самых возмутительных формах апартеида, унижений, расизма и ненависти... Классические романы вроде "Хижины дяди Тома" Гарриет Бичер-Стоу и "Убить пересмешника" Харпер Ли еще сильнее упрочняли негодование борцов за свободу по всему миру. Да, расизм со стороны белых процветал в США до 1960-х - 1970-х. Но и, конечно, эти притеснения были отличным подспорьем для социалистической пропаганды, не щадящей красок в живописании "зверств акул капитализма". С середины 1950-х в США началось сильное движение за борьбу с расовым неравенством, которое было в итоге поддержано властями и кардинально изменило ситуацию с социальными свободами к 1980-м. Обо всем этом можно прочитать хотя бы в Википедии. А что теперь?..

Иллюстрация к роману Г. Бичер-Стоу "Хижина дяди Тома". "Классическое" изображение рабского труда африканцев.Иллюстрация к роману Г. Бичер-Стоу "Хижина дяди Тома". "Классическое" изображение рабского труда африканцев.

Почти все то же, что наши родичи читали со страниц "Правды" в 1960-х, сейчас мы слышим со всех американских СМИ. Расовая несправедливость! Насилие со стороны полиции и иных слуг закона! Как мы все видели, после гибели Джорджа Флойда в США начались массовые протесты, перешедшие местами в беспорядки и погромы под лозунгом Black Lives Matter. Итог общественного мнения в США на сегодняшний день: полиция убивает чернокожих по причине массового расизма со стороны белых.

Цели исследования

Как и многим из вас (я уверен), мне часто хочется самостоятельно разобраться в каком-то вопросе, особенно если:

  • вопрос широко обсуждается и составляет предмет споров

  • освещение почти во всех СМИ носит явно окрашенный характер (т.е. налицо пропаганда той или иной позиции)

  • есть достаточное количество исходных данных, доступных для изучения

Интересно заметить, что эти три пункта связаны между собой: 1) злободневные вопросы почти всегда однобоко освещаются прессой, так как истинно свободной прессы почти нет (да и была ли когда-то?) 2) злободневные темы порождают сообщества активистов, которые начинают собирать и анализировать данные в поддержку своей точки зрения (или во имя справедливости); также данные начинают открывать / предоставлять публике официальные источники (чтобы их нельзя было обвинить в сокрытии оных). Об имеющихся данных поговорим чуть позже, а пока - цели исследования.

Я хотел для себя ответить на несколько вопросов:

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

  2. Какова статистика совершения преступлений представителями обеих рас (в абсолютном и удельном выражениях)? Представители какой расы статистически чаще совершают преступления?

  3. Имеется ли взаимосвязь между статистикой совершения преступлений и статистикой гибели от рук полиции (в целом по США, а также отдельно для белых и черных)? Можно ли сказать, что полиция стреляет насмерть пропорционально количеству совершаемых преступлений?

  4. Каким образом найденные закономерности (по пунктам 1-3) распределены между отдельными штатами США?

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

Оговорки и допущения

Вы ведь прочитали дисклеймер в начале статьи? :) Кроме того, что там написано, вот еще несколько допущений и оговорок, принятых для исследования в основном в целях упрощения:

  • Исследование касается только США и не распространяется на другие страны.

  • Представителей чернокожей расы в США для краткости я могу называть "черными", а представителей белокожей расы - "белыми"; эти краткие наименования не отражают какого-то неуважения, а приняты именно для лаконичности.

  • Представители белокожей расы ("белые") включают латиноамериканцев (проживающих на территории США), но исключают представителей азиатских рас, американских индейцев, гавайцев, эскимосов и представителей смешанных рас, в соответствии с данными по населению в Википедии, взятыми из официальной переписи населения в США.

  • Для настоящего исследования взяты только белая и черная расы; представители иных рас, а также те, чья раса не указана в источниках, не включены в исследование. Это ограничение сделано для упрощения, основываясь на том, что эти две категории составляют совместно более 80% всего населения США. При этом я не исключаю, что на будущих этапах будут добавлены и остальные расовые категории для полной картины.

Источники данных

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

  • совершенным преступлениям с указанием расовой принадлежности, видов преступления и штатов

  • гибели от рук полиции с указанием расовой принадлежности погибших и места события (штата)

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

Для данных по преступлениям использовалась открытая база данных ФБР Crime Data Explorer, обладающая расширенным API и содержащая детальные данные по преступлениям, арестам, жертвам преступлений в США с 1991 по 2018 год.

Для данных по гибели от рук полиции использовалась открытая база данных на сайте Fatal Encounters, поддерживаемая сообществом. На настоящий момент база (доступная для скачивания) содержит более 28 тысяч записей начиная с 2000 года с подробной информацией о каждом погибшем, кратким описанием события, ссылками на СМИ, местом события и т.д. В Интернете есть и другие базы данных с тем же назначением, например, на сайте MappingPoliceViolence (около 8400 записей с 2013 г.) или БД Washington Post (ок. 5600 записей с 2015 г.). Но БД Fatal Encounters (FENC) на текущий момент самая подробная и имеет самый длинный период наблюдений (20 лет), поэтому я использовал ее. Кстати сказать, официальные источники (ФБР) также обещают открыть базу данных применения силы службами порядка, но это наступит только когда наберется представительная выборка данных. Прочитать об этой будущей официальной базе можно по ссылке.

Наконец, данные по общей численности представителей различных рас взяты из Википедии, которая в свою очередь, берет эти данные из официальных источников - Бюро переписи населения США. К сожалению, данные доступны только за промежуток с 2010 по 2018 год. В связи с этим в рамках данного исследования пришлось: 1) ограничить конечную точку наблюдений 2018 годом; 2) для промежутка с 2000 по 2009 год использовать данные по численности населения, смоделированные при помощи простой линейной регрессии (что вполне оправдано учитывая линейную природу прироста населения). Таким образом, мы будем исследовать все данные за период с 2000 г. (начальная точка в БД FENC) по 2018 г. (конечная точка в данных по численности населения). Все результаты будут основаны на наблюдениях за эти 18 лет.

Подготовка данных

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

С данными по гибели от рук полиции все понятно: просто скачиваем всю БД с сайта и сохраняем как CSV (можно оставить и в XLSX, но я предпочитаю CSV для унификации и экономии). Здесь прямая ссылка на исходный датасет в Google Spreadsheets, здесь уже готовый CSV.

Поля данных (использованные в анализе выделены жирным шрифтом):
  1. Unique ID - ID в БД

  2. Subject's name - имя жертвы

  3. Subject's age - возраст жертвы

  4. Subject's gender - пол жертвы

  5. Subject's race - раса жертвы (официально указанная)

  6. Subject's race with imputations - раса жертвы (официально указанная или заполненная экспертом)

  7. Imputation probability - вероятность экспертной оценки расы

  8. URL of image of deceased - фото жертвы

  9. Date of injury resulting in death (month/day/year) - дата события

  10. Location of injury (address) - адрес события

  11. Location of death (city) - город события

  12. Location of death (state) - штат события

  13. Location of death (zip code) - почтовый индекс адреса события

  14. Location of death (county) - округ события

  15. Full Address - полный адрес события

  16. Latitude - координата широты

  17. Longitude - координата долготы

  18. Agency responsible for death - правоохранительная служба, причинившая смерть

  19. Cause of death - причина смерти

  20. A brief description of the circumstances surrounding the death - краткое описание обстоятельств

  21. Dispositions/Exclusions INTERNAL USE, NOT FOR ANALYSIS - исключения (НЕ ДЛЯ АНАЛИЗА)

  22. Intentional Use of Force (Developing) - применение силы (намеренное)

  23. Link to news article or photo of official document - ссылка на СМИ

  24. Symptoms of mental illness? INTERNAL USE, NOT FOR ANALYSIS - симптомы помешательства жертвы (НЕ ДЛЯ АНАЛИЗА)

  25. Video - видео

  26. Date&Description - дата и описание

  27. Unique ID formula - формула ID

  28. Unique identifier (redundant) - НЕ ИСПОЛЬЗУЕТСЯ

  29. Date (Year) - год события

Данные по численности населения я сохранил с Википедии и при помощи Excel дополнил модельными данными за 2000 - 2009 гг., применив простую регрессию. Здесь можете взять Excel и итоговый CSV.

Поля данных (использованные в анализе выделены жирным шрифтом):
  1. Year - год

  2. Whitepop - численность белых

  3. Blackpop - численность черных

  4. Asianpop - численность азиатов

  5. Native Hawaiianpop - численность гавайцев

  6. American Indianpop - численность индейцев и эскимосов

  7. Unknownpop - численность других рас / без указания расы

Самое интересное - это скачать и подготовить данные по преступлениям с БД ФБР. Для этого я написал программу на Python, которая подключается к публичному API при помощи API-ключа (который я специально получил на том же сайте). API использует REST для запросов к различным имеющимся базам данных и возвращает данные в виде JSON. Программа скачивает и объединяет данные в единый DataFrame, который затем сохраняется в CSV. В тот же файл добавляются и данные по численности населения с вычислением удельных показателей по преступлениям.

Поля данных (использованные в анализе выделены жирным шрифтом):
  1. Year - год

  2. Offense - вид преступления, одно из:

    • All Offenses - все преступления

    • Assault Offenses - нападения

    • Drugs Narcotic Offenses - преступления, связанные с оборотом наркотиков

    • Larceny Theft Offenses - воровство

    • Murder And Nonnegligent Manslaughter - убийство

    • Sex Offenses - преступления на сексуальной почве

    • Weapon Law Violation - нарушение хранения / оборота оружия

  3. Class - классификатор (здесь это раса, но может быть также возраст, пол и т.д.)

  4. Offender/Victim - данные по преступникам или жертвам (в этом анализе речь пока только о преступниках)

  5. Asian - количество преступлений, совершенных азиатами

  6. Native Hawaiian - количество преступлений, совершенных гавайцами

  7. Black - количество преступлений, совершенных черными

  8. American Indian - количество преступлений, совершенных индейцами и эскимосами

  9. Unknown - количество преступлений, совершенных представителями других рас

  10. White - количество преступлений, совершенных белыми

  11. Whitepop - численность белых на соответствующий год

  12. Blackpop - численность черных на соответствующий год

  13. Asianpop - численность азиатов на соответствующий год

  14. Native Hawaiianpop - численность гавайцев на соответствующий год

  15. American Indianpop - численность индейцев и эскимосов на соответствующий год

  16. Unknownpop - численность представителей других рас на соответствующий год

  17. Asian pro capita - удельное количество преступлений, совершенных азиатами (на 1 человека)

  18. Native Hawaiian pro capita - удельное количество преступлений, совершенных гавайцами (на 1 человека)

  19. Black pro capita - удельное количество преступлений, совершенных черными (на 1 человека)

  20. American Indian pro capita - удельное количество преступлений, совершенных индейцами и эскимосами (на 1 человека)

  21. Unknown pro capita - удельное количество преступлений, совершенных представителями других рас (на 1 человека)

  22. White pro capita - удельное количество преступлений, совершенных белыми (на 1 человека)

Инструменты

Весь анализ я провожу с помощью Python 3.8, используя интерактивный Jupyter Notebook. Дополнительные библиотеки:

  • pandas 1.0.3 (для анализа данных)

  • folium 0.11 (для визуализации карт)

Все это "добро" (включая сам Python) доступно мне из дистрибутива WinPython, который я давно использую на Windows из-за его очевидных преимуществ. Вы, конечно, можете использовать любой другой на ваш вкус (например Anaconda) или вообще обойтись просто Python, установив нужные пакеты.

Вообще же, этот же анализ можно с легкостью выполнить с помощью любого другого статистического / математического ПО: R, MatLab, SAS и даже Excel. Как говорится, выбирайте свое оружие :)

В следующей части приступим непосредственно к анализу.

Подробнее..

Black Olives Matter раса, криминал и огонь на поражение в США. Часть 2

04.09.2020 08:20:07 | Автор: admin

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

Поехали!

Импортируем библиотеки и определяем путь к директории со всеми файлами:

import pandas as pd, numpy as np# путь к папке с исходными файламиROOT_FOLDER = r'c:\_PROG_\Projects\us_crimes'

Гибель от рук закона

Начнем с анализа данных по жертвам полиции. Давайте подгрузим файл из CSV в DataFrame:

# Файл с БД Fatal Encounters (FENC)FENC_FILE = ROOT_FOLDER + '\\fatal_enc_db.csv'# грузим в DataFramedf_fenc = pd.read_csv(FENC_FILE, sep=';', header=0, usecols=["Date (Year)", "Subject's race with imputations", "Cause of death", "Intentional Use of Force (Developing)", "Location of death (state)"])

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

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

Переименуем столбцы для удобства и очистим строки с пропущенными данными:

df_fenc.columns = ['Race', 'State', 'Cause', 'UOF', 'Year']df_fenc.dropna(inplace=True)

Теперь нам надо унифицировать наименования расовой принадлежности для того, чтобы в дальнейшем сопоставлять эти данные с данными по преступлениям и численности населения. Классификация рас в этих источниках немного разная. БД FENC, в частности, выделяет латиноамериканцев (Hispanic/Latino), азиатов и уроженцев тихоокеанских территорий (Asian/Pacific Islander) и среднеазиатов (Middle Eastern). Нас же интересуют только белые и черные. Поэтому сделаем укрупнение:

df_fenc = df_fenc.replace({'Race': {'European-American/White': 'White', 'African-American/Black': 'Black',                           'Hispanic/Latino': 'White', 'Native American/Alaskan': 'American Indian',                          'Asian/Pacific Islander': 'Asian', 'Middle Eastern': 'Asian',                          'NA': 'Unknown', 'Race unspecified': 'Unknown'}}, value=None)

Оставляем только данные по белым (теперь с учетом латино) и черным:

df_fenc = df_fenc.loc[df_fenc['Race'].isin(['White', 'Black'])]

Зачем нам поле "UOF" (намеренное использование силы)? Для исследования мы хотим оставить только случаи, когда полиция (или иные правоохранительные органы) намеренно применяли силу против человека. Мы опускаем случаи, когда человек совершил самоубийство (например, в результате осады полицией) или погиб в результате ДТП, преследуемый полицейскими. Это допущение сделано по двум причинам: 1) обстоятельства гибели по косвенным причинам часто не позволяют провести прямую причинно-следственную связь между действиями правоохранительных органов и смертью (пример: полицейский держит на мушке человека, который затем умирает от сердечного приступа; другой пример: при задержании преступник пускает себе пулю в лоб); 2) при рассмотрении действий властей расценивается именно применение силы; так, например, будущая официальная БД по применению силы (которую я упомянул в предыдущей статье) будет содержать именно данные, отражающая намеренное применение смертельной силы против граждан. Итак, оставляем только эти данные:

df_fenc = df_fenc.loc[df_fenc['UOF'].isin(['Deadly force', 'Intentional use of force'])]

Для удобства добавим полные названия штатов. Для этого я приготовил отдельный CSV, который мы и подгрузим в наш датасет:

df_state_names = pd.read_csv(ROOT_FOLDER + '\\us_states.csv', sep=';', header=0)df_fenc = df_fenc.merge(df_state_names, how='inner', left_on='State', right_on='state_abbr')

Отобразим начальные строки командой df_fenc.head(), чтобы получить представление о датасете:

Race

State

Cause

UOF

Year

state_name

state_abbr

0

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

1

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

2

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

3

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

4

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

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

# группируем по году и расеds_fenc_agg = df_fenc.groupby(['Year', 'Race']).count()['Cause']df_fenc_agg = ds_fenc_agg.unstack(level=1)# конвертируем численные данные в UINT16 для экономииdf_fenc_agg = df_fenc_agg.astype('uint16')

В итоге получили таблицу с 2 столбцами: White (количество белых жертв) и Black (количество черных жертв), индексированную по годам (с 2000 по 2020). Давайте взглянем на эти данные в виде графика:

# белые и черные жертвы полицейских по годам (кол-во гибелей)plt = df_fenc_agg.plot(xticks=df_fenc_agg.index)plt.set_xticklabels(df_fenc_agg.index, rotation='vertical')plt

Промежуточный вывод:

В количественном (абсолютном) выражении белых жертв больше, чем черных.

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

Подгрузим данные по численности населения (по расам):

# файл CSV с данными по населению (1991 - 2018)POP_FILE = ROOT_FOLDER + '\\us_pop_1991-2018.csv'df_pop = pd.read_csv(POP_FILE, index_col=0, dtype='int64')

Добавим эти данные в наш датасет:

# выбираем только данные по числ-ти белых и черных за 2000 - 2018 гг.df_pop = df_pop.loc[2000:2018, ['White_pop', 'Black_pop']]# объединяем датафреймы, выкидываем строки с пропускамиdf_fenc_agg = df_fenc_agg.join(df_pop)df_fenc_agg.dropna(inplace=True)# конвертируем данные по численности в целочисленный типdf_fenc_agg = df_fenc_agg.astype({'White_pop': 'uint32', 'Black_pop': 'uint32'})

ОК. Осталось создать 2 столбца с удельными значениями, разделив количество жертв на численность и умножив на миллион (количество жертв на 1 млн. человек):

df_fenc_agg['White_promln'] = df_fenc_agg['White'] * 1e6 / df_fenc_agg['White_pop']df_fenc_agg['Black_promln'] = df_fenc_agg['Black'] * 1e6 / df_fenc_agg['Black_pop']

Смотрим, что получилось:

Black

White

White_pop

Black_pop

White_promln

Black_promln

Year

2000

148

291

218756353

35410436

1.330247

4.179559

2001

158

353

219843871

35758783

1.605685

4.418495

2002

161

363

220931389

36107130

1.643044

4.458953

2003

179

388

222018906

36455476

1.747599

4.910099

2004

157

435

223106424

36803823

1.949742

4.265861

2005

181

452

224193942

37152170

2.016112

4.871855

2006

212

460

225281460

37500517

2.041890

5.653255

2007

219

449

226368978

37848864

1.983487

5.786171

2008

213

442

227456495

38197211

1.943229

5.576323

2009

249

478

228544013

38545558

2.091501

6.459888

2010

219

506

229397472

38874625

2.205778

5.633495

2011

290

577

230838975

39189528

2.499578

7.399936

2012

302

632

231992377

39623138

2.724227

7.621809

2013

310

693

232969901

39919371

2.974633

7.765653

2014

264

704

233963128

40379066

3.009021

6.538041

2015

272

729

234940100

40695277

3.102919

6.683822

2016

269

723

234644039

40893369

3.081263

6.578084

2017

265

743

235507457

41393491

3.154889

6.401973

2018

265

775

236173020

41617764

3.281493

6.367473

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

plt = df_fenc_agg.loc[:, ['White_promln', 'Black_promln']].plot(xticks=df_fenc_agg.index)plt.set_xticklabels(df_fenc_agg.index, rotation='vertical')plt

Также выведем основную статистику по этим данным:

df_fenc_agg.loc[:, ['White_promln', 'Black_promln']].describe()

White_promln

Black_promln

count (количество)

19.000000

19.000000

mean (среднее арифм.)

2.336123

5.872145

std (станд. отклонение)

0.615133

1.133677

min (мин. значение)

1.330247

4.179559

25%

1.946485

4.890977

50%

2.091501

5.786171

75%

2.991827

6.558062

max (макс. значение)

3.281493

7.765653

Промежуточные выводы:

1. В среднем от рук полиции погибает 5.9 на 1 млн. черных и 2.3 на 1 млн. белых (черных в 2.6 раз больше).

2. Разброс (отклонение) в данных по черным жертвам в 1.8 раз выше, чем в данных по белым жертвам. (На графике видно, что кривая по белым жертвам гораздо более плавная, без резких скачков.)

3. Максимальное количество жертв среди черных - в 2013 г. (7.7 на миллион); максимальное количество жертв среди белых - в 2018 г. (3.3 на миллион).

4. Жертвы среди белых монотонно растут (в среднем на 0.1 - 0.2 в год), в то время как жертвы среди черных вернулись на уровень 2009 г. после пика в 2011 - 2013 гг.

Итак, на первый поставленный вопрос мы ответили:

- Можно ли сказать, что полицейские убивают черных чаще, чем белых?

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

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

Данные по преступлениям

Загружаем наш CSV по преступлениям:

CRIMES_FILE = ROOT_FOLDER + '\\culprits_victims.csv'df_crimes = pd.read_csv(CRIMES_FILE, sep=';', header=0, index_col=0, usecols=['Year', 'Offense', 'Offender/Victim', 'White', 'White pro capita', 'Black', 'Black pro capita'])

Здесь опять-таки используем только необходимые столбцы: год, вид преступления, классификатор и данные по количеству преступлений, совершенных черными и белыми (абсолютные - "White", "Black" и удельные на человека - "White pro capita", "Black pro capita").

Взглянем на данные (`df_crimes.head()`):

Offense

Offender/Victim

Black

White

Black pro capita

White pro capita

Year

1991

All Offenses

Offender

490

598

1.518188e-05

2.861673e-06

1991

All Offenses

Offender

4

4

1.239337e-07

1.914160e-08

1991

All Offenses

Offender

508

122

1.573958e-05

5.838195e-07

1991

All Offenses

Offender

155

176

4.802432e-06

8.422314e-07

1991

All Offenses

Offender

13

19

4.027846e-07

9.092270e-08

Нам пока не нужны данные по жертвам преступлений. Убираем лишние данные и столбцы:

# оставляем только преступников (убираем жертв)df_crimes1 = df_crimes.loc[df_crimes['Offender/Victim'] == 'Offender']# берем исследуемый период (2000-2018) и удаляем лишние столбцыdf_crimes1 = df_crimes1.loc[2000:2018, ['Offense', 'White', 'White pro capita', 'Black', 'Black pro capita']]

Получили такой датасет (1295 строк * 5 столбцов):

Offense

White

White pro capita

Black

Black pro capita

Year

2000

All Offenses

679

0.000003

651

0.000018

2000

All Offenses

11458

0.000052

30199

0.000853

2000

All Offenses

4439

0.000020

3188

0.000090

2000

All Offenses

10481

0.000048

5153

0.000146

2000

All Offenses

746

0.000003

63

0.000002

...

...

...

...

...

...

2018

Larceny Theft Offenses

1961

0.000008

1669

0.000040

2018

Larceny Theft Offenses

48616

0.000206

30048

0.000722

2018

Drugs Narcotic Offenses

555974

0.002354

223398

0.005368

2018

Drugs Narcotic Offenses

305052

0.001292

63785

0.001533

2018

Weapon Law Violation

70034

0.000297

58353

0.001402

Теперь нам надо превратить удельные показатели на 1 человека в удельные на 1 миллион (так как именно эти данные используются во всем исследовании). Для этого просто умножаем на миллион соответствующие столбцы:

df_crimes1['White_promln'] = df_crimes1['White pro capita'] * 1e6df_crimes1['Black_promln'] = df_crimes1['Black pro capita'] * 1e6

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

df_crimes_agg = df_crimes1.groupby(['Offense']).sum().loc[:, ['White', 'Black']]

White

Black

Offense

All Offenses

44594795

22323144

Assault Offenses

12475830

7462272

Drugs Narcotic Offenses

9624596

3453140

Larceny Theft Offenses

9563917

4202235

Murder And Nonnegligent Manslaughter

28913

39617

Sex Offenses

833088

319366

Weapon Law Violation

829485

678861

Или в виде графика:

df_crimes_agg.plot.barh()

Итак, видим, что:

  • В количественном отношении нападения, наркотики, воровство и "все преступления" сильно превалируют над преступлениями, связанными с убийством, оружием и сексом

  • В абсолютных значениях белые совершают больше преступлений, чем черные (ровно в 2 раза для категории "все преступления")

Опять понимаем, что без информации о численности никакие выводы о "криминальности" рас не сделаешь. Соответственно, посмотрим на удельные показатели:

df_crimes_agg1 = df_crimes1.groupby(['Offense']).sum().loc[:, ['White_promln', 'Black_promln']]

White_promln

Black_promln

Offense

All Offenses

194522.307758

574905.952459

Assault Offenses

54513.398833

192454.602875

Drugs Narcotic Offenses

41845.758869

88575.523095

Larceny Theft Offenses

41697.303725

108189.184125

Murder And Nonnegligent Manslaughter

125.943007

1016.403706

Sex Offenses

3633.777035

8225.144985

Weapon Law Violation

3612.671402

17389.163849

И на графике:

df_crimes_agg1.plot.barh()

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

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

# оставляем только 'All Offenses' = все преступленияdf_crimes1 = df_crimes1.loc[df_crimes1['Offense'] == 'All Offenses']# чтобы использовать другую выборку, можем, например, оставить нападения и убийства:#df_crimes1 = df_crimes1.loc[df_crimes1['Offense'].str.contains('Assault|Murder')]# убираем абсолютные значения и агрегируем по годамdf_crimes1 = df_crimes1.groupby(level=0).sum().loc[:, ['White_promln', 'Black_promln']]

Полученный датасет:

White_promln

Black_promln

Year

2000

6115.058976

17697.409882

2001

6829.701429

20431.707645

2002

7282.333249

20972.838329

2003

7857.691182

22218.966500

2004

8826.576863

26308.815799

2005

9713.826255

30616.569637

2006

10252.894313

33189.382429

2007

10566.527362

34100.495064

2008

10580.520024

34052.276749

2009

10889.263592

33954.651792

2010

10977.017218

33884.236826

2011

11035.346176

32946.454471

2012

11562.836825

33150.706035

2013

11211.113491

32207.571607

2014

11227.354594

31517.346141

2015

11564.786088

31764.865490

2016

12193.026562

33186.064958

2017

12656.261666

34900.390499

2018

13180.171893

37805.202605

Посмотрим на графике:

plt = df_crimes1.plot(xticks=df_crimes1.index)plt.set_xticklabels(df_fenc_agg.index, rotation='vertical')plt

Промежуточные выводы:

1. Белые совершают в 2 раза больше преступлений, чем черные, в абсолютном выражении, но в 3 раза меньше в относительном выражении (на миллион представителей своей расы).

2. Преступность среди белых относительно монотонно растет на протяжении всего периода (выросла в 2 раза за 18 лет). Преступность среди черных также растет, но скачкообразно: с 2001 по 2006 г. резкий рост, с 2007 по 2016 она даже убывала, с 2017 года опять резкий рост. За весь период преступность среди черных выросла также в 2 раза (аналогично белым).

3. Если не принимать во внимание спад среди черной преступности в 2007-2016 гг., преступность среди черных растет более быстрыми темпами, чем среди белых.

Итак, мы ответили на второй вопрос:

- Представители какой расы статистически чаще совершают преступления?

- Черные статистически совершают преступления в 3 раза чаще белых.

Криминальность и гибель от рук полиции

Теперь мы подошли к самому важному: необходимо ответить на третий поставленный вопрос, а именно "Можно ли сказать, что полиция стреляет насмерть пропорционально количеству совершаемых преступлений?"

То есть надо как-то проследить корреляцию между двумя нашими наборами данных - данных по жертвам полиции и данных по преступлениям.

Начнем с того, что объединим эти два датасета в один:

# объединяем датасетыdf_uof_crimes = df_fenc_agg.join(df_crimes1, lsuffix='_uof', rsuffix='_cr')# удаляем лишние столбцы (абс. показатели по жертвам)df_uof_crimes = df_uof_crimes.loc[:, 'White_pop':'Black_promln_cr']

Что получили?

White_pop

Black_pop

White_promln_uof

Black_promln_uof

White_promln_cr

Black_promln_cr

Year

2000

218756353

35410436

1.330247

4.179559

6115.058976

17697.409882

2001

219843871

35758783

1.605685

4.418495

6829.701429

20431.707645

2002

220931389

36107130

1.643044

4.458953

7282.333249

20972.838329

2003

222018906

36455476

1.747599

4.910099

7857.691182

22218.966500

2004

223106424

36803823

1.949742

4.265861

8826.576863

26308.815799

2005

224193942

37152170

2.016112

4.871855

9713.826255

30616.569637

2006

225281460

37500517

2.041890

5.653255

10252.894313

33189.382429

2007

226368978

37848864

1.983487

5.786171

10566.527362

34100.495064

2008

227456495

38197211

1.943229

5.576323

10580.520024

34052.276749

2009

228544013

38545558

2.091501

6.459888

10889.263592

33954.651792

2010

229397472

38874625

2.205778

5.633495

10977.017218

33884.236826

2011

230838975

39189528

2.499578

7.399936

11035.346176

32946.454471

2012

231992377

39623138

2.724227

7.621809

11562.836825

33150.706035

2013

232969901

39919371

2.974633

7.765653

11211.113491

32207.571607

2014

233963128

40379066

3.009021

6.538041

11227.354594

31517.346141

2015

234940100

40695277

3.102919

6.683822

11564.786088

31764.865490

2016

234644039

40893369

3.081263

6.578084

12193.026562

33186.064958

2017

235507457

41393491

3.154889

6.401973

12656.261666

34900.390499

2018

236173020

41617764

3.281493

6.367473

13180.171893

37805.202605

Давайте вспомним, что хранится в каждом поле:

  1. White_pop - численность белых

  2. Black_pop - численность черных

  3. White promln_uof - количество жертв полиции среди белых (на 1 млн)

  4. Black promln_uof - количество жертв полиции среди черных (на 1 млн)

  5. White promln_cr - количество преступлений, совершенных белыми (на 1 млн)

  6. Black promln_cr - количество преступлений, совершенных черными (на 1 млн)

Наверное, можно было бы не полениться и дать этим столбцам русские названия... Но я надеюсь, читатели меня простят :)

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

plt = df_uof_crimes['White_promln_cr'].plot(xticks=df_uof_crimes.index, legend=True)df_uof_crimes['White_promln_uof'].plot(xticks=df_uof_crimes.index, legend=True, secondary_y=True, style='g')plt.set_xticklabels(df_uof_crimes.index, rotation='vertical')plt

То же самое на диаграмме рассеяния:

Отметим мимоходом, что определенная корреляция есть. ОК, теперь то же для черных:

plt = df_uof_crimes['Black_promln_cr'].plot(xticks=df_uof_crimes.index, legend=True)df_uof_crimes['Black_promln_uof'].plot(xticks=df_uof_crimes.index, legend=True, secondary_y=True, style='g')plt.set_xticklabels(df_uof_crimes.index, rotation='vertical')plt

И скаттерплот:

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

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

df_corr = df_uof_crimes.loc[:, ['White_promln_cr', 'White_promln_uof', 'Black_promln_cr', 'Black_promln_uof']].corr(method='pearson')df_corr.style.background_gradient(cmap='PuBu')

Получаем такую картинку:

White_promln_cr

White_promln_uof

Black_promln_cr

Black_promln_uof

White_promln_cr

1.000000

0.885470

0.949909

0.802529

White_promln_uof

0.885470

1.000000

0.710052

0.795486

Black_promln_cr

0.949909

0.710052

1.000000

0.722170

Black_promln_uof

0.802529

0.795486

0.722170

1.000000

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

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

# агрегированные значения (по годам)df_uof_crimes_agg = df_uof_crimes.loc[:, ['White_promln_cr', 'White_promln_uof', 'Black_promln_cr', 'Black_promln_uof']].agg(['mean', 'sum', 'min', 'max'])# "вероятность" преступника быть застреленнымdf_uof_crimes_agg['White_uof_cr'] = df_uof_crimes_agg['White_promln_uof'] * 100. / df_uof_crimes_agg['White_promln_cr']df_uof_crimes_agg['Black_uof_cr'] = df_uof_crimes_agg['Black_promln_uof'] * 100. / df_uof_crimes_agg['Black_promln_cr']

Получаем такие данные:

White_promln_cr

White_promln_uof

Black_promln_cr

Black_promln_uof

White_uof_cr

Black_uof_cr

mean

10238.016198

2.336123

30258.208024

5.872145

0.022818

0.019407

sum

194522.307758

44.386338

574905.952459

111.570747

0.022818

0.019407

min

6115.058976

1.330247

17697.409882

4.179559

0.021754

0.023617

max

13180.171893

3.281493

37805.202605

7.765653

0.024897

0.020541

Отобразим полученные значения в виде столбчатой диаграммы:

plt = df_uof_crimes_agg.loc['mean', ['White_uof_cr', 'Black_uof_cr']].plot.bar()

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

Промежуточные выводы:

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

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

3. Белые преступники немного чаще встречают смерть от рук полиции, чем черные. Однако эта разница почти несущественна.

Итак, ответ на третий вопрос:

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

- Да, такая корреляция наблюдается, хотя она неоднородна по расам: для белых почти идеальная, для черных - почти неидеальная.

В следующей части статьи посмотрим на географическое распределение анализируемых данных по штатам США.

Подробнее..

Преступления на почве расизма в США статистический анализ

15.09.2020 04:20:57 | Автор: admin
После моей недавней статьи (части 1, 2, 3) о криминале и полицейской стрельбе в США и их связи с расовой принадлежностью я решил продолжить эту тему и в таком же ключе проанализировать другие открытые данные благо, таких еще достаточно благодаря программе криминальной отчетности ФБР.

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

Дисклеймер
Позвольте мне быть ленивым и отправить вас, уважаемые читатели, посмотреть дисклеймер в начале моей первой статьи о криминале в США :) Все, что там написано, подходит и для этого исследования.


Исходные данные


В качестве исходных данных я скачал датасет с сайта Crime Data Explorer, поддерживаемого ФБР в рамках программы криминальной отчетности США (об этом сайте я уже писал в предыдущей статье). Скачать по прямой ссылке можно здесь (4.4 МБ). Скачанный архив содержит собственно сами данные в формате CSV, а также текстовое описание. Данные я никак не трансформировал, поэтому если вы захотите повторить анализ самостоятельно, вы должны получить те же результаты.

Также я использовал данные по численности населения США с разбивкой по расовой принадлежности, полученные из официальных данных Бюро переписи населения и дополненные модельными данными за период с 1991 по 2009 г. Скачать можно здесь (Яндекс.Диск). Для анализа географического распределения удельных показателей мне понадобилась и численность населения по штатам, полученные из того же источника (скачать здесь). Эти же данные я использовал в своей предыдущей статье.

Что такое преступления на почве нетерпимости и как они регистрируются?


С сайта ФБР:
The FBIs UCR Program defines hate crime as a committed criminal offense which is motivated, in whole or in part, by the offenders bias(es) against a race, religion, disability, sexual orientation, ethnicity, gender, or gender identity.

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

В справке, сопровождающей исходный датасет по преступлениям, также указано следующее (курсив сохранен):
Because motivation is subjective, it is sometimes difficult to know with certainty whether a crime resulted from the offenders bias. Moreover, the presence of bias alone does not necessarily mean that a crime can be considered a hate crime. Only when a law enforcement investigation reveals sufficient evidence to lead a reasonable and prudent person to conclude that the offenders actions were motivated, in whole or in part, by his or her bias, should an agency report an incident as a hate crime.

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

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

Структура базы данных


База содержит данные по преступлениям на почве нетерпимости с 1991 по 2018 г. На момент написания статьи последняя запись датируется 31 декабря 2018 г., всего 201403 записи. Каждая запись один случай преступления. Таким образом, получаем в среднем 7193 преступлений в год.

Список полей базы данных в исходном CSV формате
  1. INCIDENT_ID: ID события (преступления)
  2. DATA_YEAR: год, в который совершено преступление
  3. ORI: ID агентства (службы правопорядка), предоставившего данные
  4. PUB_AGENCY_NAME: публичное название агентства / службы (обычно совпадает с городом)
  5. PUB_AGENCY_UNIT: название подразделения службы (например, округ)
  6. AGENCY_TYPE_NAME: тип службы (муниципальная / окружная)
  7. STATE_ABBR: сокращенное наименование штата
  8. STATE_NAME: полное название штата
  9. DIVISION_NAME: название региона (куда входят несколько штатов)
  10. REGION_NAME: название макрорегиона (куда входят несколько регионов)
  11. POPULATION_GROUP_CODE: код места совершения преступления по численности населения
  12. POPULATION_GROUP_DESC: описание места совершения преступления по численности населения (например город с населением от 0,5 до 1 млн.)
  13. INCIDENT_DATE: дата совершения преступления
  14. ADULT_VICTIM_COUNT: количество совершеннолетних пострадавших
  15. JUVENILE_VICTIM_COUNT: количество несовершеннолетних пострадавших
  16. TOTAL_OFFENDER_COUNT: общее количество преступников
  17. ADULT_OFFENDER_COUNT: количество совершеннолетних преступников
  18. JUVENILE_OFFENDER_COUNT: количество несовершеннолетних преступников
  19. OFFENDER_RACE: раса/-ы преступника/-ов
  20. OFFENDER_ETHNICITY: этническая группа преступника/-ов (латиноамериканская / не латиноамериканская / смешанная / неизвестная)
  21. VICTIM_COUNT: общее количество пострадавших (физических и юридических лиц)
  22. OFFENSE_NAME: вид совершенного преступления
  23. TOTAL_INDIVIDUAL_VICTIMS: общее количество пострадавших (физических лиц)
  24. LOCATION_NAME: место преступления (например, квартира, шоссе, школа и т.д.)
  25. BIAS_DESC: вид нетерпимости (мотив преступления)
  26. VICTIM_TYPES: категория/-и пострадавших (физические лица / власти / частная компания и т.п.)
  27. MULTIPLE_OFFENSE: маркер множественного преступления (т.е. если совершено не одно, а несколько видов преступлений)
  28. MULTIPLE_BIAS: маркер множественного мотива (не один, а несколько видов нетерпимости)



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

Виды преступлений


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

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

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

Все 48 категорий преступлений (названия в оригинале)
Aggravated Assault
All Other Larceny
Animal Cruelty
Arson
Assisting or Promoting Prostitution
Betting/Wagering
Bribery
Burglary/Breaking & Entering
Counterfeiting/Forgery
Credit Card/Automated Teller Machine Fraud
Destruction/Damage/Vandalism of Property
Drug Equipment Violations
Drug/Narcotic Violations
Embezzlement
Extortion/Blackmail
False Pretenses/Swindle/Confidence Game
Fondling
Hacking/Computer Invasion
Human Trafficking, Commercial Sex Acts
Identity Theft
Impersonation
Incest
Intimidation
Kidnapping/Abduction
Motor Vehicle Theft
Murder and Nonnegligent Manslaughter
Negligent Manslaughter
Not Specified
Pocket-picking
Pornography/Obscene Material
Prostitution
Purchasing Prostitution
Purse-snatching
Rape
Robbery
Sexual Assault With An Object
Shoplifting
Simple Assault
Sodomy
Statutory Rape
Stolen Property Offenses
Theft From Building
Theft From Coin-Operated Machine or Device
Theft From Motor Vehicle
Theft of Motor Vehicle Parts or Accessories
Weapon Law Violations
Welfare Fraud
Wire Fraud


Виды нетерпимости / мотивы преступлений


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


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

Категории пострадавших


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

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

Препарируем данные


image

Как и в предыдущей статье, весь анализ я выполняю в Jupyter Lab / Notebook на Python 3.8. Здесь я не буду приводить и комментировать сам код его вы можете скачать по этой ссылке (в архиве листинг и файл в формате Jupyter Notebook). Сосредоточимся больше на получаемых результатах. Все графики кликабельны.

Обзор данных и топ-листы


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

YEAR STATE_NAME OFFENDER_RACE OFFENSE_NAME BIAS_DESC VICTIM_TYPES
0 1991 Arkansas White Intimidation Anti-Black or African American Individual
1 1991 Arkansas Black or African American Simple Assault Anti-White Individual
2 1991 Arkansas Black or African American Aggravated Assault Anti-Black or African American Individual
3 1991 Arkansas Black or African American Aggravated Assault;Destruction/Damage/Vandalis... Anti-White Individual
4 1991 Arkansas Black or African American Aggravated Assault Anti-White Individual
... ... ... ... ... ... ...
201398 2018 West Virginia NaN Burglary/Breaking & Entering Anti-Black or African American Individual
201399 2018 West Virginia White Simple Assault Anti-Black or African American Individual
201400 2018 West Virginia NaN Intimidation Anti-Asian Individual
201401 2018 West Virginia White Intimidation Anti-White Law Enforcement Officer
201402 2018 West Virginia NaN Burglary/Breaking & Entering;Destruction/Damag... Anti-Other Religion Religious Organization

201403 rows 6 columns



Более 200 тысяч строк и чуть больше 8 МБ памяти. Давайте для начала посмотрим на топ-10 совершаемых преступлений, видов нетерпимости (мотивов), рас преступников и категорий жертв:

Кликабельно

Кликабельно

Итак, что мы здесь наблюдаем:
  • Самый распространенный вид преступления порча имущества / вандализм, за ним с небольшим отставанием личная угроза (intimidation). Каждый из этих видов преступления занимает почти треть всех случаев. Далее идут нападения, а грабеж и другие преступления уже гораздо менее распространены.
  • Среди видов нетерпимости с большим отрывом лидирует нетерпимость к чернокожим (больше трети всех случаев), затем в порядке убывания, но примерно на одном уровне: нетерпимость к евреям, нетерпимость к белым, нетерпимость к геям. Остальные виды нетерпимости статистически на порядок реже лидера.
  • 70% всех преступлений совершаются белыми, порядка 23% черными, остальные в пределах погрешности.
  • Преступления против личности составляют 80% всех случаев.


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


OFFENSE_COUNT TOP_OFFENSE TOP_OFFENSE_SHARE TOP_BIAS TOP_BIAS_SHARE TOP_VICTIM TOP_VICTIM_SHARE
OFFENDER_RACE
White 79514 Intimidation 36.796036 Anti-Black or African American 46.877279 Individual 92.730840
Black or African American 25956 Simple Assault 36.292187 Anti-White 46.594236 Individual 94.760364
Multiple 4047 Simple Assault 36.545589 Anti-Black or African American 29.033852 Individual 91.153941
Asian 1453 Simple Assault 31.865107 Anti-Black or African American 30.075705 Individual 93.048864
American Indian or Alaska Native 1095 Simple Assault 40.182648 Anti-White 31.415525 Individual 93.059361
Native Hawaiian or Other Pacific Islander 35 Simple Assault 45.714286 Anti-Other Religion 22.857143 Individual 77.142857


В этой таблице:

  • OFFENSE_COUNT общее количество преступлений, совершенных представителями данной расы
  • TOP_OFFENSE самый частый вид преступления для представителей данной расы
  • TOP_BIAS самый частый вид нетерпимости (мотива преступления) для представителей данной расы
  • TOP_VICTIM самая частая категория потерпевших для представителей данной расы

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

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

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

Взглянем на количество преступлений по годам для каждой расы преступников:

Кликабельно

Как и следовало предположить, преступления белых и черных на графике доминируют над остальными расами, по которым даже не видно изменений из-за разницы в масштабе. Пик со стороны белых здесь приходится на 1995 2002 гг., а со стороны черных начало 1990-х. С 2002 г. количество преступлений, совершенных белыми на почве нетерпимости, довольно уверенно спадало, снизившись в 2 раза по сравнению с пиковым 2001-м; однако после 2016 г. вновь начало круто расти. Нетерпимость среди черных плавно спадала с 1995 по 2004 г., однако затем так же плавно начала расти, выйдя в 2018 г. на уровень 1995 г.

Здесь интересно отметить, что при Бараке Обаме (который, как мы знаем, принадлежит к афроамериканской расе), то есть с 2009 по 2017 гг., количество преступлений среди белых очень резко снизилось, но в это же самое время количество преступлений среди черных обнаружило стабильный рост. Ранее при Буше (2001 2009 гг.) после пика преступлений среди белых в первый год его президентства количество преступлений, совершаемых представителями обеих рас, вышло на полку и практически не менялось. А вот при Клинтоне (1993 2001 гг.) преступления среди белых росли быстро, почти год от года, в то время как преступления среди черных, наоборот, плавно снизились.

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

Первая фильтрация: по видам преступлений и потерпевших


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

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

Посмотрим, что получилось:


YEAR STATE_NAME OFFENDER_RACE OFFENSE_NAME BIAS_DESC COUNT
0 1991 Arizona Black or African American Assault Anti-Gay (Male) 1
1 1991 Arizona Black or African American Assault Anti-White 4
2 1991 Arizona White Assault Anti-Black or African American 10
3 1991 Arkansas Black or African American Assault Anti-Black or African American 1
4 1991 Arkansas Black or African American Assault Anti-White 4
... ... ... ... ... ... ...
16428 2018 Wisconsin White Assault Anti-Hispanic or Latino 1
16429 2018 Wisconsin White Assault Anti-Hispanic or Latino;Anti-White 1
16430 2018 Wisconsin White Assault Anti-Physical Disability 1
16431 2018 Wisconsin White Assault Anti-Sikh 1
16432 2018 Wisconsin White Assault Anti-White 1

16433 rows 6 columns



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

В качестве промежуточного шага посмотрим на распределение преступлений по расе преступников:

Кликабельно

и по видам нетерпимости:

Кликабельно

Итак, белые и черные преступники составляют вместе 93% всех случаев (преступлений среди белых в два раза больше, но мы же знаем, что белых и самих в 5 раз больше). Поэтому совершенно не удивляет и то, что почти та же пропорция и по видам нетерпимости: 33% преступлений мотивировано нетерпимостью к черным, 18% нетерпимостью к белым. Здесь попутно интересно отметить, что преступления на почве нетерпимости к белым количественно примерно равны преступлениям на почве нетерпимости к гомосексуалам это третий по величине мотив.

Вторая фильтрация: по расе преступников и мотиву преступлений


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


YEAR STATE_NAME OFFENDER_RACE OFFENSE_NAME BIAS_DESC COUNT
0 1991 Arizona Black Assault Anti-White 4
1 1991 Arizona White Assault Anti-Black 10
2 1991 Arkansas Black Assault Anti-Black 1
3 1991 Arkansas Black Assault Anti-White 4
4 1991 Arkansas Black Murder Anti-White 1
... ... ... ... ... ... ...
3870 2018 West Virginia White Assault Anti-White 2
3871 2018 Wisconsin Black Assault Anti-Black 1
3872 2018 Wisconsin Black Assault Anti-White 4
3873 2018 Wisconsin White Assault Anti-Black 6
3874 2018 Wisconsin White Assault Anti-White 2

3875 rows 6 columns



Пора включить в анализ удельные показатели (на количество населения по каждой расовой группе). Для этого подгрузим данные по численности населения из файла us_pop_1991-2018.csv.

Посмотрим, как в целом распределено население США по расам (среднегодовые показатели за 1991 2018 гг.):

Кликабельно

Белых получается в 5.8 раз больше, чем черных. Все остальные расы составляют 11% населения.

Далее добавляем в наш датасет данные по численности и вычисляем удельное количество преступлений на 1 млн. представителей каждой расы.

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

Кликабельно

Отдельно для убийств и изнасилований (т.к. их на общих графиках не видно):

Кликабельно

Что же мы здесь видим? А видим вот что:

  • Из анализируемых видов преступлений с большим отрывом лидируют нападения (в 25 раз больше, чем грабеж, в 250 раз чем убийство и изнасилование).
  • Нападений, совершенных белыми, в два раза больше, но в удельном отношении черные совершают нападения почти в 3 раза чаще.
  • Грабежей, совершенных черными, в 1.5 раза больше в абсолютных цифрах и в 10 раз больше в удельных.
  • Суммарно белые совершили несколько больше убийств, чем черные, и примерно столько же изнасилований. В удельном же выражении черные насилуют в 6 раз чаще и убивают в 3.6 раз чаще, чем белые. Между убийством и изнасилованием белые предпочитают убийство, а черные изнасилование.


Преступления на почве расизма по годам


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

Кликабельно

Кликабельно

Нетрудно по этим графикам сделать очевидные выводы:

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


Взглянем и на обобщенные (среднегодовые) показатели:

Кликабельно

Лишний раз убеждаемся в сделанных наблюдениях: в среднем белые совершают на 15-16% больше преступлений на почве расизма, но при этом из-за разницы в численности белых и черных, как мы знаем, почти в 6 раз, черные в столько же раз чаще совершают такие преступления.

Только ли белые против черных, черные против белых?


Давайте к нашим обобщенным показателям добавим параметр нетерпимости, т.е. мотива преступлений. Как вы помните, в результате фильтрации мы оставили только два мотива: нетерпимость к белой расе и нетерпимость к чернокожей расе. Как они распределены между белыми и черными преступниками? 100% полярно, конечно же? А вот и нет!

Кликабельно

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

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

География преступлений


Наконец, посмотрим на распределение преступности на почве расизма по штатам США. Для вычисления удельных показателей нам, как обычно, надо будет загрузить численность по штатам и расам из файла us_pop_states_race_2010-2019.csv.

Среднегодовые показатели численности белых и черных по штатам:

Кликабельно

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

Но везде ли одинакова доля чернокожего населения пресловутые 13%? Посмотрим:

Кликабельно

Итак, черных больше половины всего Округа Колумбия (где находится славный город Пентагон Вашингтон), около трети в хлопковых южных штатах Миссисипи, Луизиане, Джорджии, Алабаме, Южной Каролине и в одном северном (Мэриленде, где Округ Колумбия, где славный город...); в остальных меньше четверти. Это, как говорится, просто для справки.

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

Кликабельно

Кликабельно

Видно, что по абсолютному количеству преступлений лидирует самый населенный штат Калифорния. Но далее Флориду и Техас потеснили Мичиган, Иллинойс и Мэриленд экономически более развитые северные штаты. А на удельном графике выбились вперед наименее населенные штаты Монтана, Вермонт, Айдахо, Северная и Южная Дакота, Аляска (все тоже на севере страны). При этом видно, что пропорция преступлений между белыми и черными не одинакова по штатам, несмотря на доминирование черных по удельным показателям (например, в белую сторону выделяется Округ Колумбия и Гавайи).

Отобразим на карте (для большей наглядности заменим среднегодовые показатели накопленной суммой).

Сначала в абсолютном выражении:

Кликабельно

Здесь выделяется северо-восток страны (особенно Мичиган) плюс Калифорния и Вашингтон на тихоокеанском побережье. И в удельном выражении:

Кликабельно

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

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

Вместо выводов


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

Конечно, этот маленький анализ далеко не полный. Он выполнен поверхностно, ведь я только рассмотрел базовые показатели, не применяя даже четверти доступного аппарата математической статистики. Конечно, и о качестве исходных данных можно спорить. Всем ли регистрируемым данным можно верить? Все ли преступления регистрируются? Кто и как определяет мотивы нетерпимости? Но как по мне, я лучше буду анализировать официальные открытые данные as-is, чем довольствоваться заявлениями вроде в крови господина N нашли отравляющее вещество X или страна W уже на протяжении десятков лет систематически нарушает права человека.

PS. В комментариях к моему предыдущему исследованию меня много раз просили разделить чисто белых и латиносов, так как культуры все-таки имеют различия. Я бы и не против, но, к сожалению, это невозможно сделать ни в том, ни в этом случае по причине скудности сведений об этом этническом признаке в исходных данных. Так, например, в данных, которые мы разбираем в этой статье, из 79514 преступлений, совершенных белыми, только 6999 имеют пометку об этнической принадлежности, причем только 489 помечены как Hispanic or Latino (это 0,6%). Конечно, такие данные нельзя использовать для анализа.
Подробнее..

Из песочницы Формат таблиц в pandas

03.10.2020 16:08:05 | Автор: admin

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


Например, в excel для этого используется условное форматирование и спарклайны. А в этой статье мы посмотрим как визуализировать данные с помощью Python и библиотеки pandas: будем использовать свойства DataFrame.style и Options and settings.


Настраиваем базовую визуализацию


Импортируем библиотеки: pandas для работы с данными и seaborn для загрузки классического набора данных penguins:


import pandas as pdimport seaborn as sns

С помощью pd.set_option настроим вывод так чтобы:


  • количество строк в таблице было не больше 5;
  • текст в ячейке отражался полностью вне зависимости от длины (это удобно, если в ячейке длинный заголовок или URL, которые хочется посмотреть);
  • все числа отражались с двумя знаками после запятой;

pd.set_option('max_rows', 5)pd.set_option('display.max_colwidth', None)pd.set_option('display.float_format', '{:.2f}'.format)

Прочитаем и посмотрим датафрейм.


penguins = sns.load_dataset(penguins)penguins

image


Если нужно вернуть настройки к дефолтным, используем pd.reset_option. Например, так, если хотим обновить все настройки разом:


pd.reset_option('all')

Полный список свойств set_option.


Настраиваем отображение данных в таблицах


Формат чисел, пропуски и регистр


У датафреймов в pandas есть свойство DataFrame.style, которое меняет отображение содержимого ячеек по условию для строк или столбцов.


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


(penguins .head(5) .style .format('{:.1f}', na_rep='-') .format({'species': lambda x:x.lower(),          'island': lambda x:x.lower(),          'sex': lambda x: '-' if pd.isna(x) else x.lower()         }))

image


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


(df.style.format({'price': '{:.2f}'}))

Дальше больше!


Выделение цветом (минимум, максимум, пропуски)


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


numeric_columns = ['bill_length_mm',                   'bill_depth_mm',                   'flipper_length_mm',                   'body_mass_g']

Подсветим минимум, максимум и пустые ячейки и выведем первые 5 строк датафрейма.


(penguins .head(5) .style .format('{:.1f}', na_rep='-') .format({'species': lambda x:x.lower(),          'island': lambda x:x.lower(),          'sex': lambda x: '-' if pd.isna(x) else x.lower()         }) .highlight_null(null_color='lightgrey') .highlight_max(color='yellowgreen', subset=numeric_columns) .highlight_min(color='coral', subset=numeric_columns))

image


Наглядно видно, что в этих 5ти строках самый длинный клюв у пингвина в строке с индексом 2 и у него (неё!) же самые длинные плавники и самый маленький вес.


Усложним ещё немного: посмотрим на разброс длины плавников пингвинов-девочек вида Adelie.


Bar chart в таблице


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


adelie_female = (penguins[(penguins['species'] == 'Adelie') &                           (penguins['sex'] == 'FEMALE')]                 .copy()                )adelie_female['flipper_l_var'] = ((adelie_female['flipper_length_mm']-                                                  adelie_female['flipper_length_mm'].mean()).round())

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


  • группу столбцов (subset), для которых будем строить график;
  • выравнивание (align): mid так как мы ожидаем, что значения будут как положительные, так и отрицательные. Подробнее про другие параметры выравнивания можно посмотреть тут;
  • цвет (color). В нашем случае 2 цвета: для отрицательных и положительных значений;
  • границы (vmin, vmax).

Отдельно с помощью set_properties пропишем, что значения в столбце 'flipper_l_var' должны стоять в центре ячейки.


(adelie_female .head(5) .style .format('{:.1f}', na_rep='-') .format({'species': lambda x:x.lower(),          'island': lambda x:x.lower(),          'sex': lambda x: '-' if pd.isna(x) else x.lower()         }) .bar(subset=['flipper_l_var'],      align='mid',      color=['coral', 'yellowgreen'],      vmin=adelie_female['flipper_l_var'].min(),      vmax=adelie_female['flipper_l_var'].max()     ) .set_properties(**{'text-align': 'center'}, subset='flipper_l_var'))

image


Heatmap в таблице


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


Посчитаем количество пингвинов разных видов и средние значения массы, длин плавников и клюва в зависимости от вида.


species_stat=(penguins             .groupby('species')             .agg(penguins_count=('species','count'),                  mean_bill_length=('bill_length_mm', 'mean'),                  mean_bill_depth=('bill_depth_mm', 'mean'),                  mean_flipper_length=('flipper_length_mm', 'mean'),                  mean_body_mass=('body_mass_g', 'mean'),                 )             )

image


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


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


(species_stat .T .style .format("{:.1f}") .background_gradient(cmap='Blues', axis=1))

image


Транспонируем таблицу так нагляднее сравнение между видами и применяем метод background_gradient со следующими параметрами:


  • цветовая карта(cmap): Blues. Это одна из дефолтных карт;
  • сравнение по строкам (axis=1).

Вывод


Форматирование таблиц в pandas с помощью DataFrame.style и Options and settings упрощает жизнь, ну или как минимум улучшает читабельность кода и отчетов. Но обработку типов данных, пропусков и регистра лучше, конечно, проводить осознанно ещё до этапа визуализации.


Дополнительно можно разобраться с:


Подробнее..
Категории: Python , Data analysis , Data visualization , Pandas

Участвуем в соревновании по Data Science. Первый опыт

01.12.2020 00:06:56 | Автор: admin
Привет, Хабр!

Давно я не писал никаких статей и, вот думаю, пришло время написать о там, как мне пригодились знания по data science, полученные по ходу обучения небезывестной специализации от Яндекса и МФТИ Машинное обучение и анализ данных. Правда, справедливости ради надо отметить, что знания до конца не получены спецуха не завершена :) Однако, решать простенькие реальные бизнесовые задачи уже можно. Или нужно? На этот вопрос будет ответ, буквально через пару абзацев.

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

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

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

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

Сопоставление названий


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

Задача


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

Для более эффективной обработки информации о потенциальных клиентах, СИБУРу необходимо знать, связаны ли два названия (т.е. принадлежат одной компании или аффилированным компаниям).

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

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

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

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

Дополнительная информация о задаче


Раскрой меня для получения дополнительной информации
Аффилированными компаниями считаются компании, принадлежащие одному холдингу или группе компаний. Например, все компании из списка: Сибур Нефтехим, ООО Сибур, Sibur Digital, СИБУР ИТ, Sibur international GMBH являются вариациями названий аффилированных компаний, а компания Сибирь International GMBH не является.
Названия компаний могут писаться на разных языках: тренировочная и тестовая выборки содержат названия компаний на русском, английском и китайском языках.
В названиях могут присутствовать сокращения, опечатки и дополнительная информация о компании, например, названия стран и провинций.
Публичная (50%) и приватная (50%) части тестового множества не пересекаются.

Правила использования внешних источников


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

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

Публичность Источник должен быть заявлен в чате до 24:00 6 декабря 2020 с хэштегом #внешниеданные и одобрен организаторами.

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

Источник должен допускать коммерческое использование.

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

Один участник может заявить не более 10 источников информации.

Использование API поисковых систем, к сожалению, противоречит пункту 2.

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


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

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

Это касается legel entities, стран, городов и т.д. Например, исключение слова Industries из всех названий допустима.

Замена значимых элементов названия компании возможна только на основании внешних данных. Ручная замена не разрешается.

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

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

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

Использование open source


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

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


Данные


train.csv тренировочное множество

test.csv тестовое множество

sample_submission.csv пример решения в правильном формате

Naming baseline.ipynb код базовое решение

baseline_submission.csv базовое решение

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

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

План решения задачи


Настройка технических инструментов


Загрузим библиотеки
Напишем вспомогательные функции

Предобработка данных


Загрузим данные
Посмотрим на данные и сделаем копии
Переведем все символы из текста в нижний регистр
Удалим названия стран
Удалим знаки и спецсимволы
Удалим цифры
Удалим первый список стоп-слов. Вручную!
Проведем транслитерацию русского текста в латиницу
Запустим автоматическое составление списка топ 50 самых часто встречающихся слов & Drop it smart. Первый ЧИТ

Генерация и анализ фич


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

Настройка и обучение модели


Итоги соревнования


Источники информации



Теперь, когда мы ознакомились с планом проведения исследования, перейдем к его реализации.

Настройка технических инструментов


Загрузка библиотек


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

Установим библиотеку для определения списка стран и последующего их удаления из текста
pip install pycountry


Установим библиотеку для определения дистанции Левенштейна между словами из текста друг сдругом и со словами из различных списков
pip install strsimpy


Установим библиотеку, с помощью которой проведем транслитерацию русского текста в латиницу
pip install cyrtranslit


Подтянем библиотеки
import pandas as pdimport numpy as npimport warningswarnings.filterwarnings('ignore')import pycountryimport refrom tqdm import tqdmtqdm.pandas()from strsimpy.levenshtein import Levenshteinfrom strsimpy.normalized_levenshtein import NormalizedLevenshteinimport matplotlib.pyplot as pltfrom matplotlib.pyplot import figureimport seaborn as snssns.set()sns.set_style("whitegrid")from sklearn.model_selection import train_test_splitfrom sklearn.model_selection import StratifiedKFoldfrom sklearn.model_selection import StratifiedShuffleSplitfrom scipy.sparse import csr_matriximport lightgbm as lgbfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_scorefrom sklearn.metrics import recall_scorefrom sklearn.metrics import precision_scorefrom sklearn.metrics import roc_auc_scorefrom sklearn.metrics import classification_report, f1_score# import googletrans# from googletrans import Translatorimport cyrtranslit



Напишем вспомогательные функции


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

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

Итак, первая функция переводит текст в нижний регистр
Код
# convert text to lowercasedef lower_str(data,column):    data[column] = data[column].str.lower()


Следующие четыре функции помогают визуализировать пространство исследуемых признаков и их способность разделять объекты по целевым меткам 0 или 1.
Код
# statistic table for analyse float values (it needs to make histogramms and boxplots)def data_statistics(data,analyse,title_print):    data0 = data[data['target']==0][analyse]    data1 = data[data['target']==1][analyse]    data_describe = pd.DataFrame()    data_describe['target_0'] = data0.describe()    data_describe['target_1'] = data1.describe()    data_describe = data_describe.T        if title_print == 'yes':        print ('\033[1m' + 'Дополнительные статистики по признаку',analyse,'\033[m')     elif title_print == 'no':        None        return data_describe# histogramms for float valuesdef hist_fz(data,data_describe,analyse,size):    print ()    print ('\033[1m' + 'Information about',analyse,'\033[m')    print ()        data_0 = data[data['target'] == 0][analyse]    data_1 = data[data['target'] == 1][analyse]        min_data = data_describe['min'].min()    max_data = data_describe['max'].max()        data0_mean = data_describe.loc['target_0']['mean']    data0_median = data_describe.loc['target_0']['50%']    data0_min = data_describe.loc['target_0']['min']    data0_max = data_describe.loc['target_0']['max']    data0_count = data_describe.loc['target_0']['count']            data1_mean = data_describe.loc['target_1']['mean']    data1_median = data_describe.loc['target_1']['50%']     data1_min = data_describe.loc['target_1']['min']    data1_max = data_describe.loc['target_1']['max']    data1_count = data_describe.loc['target_1']['count']            print ('\033[4m' + 'Analyse'+ '\033[m','No duplicates')        figure(figsize=size)    sns.distplot(data_0,color='darkgreen',kde = False)        plt.scatter(data0_mean,0,s=200,marker='o',c='dimgray',label='Mean')    plt.scatter(data0_median,0,s=250,marker='|',c='black',label='Median')    plt.legend(scatterpoints=1,               loc='upper right',               ncol=3,               fontsize=16)    plt.xlim(min_data, max_data)    plt.show()    print ('Quantity:', data0_count,           '         Min:', round(data0_min,2),           '         Max:', round(data0_max,2),           '         Mean:', round(data0_mean,2),           '         Median:', round(data0_median,2))        print ()    print ('\033[4m' + 'Analyse'+ '\033[m','Duplicates')        figure(figsize=size)    sns.distplot(data_1,color='darkred',kde = False)    plt.scatter(data1_mean,0,s=200,marker='o',c='dimgray',label='Mean')    plt.scatter(data1_median,0,s=250,marker='|',c='black',label='Median')    plt.legend(scatterpoints=1,               loc='upper right',               ncol=3,               fontsize=16)    plt.xlim(min_data, max_data)    plt.show()    print ('Quantity:', data_1.count(),           '         Min:', round(data1_min,2),           '         Max:', round(data1_max,2),           '         Mean:', round(data1_mean,2),           '         Median:', round(data1_median,2))# draw boxplotdef boxplot(data,analyse,size):    print ('\033[4m' + 'Analyse'+ '\033[m','All pairs')        data_0 = data[data['target'] == 0][analyse]    data_1 = data[data['target'] == 1][analyse]        figure(figsize=size)    sns.boxplot(x=analyse,y='target',data=data,orient='h',                showmeans=True,                                meanprops={"marker":"o",                           "markerfacecolor":"dimgray",                            "markeredgecolor":"black",                          "markersize":"14"},               palette=['palegreen', 'salmon'])    plt.ylabel('target', size=14)    plt.xlabel(analyse, size=14)    plt.show()# draw graph for analyse two choosing features for predict traget labeldef two_features(data,analyse1,analyse2,size):    fig = plt.subplots(figsize=size)        x0 = data[data['target']==0][analyse1]    y0 = data[data['target']==0][analyse2]    x1 = data[data['target']==1][analyse1]    y1 = data[data['target']==1][analyse2]        plt.scatter(x0,y0,c='green',marker='.')    plt.scatter(x1,y1,c='black',marker='+')    plt.xlabel(analyse1)    plt.ylabel(analyse2)    title = [analyse1,analyse2]    plt.title(title)    plt.show()


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

Иными словами, после формирования вектора прогнозов, нам потребуется сопоставить прогноз с целевыми метками. Результатом такого сопоставления должна получиться таблица сопряжения для каждой пары компаний из обучающей выборки. В таблице сопряжения для каждой пары будет определен результат соответствия прогноза к классу из обучающей выборки. Классификация соответствия принята такой: 'True positive', 'False positive', 'True negative' или 'False negative'. Эти данные очень важны для анализа работы алгоритма и принятия решений по доработке модели и признакового пространства.
Код
def contingency_table(X,features,probability_level,tridx,cvidx,model):    tr_predict_proba = model.predict_proba(X.iloc[tridx][features].values)    cv_predict_proba = model.predict_proba(X.iloc[cvidx][features].values)    tr_predict_target = (tr_predict_proba[:, 1] > probability_level).astype(np.int)    cv_predict_target = (cv_predict_proba[:, 1] > probability_level).astype(np.int)    X_tr = X.iloc[tridx]    X_cv = X.iloc[cvidx]    X_tr['predict_proba'] = tr_predict_proba[:,1]    X_cv['predict_proba'] = cv_predict_proba[:,1]    X_tr['predict_target'] = tr_predict_target    X_cv['predict_target'] = cv_predict_target    # make true positive column    data = pd.DataFrame(X_tr[X_tr['target']==1][X_tr['predict_target']==1]['pair_id'])    data['True_Positive'] = 1    X_tr = X_tr.merge(data,on='pair_id',how='left')    data = pd.DataFrame(X_cv[X_cv['target']==1][X_cv['predict_target']==1]['pair_id'])    data['True_Positive'] = 1    X_cv = X_cv.merge(data,on='pair_id',how='left')    # make false positive column    data = pd.DataFrame(X_tr[X_tr['target']==0][X_tr['predict_target']==1]['pair_id'])    data['False_Positive'] = 1    X_tr = X_tr.merge(data,on='pair_id',how='left')    data = pd.DataFrame(X_cv[X_cv['target']==0][X_cv['predict_target']==1]['pair_id'])    data['False_Positive'] = 1    X_cv = X_cv.merge(data,on='pair_id',how='left')    # make true negative column    data = pd.DataFrame(X_tr[X_tr['target']==0][X_tr['predict_target']==0]['pair_id'])    data['True_Negative'] = 1    X_tr = X_tr.merge(data,on='pair_id',how='left')    data = pd.DataFrame(X_cv[X_cv['target']==0][X_cv['predict_target']==0]['pair_id'])    data['True_Negative'] = 1    X_cv = X_cv.merge(data,on='pair_id',how='left')    # make false negative column    data = pd.DataFrame(X_tr[X_tr['target']==1][X_tr['predict_target']==0]['pair_id'])    data['False_Negative'] = 1    X_tr = X_tr.merge(data,on='pair_id',how='left')    data = pd.DataFrame(X_cv[X_cv['target']==1][X_cv['predict_target']==0]['pair_id'])    data['False_Negative'] = 1    X_cv = X_cv.merge(data,on='pair_id',how='left')        return X_tr,X_cv


Шестая функция предназначена для формирования матрицы сопряжения. Не путайте с таблицей сопряжения. Хотя одно следует из другого. Вы сами все увидете дальше
Код
def matrix_confusion(X):    list_matrix = ['True_Positive','False_Positive','True_Negative','False_Negative']    tr_pos = X[list_matrix].sum().loc['True_Positive']    f_pos = X[list_matrix].sum().loc['False_Positive']    tr_neg = X[list_matrix].sum().loc['True_Negative']    f_neg = X[list_matrix].sum().loc['False_Negative']    matrix_confusion = pd.DataFrame()    matrix_confusion['0_algorythm'] = np.array([tr_neg,f_neg]).T    matrix_confusion['1_algorythm'] = np.array([f_pos,tr_pos]).T    matrix_confusion = matrix_confusion.rename(index={0: '0_target', 1: '1_target'})        return matrix_confusion


Седьмая функция предназначена для визуализации отчета о работе алгоритма, который включает в себя матрицу сопряжения, значения метрик precision, recall, f1
Код
def report_score(tr_matrix_confusion,                 cv_matrix_confusion,                 data,tridx,cvidx,                 X_tr,X_cv):    # print some imporatant information    print ('\033[1m'+'Matrix confusion on train data'+'\033[m')    display(tr_matrix_confusion)    print ()    print(classification_report(data.iloc[tridx]["target"].values, X_tr['predict_target']))    print ('******************************************************')    print ()    print ()    print ('\033[1m'+'Matrix confusion on test(cv) data'+'\033[m')    display(cv_matrix_confusion)    print ()    print(classification_report(data.iloc[cvidx]["target"].values, X_cv['predict_target']))    print ('******************************************************')


С помощью восьмой и девятой функции проведем анализ на полезность признаков для используемой модели из Light GBM с точки зрения значения коэффициента 'Information gain' для каждого исследуемого признака
Код
def table_gain_coef(model,features,start,stop):        data_gain = pd.DataFrame()    data_gain['Features'] = features    data_gain['Gain'] = model.booster_.feature_importance(importance_type='gain')    return data_gain.sort_values('Gain', ascending=False)[start:stop]def gain_hist(df,size,start,stop):    fig, ax = plt.subplots(figsize=(size))    x = (df.sort_values('Gain', ascending=False)['Features'][start:stop])    y = (df.sort_values('Gain', ascending=False)['Gain'][start:stop])    plt.bar(x,y)    plt.xlabel('Features')    plt.ylabel('Gain')    plt.xticks(rotation=90)    plt.show()


Десятая функция нужна для формирования массива количества совпадающих слов для каждой пары компаний.

Эту функцию также можно использовать для формирования массива НЕ совпадающих слов.
Код
def compair_metrics(data):    duplicate_count = []    duplicate_sum = []    for i in range(len(data)):        count=len(data[i])        duplicate_count.append(count)        if count <= 0:            duplicate_sum.append(0)        elif count > 0:            temp_sum = 0            for j in range(len(data[i])):                temp_sum +=len(data[i][j])            duplicate_sum.append(temp_sum)    return duplicate_count,duplicate_sum    


Одинадцатая функция проводит транслитерацию русского текста в латиницу
Код
def transliterate(data):    text_transliterate = []    for i in range(data.shape[0]):        temp_list = list(data[i:i+1])        temp_str = ''.join(temp_list)        result = cyrtranslit.to_latin(temp_str,'ru')        text_transliterate.append(result)Двенадцатая функция нужна для переименования столбцов таблицы после агрегирования данных. Дело в том, что после агрегации данных, названия столбцов, как бы распадаются на два уровня. В итоге, для приведения таблицы к принятому в исследовании формату, используем самописную функцию<spoiler title="Код"><source lang="python">def rename_agg_columns(id_client,data,rename):    columns = [id_client]    for lev_0 in data.columns.levels[0]:        if lev_0 != id_client:            for lev_1 in data.columns.levels[1][:-1]:                columns.append(rename % (lev_0, lev_1))    data.columns = columns    return data


return text_transliterate

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

Что это вообще за таблица, какие в ней метрики и как она формируется? Давайте рассмотрим пошагово формирование таблицы:
Шаг 1. Определим какие данные нам будут нужны. ID пары, финишная обработка текста оба столбца, список названий холдингов (топ 50 компаний нефтехимической и строительной индустрии).
Шаг 2. В столбце 1 в каждой паре от каждого слова замерим дистанцию Левенштейна до каждого слова из списка названий холдингов, а также длину каждого слова и отношение дистанции к длине.
Шаг 3. В случае, если значение отношения окажется меньше или равно 0.4, то от сравниваемого слова из списка названий топ холдингов определим дистанцию до каждого слова из второго столбца соответствующей id пары, а также длину каждого из слов и отношение дистанции к длине.
Шаг 4. В случае, если в очередной раз отношение оказывается меньше или равно 0.4, то все собранные метрики фиксируются.
Шаг 5. По завершению алгоритма будет сформирована таблица, в которой первый столбец ID пары, а последующие столбцы метрики. Данные в таблице необходимо агрегировать по id пары (так как могут быть случаи сильного соответствия слов из одной id пары двум названиям холдингов). При агрегировании данных выбираем минимальные значения.
Шаг 6. Склеиваем полученную таблицу с таблицей исследования.

Важная особенность: расчет занимает продолжительное время из-за написанного на скорую руку кода
Код
def dist_name_to_top_list_view(data,column1,column2,list_top_companies):    id_pair = []    r1 = []    r2 = []    words1 = []    words2 = []    top_words = []    for n in range(0, data.shape[0], 1):        for line1 in data[column1][n:n+1]:            line1 = line1.split()            for word1 in line1:                if len(word1) >=3:                    for top_word in list_top_companies:                        dist1 = levenshtein.distance(word1, top_word)                        ratio = max(dist1/float(len(top_word)),dist1/float(len(word1)))                        if ratio <= 0.4:                            ratio1 = ratio                            break                    if ratio <= 0.4:                        for line2 in data[column2][n:n+1]:                            line2 = line2.split()                            for word2 in line2:                                dist2 = levenshtein.distance(word2, top_word)                                ratio = max(dist2/float(len(top_word)),dist2/float(len(word2)))                                if ratio <= 0.4:                                    ratio2 = ratio                                    id_pair.append(int(data['pair_id'][n:n+1].values))                                    r1.append(ratio1)                                    r2.append(ratio2)                                    break    df = pd.DataFrame()    df['pair_id'] = id_pair    df['levenstein_dist_w1_top_w'] = dist1    df['levenstein_dist_w2_top_w'] = dist2    df['length_w1_top_w'] = len(word1)    df['length_w2_top_w'] = len(word2)    df['length_top_w'] = len(top_word)    df['ratio_dist_w1_to_top_w'] = r1    df['ratio_dist_w2_to_top_w'] = r2    feature = df.groupby(['pair_id']).agg([min]).reset_index()    feature = rename_agg_columns(id_client='pair_id',data=feature,rename='%s_%s')    data = data.merge(feature,on='pair_id',how='left')    display(data)    print ('Words:', word1,word2,top_word)    print ('Levenstein distance:',dist1,dist2)    print ('Length of word:',len(word1),len(word2),len(top_word))    print ('Ratio (distance/length word):',ratio1,ratio2)def dist_name_to_top_list_make(data,column1,column2,list_top_companies):    id_pair = []    r1 = []    r2 = []    dist_w1 = []    dist_w2 = []    length_w1 = []    length_w2 = []    length_top_w = []    for n in range(0, data.shape[0], 1):        for line1 in data[column1][n:n+1]:            line1 = line1.split()            for word1 in line1:                if len(word1) >=3:                    for top_word in list_top_companies:                        dist1 = levenshtein.distance(word1, top_word)                        ratio = max(dist1/float(len(top_word)),dist1/float(len(word1)))                        if ratio <= 0.4:                            ratio1 = ratio                            break                    if ratio <= 0.4:                        for line2 in data[column2][n:n+1]:                            line2 = line2.split()                            for word2 in line2:                                dist2 = levenshtein.distance(word2, top_word)                                ratio = max(dist2/float(len(top_word)),dist2/float(len(word2)))                                if ratio <= 0.4:                                    ratio2 = ratio                                    id_pair.append(int(data['pair_id'][n:n+1].values))                                    r1.append(ratio1)                                    r2.append(ratio2)                                    dist_w1.append(dist1)                                    dist_w2.append(dist2)                                    length_w1.append(float(len(word1)))                                    length_w2.append(float(len(word2)))                                    length_top_w.append(float(len(top_word)))                                    break    df = pd.DataFrame()    df['pair_id'] = id_pair    df['levenstein_dist_w1_top_w'] = dist_w1    df['levenstein_dist_w2_top_w'] = dist_w2    df['length_w1_top_w'] = length_w1    df['length_w2_top_w'] = length_w2    df['length_top_w'] = length_top_w    df['ratio_dist_w1_to_top_w'] = r1    df['ratio_dist_w2_to_top_w'] = r2    feature = df.groupby(['pair_id']).agg([min]).reset_index()    feature = rename_agg_columns(id_client='pair_id',data=feature,rename='%s_%s')    data = data.merge(feature,on='pair_id',how='left')    return data


Предобработка данных


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

Загрузим данные


Здесь все очень просто. Загрузим данные и заменим название столбца с целевой меткой is_duplicate на target. Это делается для удобства использования функций некоторые из них были написаны в рамках более ранних исследований и они используют название столбца с целевой меткой как target.
Код
# DOWNLOAD DATAtext_train = pd.read_csv('train.csv')text_test = pd.read_csv('test.csv')# RENAME DATAtext_train = text_train.rename(columns={"is_duplicate": "target"})


Посмотрим на данные


Данные загрузили. Давайте посмотрим сколько всего объектов и насколько они сбалансированны.
Код
# ANALYSE BALANCE OF DATAtarget_1 = text_train[text_train['target']==1]['target'].count()target_0 = text_train[text_train['target']==0]['target'].count()print ('There are', text_train.shape[0], 'objects')print ('There are', target_1, 'objects with target 1')print ('There are', target_0, 'objects with target 0')print ('Balance is', round(100*target_1/target_0,2),'%')


Таблица 1 Баланс меток


Объектов не мало почти 500 тысяч и они вообще никак не сбалансированы. То есть из почти 500 тысяч объектов, всего менее 4 тысяч имеют целевую метку 1 (менее 1%)

Давайте посмотрим на саму таблицу. Посмотрим на первые пять объектов с разметкой 0 и первые пять объектов с разметкой 1.
Код
display(text_train[text_train['target']==0].head(5))display(text_train[text_train['target']==1].head(5))


Таблица 2 Первые 5 объектов класса 0, таблица 3 Первые 5 объектов класса 1


Сразу напрашиваются некоторые простые шаги: привести текст к одному регистру, убрать всякие стоп-слова, типа 'ltd', удалить страны и заодно названия географических объектов.

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

Сделаем копии


Если честно, то не знаю зачем я это делаю, но почему-то всегда это делаю. Сделаю и в этот раз
Код
baseline_train = text_train.copy()baseline_test = text_test.copy()


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


Код
# convert text to lowercasecolumns = ['name_1','name_2']for column in columns:    lower_str(baseline_train,column)for column in columns:    lower_str(baseline_test,column)


Удалим названия стран


Надо отметить, что организаторы конкурса большие молодцы! Вместе с заданием они дали ноутбук с очень простым baseline, в котором был предоставлен, в том числе и нижеприведенный код.
Код
# drop any names of countriescountries = [country.name.lower() for country in pycountry.countries]for country in tqdm(countries):    baseline_train.replace(re.compile(country), "", inplace=True)    baseline_test.replace(re.compile(country), "", inplace=True)


Удалим знаки и спецсимволы


Код
# drop punctuation marksbaseline_train.replace(re.compile(r"\s+\(.*\)"), "", inplace=True)baseline_test.replace(re.compile(r"\s+\(.*\)"), "", inplace=True)baseline_train.replace(re.compile(r"[^\w\s]"), "", inplace=True)baseline_test.replace(re.compile(r"[^\w\s]"), "", inplace=True)


Удалим цифры


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

Также обратите внимание, что до этого момента мы проводили преобразование прямо в столбцах, которые нам были даны. Давайте теперь будем создавать новые столбцы для каждой предобработки. Столбцов получится больше, зато если где-то на каком-то этапе предобработки произойдет сбой ничего страшного, делать все с самого начала не нужно, ведь у нас будут столбы с каждого этапа предобработки.
Код, который испортил качество. Нужно деликатнее быть
# # first: make dictionary of frequency every word# list_words = baseline_train['name_1'].to_string(index=False).split() +\#                 baseline_train['name_2'].to_string(index=False).split()# freq_words = {}# for w in list_words:#     freq_words[w] = freq_words.get(w, 0) + 1    # # second: make data frame of frequency words# df_freq = pd.DataFrame.from_dict(freq_words,orient='index').reset_index()# df_freq.columns = ['word','frequency']# df_freq_agg = df_freq.groupby(['word']).agg([sum]).reset_index()# df_freq_agg = rename_agg_columns(id_client='word',data=df_freq_agg,rename='%s_%s')# df_freq_agg = df_freq_agg.sort_values(by=['frequency_sum'], ascending=False)# # third: make drop list of digits# string = df_freq_agg['word'].to_string(index=False)# digits = [int(digit) for digit in string.split() if digit.isdigit()]# digits = set(digits)# digits = list(digits)# # drop the digits# baseline_train['name_1_no_digits'] =\#     baseline_train['name_1'].apply(#     lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))# baseline_train['name_2_no_digits'] =\#     baseline_train['name_2'].apply(#     lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))# baseline_test['name_1_no_digits'] =\#     baseline_test['name_1'].apply(#     lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))# baseline_test['name_2_no_digits'] =\#     baseline_test['name_2'].apply(#     lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))


Удалим первый список стоп-слов. Вручную!


Теперь предлагается определить и удалить стоп-слова из списка слов в названиях компаний.

Список мы составили на основании ручного просмотра обучающей выборки. По логике, такой список нужно составлять автоматически, используя следующие подходы:
во-первых, использовать топ 10 (20,50,100) часто встречающихся слов.
во-вторых, использовать стандартные библиотеки стоп-слов на различных языках. Например, обозначения организационно-правовых форм организаций на различных языках (ООО, ПАО, ЗАО, ltd, gmbh, inc и др.)
в-третьих, имеет смысл составить список географических названий на различных языках

К первому варианту автоматического составления списка топ часто встречающихся слов мы еще вернемся, а пока смотрим на ручную предобработку.
Код
# drop some stop-wordsdrop_list = ["ltd.", "co.", "inc.", "b.v.", "s.c.r.l.", "gmbh", "pvt.",                          'retail','usa','asia','ceska republika','limited','tradig','llc','group',             'international','plc','retail','tire','mills','chemical','korea','brasil',             'holding','vietnam','tyre','venezuela','polska','americas','industrial','taiwan',             'europe','america','north','czech republic','retailers','retails',                        'mexicana','corporation','corp','ltd','co','toronto','nederland','shanghai','gmb','pacific',            'industries','industrias',                        'inc', 'ltda', 'ооо', 'ООО', 'зао', 'ЗАО', 'оао', 'ОАО', 'пао', 'ПАО', 'ceska republika', 'ltda',             'sibur', 'enterprises', 'electronics', 'products', 'distribution', 'logistics', 'development',            'technologies', 'pvt', 'technologies', 'comercio', 'industria', 'trading', 'internacionais',             'bank', 'sports',                        'express','east', 'west', 'south', 'north', 'factory', 'transportes', 'trade', 'banco',            'management', 'engineering', 'investments', 'enterprise', 'city', 'national', 'express', 'tech',             'auto', 'transporte', 'technology', 'and', 'central', 'american',                        'logistica','global','exportacao', 'ceska republika', 'vancouver', 'deutschland',                        'sro','rus','chemicals','private','distributors','tyres','industry','services','italia','beijing',                        'рус','company','the','und']baseline_train['name_1_non_stop_words'] =\    baseline_train['name_1'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))baseline_train['name_2_non_stop_words'] =\    baseline_train['name_2'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))baseline_test['name_1_non_stop_words'] =\    baseline_test['name_1'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))baseline_test['name_2_non_stop_words'] =\    baseline_test['name_2'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))


Давайте выборочно проверим, что наши стоп слова были действительно удалены из текста
Код
baseline_train[baseline_train.name_1_non_stop_words.str.contains("factory")].head(3)


Таблица 4 Выборочная проверка работы кода по удалению стоп-слов

Вроде все работает. Удалены все стоп-слова, которые отделены пробелом. То, что мы и хотели. Двигаемся дальше.

Проведем транслитерацию русского текста в латиницу


Я использую для этого свою самописную функцию и библиотеку cyrtranslit. Вроде работает. Проверял вручную.
Код
# transliteration to latinbaseline_train['name_1_transliterated'] = transliterate(baseline_train['name_1_non_stop_words'])baseline_train['name_2_transliterated'] = transliterate(baseline_train['name_2_non_stop_words'])baseline_test['name_1_transliterated'] = transliterate(baseline_test['name_1_non_stop_words'])baseline_test['name_2_transliterated'] = transliterate(baseline_test['name_2_non_stop_words'])


Давайте посмотрим на пару с id 353150. В ней второй столбец (name_2) имеет слово мишлен, после предобработки слово уже пишется так mishlen (см. столбец name_2_transliterated). Не совсем правильно, но явно лучше.
Код
pair_id = 353150baseline_train[baseline_train['pair_id']==353150]


Таблица 5 Выборочная проверка кода по транслитерации


Запустим автоматическое составление списка топ 50 самых часто встречающихся слов & Drop it smart. Первый ЧИТ


Немного мудреный заголовок. Давайте по порядку разберем, что мы тут будем делать.

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

Во-вторых, выберем топ 50 таких слов. И казалось бы можно их удалить, но нет. В этих словах могут быть названия холдингов ('total', 'knauf', 'shell',...), а это очень важная информация и ее нельзя потерять, так как далее мы будем ее использовать. Поэтому мы пойдем на читерский (запрещенный) прием. Для начала, на основании внимательного, выборочного изучения обучающей выборки, составим список названий часто встречающихся холдингов. Список будет не полный, иначе это было бы совсем не честно :) Хотя, так как мы не гонимся за призовым местом, то почему бы и нет. Затем мы сравним массив топ 50 часто встречающихся слов со списком названий холдингов и выкинем из списка слова, которые совпадают с названиями холдингов.

Теперь второй список стоп-слов готов. Можно удалять слова из текста.

Но перед тем, хотелось бы вставить небольшую ремарочку касательно читерского списка названий холдингов. То, что мы составили на основании наблюдений список из названий холдингов сильно упростило нам жизнь. Но на самом деле, мы могли составить такой список другим способом. Например, можно взять рейтинги крупнейших компаний в нефтехимической, строительной, автомобильной и других отраслях, объединить их и взять оттуда названия холдингов. Но в целях нашего исследования, мы ограничимся простым подходом. Такой подход запрещен в рамках соревнования! Более того, организаторы соревнований, работы кандидатов на призовые места проверяют на предмет запрещенных приемов. Будьте внимательны!
Код
list_top_companies = ['arlanxeo', 'basf', 'bayer', 'bdp', 'bosch', 'brenntag', 'contitech',                      'daewoo', 'dow', 'dupont', 'evonik', 'exxon', 'exxonmobil', 'freudenberg',                      'goodyear', 'goter', 'henkel', 'hp', 'hyundai', 'isover', 'itochu', 'kia', 'knauf',                      'kraton', 'kumho', 'lusocopla', 'michelin', 'paul bauder', 'pirelli', 'ravago',                      'rehau', 'reliance', 'sabic', 'sanyo', 'shell', 'sherwinwilliams', 'sojitz',                      'soprema', 'steico', 'strabag', 'sumitomo', 'synthomer', 'synthos',                      'total', 'trelleborg', 'trinseo', 'yokohama']# drop top 50 common words (NAME 1 & NAME 2) exept names of top companies# first: make dictionary of frequency every wordlist_words = baseline_train['name_1_transliterated'].to_string(index=False).split() +\                baseline_train['name_2_transliterated'].to_string(index=False).split()freq_words = {}for w in list_words:    freq_words[w] = freq_words.get(w, 0) + 1    # # second: make data framedf_freq = pd.DataFrame.from_dict(freq_words,orient='index').reset_index()df_freq.columns = ['word','frequency']df_freq_agg = df_freq.groupby(['word']).agg([sum]).reset_index()df_freq_agg = rename_agg_columns(id_client='word',data=df_freq_agg,rename='%s_%s')df_freq_agg = df_freq_agg.sort_values(by=['frequency_sum'], ascending=False)drop_list = list(set(df_freq_agg[0:50]['word'].to_string(index=False).split()) - set(list_top_companies))# # check list of top 50 common words# print (drop_list) # drop the top 50 wordsbaseline_train['name_1_finish'] =\    baseline_train['name_1_transliterated'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))baseline_train['name_2_finish'] =\    baseline_train['name_2_transliterated'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))baseline_test['name_1_finish'] =\    baseline_test['name_1_transliterated'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))baseline_test['name_2_finish'] =\    baseline_test['name_2_transliterated'].apply(    lambda x: ' '.join([word for word in x.split() if word not in (drop_list)]))


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

Генерация и анализ фич


Посчитаем дистанцию Левенштейна


Воспользуемся библиотекой strsimpy и в каждой паре (после всех предобработок) посчитаем дистанцию Левенштейна от названия компании из первого столбца до названия компании во втором столбце.
Код
# create feature with LEVENSTAIN DISTANCElevenshtein = Levenshtein()column_1 = 'name_1_finish'column_2 = 'name_2_finish'baseline_train["levenstein"] = baseline_train.progress_apply(    lambda r: levenshtein.distance(r[column_1], r[column_2]), axis=1)baseline_test["levenstein"] = baseline_test.progress_apply(    lambda r: levenshtein.distance(r[column_1], r[column_2]), axis=1)


Посчитаем нормализованную дистанцию Левенштейна


Все тоже самое, что и выше, только считать мы будем нормированную дистанцию.
Заголовок спойлера
# create feature with NORMALIZATION LEVENSTAIN DISTANCEnormalized_levenshtein = NormalizedLevenshtein()column_1 = 'name_1_finish'column_2 = 'name_2_finish'baseline_train["norm_levenstein"] = baseline_train.progress_apply(    lambda r: normalized_levenshtein.distance(r[column_1], r[column_2]),axis=1)baseline_test["norm_levenstein"] = baseline_test.progress_apply(    lambda r: normalized_levenshtein.distance(r[column_1], r[column_2]),axis=1)


Посчитали, а теперь визуализируем

Визуализируем фичи


Посмотрим на распределение признака 'levenstein'
Код
data = baseline_trainanalyse = 'levenstein'size = (12,2)dd = data_statistics(data,analyse,title_print='no')hist_fz(data,dd,analyse,size)boxplot(data,analyse,size)


Графики 1 Гистограмма и ящик с усами для оценки значимости признака

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

Посмотрим на распределение признака 'norm_levenstein'
Заголовок спойлера
data = baseline_trainanalyse = 'norm_levenstein'size = (14,2)dd = data_statistics(data,analyse,title_print='no')hist_fz(data,dd,analyse,size)boxplot(data,analyse,size)


Графики 2 Гистограмма и ящик с усами для оценки значимости признака

Уже лучше. А теперь, давайте посмотрим на то, как две вместе взятые фичи будут разделять пространство на объекты 0 и 1.
Код
data = baseline_trainanalyse1 = 'levenstein'analyse2 = 'norm_levenstein'size = (14,6)two_features(data,analyse1,analyse2,size)


График 3 Диаграмма рассеяния

Очень даже неплохая разметка получается. Значит не зря мы столько предобрабатывали данные :)
Всем же понятно, что по горизонтали значения метрики levenstein, а по вертикали значения метрики norm_levenstein, а зелененькие и черненькие точки это объекты 0 и 1. Двигаемся дальше.

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


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

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

Код здесь, наверное, не очень дружелюбный, так как опять-таки, написан был на скорую руку. Но он работает, а для быстрого исследования пойдет.
Код
# make some information about duplicates and differences for TRAINcolumn_1 = 'name_1_finish'column_2 = 'name_2_finish'duplicates = []difference = []for i in range(baseline_train.shape[0]):    list1 = list(baseline_train[i:i+1][column_1])    str1 = ''.join(list1).split()    list2 = list(baseline_train[i:i+1][column_2])    str2 = ''.join(list2).split()        duplicates.append(list(set(str1) & set(str2)))    difference.append(list(set(str1).symmetric_difference(set(str2))))    # continue make information about duplicatesduplicate_count,duplicate_sum = compair_metrics(duplicates)dif_count,dif_sum = compair_metrics(difference)# create features have information about duplicates and differences for TRAINbaseline_train['duplicate'] = duplicatesbaseline_train['difference'] = differencebaseline_train['duplicate_count'] = duplicate_countbaseline_train['duplicate_sum'] = duplicate_sumbaseline_train['duplicate_mean'] = baseline_train['duplicate_sum'] / baseline_train['duplicate_count']baseline_train['duplicate_mean'] = baseline_train['duplicate_mean'].fillna(0)baseline_train['dif_count'] = dif_countbaseline_train['dif_sum'] = dif_sumbaseline_train['dif_mean'] = baseline_train['dif_sum'] / baseline_train['dif_count']baseline_train['dif_mean'] = baseline_train['dif_mean'].fillna(0)baseline_train['ratio_duplicate/dif_count'] = baseline_train['duplicate_count'] / baseline_train['dif_count']# make some information about duplicates and differences for TESTcolumn_1 = 'name_1_finish'column_2 = 'name_2_finish'duplicates = []difference = []for i in range(baseline_test.shape[0]):    list1 = list(baseline_test[i:i+1][column_1])    str1 = ''.join(list1).split()    list2 = list(baseline_test[i:i+1][column_2])    str2 = ''.join(list2).split()        duplicates.append(list(set(str1) & set(str2)))    difference.append(list(set(str1).symmetric_difference(set(str2))))    # continue make information about duplicatesduplicate_count,duplicate_sum = compair_metrics(duplicates)dif_count,dif_sum = compair_metrics(difference)# create features have information about duplicates and differences for TESTbaseline_test['duplicate'] = duplicatesbaseline_test['difference'] = differencebaseline_test['duplicate_count'] = duplicate_countbaseline_test['duplicate_sum'] = duplicate_sumbaseline_test['duplicate_mean'] = baseline_test['duplicate_sum'] / baseline_test['duplicate_count']baseline_test['duplicate_mean'] = baseline_test['duplicate_mean'].fillna(0)baseline_test['dif_count'] = dif_countbaseline_test['dif_sum'] = dif_sumbaseline_test['dif_mean'] = baseline_test['dif_sum'] / baseline_test['dif_count']baseline_test['dif_mean'] = baseline_test['dif_mean'].fillna(0)baseline_test['ratio_duplicate/dif_count'] = baseline_test['duplicate_count'] / baseline_test['dif_count']


Визуализируем некоторые признаки.
Код
data = baseline_trainanalyse = 'dif_sum'size = (14,2)dd = data_statistics(data,analyse,title_print='no')hist_fz(data,dd,analyse,size)boxplot(data,analyse,size)


Графики 4 Гистограмма и ящик с усами для оценки значимости признака

Код
data = baseline_trainanalyse1 = 'duplicate_mean'analyse2 = 'dif_mean'size = (14,6)two_features(data,analyse1,analyse2,size)


График 5 Диаграмма рассеяния

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

Давайте взглянем на табличные данные, подготовим запрос по первому случаю: дублей в названии компаний ноль, а компании одинаковые.
Код
baseline_train[    baseline_train['duplicate_mean']==0][    baseline_train['target']==1].drop(        ['duplicate', 'difference',        'name_1_non_stop_words',        'name_2_non_stop_words', 'name_1_transliterated',        'name_2_transliterated'],axis=1)



Очевидно, есть системная ошибка в нашей обработке. Мы не учли, что слова могут писаться не только с ошибками, но и просто слитно или наоборот раздельно там, где этого не требуется. Например, пара 9764. В первом столбце 'bitmat' во втором 'bit mat' и вот уже это не дубль, а компания та одинаковая. Или другой пример, пара 482600 'bridgestoneshenyang' и 'bridgestone'.

Что можно было бы сделать. Первое, что мне пришло в голову сопоставлять не напрямую в лоб, а с помощью метрики Левенштейна. Но и здесь нас подстерегает засада: расстояние между 'bridgestoneshenyang' и 'bridgestone' будет не маленьким. Возможно на помощь придет лемматизация, но опять-таки сходу не ясно как можно лемматизировать названия компаний. Или можно использовать коэффициент Тамимото, но оставим этот момент для более опытных товарищей и двигаемся дальше.

Сопоставим слова из текста со словами из названий топ 50 холдинговых брендов нефтехимической, строительной и других отраслей. Получим вторую большую кучу признаков. Второй ЧИТ


На самом деле здесь целых два нарушения правил участия соревнования:

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

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

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

Итак, определим дистанцию Левенштейна от каждого слова в каждой строчке первого столбца с названием компании до каждого слова из списка топ нефтехимических компаний (и не только).

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

Если и второй коэффициент (отношение дистанции к длине слова из списка топ компаний) оказывается ниже или равен 0.4, то мы фиксируем следующие значения в таблицу:
дистанция Левенштейна от слова из списка 1 компаний до слова в списке топ компаний
дистанция Левенштейна от слова из списка 2 компаний до слова в списке топ компаний
длина слова из списка 1
длина слова из списка 2
длина слова из списка топ компаний
отношение длина слова из списка 1 к дистанции
отношение длина слова из списка 2 к дистанции

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

Хотелось бы еще раз обратить внимание, на то, что предложенный способ генерации фич, достаточно ресурсоемкий и в случае получения списка из внешнего источника потребуется изменение в коде по составлению метрик.
Код
# create information about duplicate name of petrochemical companies from top listlist_top_companies = list_top_companiesdp_train = []for i in list(baseline_train['duplicate']):    dp_train.append(''.join(list(set(i) & set(list_top_companies))))    dp_test = []for i in list(baseline_test['duplicate']):    dp_test.append(''.join(list(set(i) & set(list_top_companies))))    baseline_train['duplicate_name_company'] = dp_trainbaseline_test['duplicate_name_company'] = dp_test# replace name duplicate to numberbaseline_train['duplicate_name_company'] =\    baseline_train['duplicate_name_company'].replace('',0,regex=True)baseline_train.loc[baseline_train['duplicate_name_company'] != 0, 'duplicate_name_company'] = 1baseline_test['duplicate_name_company'] =\    baseline_test['duplicate_name_company'].replace('',0,regex=True)baseline_test.loc[baseline_test['duplicate_name_company'] != 0, 'duplicate_name_company'] = 1# create some important feature about similar words in the data and names of top companies for TRAIN# (levenstein distance, length of word, ratio distance to length)baseline_train = dist_name_to_top_list_make(baseline_train,                      'name_1_finish','name_2_finish',list_top_companies)# create some important feature about similar words in the data and names of top companies for TEST# (levenstein distance, length of word, ratio distance to length)baseline_test = dist_name_to_top_list_make(baseline_test,                      'name_1_finish','name_2_finish',list_top_companies)


Посмотрим на полезность признаков сквозь призму графиков
Код
data = baseline_trainanalyse = 'levenstein_dist_w1_top_w_min'size = (14,2)dd = data_statistics(data,analyse,title_print='no')hist_fz(data,dd,analyse,size)boxplot(data,analyse,size)



Очень хорошо.

Готовим данные для подачи в модель


У нас получилась большая таблица и далеко не все данные для анализа нам нужны. Посмотрим на названия столбцов таблицы.
Код
baseline_train.columns



Выберем те столбцы, которые будем анализировать.

Зафиксируем seed для воспроизводимости результата.
Код
# fix some parametersfeatures = ['levenstein','norm_levenstein',                        'duplicate_count','duplicate_sum','duplicate_mean',            'dif_count','dif_sum','dif_mean','ratio_duplicate/dif_count',                        'duplicate_name_company',                       'levenstein_dist_w1_top_w_min', 'levenstein_dist_w2_top_w_min',       'length_w1_top_w_min', 'length_w2_top_w_min', 'length_top_w_min',       'ratio_dist_w1_to_top_w_min', 'ratio_dist_w2_to_top_w_min'            ]seed = 42


Перед тем, как окончательно обучить модель на всех доступных данных и отправить решение на проверку имеет смысл протестировать модель. Для этого разобьем обучающую выборку на условно обучающую и условно тестовую. Померим на ней качество и если нас оно устраивает, то будем отправлять решение на конкурс.
Код
# provides train/test indices to split data in train/test setssplit = StratifiedShuffleSplit(n_splits=1, train_size=0.8, random_state=seed)tridx, cvidx = list(split.split(baseline_train[features],                                baseline_train["target"]))[0]print ('Split baseline data train',baseline_train.shape[0])print (' - new train data:',tridx.shape[0])print (' - new test data:',cvidx.shape[0])


Настройка и обучение модели


В качестве модели будем использовать решающее дерево из библиотеки Light GBM.

Сильно накручивать параметры не имеет смысла. Смотрим код.
Код
# learning Light GBM Classificierseed = 50params = {'n_estimators': 1,          'objective': 'binary',          'max_depth': 40,          'min_child_samples': 5,          'learning_rate': 1, #           'reg_lambda': 0.75,#           'subsample': 0.75,#           'colsample_bytree': 0.4,#           'min_split_gain': 0.02,#           'min_child_weight': 40,          'random_state': seed}model = lgb.LGBMClassifier(**params)model.fit(baseline_train.iloc[tridx][features].values,          baseline_train.iloc[tridx]["target"].values)


Модель настроили и обучили. Теперь давайте посмотрим на результаты.
Код
# make predict proba and predict targetprobability_level = 0.99X = baseline_traintridx = tridxcvidx = cvidxmodel = modelX_tr, X_cv = contingency_table(X,features,probability_level,tridx,cvidx,model)train_matrix_confusion = matrix_confusion(X_tr)cv_matrix_confusion = matrix_confusion(X_cv)report_score(train_matrix_confusion,             cv_matrix_confusion,             baseline_train,             tridx,cvidx,             X_tr,X_cv)



Обратите внимание на то, что мы в качестве оценки модели используем метрику качества f1. Значит, имеет смысл регулировать уровень вероятности отнесения объекта к классу 1 или 0. Мы выбрали уровень 0.99, то есть при вероятности равной и выше 0.99 объект будет отнесен к классу 1, ниже 0.99 к классу 0. Это важный момент можно существенно поправить скор таким не хитрым простым трюком.

Качество вроде бы не плохое. На условно тестовой выборке алгоритм допустил ошибки при определении 222 объектов класса 0 и на 90 объектах, относящихся к классу 0 ошибся и отнес их к классу 1 (см.Matrix confusion on test(cv) data).

Давайте посмотрим какие признаки были наиболее важными, а какие нет.
Код
start = 0stop = 50size = (12,6)tg = table_gain_coef(model,features,start,stop)gain_hist(tg,size,start,stop)display(tg)




Заметим, что для оценки значимости признаков мы использовали параметр 'gain', а не 'split'. Это важно так как в очень упрощенном варианте первый параметр означает вклад признака в уменьшении энтропии, а второй указывает на то, сколько раз признак использовался для разметки пространства.

На первый взгляд, признак, который мы делали очень долго levenstein_dist_w1_top_w_min оказался совсем не информативным его вклад равен 0. Но это только на первый взгляд. Он просто почти полностью дублируется по смыслу с признаком duplicate_name_company. Если удалить duplicate_name_company и оставить levenstein_dist_w1_top_w_min, то второй признак займет место первого и качество не поменяется. Проверено!

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

Давайте посмотрим на таблицу сопряжения. В первую очередь посмотрим на объекты False Positive, то есть те, которые наш алгоритм определил как одинаковые и отнес их к классу 1, а на самом деле они относятся к классу 0.
Код
X_cv[X_cv['False_Positive']==1][0:50].drop(['name_1_non_stop_words',       'name_2_non_stop_words', 'name_1_transliterated',       'name_2_transliterated', 'duplicate', 'difference',       'levenstein',       'levenstein_dist_w1_top_w_min', 'levenstein_dist_w2_top_w_min',       'length_w1_top_w_min', 'length_w2_top_w_min', 'length_top_w_min',       'ratio_dist_w1_to_top_w_min', 'ratio_dist_w2_to_top_w_min',       'True_Positive','True_Negative','False_Negative'],axis=1)



Да уж. Здесь и человек то сходу не определит 0 или 1. Например, пара 146825 mitsubishi corp и mitsubishi corp l. Глаза говорят что это одно и тоже, а выборка говорит, что разные компании. Кому верить?

Скажем так, что сходу можно было выжать мы выжали. Остальную работу оставим опытным товарищам :)

Давайте загрузим данные на сайт организатора и узнаем оценку качества работы.

Итоги соревнования


Код
model = lgb.LGBMClassifier(**params)model.fit(baseline_train[features].values,          baseline_train["target"].values)sample_sub = pd.read_csv('sample_submission.csv', index_col="pair_id")sample_sub['is_duplicate'] = (model.predict_proba(    baseline_test[features].values)[:, 1] > probability_level).astype(np.int)sample_sub.to_csv('baseline_submission.csv')


Итак, наш скор с учетом запрещенного приема: 0.5999

Без него, качество было где-то между 0.3 и 0.4. Надо перезапускать модель для точности, а мне немного лень :)

Давайте лучше резюмируем полученный опыт.

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

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

В-третьих, всегда, прямо-таки всегда, в задачах классификации, формируйте и таблицу и матрицу сопряжения. По таблице Вы легко найдете на каких объектах ошибается алгоритм. Для начала старайтесь подмечать те ошибки, которые что называются системные, они требуют меньше работы по исправлению, а дают больший результат. Потом уже как разберете системные ошибки, переходите на частные случаи. По матрице ошибок Вы увидите где больше ошибается алгоритм: на классе 0 или 1. Отсюда Вы и будете копать ошибки. Например, я заметил, что мое дерево хорошо определяет классы 1, но допускает много ошибок на классе 0, то есть дерево часто говорит, что этот объект класса 1, тогда как на самом деле он 0. Я предположил, что это может быть связано с уровнем вероятности отнесения объекта к классу 0 или 1. У меня уровень был зафиксирован на 0.9. Увеличение уровня вероятности отнесения объекта к классу 1 до 0.99, сделало отбор объектов класса 1 жестче и вуаля наш скор дал существенный прирост.

Еще раз отмечу, что целью участия в соревновании у меня было не занятие призового места, а получение опыта. Учитывая, что до начала соревнования я понятия не имел как работать с текстами в машинном обучении, а в итоге за несколько дней получилась простая, но все же рабочая модель, то можно сказать цель достигнута. Также и для любого начинающего самурая в мире data science я считаю важно получение опыта, а не приз, вернее опыт это и есть приз. Поэтому не бойтесь участия в соревнованиях, дерзайте, всем бобра!

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

А Вы уважаемый читатель, если располагаете идеями о том, как увеличить скор уже сейчас, пишите в комментах. Сделаете доброе дело :)

Источники информации, вспомогательные материалы



1. Гитхаб с данными и Jupyter Notebook

2.Платформа соревнования SIBUR CHALLENGE 2020

3.Сайт организатора соревнования SIBUR CHALLENGE 2020

4.Хорошая статья на Хабре Основы Natural Language Processing для текста

5.Еще одна хорошая статья на Хабре Нечёткое сравнение строк: пойми меня, если сможешь

6.Публикация из журнала АПНИ

7.Статья о не использованном здесь коэффициенте Танимото Степень схожести строк
Подробнее..

Нападения на полицейских в США статистический обзор

18.01.2021 02:05:59 | Автор: admin

Эта статья логическое продолжение серии статей, которые я написал в этом году на тему криминала и правопорядка в США и их связи с расовой принадлежностью (раз, два). В первой серии статей, напомню, мы подробно рассматривали данные по гибели граждан от рук полицейских. А сегодня мы взглянем на этот вопрос с другой стороны: будем разбирать статистику нападений на самих полицейских и постараемся так же проследить закономерности и сделать выводы. Оружием преступников может быть пистолет или нож, а нашим оружием, как и прежде, будет python + pandas. Поехали!


Вместо предыстории


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

Кто-то из моих читателей оставил комментарий вроде понятно, почему в США полицейские не снимают палец со спускового крючка их самих отстреливают ежедневно. И я, естественно, захотел копнуть эту тему немного глубже и рассмотреть ее по другую сторону бронежилета: то есть, данные о нападениях на полицейских и гибели полицейских при исполнении обязанностей. Конечно же, тоже в США. Конечно же, тоже на основе открытых источников. Конечно же, опять python + pandas :)

Исходные данные


Задумано сделано. Благо, данные у меня уже были, я их накачал при изучении материала для предыдущего исследования. Напомню (или, вернее, сообщу) тем, кто по каким-то причинам не читал предыдущих статей, что все данные по преступности, численности полицейских, а также по нападениям на сотрудников правоохранительных органов в США я брал с сайта Crime Data Explorer, обеспечивающего публичный доступ к статистическим базам ФБР. При желании вы сами можете зайти и скачать оттуда те же исходные данные, которые использую я в своей работе.

Для ленивых же (то есть, для таких, как я сам) вот здесь возьмите готовый архив со всеми скачанными и отформатированными данными.

Итак, какие файлы данных используются:

  • LEOKA_ASSAULT_TIME_WEAPON_INJURY_1995_2018.csv набор данных по нападениям на полицейских в США с 1995 по 2018 г. с указанием места, штата, города, времени, нанесенных телесных повреждений, случая гибели и т.п. (естественно, без личных сведений о преступниках и потерпевших)
  • crimes_by_state.csv количество совершенных преступлений насильственного характера (убийства, изнасилования, грабежи и нападения с отягчающими обстоятельствами) по годам и штатам
  • police_employee_1960_2019.csv количество работающих штатных и внештатных полицейских по годам (с 1960 по 2019 г.) и штатам, с указанием также общей численности населения штатов и доли полицейских от населения
  • us_pop_2010-2019_state_race.csv население по всем штатам США с 2010 по 2019 г., разбитое по расовой принадлежности (белые, черные, азиаты, гавайцы, индейцы и остальные) эти данные взяты с сайта Бюро переписи населения США (из России сайт не работает, можете не пытаться нажимать без VPN или прочих разных Торов)
  • us_states.csv перечень штатов США с сокращенными и полными названиями
  • us-states-geo.json координаты границ штатов США (для отображения данных на карте) взят отсюда
  • leoka_felony_2010-2019.xlsx подробные данные по убитым полицейским с 2010 по 2019 г. (включая информацию о возрасте, расе и статусе преступников и жертв и прочие полезные сведения)

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

Analiyze This...


Сразу ссылка на исходник Jupyter / IPython. Приводить полный код я в статье не буду для экономии места мы будем сразу смотреть на получаемые таблицы и графики.

Основной массив данных


После загрузки и объединения данных из различных источников получается вот такая большая таблица (показаны первые 15 из 1171 строк):

DATA_YEAR STATE_ABBR STATE_NAME TIME_0001_0200_CNT TIME_0201_0400_CNT TIME_0401_0600_CNT TIME_0601_0800_CNT TIME_0801_1000_CNT TIME_1001_1200_CNT TIME_1201_1400_CNT ... BLACK_POP POP OFFICER_COUNT OFFICER_RATE_PER_1000 ASSAULTS_PERMLN LETHAL_PERMLN LETHAL_ASSAULTS_PERC ASSAULTS_PER_OFFICERSCNT LETHAL_PER_OFFICERSCNT CRIME
0 1995 AK Alaska 24 31 9 4 4 4 7 ... 0.0 604000 1134 1.88 226.821192 0.000000 0.000000 0.120811 0.000000 4656
1 1995 AL Alabama 16 17 6 3 6 6 16 ... 0.0 4319212 9191 2.13 40.516650 0.231524 0.571429 0.019040 0.000109 26894
2 1995 AR Arkansas 49 30 22 17 21 20 17 ... 0.0 2540304 4743 1.87 173.994923 0.787307 0.452489 0.093190 0.000422 13741
3 1995 AZ Arizona 255 143 75 47 69 80 100 ... 0.0 4236915 8706 2.05 459.768487 0.708062 0.154004 0.223754 0.000345 30095
4 1995 CA California 893 537 212 159 280 328 469 ... 0.0 31854695 63218 1.98 235.538278 0.219748 0.093296 0.118685 0.000111 305154
5 1995 CO Colorado 61 29 17 4 13 15 24 ... 0.0 4892816 10982 2.24 81.956894 0.000000 0.000000 0.036514 0.000000 16494
6 1995 CT Connecticut 105 62 12 20 20 39 37 ... 0.0 3356858 7423 2.21 215.677875 0.000000 0.000000 0.097535 0.000000 13293
7 1995 DC District of Columbia 0 0 0 0 0 0 0 ... 0.0 554000 3691 6.66 0.000000 0.000000 0.000000 0.000000 0.000000 14744
8 1995 DE Delaware 1 5 1 0 0 0 0 ... 0.0 736565 2107 2.86 24.437762 0.000000 0.000000 0.008543 0.000000 5198
9 1995 FL Florida 1389 1041 430 260 318 565 508 ... 0.0 14214968 36383 2.56 705.523924 0.000000 0.000000 0.275651 0.000000 151711
10 1995 GA Georgia 277 58 34 12 45 42 41 ... 0.0 7259408 31523 4.34 109.099805 0.275505 0.252525 0.025125 0.000063 47317
11 1995 HI Hawaii 67 51 19 21 14 25 24 ... 0.0 1241665 2805 2.26 369.664926 0.000000 0.000000 0.163636 0.000000 3509
12 1995 IA Iowa 9 7 1 2 1 1 0 ... 0.0 3041956 4955 1.63 17.751736 0.000000 0.000000 0.010898 0.000000 10071
13 1995 ID Idaho 68 38 12 12 10 7 19 ... 0.0 1215666 2225 1.83 328.215151 0.000000 0.000000 0.179326 0.000000 3745
14 1995 IL Illinois 0 0 0 0 0 0 0 ... 0.0 12093701 32931 2.72 0.000000 0.000000 0.000000 0.000000 0.000000 117836


В таблице этой аж 37 столбцов! Давайте сразу разберемся, что тут есть:

Столбец Тип Описание
DATA_YEAR uint16 Год
STATE_ABBR object Краткое название штата
STATE_NAME object Полное название штата
TIME_0001_0200_CNT uint16 Кол-во нападений с 00:01 до 02:00 часов
TIME_0201_0400_CNT uint16 Кол-во нападений с 02:01 до 04:00 часов
TIME_0401_0600_CNT uint16 Кол-во нападений с 04:01 до 06:00 часов
TIME_0601_0800_CNT uint16 Кол-во нападений с 06:01 до 08:00 часов
TIME_0801_1000_CNT uint16 Кол-во нападений с 08:01 до 10:00 часов
TIME_1001_1200_CNT uint16 Кол-во нападений с 10:01 до 12:00 часов
TIME_1201_1400_CNT uint16 Кол-во нападений с 12:01 до 14:00 часов
TIME_1401_1600_CNT uint16 Кол-во нападений с 14:01 до 16:00 часов
TIME_1601_1800_CNT uint16 Кол-во нападений с 16:01 до 18:00 часов
TIME_1801_2000_CNT uint16 Кол-во нападений с 18:01 до 20:00 часов
TIME_2001_2200_CNT uint16 Кол-во нападений с 20:01 до 22:00 часов
TIME_2201_0000_CNT uint16 Кол-во нападений с 22:01 до 00:00 часов
FIREARM_INJURY_CNT uint16 Кол-во нападений с применением огнестрельного оружия
FIREARM_NO_INJURY_CNT uint16 Кол-во нападений с применением огнестрельного оружия
KNIFE_INJURY_CNT uint16 Кол-во нападений с применением холодного оружия
KNIFE_NO_INJURY_CNT uint16 Кол-во нападений с применением холодного оружия
HANDS_FISTS_FEET_INJURY_CNT uint16 Кол-во нападений с ударами руками, ногами (с телесными повреждениями)
HANDS_FISTS_FEET_NO_INJURY_CNT uint16 Кол-во нападений с ударами руками, ногами (без повреждений)
OTHER_INJURY_CNT uint16 Кол-во нападений с применением иного орудия (с телесными повреждениями)
OTHER_NO_INJURY_CNT uint16 Кол-во нападений с применением иного орудия (без повреждений)
LEOKA_FELONY_KILLED uint16 Кол-во предумышленных убийств
LEOKA_ACCIDENT_KILLED uint16 Кол-во непредумышленных убийств
ASSAULTS uint32 Кол-во нападений (общее)
WHITE_POP float64 Кол-во белого населения (чел.)
BLACK_POP float64 Кол-во черного населения (чел.)
POP uint32 Общее кол-во населения (чел.)
OFFICER_COUNT uint32 Численность сотрудников полиции (чел.)
OFFICER_RATE_PER_1000 float64 Численность сотрудников полиции на 1000 чел.
ASSAULTS_PERMLN float64 Кол-во нападений на 1 млн. населения
LETHAL_PERMLN float64 Кол-во убийств полицейских на 1 млн. населения
LETHAL_ASSAULTS_PERC float64 % убийств от нападений
ASSAULTS_PER_OFFICERSCNT float64 Кол-во нападений на 1 работающего полицейского
LETHAL_PER_OFFICERSCNT float64 Кол-во убийств полицейских на 1 работающего полицейского
CRIME uint32 Кол-во тяжких совершенных преступлений


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

Среднегодовые показатели по штатам


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



Картинка здесь примерно та же, которую мы видели при анализе распределения уровня преступности по штатам: самые населенные штаты ожидаемо доминируют (Калифорния, Флорида, Техас), поскольку, естественно, чем больше народа, тем больше и полиции, и конфликтов с полицией. Это подтверждает и зеленая кривая тяжких преступлений, которая по большому счету повторяет очертания синих столбиков. Но здесь обращает на себя внимание Мэриленд далеко не самый населенный штат (на 19-м месте по населению) в непосредственной близи от столицы страны. К этому моменту мы еще вернемся, а пока давайте попробуем отфильтровать составляющую населения и посмотреть на удельные показатели (на 1 млн. человек):



Здесь на первом плане не самые населенные штаты Округ Колумбия (т.е. Вашингтон), тот же Мэриленд, Делавэр, Аляска и та же Флорида. Отсюда можно сделать вывод: в Мэриленде и Флориде нападения на полицейских происходят практически независимо от того, сколько людей проживает в этих штатах. Также здесь налицо отсутствие четкой корреляции с данными по преступлениям: в лидирующих штатах по нападениям на полицейских (кроме Округа Колумбия) удельный уровень преступности не самый высокий, как, например, в Нью-Мексико, Неваде или Южной Каролине.

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



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

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



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

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



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



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

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





Интересно: неожиданно выстреливает (простите за дурной каламбур) Южная Дакота и четыре хлопковых штата на юго-востоке: Миссисипи, Алабама, Джорджия и Южная Каролина. Южная Дакота занимает одно из последних мест по количеству и плотности населения, а также 3-е место по численности коренного населения (индейцев около 8,5%). В индейских резервациях довольно низкий доход на душу населения, и, возможно, поэтому Впрочем, мы договорились: никаких гипотез, не кормим хейтеров :)

Нападения по времени суток


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



Так и есть! Количество неадекватовграждан, склонных решать свои разногласия с правоохранительными органами физическим путем, возрастает с наступлением ночи, достигая своего пика с часу до двух утра, и постепенно спадает под утро, с минимумом в промежуток с 06:00 до 08:00. Как говорится, мафия засыпает.

Идем дальше!

Годовые изменения общих показателей по стране


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



Что мы здесь можем увидеть? До 2008 г. численность полиции активно наращивается, но количество нападений остается примерно на одном уровне (в районе 60 тыс. в год). С 2008 по 2016 г. полицейские подразделения особо не растут в численности, зато нападения становятся год от года все реже, хотя и спад едва заметен. В 2016 году происходит какой-то всплеск насилия (с возвратом на уровень 1998 г.), а также достигает своего пика численность полиции. А после 2016 г. штат полиции начинает резко сокращаться до уровня 2000 г. в 2018 г., при этом снижающийся тренд нападений удается сохранить.

Сокращение штата полиции началось при Дональде Трампе в 2017 г. и, вполне вероятно, будет продолжено при новом президенте на фоне публичных движений против полиции (BLM, Defund the Police и т.д.). При этом сохранение спада нападений на полицейских может, конечно, объясняться как раз сокращением самих полицейских, однако здесь важнее прогноз уровня преступности в стране, который, естественно, зависит от наличия и количества стражей порядка. Если эта тема интересна, в этой статье представлен неплохой обзор.

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



А что с убийствами полицейских? Давайте посмотрим:



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

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



Очевидно, здесь аналогичная картина.

Но черт возьми, давайте уже разберемся с 2016 годом! Что за всплеск насилия против полиции? Посмотрим на этот год в разрезе штатов, отобразив как убийства, так и нападения:



В половине штатов убийств вообще не было, в четверти штатов было по 1-2 случаям, а остальные случаи приходятся, в основном, на южные и восточные штаты. Про Техас и Калифорнию мы уже знаем Но в чем проблема в Вирджинии? Синяя точка показывает, что в этот год в Вирджинии было не так уж много нападений на полицейских (чуть больше 1500) не то, что в Техасе, Калифорнии и Флориде. И при этом 11 убитых полицейских! Анализ исходного файла (LEOKA_ASSAULT_TIME_WEAPON_INJURY_1995_2018.csv) показывает, что было зарегистрировано 9 случаев, из которых в 8 случаях убито по 1 полицейскому, а в 1 случае убито сразу 3.

Здесь необходимо отдельно указать, что, конечно же, нельзя считать исходную базу ФБР, откуда мы берем данные по нападениям и убийствам полицейских, на 100% полной. Эти данные собираются ФБР на основе добровольно направляемой информации различными агентствами (подразделениями полиции). То есть если какое-то подразделение не участвует в программе предоставления сводной информации, соответствующих случаев в базе попросту не будет! Простой пример: за тот же 2016 г. в штате Айова в базе не зарегистрировано ни одного намеренного убийства полицейских. При этом был довольно громкий случай стрельбы в г. Де-Мойн из засады с гибелью целых двух полицейских, который даже описан на отдельной странице в Википедии. Кстати, обратите внимание на этот список убитых полицейских пока он не полный, но начиная с 2018 г. уже есть контент и ссылки на источники. В частности, отдельно освещены стрельбы в Техасе и Луизиане в 2016 г. Дополнительно, если нужны более детальные описания случаев убийства полицейских, на том же сайте ФБР доступна ежегодно пополняемая публикация, подробно освещающая эти события по каждому штату на настоящий момент книжечка покрывает события с 2002 по 2019 г. и занимает 563 страницы без картинок :)


Виды телесных повреждений


Давайте теперь посмотрим на статистику нападений по видам наносимых телесных повреждений. Зачем? Потому что мы можем :)



ОК, по годам доли остаются примерно теми же. Посмотрим в укрупненном виде, усреднив данные по всем наблюдениям:



Итак, подавляющее большинство всех телесных повреждений при нападениях наносится голыми руками и ногами. Огнестрельное и холодное оружие используется только в 3% нападений, и остальные 13% может быть чем угодно (хоть плюшевым мишкой). Впрочем, средняя доля смертельных случаев составляет всего 0,07%, поэтому эти 3% огнестрельного и холодного оружия (а также не забываем про все остальное) вполне объясняют смертельные случаи.

Расовый и возрастной состав преступников и их жертв


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

Начнем с расового состава преступников (т.е. убийц полицейских):



Ожидаемо белая раса выделяется просто потому, что белых в США больше. Но обратим мимоходом внимание: пропорция не всегда была одинаковой (например, в 2010 чернокожие преступники даже численно превосходили, а в 2015 г. примерно 50/50). Но давайте посмотрим на усредненные показатели:



56% белых, 37% черных и 6% всех остальных. При этом если брать все население США, расовый состав довольно сильно отличается. Так представители каких же рас статистически чаще убивают полицейских? Давайте посмотрим на удельные показатели:



Здесь однозначно доминируют черные и индейцы. Примерно на каждые 2 млн. чернокожих или индейцев один убийца. Для белых же этот показатель 1 убийца на каждые 8 млн. (т.е. в 4 раза меньше). Это наблюдение согласуется с результатами предыдущего исследования по преступности в США, где также была выявлена большая склонность чернокожих к совершению преступлений (под склонностью я имею в виду, конечно, не этническую или психологическую расположенность, а статистическую закономерность).

Давайте теперь перейдем к возрастному составу убийц:



Картина здесь также довольно обычная: половина всех убийств приходится на молодых людей в возрасте от 18 до 30 лет. Целесообразно предположить, что именно этот промежуток жизни в целом наиболее связан с наиболее активными действиями (плохими или хорошими). По мере старения (после 35 лет) убивают все меньше, хотя и на возраст после 60 лет приходится 2% убийств старый не значит безобидный :) Несовершеннолетних убийц 3%.

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



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



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

Последнее, на что осталось взглянуть, это возрастной состав убитых:



Здесь видим довольно ровное распределение возрастных групп от 25 до 50 лет (с небольшим превалированием группы с 31 до 35 лет), что говорит о таком же равномерном распределении оперативных работников полиции по возрасту. Убитых в возрасте до 25 лет всего 4%, из чего можем заключить, что новичков обычно не отправляют на опасные задания, связанные с риском для жизни. Впрочем, чтобы эти заключения были более достоверными, также необходимы данные по возрастному составу всех полицейских.

Корреляции


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

Для анализа же возьмем самые основные показатели: количество нападений на полицейских, количество совершаемых преступлений, численность полиции и численность населения:
Нападения Преступления Кол-во полицейских Население
Нападения 1.000000 0.827702 0.739385 0.779203
Преступления 0.827702 1.000000 0.904080 0.931361
Кол-во полицейских 0.739385 0.904080 1.000000 0.958782
Население 0.779203 0.931361 0.958782 1.000000

Здесь все наиболее значимые зависимости выделены жирным шрифтом. Таким образом, очевидны следующие взаимосвязи:

  • Между количеством нападений на полицейских и количеством тяжких преступлений: логично предположить, что конфликты, приводящие к физической агрессии против полицейских, в свою очередь связаны с уровнем преступности. Чем больше преступлений, тем больше стычек с полицией (спасибо, кэп!).
  • Между количеством нападений на полицейских и численностью полиции. Здесь также все однозначно: чем больше объектов нападения, тем больше и самих нападений. Так как нападение (в отличие, в какой-то мере, от убийства) трудно предотвратить (например, теоретически любой может подойти к человечку в форме и дать ему пинка это будет уже нападением), отсюда и корреляция. Кстати, мы уже говорили, что именно поэтому количество нападений на полицейских снижается с 2017 г. потому что снижается и численность самих полицейских. Что, конечно, не говорит о повышении безопасности в обществе.
  • Между количеством нападений на полицейских и населением. Тут тоже все ясно: больше людей больше преступников и полицейских больше конфликтов между ними.
  • Между количеством преступлений и численностью полиции: численность полиции наращивается в ответ на увеличение преступности или, говоря по-другому, в регионах, где совершается больше преступлений, нанимается больше полиции для сдерживания преступности.
  • Между количеством преступлений и населением: эту корреляцию мы уже рассматривали в предыдущих исследованиях, она самоочевидна.
  • Между численностью полиции и населением. Здесь логика та же: больше народа больше преступников больше полицейских.


На этом наш небольшой анализ подошел к концу. Спасибо за ваши отзывы!
Подробнее..
Категории: Python , Open source , Big data , Data mining , Pandas , Usa , Police , Statistics

Перевод Запускаем модель машинного обучения на iPhone

18.04.2021 18:19:42 | Автор: admin

Чего уж только на Хабре не было, и DOOM на осциллографе, тесте на беременности и калькуляторе запускали, даже сервер Minecraftна зеркалке Canon 200D поднимали. Сегодня же, специально к старту нового потока курса по Machine Learning и углубленного Machine Learning и Deep Learning, попробуем описать кратчайший путь от обучения модели машинного обучения на Python до доказательства концепции iOS-приложения, которое можно развернуть на iPhone. Цель статьи дать базовый скаффолдинг, оставляя место для дальнейшей настройки, подходящей для конкретного случая использования.


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

Шаг 1. Настройка среды

Во-первых, давайте создадим виртуальную среду Python под названием .core_ml_demo, а затем установим необходимые библиотеки: pandas, scikit-learn и coremltools. Чтобы создать виртуальную среду, выполните в терминале эти команды:

python3 -m venv ~/.core_ml_demosource  ~/.core_ml_demo/bin/activatepython3 -m pip install \pandas==1.1.1 \scikit-learn==0.19.2 \coremltools==4.0

Далее установим Xcode. XCode это инструментарий разработки для продуктов Apple. Обратите внимание, что Xcode довольно большой (больше 10 ГБ). Я бы порекомендовал выпить чашку кофе или запустить установку на ночь.

Примечание: в этом туториале используется Xcode 12.3 (12C33) на MacOS Catalina 10.15.5.

Шаг 2. Обучение модели

Мы будем использовать набор данных Boston Housing Price от scikit-learn для обучения модели линейной регрессии и прогнозирования цен на жильё на основе свойств и социально-экономических атрибутов.

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

import pandas as pdfrom sklearn.linear_model import LinearRegressionfrom sklearn.datasets import load_boston# Load databoston = load_boston()boston_df = pd.DataFrame(boston["data"])boston_df.columns = boston["feature_names"]boston_df["PRICE"]= boston["target"]y = boston_df["PRICE"]X = boston_df.loc[:,["RM", "AGE", "PTRATIO"]]# Train a modellm = LinearRegression()lm.fit(X, y)

Шаг 3. Преобразование модели в Core ML

Apple предоставляет два способа разработки моделей для iOS. Первый, Create ML, позволяет создавать модели полностью в рамках экосистемы Apple. Второй, Core ML, позволяет интегрировать модели от третьих лиц в платформу Apple, преобразовав их в формат Core ML. Поскольку мы заинтересованы в запуске обученной модели на iOS, воспользуемся вторым способом.

Перед импортом в Xcode мы преобразуем нашу модель sklearn в формат Core ML (.mlmodel) с помощью пакета python coremltool; coremltools позволяет назначать метаданные объекту модели, такие как информация об авторстве, описание функций модели и результатов.

# Convert sklearn model to CoreMLimport coremltools as cmlmodel = cml.converters.sklearn. \convert(lm,        ["RM", "AGE", "PTRATIO"],        "PRICE")# Assign model metadatamodel.author = "Medium Author"model.license = "MIT"model.short_description = \"Predicts house price in Boston"# Assign feature descriptionsmodel.input_description["RM"] = \"Number of bedrooms"model.input_description["AGE"] = \"Proportion of units built pre 1940"model.input_description["PTRATIO"] = \"Pupil-teacher ratio by town"# Assign the output descriptionmodel.output_description["PRICE"] = \"Median Value in 1k (USD)"# Save modelmodel.save('bhousing.mlmodel')

Шаг 4. Создание нового проекта в Xcode

С Python мы закончили. Теперь можно завершить прототип приложения при помощи только Xcode и Swift. Это можно сделать так:

  1. Откройте Xcode и создайте новый проект.

  2. Выберите iOS как тип мультиплатформы.

  3. Выберите тип приложения App.

Создание нового проекта Xcode для iOSСоздание нового проекта Xcode для iOS

4. Дайте проекту название и выберите интерфейс SwiftUI.

Конфигурация проектаКонфигурация проекта

Теперь просто перетащите созданный на третьем шаге файл модели .ml в каталог Xcode. Xcode автоматически сгенерирует класс Swift для вашей модели, как показано в редакторе ниже. Если вы посмотрите на класс, то заметите, что он содержит детали, которые мы ввели при сохранении нашей модели на Python с помощью coremltools, такие как описания объектов и целевых полей. Это удобно при управлении моделью.

Импорт файла .coreml в проект XcodeИмпорт файла .coreml в проект Xcode

Шаг 5. Создание UI модели

Далее создадим базовый пользовательский интерфейс, изменив файл contentView.swift. Приведённый ниже код на Swift отображает пользовательский интерфейс, который позволяет пользователям настраивать атрибуты дома, а затем прогнозировать его цену. Есть несколько элементов, которые мы можем здесь рассмотреть.

NavigationView содержит необходимый пользовательский интерфейс. Он включает:

  • Степпер (строки 1930) для каждой из наших трёх функций, который позволяет пользователям изменять значения функций. Степперы это в основном виджеты, которые изменяют @State атрибутных переменных нашего дома (строки 68).

  • Кнопку на панели навигации (строки 3140) для вызова нашей модели из функции predictPrice (строка 46). На экране появится предупреждающее сообщение с прогнозируемой ценой.

За пределами NavigationView у нас есть функция predictPrice (строки 4662). Она создаёт экземпляр класса Swift Core ML model и генерирует прогноз в соответствии с хранящимися в состояниях объектов значениями.

import SwiftUIimport CoreMLimport Foundationstruct ContentView: View {  @State private var rm = 6.5  @State private var age = 84.0  @State private var ptratio = 16.5      @State private var alertTitle = ""  @State private var alertMessage = ""  @State private var showingAlert = false      var body: some View {      NavigationView {        VStack {        Text("House Attributes")            .font(.title)        Stepper(value: $rm, in: 1...10,                step: 0.5) {            Text("Rooms: \(rm)")          }          Stepper(value: $age, in: 1...100,              step: 0.5) {          Text("Age: \(age)")          }          Stepper(value: $ptratio, in: 12...22,              step: 0.5) {          Text("Pupil-teacher ratio: \(ptratio)")          }          .navigationBarTitle("Price Predictor")          .navigationBarItems(trailing:              Button(action: predictPrice) {                  Text("Predict Price")              }          )          .alert(isPresented: $showingAlert) {              Alert(title: Text(alertTitle),                    message: Text(alertMessage),              dismissButton: .default(Text("OK")))          }        }      }  }            func predictPrice() {    let model = bhousing()    do { let p = try      model.prediction(          RM: Double(rm),          AGE: Double(age),          PTRATIO: Double(ptratio))        alertMessage = "$\(String((p.PRICE * 1000)))"      alertTitle = "The predicted price is:"  } catch {    alertTitle = "Error"    alertMessage = "Please retry."  }    showingAlert = true}}struct ContentView_Previews: PreviewProvider {    static var previews: some View {        ContentView()    }}

И, наконец, самое интересное: мы можем создать и запустить симуляцию приложения в Xcode, чтобы увидеть нашу модель в действии. В приведённом ниже примере я создал симуляцию с помощью iPhone 12.

Симуляция модели работает на iOS.Симуляция модели работает на iOS.

Заключение

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

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

Если вы кодите на Python и столкнулись в работе с задачами машинного обучения обратите внимание на наш курс Machine Learning, на котором вы сможете систематизировать и углубить полученные самостоятельно знания, пообщаться с профессионалами и применить модели Machine Learning на практике. Даже можно будет ворваться на хакатоны Kaggle.

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Звездные войны или подробный гайд по dplyr

04.05.2021 18:23:50 | Автор: admin

Сегодня, 4 мая, в день Звездных войн мы подготовили для Вас подробный гайд по основным функциям библиотеки dplyr. Почему именно в день Звездных войн? А потому что разбирать мы все будем на примере датасета starwars.

Ну что, начнем!

Если Вы хотите получать еще больше интересных материалов по программированию, Data Science и математике, то подписывайтесь на нашу группу ВК, канал в Телеграме и Инстаграм. Каждый день мы публикуем полезный контент и вопросы с реальных собеседований.

Кстати говоря, а Вы знаете, почему день Звездных войн отмечается именно 4 мая? Все очень просто - знаменитая фраза May the fource be with you крайне созвучна с May, the 4th, т.е. 4 мая :)

Знакомство с датасетом

Для начала, давайте подключим библиотеку dplyr. Делается это с помощью функции library.

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

  1. name - имя или прозвище героя вселенной Звездных войн. Например, Оби-Ван Кеноби.

  2. height - высота персонажа

  3. mass - масса персонажа

  4. hair_color - цвет волос

  5. skin_color - цвет кожи

  6. eye_color - цвет глаз

  7. birth_year - год рождения (до битвы на Явине)

  8. sex - биологический пол (есть существа без пола и гермафродиты)

  9. gender - поведенческий пол персонажа (например, на какой пол запрограммированы дроиды)

  10. homeworld - из какой вселенной существо

  11. species - биологический вид

  12. films - список фильмов, в которых появилось существо

  13. vehicles - список транспорта, которым существо управляло

  14. starships - список космических кораблей, которыми существо управляло

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

Знакомство с dplyr

dplyr - это один из основных пакетов семейства tidyverse. Его ближайший аналог в Python - библиотека Pandas. Библиотека dplyr служит для манипуляции с данными: фильтрация, сортировка, создание вычислимых столбцов и так далее.

По своему функционалу библиотека dplyr очень похожа на стандартный синтаксис SQL. Немного ранее мы вместе с Алексеем Селезневым из Netpeak делали карточки: сравнение глаголов dplyr и операторов SQL. Вы можете посмотреть их здесь.

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

  1. Каждая переменная находится в отдельном столбце

  2. Каждое измерение находится в отдельной строке

  3. Каждое значение находится в отдельной ячейке

Кстати говоря, наш датасет starwars не совсем соответствует этим правилам. Нам это не помешает, но сможете ли Вы найти, в чем именно несоответствие? ;)

Если Вы хотите поподробней познакомиться с tidy data, рекомендуем нашу статью.

Итак, вернемся к глаголам dplyr. Пакет dplyr позволяет делать несколько основных операций:

  • Отбор столбцов

  • Фильтрация строк

  • Сортировка строк

  • Группировка

  • Агрегирование

  • Создание вычислимых столбцов

  • Объединение таблиц

Давайте переходить к практике - хватит теории!

Отбор столбцов

Давайте начнем с самого простого - как выбрать только определенные столбцы из таблицы? Очень просто - с помощью функции select.

А что если нам проще указать, какие столбцы не надо отбирать? Например, в таблице 20 столбцов, а нам нужно исключить только первый столбец. Или столбец с определенными названиями. Не проблема - просто добавьте знак минус:

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

  • contains: название столбца содержит

  • ends_with: название столбца заканчивается на

  • matches: название столбца соответствует регулярному выражению

  • num_range: поиск занумерованных столбцов, например, V1, V2, V3...

  • one_of: название столбца соответствует одному из вариантов

  • starts_with: название столбца начинается с

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

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

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

Обратите внимание, все наши запросы возвращали таблицу tibble. А что, если мы хотим отобрать столбец и сразу начать работать с ним, как с вектором? Мало кто знает, но для этого в dplyr есть специальная функция pull. Она возвращает не таблицу, как остальные глаголы dplyr, а вектор.

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

Фильтрация строк

Фильтрация строк по значениям - это аналог привычного оператора WHERE в SQL. В синтаксисе dplyr же для этого используется глагол filter (как неожиданно, правда?).

Использовать filter очень просто - задаем некое логическое выражение и функция вернет только те строки, для которых это выражение True. Например:

Вы можете комбинировать несколько условий с помощью & и |:

Логические выражения Вы можете конструировать не только с помощью >/<, но и с помощью других логических операторов:

  • >=

  • <=

  • is.na

  • !is.na

  • %in%

  • !

Например:

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

Другая функция, sample_n, отбирает n случайных строк.

Функция slice же, например, позволяет отбирать строки по их индексу:

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

Итак, с фильтрацией строк мы тоже разобрались, переходим к следующему разделу.

Сортировка строк

За сортировку строк в SQL отвечает оператор ORDER BY. В dplyr для этого существует функция arrange.

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

Чтобы отсортировать строки по убыванию, достаточно добавить функцию desc.

А если отсортировать нужно по нескольким столбцам? Легко, просто указываем их названия :)

Кстати говоря, с arrange Вы можете также использовать вспомогательные глаголы, которые мы обсуждали в блоке с select. Для этого нужно использовать функцию across. Например:

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

Группировка и агрегатные функции

Группировка - базовая операция, которая необходима для расчета различных характеристик - средних значений, медиан, сумм, количества строк в группе и так далее. В SQL для этого используется оператор GROUP BY и агрегатные функции sum, min, max и так далее. В dplyr же все то же самое :) Ну, почти.

Давайте для начала сгруппируем наши строки по полю eye_color:

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

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

Обратите внимание, здесь мы дополнительно воспользовались функцией drop_na из пакета tidyr, чтобы удалить строки, в которых есть пропуски. Сделали мы это, чтобы при расчете наших максимальных/минимальных/других агрегатных значений не вылезали значения NA.

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

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

  • n_distinct - считает количество уникальных элементов в группе

  • last - возвращает последнее значение в группе

  • nth - возвращает n-ое значение из группы

  • quantile - возвращает заданную квантиль

  • IQR - межквартильный размах, inter-quartile range

  • mad - медианное абсолютное отклонение, median absolute deviation

  • sd - стандартное отклонение

  • var - вариация

Хорошо, с базовой группировкой мы разобрались. А что если пойти дальше

Продвинутая группировка

А как Вам такая задача - рассчитать все те же самые характеристики, что и в прошлый раз, но сразу для нескольких столбцов? Например, мы делали для mass, а теперь давайте сделаем для mass и height.

Без проблем - достаточно применить уже известную функцию across.

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

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

Вычислимые столбцы

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

Для начала базовая вещь - создадим вычислимый столбец отношения веса к росту.

А если нам нужно применить одну и ту же функцию к нескольким столбцам? Без проблем - снова нас выручит across. Например, давайте умножим на 10 все столбцы с численным типом данных. При этом мы не будем создавать новые столбцы - мы модифицируем старые.

Видно, что в столбцах с весом, ростом и возрастом все значения умножились на 10.

Давайте и с текстом немного поработаем - ко всем строковым значениям добавим суффикс _new. Для этого нам понадобится библиотека stringr все из того же семейства tidyverse.

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

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

Видно, что в конец таблицы добавился новый столбец rnk с рангами для каждой строки.

Таких функций, на самом деле, масса. Вот некоторые из них:

  • lag

  • lead

  • cumsum

  • dense_rank

  • ntile

  • row_number

  • case_when

  • coalesce

Это, пожалуй, самые популярные и все они на 100% перекликаются с SQL. Приведем несколько примеров.

Ну что, давайте переходить к последнему пункту - к объединению таблиц.

Объединение таблиц

Пожалуй, последняя необходимая для полноценной работы с датафреймами операция - объединение таблиц. Операция объединения в dplyr тесно связана с джоинами в SQL. Вот перечень основных функций:

  • left_join

  • right_join

  • inner_join

  • full_join

Аналогичные глаголы присутствуют и в стандартном SQL. Давайте создадим новую таблицу - возьмем датасет starwars и оставим только те строки, где вид существа - человек. А после попробуем различные виды джоинов.

Обратите внимание, мы воспользовались функцией rename и переименовали поле name. Сделали мы это намеренно, чтобы показать работу аргумента by (аналог ON в SQL) для связи столбцов при джоине.

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

Если сделать full_join аналогичным образом, то строк в итоговой таблице будет 87, т.к. в таблице starwars их 87.

Внимательный читатель может заметить, что при джоине одни и те же столбцы продублировались из двух таблиц. Т.е. столбец mass, например, в итоговой таблице фигурирует с суффиксом .x и .y. Как от этого избавиться?

1 вариант:

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

2 вариант:

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

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

  • bind_rows - помещает одну таблицу под другой

  • bind_cols - ставит одну таблицу справа от другой

  • intersect - находит пересекающиеся строки

  • setdiff - разность таблиц, т.е. строки из первой таблицы, которых нет во второй

  • union - возвращает строки, которые есть в любой из таблиц (дубликаты исключаются)

  • union_all - аналогично union, но оставляет дуликаты

Эпилог

Мы с Вами рассмотрели все основные операции библиотеки dplyr и почти все основные функции. Все остальное - практика, практика и еще раз практика. Если у Вас будут вопросы - рады будем помочь и ответить в комментариях :)

Если Вы хотите получать еще больше интересных материалов по программированию, Data Science и математике, то подписывайтесь на нашу группу ВК, канал в Телеграме и Инстаграм. Каждый день мы публикуем полезный контент и вопросы с реальных собеседований.

А, и совсем забыли. May the fource be with you!

P.S. Здесь Вы можете найти официальную шпаргалку по всем функциям dplyr. Все удобно и компактно собрано в одном месте :)

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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