#!/usr/bin/env python # coding: utf-8 # # Основы программирования в Python # # *Алла Тамбовцева, НИУ ВШЭ* # # ## Датафреймы `pandas` # Продолжим работать с таблицами в Python. Для начала импортируем саму библиотеку. # In[1]: import pandas as pd # Загрузим таблицу: # In[2]: df = pd.read_csv("http://math-info.hse.ru/f/2017-18/py-prog/scores2.csv") # И посмотрим на её структуру более внимательно. Выберем первый столбец с *id*: # In[3]: df['id'] # Столбец датафрейма `df` имеет особый тип *Series*. Внешне *Series* отличается от обычного списка значений, потому что, во-первых, при вызове столбца на экран выводятся не только сами элементы, но их номер (номер строки), а во-вторых, на экран выводится строка с названием столбца (`Name: id`) и его тип (`dtype: object`, текстовый). Первая особенность роднит *Series* со словарями: он представляет собой пары *ключ-значение*, то есть *номер-значение*. Вторая особенность роднит *Series* с массивами `numpy`: элементы обычно должны быть одного типа. # # Библиотеку `numpy` мы еще не обсуждали, но обязательно обсудим позже, так как во многих задачах использовать массивы `numpy` гораздо удобнее, чем списки. # Можно вывести первые или последние строки таблицы, используя методы `.head()` и `.tail()`. # In[4]: df.head() # In[5]: df.tail() # **Внимание:** это просто первые и последние строки таблицы «как есть». Никакой сортировки не происходит! # # По умолчанию эти методы выводят пять строк, но при желании это легко изменить. Достаточно в скобках указать желаемое число строк. # In[6]: df.head(10) # первые 10 строк # Давайте кое-что подкорректруем. Сделаем так, чтобы строки в таблице назывались в соответствии с `id`. Другими словами, сделаем так, чтобы первый столбец считался индексом строки: # In[7]: df = pd.read_csv("http://math-info.hse.ru/f/2017-18/py-prog/scores2.csv", index_col = 0) # In[8]: df.head() # теперь так # Иногда такой подход может быть полезен. Представьте, что все переменные в таблице, кроме *id*, измерены в количественной шкале, и мы планируем реализовать на них статистический метод, который работает исключительно с числовыми данными. Если мы просто выкинем столбец с *id*, мы потеряем информацию о наблюдении, если мы его оставим, нам придется собирать в отдельную таблицу показатели, к которым будем применять метод, так как сохраненный в исходной таблице текст будет мешать. Если же мы назовем строки в соответствии с *id*, мы убьем сразу двух зайцев: избавимся от столбца с текстом и не потеряем информацию о наблюдении (код, имя респондента, название страны и прочее). # Когда таблица большая, увидеть все столбцы разом не получится. Поэтому полезно знать, как получить список названий столбцов. # In[9]: df.columns # Обратите внимание: полученный объект не является обычным списком: # In[10]: type(df.columns) # это Index из pandas # Чтобы получить список названий, достаточно сконвертировать тип с помощью привычного `list()`: # In[11]: c = list(df.columns) print(c) # Аналогичная история со строками: # In[12]: df.index # ### Переименование столбцов и строк # Раз названия столбцов можно вывести в виде некоторого перечня, то этот перечень можно редактировать. Посмотрим на названия столбцов ещё раз. # In[13]: df.columns # Давайте переименуем переменную `catps` в `cps`, чтобы думать о политической науке, а не о котах :) Для этого сохраним названия в список `my_cols` и изменим в списке первый элемент: # In[14]: my_cols = list(df.columns) my_cols[0] = "cps" # In[15]: df.columns = my_cols # сохраним изменения в самой базе df df.columns # все обновилось! # Обратите внимание: для того, чтобы изменить одно или несколько названий, совсем необязательно создавать новый список «с нуля». Достаточно определить индексы нужных элементов и поправить только необходимые названия. # # Точно так же можно было поступить со строками. Но давайте лучше попробуем внести изменения в названия всех столбцов: сделаем так, чтобы все названия столбцов начинались с большой буквы. Для начала напишем функцию, которая принимает на вход датафрейм, обращается к его столбцам и переименовывает их, делая первую букву заглавной. # In[16]: def rename_cols(df): oldnames = list(df.columns) # список старых названий newnames = [i.capitalize() for i in oldnames] # список новых названий df.columns = newnames # сохранение изменений return df # Теперь применим нашу функцию `rename_cols()` к базе `df`: # In[17]: df2 = rename_cols(df) df2.head() # Кажется, что таким способом мы сохранили изменения в новой базе `df2`, а старую базу `df` не тронули. Однако, если мы посмотрим на базу `df`, мы увидим, что она тоже изменилась! # In[18]: df.head() # Почему это произошло? Потому что датафреймы являются изменяемой структурой данных (да-да, как списки). Поэтому, применяя методы к объекту типа `DataFrame`, мы меняем исходный датафрейм, и к этому надо быть готовым. Если вы не планируете вносить изменения в исходную базу, имеет смысл сделать её копию и работать с ней. Например, вот так: # In[19]: # метод copy df_new = df.copy() # вносим изменения в df_new - переименовываем один столбец new_cols = list(df_new.columns) new_cols[1] = "Matstat" df_new.columns = new_cols # In[20]: # сравниваем print(df.head(2)) print("\n") # для пустой строчки между df и df_new print(df_new.head(2)) # **Обратите внимание:** создать копию обычным присваиванием не получится, код вида `df_new = df` создаст новую ссылку на датафрейм, но не новый датафрейм. Поэтому при изменении `df_new` база `df` также изменится (вспомните историю о коварстве списков). # ### Выбор столбцов и строк таблицы # **Выбор столбцов по названию** # Часто удобнее всего выбирать столбец по названию. Для этого достаточно указать название столбца в квадратных скобках (и обязательно в кавычках, так как название является строкой): # In[21]: df['Mstat'] # Ещё столбец можно выбрать, не используя квадратные скобки, а просто указав его название через точку: # In[22]: df.Mstat # Однако такой способ не универсален. В случае, если в названии столбца используются недопустимые для переменных символы (пробелы, тире, кириллические буквы), этот метод не подойдет. # Если нам нужно выбрать более одного столбца, то названия столбцов указываются внутри списка – появляются двойные квадратные скобки: # In[23]: df2[["Soc", "Polsoc"]] # Если нам нужно несколько столбцов подряд, начиная с одного названия и заканчивая другим, можно воспользоваться методом `.loc`: # In[24]: df.loc[:, 'Econ' : 'Law'] # Откуда в квадратных скобках взялось двоеточие? Дело в том, что метод `.loc` – более универсальный, и позволяет выбирать не только столбцы, но и строки. При этом нужные строки указываются на первом месте, а столбцы – на втором. Когда мы пишем `.loc[:, 1]`, мы сообщаем Python, что нам нужны все строки (`:`) и столбцы, начиная с `Econ` и до `Law` включительно. # # **Внимание:** выбор столбцов по названиям через двоеточие очень напоминает срезы (*slices*) в списках. Но есть важное отличие. В случае текстовых названий, оба конца среза (левый и правый) включаются. Если бы срезы по названиям были бы устроены как срезы по числовым индексам, код выше выдавал бы столбцы с `Econ` и до `Phist`, не включая колонку `Law`, так как в обычных срезах правый конец исключается. # **Выбор столбцов по номеру** # # Иногда может возникнуть необходимость выбрать столбец по его порядковому номеру. Например, когда названий столбцов нет как таковых или когда названия слишком длинные, а переименовывать их нежелательно. Сделать это можно с помощью метода `.iloc`: # In[25]: df.iloc[:, 1] # Синтаксис кода с `.iloc` несильно отличается от синтаксиса `.loc`. В чем разница? Разница заключается в том, что метод `.loc` работает с текстовыми названиями, а метод `.iloc` – с числовыми индексами. Отсюда и префикс `i` в названии (*i* – индекс, *loc* – location). Если мы попытаемся в `.iloc` указать названия столбцов, Python выдаст ошибку: # In[26]: df.iloc[:, 'Mstat': 'Econ'] # Python пишет, что невозможно взять срез по индексам, которые имеют строковый тип (`class 'str'`), так как в квадратных скобках ожидаются числовые (целочисленные) индексы. # # Если нужно выбрать несколько столбцов подряд, можно воспользоваться срезами: # In[27]: df.iloc[:, 1:3] # Числовые срезы в `pandas` уже ничем не отличаются от списковых срезов: правый конец среза не включается. В нашем случае мы выбрали только столбцы с индексами 1 и 2. # **Выбор строк по названию** # Выбор строки по названию происходит аналогичным образом, только здесь метод `.loc` уже обязателен. # In[28]: df.loc['М141БПЛТЛ031'] # строка для студента с номером М141БПЛТЛ031 # При этом ставить запятую и двоеточие, показывая, что нам нужна одна строка и все столбцы, уже не нужно. Если нам нужно выбрать несколько строк подряд, то `.loc` не нужен: # In[29]: df["М141БПЛТЛ024":'М141БПЛТЛ069'] # Как Python понимает, что мы просим вывести именно строки с такими названиями, а не столбцы? Потому что у нас стоят одинарные квадратные скобки, а не двойные, как в случае со столбцами. (Да, в `pandas` много всяких тонкостей, но чтобы хорошо в них разбираться, нужно просто попрактиковаться и привыкнуть). # # Обратите внимание: разницы между двойными и одинарными кавычками нет, строки можно вводить в любых кавычках, как в примере выше. # **Выбор строк по номеру** # # В этом случае достаточно указать номер в квадратных скобках в `.iloc`: # In[30]: df.iloc[2] # Если нужно несколько строк подряд, можно воспользоваться срезами: # In[31]: df[1:3] # и без iloc # Если нужно несколько строк не подряд, можно просто перечислить внутри списка в `.iloc`: # In[32]: df.iloc[[1, 2, 5, 10]] # **Выбор наблюдений по названиям строк и столбцов** # Если нам нужно выбрать одно наблюдение на пересечении строки и столбца, можно воспользоваться методом `.at`: сначала указать название строки, потом ‒ столбца: # In[33]: df.at['М141БПЛТЛ078', 'Game'] # оценка по теории игр у студента М141БПЛТЛ078 # Кроме того, можно применить метод `.loc`: # In[34]: df.loc["М141БПЛТЛ075", "Soc"] # оценка по социологии у студента М141БПЛТЛ075 # В чем разница между `.at` и `.loc`? Метод `.loc` более универсален. В то время как `.at` используется для нахождения *одного* наблюдения на пересечении строки и столбца, `.loc` позволяет выбрать несколько наблюдений (строк и столбцов) сразу. Например, так: # In[35]: df.loc["М141БПЛТЛ024":"М141БПЛТЛ073", "Mstat"] # Если нужно выбрать какое-то одно значение, метод `.at` будет работать более быстро, чем `.loc`. # **Выбор наблюдения по номеру строки и столбца ** # Выбор наблюдения по номеру строки и столбца осуществляется аналогичным образом, только теперь мы используем методы с префиксом `i` для индексов: `.iat` и `.iloc`. # In[36]: df.iat[4, 6] # оценка на пересечении строки 4 и столбца 6 # In[37]: df.iloc[8, 4] # оценка на пересечении строки 8 и столбца 4 # Убедимся, что все верно: # In[38]: df.head(8) # ### Выбор строк по условию (фильтрация наблюдений) # Часто в исследованиях нас не интересует выбор отдельных строк по названию или номеру, мы хотим отбирать строки в таблице согласно некорому условию (условиям). Другими словами, проводить фильтрацию наблюдений. Для этого интересующее нас условие необходимо указать в квадратных скобках. Выберем из датафрейма `df`строки, которые соответствуют студентам с оценкой по экономике выше 6. # In[39]: df[df["Econ"] > 6] # Почему нельзя было написать проще, то есть `df["Econ"] > 6`? Давайте напишем, и посмотрим, что получится: # In[40]: df["Econ"] > 6 # Что мы увидели? Просто результат проверки условия, набор из *True* и *False*. Когда мы подставляем это выражение в квадратные скобки, Python выбирает из `df` те строки, где выражение принимает значение *True*. # # Все операторы проверки условий работают как обычно: # In[41]: df[df["Econ"] == 9] # двойное равенство для равенства # Можно формулировать сложные условия. Выберем студентов с оценкой по экономике от 6 до 8 (8 не включается). # In[42]: df[(df["Econ"] >= 6) & (df["Econ"] < 8)] # В качестве символа для одновременного выполнения условий используется оператор `&`. И не забудьте про круглые скобки. А теперь выберем студентов с оценкой по английскому выше 9 и оценкой по праву ниже 9: # In[43]: df[(df["Eng"] > 9) & (df["Law"] < 9)] # При формулировании сложных (составных) условий обращайте особое внимание на порядок круглых скобках, потому что, если вы расставите скобки неправильно, результат получится неверный: # In[44]: df[(df["Eng"]) > 9 & (df["Law"] < 9)] # первая закрывающая скобка не после 9 # Результат получился совсем неверным. Потому что Python понял наше условие не так, как нужно. Теперь выберем студентов с оценкой по политической истории ниже 5 или с оценкой по истории политических учений ниже 5: # In[45]: df[(df["Phist"] < 5) | (df["Polth"] < 5)] # оператор | для условия или # Здесь наше выражение в квадратных скобках принимает значение *True*, когда хотя бы одно из условий верно: либо верно первое, либо второе, либо оба. # ### Добавление новых столбцов в таблице и удаление пропущенных значений # Давайте добавим в нашу таблицу `df` новый столбец, который будет представлять собой среднюю оценку по социологии (посчитаем среднее арифметическое оценок по социологии и политической социологии). Чтобы добавить новый столбец, нужно указать его название в квадратных скобках: # In[46]: df["Avg_Soc"] = (df["Soc"] + df["Polsoc"]) / 2 # In[47]: df.head() # Теперь внесем изменения в уже существующий столбец в таблице. В самом начале мы заметили, что некоторые столбцы имеют тип `float` (числа с плавающей точкой), а не `integer` (целые числа). Давайте попробуем сделать столбец с политической историей целочисленным. # In[48]: newh = [int(i) for i in df["Phist"]] # Не получается! Почему? Python пишет, что не может превратить *NaN* в *integer*. Действительно, сложно превратить объект *Not a number* в целое число. Тип *float* относится к нему толерантно, а вот тип *integer* уже нет. Как быть? Давайте просто удалим из датафрейма все пропущенные значения (то есть строки, содержащие пропущенные значения). # In[49]: df = df.dropna() # удаляем и сохраняем изменения # Теперь проделаем те же операции: # In[50]: newh = [int(i) for i in df["Phist"]] # In[51]: df["Phist"] = newh # Получилось! # In[52]: df.head() # Phist уже с целыми значениями # ### Еще немного про описательные статистики # В самом начале мы обсуждали описание базы данных с помощью метода `.describe()`. Помимо этого метода существует много методов, которые выводят отдельные статистики. # In[53]: df.median() # медиана (для всех показателей) # Можно запрашивать статистики по отдельным переменным (столбцам): # In[54]: df.Phist.mean() # среднее арифметическое # Или по наблюдениям (строкам): # In[55]: df.loc["М141БПЛТЛ023"].mean() # Давайте теперь построим какие-нибудь графики. Библиотеку `pandas` удобно использовать в сочетании с библиотекой для построения графиков matplotlib. Давайте её импортируем (эта библиотека должна была быть установлена на ваш компьютер вместе с Anaconda). # In[56]: import matplotlib # Теперь добавим элементы магии :) Магическую строку в Jupyter Notebook (*Python magic*). Эта строка позволит выводить графики прямо внутри ноутбука, файла `.ipynb`, а не в отдельном окне. # In[57]: get_ipython().run_line_magic('matplotlib', 'inline') # Построим гистограмму для оценок по теории игр. # In[58]: df["Game"].plot.hist() # histogram # Что показывает этот график? Он показывает, сколько студентов получили те или иные оценки. По гистограмме видно, что больше всего по этому курсу оценок 4 и 7. # # Можно поменять цвет гистограммы: # In[59]: df["Game"].plot.hist(color = "red") # Можно пытаться строить другие графики. Например, построить ящик с усами. # In[60]: df["Game"].plot.box() # boxplot # Этот график визуализирует основные описательные статистики переменной и отображает форму её распределения. Нижняя граница яшика – это нижний квартиль, верхняя – верхний квартиль, линяя внутри ящика – медиана. Усы графика могут откладываться по-разному: если в переменной встречаются нетипичные значения (выбросы), то границы усов совпадают с границами типичных значений, если нетипичных значений нет, границы усов соответствуют минимальному и максимальному значению переменной. Подробнее про ящик с усами см. [здесь](https://ru.wikipedia.org/wiki/%D0%AF%D1%89%D0%B8%D0%BA_%D1%81_%D1%83%D1%81%D0%B0%D0%BC%D0%B8).