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

Scipy

Перевод О нет! Моя Data Science ржавеет

04.07.2020 14:10:42 | Автор: admin
Привет, Хабр!

Предлагаем вашему вниманию перевод интереснейшего исследования от компании Crowdstrike. Материал посвящен использованию языка Rust в области Data Science (применительно к malware analysis) и демонстрирует, в чем Rust на таком поле может посоперничать даже с NumPy и SciPy, не говоря уж о чистом Python.


Приятного чтения!

Python один из самых популярных языков программирования для работы с data science, и неслучайно. В индексе пакетов Python (PyPI) найдется огромное множество впечатляющих библиотек для работы с data science, в частности, NumPy, SciPy, Natural Language Toolkit, Pandas и Matplotlib. Благодаря изобилию высококачественных аналитических библиотек в доступе и обширному сообществу разработчиков, Python очевидный выбор для многих исследователей данных.

Многие из этих библиотек реализованы на C и C++ из соображений производительности, но предоставляют интерфейсы внешних функций (FFI) или привязки Python, так, чтобы из функции можно было вызывать из Python. Эти реализации на более низкоуровневых языках призваны смягчить наиболее заметные недостатки Python, связанные, в частности, с длительностью выполнения и потреблением памяти. Если удается ограничить время выполнения и потребление памяти, то сильно упрощается масштабируемость, что критически важно для сокращения расходов. Если мы сможем писать высокопроизводительный код, решающий задачи data science, то интеграция такого кода с Python станет серьезным преимуществом.

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

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

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

Пример приложения для Data Science


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



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

Давайте испробуем Rust и посмотрим, как он справляется с вычислением энтропии по сравнению с чистым Python, а также с некоторыми популярнейшими библиотеками Python, упомянутыми выше. Это упрощенная оценка потенциальной производительности Rust в области data science; данный эксперимент не является критикой Python или отличных библиотек, имеющихся в нем. В этих примерах мы сгенерируем собственную библиотеку C из кода Rust, который сможем импортировать из Python. Все тесты проводились на Ubuntu 18.04.

Чистый Python


Начнем с простой функции на чистом Python (в entropy.py) для расчета энтропии bytearray, воспользуемся при этом только математическим модулем из стандартной библиотеки. Эта функция не оптимизирована, возьмем ее в качестве отправной точки для модификаций и измерения производительности.

import mathdef compute_entropy_pure_python(data):    """Compute entropy on bytearray `data`."""    counts = [0] * 256    entropy = 0.0    length = len(data)    for byte in data:        counts[byte] += 1    for count in counts:        if count != 0:            probability = float(count) / length            entropy -= probability * math.log(probability, 2)    return entropy

Python с NumPy и SciPy


Неудивительно, что в SciPy предоставляется функция для расчета энтропии. Но сначала мы воспользуемся функцией unique() из NumPy для расчета частот байтов. Сравнивать производительность энтропийной функции SciPy с другими реализациями немного нечестно, так как в реализации из SciPy есть дополнительный функционал для расчета относительной энтропии (расстояния Кульбака-Лейблера). Опять же, мы собираемся провести (надеюсь, не слишком медленный) тест-драйв, чтобы посмотреть, какова будет производительность скомпилированных библиотек Rust, импортированных из Python. Будем придерживаться реализации из SciPy, включенной в наш скрипт entropy.py.

import numpy as npfrom scipy.stats import entropy as scipy_entropydef compute_entropy_scipy_numpy(data):    """Вычисляем энтропию bytearray `data` с SciPy и NumPy."""    counts = np.bincount(bytearray(data), minlength=256)    return scipy_entropy(counts, base=2)

Python с Rust


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

cargo new --lib rust_entropyCargo.toml

Начинаем с обязательного файла манифеста Cargo.toml, в котором определяем пакет Cargo и указываем имя библиотеки, rust_entropy_lib. Используем общедоступный контейнер cpython (v0.4.1), доступный на сайте crates.io, в реестре пакетов Rust Package Registry. В статье мы используем Rust v1.42.0, новейшую стабильную версию, доступную на момент написания.

[package] name = "rust-entropy"version = "0.1.0"authors = ["Nobody <nobody@nowhere.com>"] edition = "2018"[lib] name = "rust_entropy_lib"crate-type = ["dylib"][dependencies.cpython] version = "0.4.1"features = ["extension-module"]

lib.rs


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

use cpython::{py_fn, py_module_initializer, PyResult, Python};/// вычисляем энтропию массива байтfn compute_entropy_pure_rust(data: &[u8]) -> f64 {    let mut counts = [0; 256];    let mut entropy = 0_f64;    let length = data.len() as f64;    // collect byte counts    for &byte in data.iter() {        counts[usize::from(byte)] += 1;    }    // вычисление энтропии    for &count in counts.iter() {        if count != 0 {            let probability = f64::from(count) / length;            entropy -= probability * probability.log2();        }    }    entropy}

Все, что нам остается взять из lib.rs это механизм для вызова чистой функции Rust из Python. Мы включаем в lib.rs функцию, приспособленную к работе с CPython (compute_entropy_cpython()) для вызова нашей чистой функции Rust (compute_entropy_pure_rust()). Поступая таким образом, мы только выигрываем, так как будем поддерживать единственную чистую реализацию Rust, а также предоставим обертку, удобную для работы с CPython.

/// Функция Rust для работы с CPython fn compute_entropy_cpython(_: Python, data: &[u8]) -> PyResult<f64> {    let _gil = Python::acquire_gil();    let entropy = compute_entropy_pure_rust(data);    Ok(entropy)}// инициализируем модуль Python и добавляем функцию Rust для работы с CPython py_module_initializer!(    librust_entropy_lib,    initlibrust_entropy_lib,    PyInit_rust_entropy_lib,    |py, m | {        m.add(py, "__doc__", "Entropy module implemented in Rust")?;        m.add(            py,            "compute_entropy_cpython",            py_fn!(py, compute_entropy_cpython(data: &[u8])            )        )?;        Ok(())    });

Вызов кода Rust из Python


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

import rust_entropy_libdef compute_entropy_rust_from_python(data):    ""Вычисляем энтропию bytearray `data` при помощи Rust."""    return rust_entropy_lib.compute_entropy_cpython(data)

Мы собираем вышеприведенный библиотечный пакет Rust на Ubuntu 18.04 при помощи Cargo. (Эта ссылка может пригодиться пользователям OS X).

cargo build --release

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

Проверка производительности: результаты


Мы измеряли производительность каждой реализации функции при помощи контрольных точек pytest, рассчитав энтропию более чем для 1 миллиона случайно выбранных байт. Все реализации показаны на одних и тех же данных. Эталонные тесты (также включенные в entropy.py) показаны ниже.

# ### КОНТРОЛЬНЕ ТОЧКИ #### генерируем случайные байты для тестирования w/ NumPyNUM = 1000000VAL = np.random.randint(0, 256, size=(NUM, ), dtype=np.uint8)def test_pure_python(benchmark):    """тестируем чистый Python."""    benchmark(compute_entropy_pure_python, VAL)def test_python_scipy_numpy(benchmark):    """тестируем чистый Python со SciPy."""    benchmark(compute_entropy_scipy_numpy, VAL)def test_rust(benchmark):    """тестируем реализацию Rust, вызываемую из Python."""    benchmark(compute_entropy_rust_from_python, VAL)

Наконец, делаем отдельные простые драйверные скрипты для каждого метода, нужного для расчета энтропии. Далее идет репрезентативный драйверный скрипт для тестирования реализации на чистом Python. В файле testdata.bin 1 000 000 случайных байт, используемых для тестирования всех методов. Каждый из методов повторяет вычисления по 100 раз, чтобы упростить захват данных об использовании памяти.

import entropywith open('testdata.bin', 'rb') as f:    DATA = f.read()for _ in range(100):    entropy.compute_entropy_pure_python(DATA)

Реализации как для SciPy/NumPy, так и для Rust показали хорошую производительность, легко обставив неоптимизированную реализацию на чистом Python более чем в 100 раз. Версия на Rust показала себя лишь немного лучше, чем версия на SciPy/NumPy, но результаты подтвердили наши ожидания: чистый Python гораздо медленнее скомпилированных языков, а расширения, написанные на Rust, могут весьма успешно конкурировать с аналогами на C (побеждая их даже в таком микротестировании).

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



Мы также измерили расход памяти для каждой реализации функции при помощи приложения GNU time (не путайте со встроенной командой оболочки time). В частности, мы измерили максимальный размер резидентной части памяти (resident set size).

Тогда как в реализациях на чистом Python и Rust максимальные размеры этой части весьма схожи, реализация SciPy/NumPy потребляет ощутимо больше памяти по данному контрольному показателю. Предположительно это связано с дополнительными возможностями, загружаемыми в память при импорте. Как бы то ни было, вызов кода Rust из Python, по-видимому, не привносит серьезных накладных расходов памяти.



Итоги


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

Rust показал не только отличное время выполнения; следует отметить, что и накладные расходы памяти в этих тестах также оказались минимальными. Такие характеристики времени выполнения и использования памяти представляются идеальными для целей масштабирования. Производительность реализаций SciPy и NumPy C FFI определенно сопоставима, но с Rust мы получаем дополнительные плюсы, которых не дают нам C и C++. Гарантии по безопасности памяти и потокобезопасности это очень привлекательное преимущество.

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

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

Вычислительная геология и визуализация пример 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 канале.

Подробнее..

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

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

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

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

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

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

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

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

Статистика

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

Иосиф Сталин

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Press Association Reference

Constituency Name

Region

Election Year

Electorate

Votes

AC

AD

AGS

...

650

NaN

NaN

NaN

NaN

NaN

29687604

NaN

NaN

NaN

...

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

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

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

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

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

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

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

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

Подробнее..

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Асимметрия

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Подробнее..

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Дисперсия

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

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

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

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

Выражение

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

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

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

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

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

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

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

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

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

[10 11 15 21 22.5 28 30]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Гистограммы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Подробнее..

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Выводы

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

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

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

Подробнее..

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Region

Electorate

Con

LD

12

Northern Ireland

60204.0

NaN

NaN

13

Northern Ireland

73338.0

NaN

NaN

14

Northern Ireland

63054.0

NaN

NaN

584

Northern Ireland

64594.0

NaN

NaN

585

Northern Ireland

74732.0

NaN

NaN

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

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

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

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

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

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

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

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

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

Подробнее..

Студенты, лабы и python обработка данных

22.03.2021 12:19:24 | Автор: admin

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


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

from random import normalvariatefrom math   import pi, cosGtoR, Uo, To, D, B = pi/180., 0.7, 56.0, 1.0, 0.02def U(T):     return Uo * cos(GtoR*(T + normalvariate(0.0, D) - To))**2 + B with open('pyexp.dat', 'w') as fp:  for T in range(0,360,10):     fp.write('{:5.1f}  {:5.3f}\n'.format(T, U(T)))

Здесь я буду использовать минимальный набор импортируемых в python модулей, необходимых для решения поставленных задач. Поэтому, в частности, мы не будем использовать пакеты а-ля csv или pandas для записи/чтения табличных данных. Также воздержимся от прямого импорта numpy (его импортирует и использует matplotlib). Получившийся в итоге минималистичный python-сценарий обработки данных выглядит так:

from math           import pi, cos, sin   # для вычисленийfrom scipy.optimize import curve_fit      # алгоритм подгонкиfrom scipy.special  import gammainc       # чтобы определить p-valuefrom matplotlib     import pyplot as plt  # для рисования графиковGtoR = pi/180. angle, experiment = [], [] with open('pyexp.dat','r') as fp:  data = fp.readlines()for row in data:  a, u = row.strip().split()  angle.append(GtoR*float(a))    experiment.append(float(u))  def U(X, Uo, To, B): # Теоретическая функция  Y = []  for x in X: Y.append(Uo * cos(x - To)**2 + B )  return Ydef V(X, Uo, To, D): # Её производная  Y = []  for x in X: Y.append(abs(D*(Uo * sin(2*(To - x)))))  return Y# Подгонка без учёта ошибок измеренийpopt, pcov = curve_fit(U, angle, experiment, p0 = [0.7, 1.0, 0.01], bounds=(0.0, [1., 180., 0.5]))# Переводим ошибки измерений углов (1 градус) в ошибки напряжений errors = V(angle, popt[0], popt[1], 1.0*GtoR)# Подгонка с учётом ошибокpopt, pcov = curve_fit(U, angle, experiment, p0 = popt, sigma=errors, absolute_sigma=True)# "Вытаскиваем" результаты подгонкиtheory = U(angle, *popt)Chi2, NDF = 0.0, len(angle)-len(popt)for p in range(len(angle)): Chi2 += ((theory[p]-experiment[p])/errors[p])**2pvalue = 1 - gammainc(0.5*NDF, 0.5*Chi2) # gammainc - regularized upper incomplete gamma functionTo, dTo = popt[1]/GtoR, pcov[1][1]**0.5/GtoRtlabel  = 'теория: ' tlabel += r'$\chi^2/NDF$ = {:.1f}/{:2d}'.format(Chi2,NDF)tlabel += '\nP-value = {:.2f}%'.format(100*pvalue)theta   = [GtoR*i for i in range(0,360)] # чтобы рисовать функцию без изломовplt.rc('grid', color='#316931', linewidth=0.5, linestyle='-.')fig = plt.figure(figsize=(8, 8))ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], projection='polar', facecolor='#fffff0')ax.set_title('Поворот оси поляроида: ' + r'$\theta_0={:.2f}^\circ\pm{:.2f}^\circ$'.format(To, dTo)+'\n')ax.plot(angle, experiment, 'ro', label='эксперимент')ax.plot(theta, U(theta, *popt), 'b-', label = tlabel)ax.legend()plt.show()

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

Заключение

Итак, для решения поставленной задачи мы использовали другой набор инструментов
ударную тройку python + scipy + matplotlib вместо gnuplot. И да, задача решена. Преимущества такого метода решения приведены во введении, кроме того, использование python расширяет границы дозволенного почти до бесконечности. Недостатки, по сравнению с gnuplot-решением, на мой взгляд, таковы:

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

  2. Библиотека scipy предоставляет необходимые алгоритмы для подгонки теоретической функции к экспериментальным данным. Однако, для доступа к результатам пришлось повозиться: ошибки определения параметров подгонки извлечь из ковариационной матрицы, \chi^2 посчитать вручную, для определения p-value использовать регуляризованную гамма-функцию.

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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