{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Основы анализа данных в Python\n", "\n", "*Тамбовцева Алла, НИУ ВШЭ*\n", "\n", "## Повторение: доверительные интервалы, проверка гипотез, коэффициенты корреляции" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Информация о данных\n", "\n", "В рамках этого файла используем данные с результатами опроса преподавателей двух каталонских универсистетов (Открытый университет Каталонии и университет Помпеу Фабра), посвященного использованию Википедии в методических и академических целях.\n", "\n", "* Codebook для набора данных: [ссылка](https://github.com/allatambov/PyDataAnalysis/blob/main/wiki_codebook.pdf).\n", "* Оригинальное описание на платформе data.world: [ссылка](https://data.world/uci/wiki4he)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Загрузка данных \n", "\n", "Импортируем библиотеку `pandas` с сокращенным названием `pd` (понадобится для работы с данными) и библиотеку `numpy` с сокращенным названием `np` (понадобится для более удобных вычислений):" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Загрузим данные из файла `\"wiki.csv\"` и сохраним их в датафрейм `wiki`, сообщив Python, что в качестве разделителя столбцов используется точка с запятой, а не запятая:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "wiki = pd.read_csv(\"wiki.csv\", sep = \";\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если мы не добавим аргумент `sep` с разделителем, все данные склеятся в один столбец, так как по умолчанию функция `read_csv()` подразумевает разделение по запятой (ведь название формата CSV – это сокращение от *comma separated values*).\n", "\n", "**Примечание для Jupyter Notebook.** Удобно, если файл с данными при работе лежит в той же папке, что и текущий ipynb-файл, в котором мы запускаем код, так не придется полностью прописывать к нему путь, достаточно одного названия с расширением.\n", "\n", "**Примечание для Google Colab.** Загрузить файл с данными в облачное хранилище можно через кнопку *Files* (значок папки слева от рабочей области с ячейками), при нажатии на которую появляется возможность выбрать файл с компьютера (значок стрелки). После добавления файла его можно выбрать, кликнуть на три точки справа от названия, скопировать путь через *Copy path* и вставить его в функцию `read_csv()`. Например:\n", "\n", " wiki = pd.read_csv(\"/content/wiki.csv\", sep = \";\") " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Определим размерность датафрейма – число строк и число столбцов:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(913, 53)" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, в датафрейме 913 строк и 53 столбца. Значит, всего было опрошено 913 человек." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Работа с долями: бинарные переменные" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выберем столбец `GENDER`, который соответствует полу респондента (0 – мужской, 1 – женский), укажем название этого столбца в квадратных скобках:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0 0\n", "1 0\n", "2 0\n", "3 0\n", "4 0\n", " ..\n", "908 0\n", "909 0\n", "910 0\n", "911 0\n", "912 1\n", "Name: GENDER, Length: 913, dtype: int64" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"GENDER\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим, что в этом столбце нет значений, кроме 0 и 1. Запросим уникальные значения с помощью метода `.unique()`:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0, 1])" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"GENDER\"].unique()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Все нормально, лишнего ничего нет. Посчитаем, сколько в столбце нулей и единиц. Для этого сформируем таблицу частот с помощью метода `.value_counts()`:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0 525\n", "1 388\n", "Name: GENDER, dtype: int64" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"GENDER\"].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, в опросе участвовало 525 женщин и 388 мужчин. Допустим, нас интересует доля мужчин, принявших участие в опросе. В нашем случае эта доля примерно соответствует доле мужчин, работающих в двух университетах Каталонии (Открытом университете и университете Помпеу-Фабра). \n", "\n", "Понятно, что чтобы определить такую долю, нам нужно 388 поделить на общее число респондентов, а их у нас 913 (общее число строк в датафрейме). \n", "\n", "Вопрос: а как посчитать долю более универсальным образом? Ответ: автоматически посчитать число единиц, автоматически посчитать общее число элементов в столбце и поделить одно на другое. Если у нас есть набор из 0 и 1, число единиц совпадает с суммой элементов (сумма нулей ничего нового не дает). Посчитаем сумму элементов столбца через метод `.sum()`:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "388" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"GENDER\"].sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Общее число элементов в столбце хранится в атрибуте `.size`:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "913" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"GENDER\"].size" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Пояснение.* Атрибут – некоторая фиксированная характеристика объекта (число строк или столбцов в датафрейме, названия строк или столбцов, тип данных), метод – функция, которая выполняет некоторую операцию над объектом. И атрибуты, и методы вызываются через точку, но в конце метода всегда есть круглые скобки (например, `.sum()`), они означают, что мы хотим применить метод, а не просто вызвать и посмотреть на него." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для вычисления доли поделим одно на другое:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.4249726177437021" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"GENDER\"].sum() / wiki[\"GENDER\"].size" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Несложно догадаться, что мы сейчас просто посчитали среднее арифметическое столбца `GENDER`. Конечно, можно было поступить проще — воспользоваться методом `.mean()`. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.4249726177437021" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"GENDER\"].mean()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Но наши вычисления нам тоже сейчас пригодятся." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Доверительный интервал для доли" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вспомним, как вычисляется 95%-ный доверительный интервал для доли (примерно):\n", "\n", "$$\n", "\\hat{p} - 2 \\times s_{\\hat{p}} < p < \\hat{p} + 2 \\times s_{\\hat{p}}.\n", "$$\n", "\n", "Здесь $p$ – доля в генеральной совокупности, которая нам неизвестна и которую мы хотим оценить с помощью доверительного интервала, $\\hat{p}$ – доля, полученная по выборке, $s_{\\hat{p}}$ – стандартное отклонение доли, тоже полученное по выборке, которое вычисляется так ($n$ – число наблюдений в выборке):\n", "\n", "$$\n", "s_{\\hat{p}} = \\sqrt{\\frac{\\hat{p}(1-\\hat{p})}{n}}.\n", "$$\n", "\n", "Давайте используем строчки кода, написанные ранее, для вычисления характеристик, которые присутствуют в формулах, и построим 95%-ный доверительный интервал для доли мужчин среди профессорско-преподавательского состава двух университетов Каталонии." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "n = wiki[\"GENDER\"].size # размер выборки\n", "p = wiki[\"GENDER\"].sum() / n # доля p^" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь, зная $n$ и $\\hat{p}$, мы можем посчитать стандартное отклонение доли $s_{\\hat{p}}$. Для извлечения квадратного корня возьмем функцию `sqrt()` из библиотеки `numpy`:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.016360227864035466" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "s_p = np.sqrt(p * (1 - p) / n) \n", "s_p" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отлично! Осталось все собрать воедино и посчитать границы доверительного интервала! Подставим все в формулу:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0.3922521620156312, 0.457693073471773)" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(p - 2 * s_p, p + 2 * s_p) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, можем сказать, что с 95% уверенностью можно считать, что истинная доля мужчин, работающих в университетах Каталонии, лежит в интервале от 0.39 до 0.46." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тот же самый интервал мы могли бы посчитать, воспользовавшись специальной функцией из модуля `stats` библиотеки `scipy` (от *scientific python*). Этот модуль часто импортируется с сокращенным названием:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "import scipy.stats as st" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сама функция называется `norm.interval()` позволяет строить некоторые интервалы, предполагая, что те оценки, для которых мы эти интервалы строим, берутся из нормального распределения с некоторым средним (`loc`) и некоторым стандартным отклонением (`scale`). Можем подставить наши значения `p` и `s_p` в качестве параметров такого распределения и зафиксировать уровень доверия:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0.39290716035132395, 0.45703807513608025)" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "st.norm.interval(0.95, loc = p, scale = s_p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Данный интервал чуть более точный, чем тот, который мы посчитали ранее, потому что значение, на которое мы домножаем `s_p`, на самом деле, не совсем 2, а чуть поменьше (1.96, если вспомнить курс по теории вероятностей)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Доверительный интервал для среднего\n", "\n", "Допустим, мы хотим понять, в каком интервале, с 95%-ной уверенностью, лежит среднее значение возраста сотрудников двух каталонских университетов. \n", "\n", "Для начала посмотрим на описательные статистики столбца `AGE`:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "count 913.000000\n", "mean 42.246440\n", "std 8.058418\n", "min 23.000000\n", "25% 36.000000\n", "50% 42.000000\n", "75% 47.000000\n", "max 69.000000\n", "Name: AGE, dtype: float64" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"AGE\"].describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, средний возраст примерно равен 42 годам, медианный тоже. Но, на самом деле, истинное среднее значение возраста может быть немного иным, исследователи же не смогли опросить всех сотрудников обоих университетов, их там много!\n", "\n", "Чтобы учесть такую неопределенность в наших оценках, построим 95%-ный доверительный интервал для среднего. Вспомним, как он считается:\n", "\n", "$$\n", "\\hat{\\mu} - 2 \\times s_{\\hat{\\mu}} < \\mu < \\hat{\\mu} + 2 \\times s_{\\hat{\\mu}}.\n", "$$\n", "\n", "Здесь $\\mu$ – среднее генеральной совокупности, которое нам неизвестно и которое мы хотим оценить с помощью доверительного интервала, $\\hat{\\mu}$ – среднее, полученное по выборке, $s_{\\hat{\\mu}}$ – стандартное отклонение среднего, тоже полученное по выборке, которое вычисляется на основе стандартного отклонения самой выборки $s$ и ее объема $n$:\n", "\n", "$$\n", "s_{\\hat{\\mu}} = \\frac{s}{\\sqrt{n}}\n", "$$\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, опять соберем все составные части для формулы. " ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "n = wiki[\"AGE\"].size # размер выборки\n", "mu = wiki[\"AGE\"].mean() # среднее выборки mu^\n", "s = wiki[\"AGE\"].std() # ст отклонение выборки s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вычислим стандартное отклонение среднего:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.26669471758704505" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "s_mu = s / np.sqrt(n)\n", "s_mu" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для построения доверительного интервала для среднего также воспользуемся специальной функцией из модуля `stats`. В случае с долей мы использовали функцию `norm.interval()`, так как там нам нужно было нормальное распределение, а здесь нам понадобится функция `t.interval()` для распределения Стьюдента. Помимо уровня доверия, значение `mu` и `s_mu` нам необходимо число степеней свободы для распределения Стьюдента, оно равно $\\text{df} = n - 1$. Подставлям все и вычисляем:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(41.723033639899846, 42.7698469734627)" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "st.t.interval(0.95, df = n - 1, loc = mu, scale = s_mu) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, доверительный интервал получился довольно узким. С 95%-ной уверенностью можно утверждать, что средний возраст сотрудников каталонских университетов лежит в интервале от 42 до 43 лет (тут разумнее округлить значения до целых)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Проверка гипотез: сравнение средних двух групп" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Допустим, мы хотим выяснить, отличается ли средний возраст сотрудников каталонских университетов, которые считают использование Википедии полезным для преподавания, и тех сотрудников, которые так не считают.\n", "\n", "Полезность Википедии для преподавания участники опроса оценивали в вопросе `QU3`. Они выражали свою степень согласия с утверждением *Wikipedia is useful for teaching*. Посмотрим на ответы:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3 312\n", "4 250\n", "5 175\n", "2 151\n", "1 20\n", "? 5\n", "Name: PU3, dtype: int64" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"PU3\"].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Давайте создадим столбец, который будет состоять из значений `USEFUL`, если респондент считает Википедию полезной (высокая степень согласия с утверждением, значения 4 или 5 по шкале Лайкерта), и значений `USELESS`, если респондент так не считает (низкая степень согласия с утверждением, значения 1 или 2 по шкале Лайкерта). Пропущенные ответы `?` и категорию *Neither disagree nor agree* проигнорируем, потом исключим.\n", "\n", "Как удобнее всего это сделать, чтобы избежать циклов для пробегания по всем ячейкам столбца `PU3`? Написать функцию, которая принимает на вход значение *одной ячейки* и возвращает ответ `USEFUL` или `USELESS`, а затем применить ее ко всему столбцу с помощью специального метода." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Напишем функцию под названием `check_use()`, на вход она принимает значение `x`, проверяет условия, и, в зависимости от их выполнения, возвращает строку с тем или иным ответом:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "def check_use(x):\n", " \n", " # если 4 или 5 возвращаем USEFUL\n", " # если иначе, тестим следующее условие для 1 и 2\n", " # если и оно не выполняется, ставим пропуск – пустое значение\n", " \n", " if (x == '4') | (x == '5'):\n", " r = 'USEFUL'\n", " elif (x == '1') | (x == '2'):\n", " r = 'USELESS'\n", " else:\n", " r = None\n", " return r" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим работу функции на отдельных значениях:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'USEFUL'" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "check_use('4')" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'USELESS'" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "check_use('1')" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "check_use('?') # None " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь применим функцию ко всем ячейкам в столбце, используя метод `.apply()`, и сохраним значения в новый столбец `USEFULNESS`:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "wiki[\"USEFULNESS\"] = wiki[\"PU3\"].apply(check_use)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим, что получилось:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "USEFUL 425\n", "USELESS 171\n", "Name: USEFULNESS, dtype: int64" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"USEFULNESS\"].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Все прекрасно, но метод `.value_counts()` игнорирует пропуски, не показывает, сколько их. Воспользуемся методом `.isna()`:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0 True\n", "1 True\n", "2 False\n", "3 False\n", "4 False\n", " ... \n", "908 True\n", "909 False\n", "910 True\n", "911 False\n", "912 True\n", "Name: USEFULNESS, Length: 913, dtype: bool" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"USEFULNESS\"].isna()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Этот метод для каждой ячейки в столбце возвращает значение `True` (пропуск есть) или `False` (пропуска нет). Чтобы посчитать количество пропусков, нужно просто посчитать сумму по этому набору `True` и `False`, так как для Python значение `True` равносильно 1, а значение `False` равносильно 0. А про суммирование набора из нулей и единиц мы уже поговорили:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "317" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"USEFULNESS\"].isna().sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итого, 317 строк с пропущенными значениями в столбце `USEFULNESS`. Давайте их удалим, но сделаем это осторожно. Если мы применим метод `.dropna()` для удаления строк с пропущенными значениями ко всему датафрейму, удалятся все строки, где есть хотя бы одно пропущенное значение (не только в столбце `USEFULNESS`). В нашем случае это не так страшно, потому что «чистых» пропусков в датафрейме нет, (кроме тех, которые мы сами создали, добавив `None`), отсутствие ответов закодировано `?`. Но вообще это может быть большой проблемой, особенно, если набор данных большой. Если столбцов много, есть риск, что не все они заполнены полностью, то есть, теоретически, в каждой строке будет хотя бы один пропуск. Если использовать `.dropna()` бездумно, можно совсем без данных остаться!\n", "\n", "Поэтому мы поступим аккуратно (хоть в конкретном случае это не требуется), удалим строки с пропусками в столбце `USEFULNESS`:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [], "source": [ "# в suubset можно вписать целый список названий столбцов\n", "\n", "wiki = wiki.dropna(subset = [\"USEFULNESS\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ура! Наконец-то все готово для сравнения средних. Для начала сравним выборочные средние. Сгруппируем данные по столбцу `USEFULNESS` и посчитаем средние:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
AGEGENDERPhDUNIVERSITY
USEFULNESS
USEFUL42.1976470.3270590.4517651.131765
USELESS43.0116960.4912280.4561401.070175
\n", "
" ], "text/plain": [ " AGE GENDER PhD UNIVERSITY\n", "USEFULNESS \n", "USEFUL 42.197647 0.327059 0.451765 1.131765\n", "USELESS 43.011696 0.491228 0.456140 1.070175" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki.groupby(\"USEFULNESS\").mean()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "По умолчанию функция `.mean()` вычисляет средние для всех числовых столбцов. Если нам это не нравится, можем выбрать только один нужный (обычный выбор столбца по названию из маленького датафрейма выше):" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "USEFULNESS\n", "USEFUL 42.197647\n", "USELESS 43.011696\n", "Name: AGE, dtype: float64" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki.groupby(\"USEFULNESS\").mean()[\"AGE\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Судя по всему, различия в средних небольшие. Но не помешает проверить более строго, вдруг эта разница в средних в один год все-таки существенна из-за небольшого разброса данных? \n", "\n", "Для проверки гипотезы о равенстве средних в двух группах с помощью t-критерия Стьюдента можно воспользоваться функцией `ttest_ind` все из того же модуля `stats`. Часть с `_ind` отвечает за *independent samples*, независимые выборки (например, разные люди в двух выборках, а не одни и те же в начале исследования и в конце). Эта функция всем хороша, но есть небольшой минус: она принимает на вход две отдельные выборки, поэтому их сначала надо будет получить, отфильтровать соответствующие строки." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "wiki_useful = wiki[wiki[\"USEFULNESS\"] == \"USEFUL\"]\n", "wiki_useless = wiki[wiki[\"USEFULNESS\"] == \"USELESS\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как устроен код выше? В квадратных скобках, как обычно, указывается некоторый критерий отбора, здесь он представлен в виде условия (помним, что оператор `==` используется для проверки равенства, а оператор `=` для присваивания, второй нам здесь не подойдет). Выражение в квадратных скобках, само по себе, проверяет условие для каждой строки в `wiki` и возвращает набор из логических значений `True` (если условие выполнено) и `False` (если условие не выполнено):" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2 False\n", "3 True\n", "4 True\n", "6 True\n", "7 True\n", " ... \n", "903 True\n", "904 True\n", "907 False\n", "909 True\n", "911 True\n", "Name: USEFULNESS, Length: 596, dtype: bool" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"USEFULNESS\"] == \"USEFUL\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Соответственно, строка кода в ячейке выше отбирает те строки из `wiki`, где выражение в квадратных скобках вернуло `True`. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Все, теперь точно все готово для проверки гипотезы. Проверяем гипотезу о равенстве среднего возраста тех сотрудников, которые считают Википедию полезной для преподавания и тех, кто считает ее бесполезной:" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Ttest_indResult(statistic=-1.1245735952953264, pvalue=0.2612239945157787)" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# в скобках – две выборки, \n", "# столбец AGE из одной группы и из другой\n", "\n", "st.ttest_ind(wiki_useful[\"AGE\"], wiki_useless[\"AGE\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Функция вернула нам два значения: значение t-статистики и p-value. Если мы примем уровень значимости равным 5% (или уровень доверия 95%), у нас не будет оснований отвергнуть нулевую гипотезу (p-value > 0.05). Данные не позволяют выявить различия в средних, можно считать средние значения возраста сотрудников, считающих Википедию полезной и бесполезной, равными." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Важное замечание: по умолчанию `ttest_ind()` использует двустроннюю альтернативу. Если мы хотим выбрать левостороннюю (первое среднее меньше второго) или правосторонюю (первое среднее больше второго), понадобится аргумент `alternative`, равный `\"less\"` или `\"right\"`. Правда, этот аргумент доступен только с версии `stats` 1.7.0, поэтому, если код ниже не работает, можно просто поделить p-value из предыдущего результата на 2, пользуясь тем, что распределение симметрично." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "st.ttest_ind(wiki_useful[\"AGE\"], wiki_useless[\"AGE\"], \n", " alternative = \"left\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Коэффициенты корреляции" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В завершение работы с данными давайте посмотрим на то, как вычислять в Python коэффициенты корреляции Пирсона и Спирмена. Это довольно просто.\n", "\n", "Коэффициент Пирсона можно вычислить несколькими способами. Например, воспользоваться методом `.corr()` от библиотеки `pandas`. Для этого надо выбрать нужные числовые столбцы и применить к ним этот метод. Выберем столбцы с возрастом респондента и числом лет опыта работы:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
AGE
AGE1.0
\n", "
" ], "text/plain": [ " AGE\n", "AGE 1.0" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[[\"AGE\", \"YEARSEXP\"]].corr()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Что-то пошло не так. Python показал нам только корреляцию `AGE` с `AGE`, коэффициент равный 1. Почему? Потому что, к сожалению, как это бывает при работе с реальными данными, столбец `YEARSEXP` оказался текстовым, хотя по логике должен состоять из чисел. Посмотрим, что не так с `YEARSEXP`. Выведем уникальные значения:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['13', '8', '11', '12', '14', '25', '15', '18', '20', '5', '10',\n", " '9', '6', '?', '7', '36', '4', '2', '17', '23', '16', '3', '21',\n", " '22', '1', '24', '27', '26', '19', '30', '28', '37', '43', '35',\n", " '39', '0'], dtype=object)" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"YEARSEXP\"].unique() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ага! Проблема в знаке вопроса – так в этих данных были закодированы пропущенные ответы. Выкинем строки с пропущенными ответами – выберем только те строки, где значение в столбце `YEARSEXP` не `?`:" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "wiki = wiki[wiki[\"YEARSEXP\"] != \"?\"] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим:" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['13', '8', '11', '12', '14', '25', '15', '18', '20', '5', '10',\n", " '9', '6', '7', '36', '4', '2', '17', '23', '16', '3', '21', '22',\n", " '1', '24', '27', '26', '19', '30', '28', '37', '43', '35', '39',\n", " '0'], dtype=object)" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[\"YEARSEXP\"].unique() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Лишнее выкинулось, но небольшая проблема осталась. Значения по-прежнему текстовые! Первое, что хочется сделать – пройтись в цикле по всем элементам массива выше и преобразовать каждый в число. Но, к счастью, есть более быстрый и компактный способ – воспользоваться методом `.astype()`, он умеет изменять тип сразу всех элементов массива или столбца:" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "wiki[\"YEARSEXP\"] = wiki[\"YEARSEXP\"].astype(int) # переделываем в integer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь метод `.corr()` работает как надо:" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
AGEYEARSEXP
AGE1.0000000.559238
YEARSEXP0.5592381.000000
\n", "
" ], "text/plain": [ " AGE YEARSEXP\n", "AGE 1.000000 0.559238\n", "YEARSEXP 0.559238 1.000000" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[[\"AGE\", \"YEARSEXP\"]].corr()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мы получили корреляционную матрицу, в ней сохранены коэффициенты для пары `AGE` и `YEARSEXP` и `YEARSEXP` и `AGE`. Коэффициенты одинаковы, нет разницы, смотрим ли мы на связь первого со вторым или второго с первым. Связь положительная (что ожидаемо для возраста и опыта работы), умеренная. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если нас не интересует корреляционная матрица, а интересует только одно значение коэффициента Пирсона, можем воспользоваться функцией `pearsonr()` из модуля `stats`:" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0.5592377622336536, 7.276531536556025e-49)" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# в скобках оба столбца через запятую\n", "\n", "st.pearsonr(wiki[\"AGE\"], wiki[\"YEARSEXP\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Эта функция возвращает кортеж из двух чисел: значение коэффициента Пирсона и p-value, полученное по итогам проверки значимости этого коэффициента. Здесь p-value практически 0, следовательно, гипотезу о равенстве нулю истинного коэффициента корреляции стоит отвергнуть (на любом разумном уровне значимости или уровне доверия), показатели связаны. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для коэффициента Спирмена все аналогично, только в `.corr()` добавляется аргумент с указанием типа коэффициента:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
AGEYEARSEXP
AGE1.0000000.502484
YEARSEXP0.5024841.000000
\n", "
" ], "text/plain": [ " AGE YEARSEXP\n", "AGE 1.000000 0.502484\n", "YEARSEXP 0.502484 1.000000" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wiki[[\"AGE\", \"YEARSEXP\"]].corr(method = \"spearman\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "А в `stats` используется функция с соответствующим названием:" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "SpearmanrResult(correlation=0.5024836891059221, pvalue=2.629924812320569e-38)" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "st.spearmanr(wiki[\"AGE\"], wiki[\"YEARSEXP\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выводы те же, связь есть (или согласованность рангов, если точнее)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.4" } }, "nbformat": 4, "nbformat_minor": 2 }