Текст лекции: Щуров И.В., НИУ ВШЭ
Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.
Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на странице курса.
Мы обсуждали разные структуры данных — списки, словари, массивы, сложные структуры, составленные из списков и словарей, XML-файлы и т.д. Однако самый распространённый вид, в котором обычно представляют данные для их анализа — это таблицы. Проще всего сохранить таблицу в Python в списке, элементами которого являются списки — строки таблицы. Например, вот такая табличка:
1 | 2 | 3 |
4 | 5 | 6 |
будет записана вот в таком виде:
table = [[1, 2, 3], [4, 5, 6]]
table
[[1, 2, 3], [4, 5, 6]]
Например, можно получить третий элемент второй строки вот так:
table[1][2]
6
Или целиком первую строку:
table[0]
[1, 2, 3]
Эта структура простая и понятная, однако не слишком удобная. Например, получить первый столбец уже не так просто. (Хорошее упражнение — написать для этого правильный код.)
Есть ещё одна проблема: такая структура очень медленная. К счастью, хорошие люди уже написали для нас библиотеку по работе с табличными данными. Она называется pandas.
import pandas as pd
В pandas реализованы типы данных с разным числом измерений: одномерный тип (просто ряд) называется Series
, двумерный (табличка) — DataFrame
, трёхмерный — Panel
. Мы будем обсуждать преимущественно DataFrame
. Про Series
скажем пока только то, что эта штука очень похожа на np.array
.
Давайте сделаем датафрейм из нашей таблички.
df = pd.DataFrame(table)
df
0 | 1 | 2 | |
---|---|---|---|
0 | 1 | 2 | 3 |
1 | 4 | 5 | 6 |
У такой таблицы можно обращаться к строкам, а можно и к столбцам:
df[0] # первый столбец
0 1 1 4 Name: 0, dtype: int64
Номера слева соответствуют номерам (а точнее индексам) строк. Перед нами объект pd.Series
, представляющий собой нечто среднее между np.array
и словарём (ниже мы столкнёмся с этим более явно, когда индексы будут не последовательными целыми числами, а строками).
df.loc[0] # первая строка
0 1 1 2 2 3 Name: 0, dtype: int64
df.loc[1] # вторая строка
0 4 1 5 2 6 Name: 1, dtype: int64
Для дальнейшего рассмотрим простой CSV-файл с данными, изображающими результаты нескольких виртуальных студентов по некоторым домашкам. Я подготовил этот файл и сохранил его на сервере. Давайте для начала посмотрим на него «глазами»:
import requests
r = requests.get("http://math-info.hse.ru/f/2015-16/all-py/data/simple.csv")
print(r.text)
Assignment, Ann,Bob,Claudia,Daniel Limits, 1,2,3,2 Derivatives, 2.3,1.2,3.2,1.2 Integrals, 2.1,2.3,2.3,2.2 Vectors, 4,2.5,2.4,1.2 Operators, 6,5,4,5
Здесь Ann
, Bob
и т.д. — имена студентов, Limits
, Derivatives
и т.д. — названия домашних работ. Загрузим наш файл в Pandas
— для этого его даже не нужно скачивать — можно передать функции pd.read_csv()
URL (сетевой адрес) нашего файла (хотя, конечно, можно было бы открыть и локальный файл с тем же успехом):
df = pd.read_csv("http://math-info.hse.ru/f/2015-16/all-py/data/simple.csv")
df
Assignment | Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|---|
0 | Limits | 1.0 | 2.0 | 3.0 | 2.0 |
1 | Derivatives | 2.3 | 1.2 | 3.2 | 1.2 |
2 | Integrals | 2.1 | 2.3 | 2.3 | 2.2 |
3 | Vectors | 4.0 | 2.5 | 2.4 | 1.2 |
4 | Operators | 6.0 | 5.0 | 4.0 | 5.0 |
Заметим, что верхняя строчка выделена полужирным — это потому, что она рассматривается не как строчка с данными, а как строчка с именами столбцов. К столбцам можно обращаться по имени (примерно как к элементам словарей).
df['Bob']
0 2.0 1 1.2 2 2.3 3 2.5 4 5.0 Name: Bob, dtype: float64
У строк по умолчанию нет имён, и им присваивются номера (левая полужирная колонка). Можно было бы загрузить файл таким образом, чтобы первый столбец рассматривался как столбец с именами строк (индексам), для этого надо передать pd.read_csv()
параметр index_col
с номером нужного столбца:
df = pd.read_csv(
"http://math-info.hse.ru/f/2015-16/all-py/data/simple.csv",
index_col=0
)
df
Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|
Assignment | ||||
Limits | 1.0 | 2.0 | 3.0 | 2.0 |
Derivatives | 2.3 | 1.2 | 3.2 | 1.2 |
Integrals | 2.1 | 2.3 | 2.3 | 2.2 |
Vectors | 4.0 | 2.5 | 2.4 | 1.2 |
Operators | 6.0 | 5.0 | 4.0 | 5.0 |
Теперь можно посмотреть на строчку, обратившись к ней по имени (то есть по названию домашки):
df.loc['Limits']
Ann 1 Bob 2 Claudia 3 Daniel 2 Name: Limits, dtype: float64
Заметим, что теперь столбец слева состоит не из последовательных чисел, а из строк, являющихся именами соответствующих столбцов в исходной таблице.
В статистике, строчка датафрейма называется «наблюдением» (observation), а столбец — «переменной» (variable). Данные в столбце должны быть однородны (например, может быть столбец, состоящий только из чисел или только из строк, но не может быть столбца, в котором перемешаны строки и числа), а по строкам могут быть разнородны.
Слово Assignment
при отображении df
— не имя какой-то строчки, а имя столбца с индексами.
df.index
Index(['Limits', 'Derivatives', 'Integrals', 'Vectors', 'Operators'], dtype='object', name='Assignment')
Его можно убрать вот так:
df.index.name = None
df
Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|
Limits | 1.0 | 2.0 | 3.0 | 2.0 |
Derivatives | 2.3 | 1.2 | 3.2 | 1.2 |
Integrals | 2.1 | 2.3 | 2.3 | 2.2 |
Vectors | 4.0 | 2.5 | 2.4 | 1.2 |
Operators | 6.0 | 5.0 | 4.0 | 5.0 |
Вот так можно обратиться к конкретному элементы таблицы:
df.at['Limits', 'Bob']
2.0
Попробуем то же самое с Ann
:
df.at['Limits', 'Ann']
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-22-6955a711dc3f> in <module>() ----> 1 df.at['Limits', 'Ann'] /usr/local/lib/python3.5/site-packages/pandas/core/indexing.py in __getitem__(self, key) 1524 1525 key = self._convert_key(key) -> 1526 return self.obj.get_value(*key, takeable=self._takeable) 1527 1528 def __setitem__(self, key, value): /usr/local/lib/python3.5/site-packages/pandas/core/frame.py in get_value(self, index, col, takeable) 1754 return _maybe_box_datetimelike(series._values[index]) 1755 -> 1756 series = self._get_item_cache(col) 1757 engine = self.index._engine 1758 return engine.get_value(series.get_values(), index) /usr/local/lib/python3.5/site-packages/pandas/core/generic.py in _get_item_cache(self, item) 1088 res = cache.get(item) 1089 if res is None: -> 1090 values = self._data.get(item) 1091 res = self._box_item_values(item, values) 1092 cache[item] = res /usr/local/lib/python3.5/site-packages/pandas/core/internals.py in get(self, item, fastpath) 3100 3101 if not isnull(item): -> 3102 loc = self.items.get_loc(item) 3103 else: 3104 indexer = np.arange(len(self.items))[isnull(self.items)] /usr/local/lib/python3.5/site-packages/pandas/core/index.py in get_loc(self, key, method, tolerance) 1690 raise ValueError('tolerance argument only valid if using pad, ' 1691 'backfill or nearest lookups') -> 1692 return self._engine.get_loc(_values_from_object(key)) 1693 1694 indexer = self.get_indexer([key], method=method, pandas/index.pyx in pandas.index.IndexEngine.get_loc (pandas/index.c:3979)() pandas/index.pyx in pandas.index.IndexEngine.get_loc (pandas/index.c:3843)() pandas/hashtable.pyx in pandas.hashtable.PyObjectHashTable.get_item (pandas/hashtable.c:12265)() pandas/hashtable.pyx in pandas.hashtable.PyObjectHashTable.get_item (pandas/hashtable.c:12216)() KeyError: 'Ann'
Что-то не сработало, хотя Ann
, очевидно, присутствует в нашем датафрейме. Посмотрим поближе на список имён столбцов:
df.columns
Index([' Ann', 'Bob', 'Claudia', 'Daniel'], dtype='object')
Вот в чём дело! Перед именем Ann
вкрался лишний пробел. Это потому, что я делал CSV-файл ручками и решил, что так будет более красиво. Ну что же, давайте переименуем первый столбец, убрав оттуда пробел. Сделать это, напрямую манипулируя элементами df.columns
не получится (там неизменяемый объект), но зато можно присвоить этой переменной новый список, в котором имя первого столбца будет правильным.
my_columns = list(df.columns)
print(my_columns)
my_columns[0] = 'Ann'
print(my_columns)
[' Ann', 'Bob', 'Claudia', 'Daniel'] ['Ann', 'Bob', 'Claudia', 'Daniel']
df.columns = my_columns
df['Ann']
Limits 1.0 Derivatives 2.3 Integrals 2.1 Vectors 4.0 Operators 6.0 Name: Ann, dtype: float64
Несмотря на то, что просто квадратные скобки (безо всяких .loc
перед ними) позволяют обращаться к столбцам таблицы, те же самые квадратные скобки, используемые вместе со срезами, работают «по строчкам». Например, вот так можно получить вторую и третью строчки таблицы:
df[1:3]
Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|
Derivatives | 2.3 | 1.2 | 3.2 | 1.2 |
Integrals | 2.1 | 2.3 | 2.3 | 2.2 |
Срезы можно делать не только по номерам строк, но и по их именам:
df['Limits':'Vectors']
Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|
Limits | 1.0 | 2.0 | 3.0 | 2.0 |
Derivatives | 2.3 | 1.2 | 3.2 | 1.2 |
Integrals | 2.1 | 2.3 | 2.3 | 2.2 |
Vectors | 4.0 | 2.5 | 2.4 | 1.2 |
Важная разница состоит в том, что теперь срез включает последний элемент (в отличие от всего, что вы знали раньше о срезах). Срезы с номерами строк ведут себя как обычно (последний элемент не включается):
df[0:1]
Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|
Limits | 1 | 2 | 3 | 2 |
Ещё можно обратиться к столбцу по его имени вот так:
df.Ann
Limits 1.0 Derivatives 2.3 Integrals 2.1 Vectors 4.0 Operators 6.0 Name: Ann, dtype: float64
Но это не самый безопасный способ. Во-первых, чтобы он сработал, имя столбца должно быть валидным идентификатором Python (то есть таким словом, каким можно назвать переменную), а это не всегда верно — например, в имени столбца могут встречаться пробелы, и в этом случае способ обращения через точку не сработает. Во-вторых, использовать такое обращение при присваивании небезопасно. Например, редактирование конкретной ячейки в уже созданном столбце сработает:
df.Ann['Limits'] = 2.0 # Ann пересдала домашку по пределам
df
Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|
Limits | 2.0 | 2.0 | 3.0 | 2.0 |
Derivatives | 2.3 | 1.2 | 3.2 | 1.2 |
Integrals | 2.1 | 2.3 | 2.3 | 2.2 |
Vectors | 4.0 | 2.5 | 2.4 | 1.2 |
Operators | 6.0 | 5.0 | 4.0 | 5.0 |
А создание нового столбца таким образом не сработает:
df.Julia = [1, 2, 3, 4, 5]
df
Ann | Bob | Claudia | Daniel | |
---|---|---|---|---|
Limits | 2.0 | 2.0 | 3.0 | 2.0 |
Derivatives | 2.3 | 1.2 | 3.2 | 1.2 |
Integrals | 2.1 | 2.3 | 2.3 | 2.2 |
Vectors | 4.0 | 2.5 | 2.4 | 1.2 |
Operators | 6.0 | 5.0 | 4.0 | 5.0 |
Если бы мы хотели добавить новый столбец, нужно было бы использовать синтаксис с квадратными скобками:
df['Julia'] = [1, 2, 3, 4, 5]
df
Ann | Bob | Claudia | Daniel | Julia | |
---|---|---|---|---|---|
Limits | 2.0 | 2.0 | 3.0 | 2.0 | 1 |
Derivatives | 2.3 | 1.2 | 3.2 | 1.2 | 2 |
Integrals | 2.1 | 2.3 | 2.3 | 2.2 | 3 |
Vectors | 4.0 | 2.5 | 2.4 | 1.2 | 4 |
Operators | 6.0 | 5.0 | 4.0 | 5.0 | 5 |
Как мы уже видели выше, есть несколько способов обращаться к элементам датафрейма. Во-первых, просто квадратные скобки. Если передать им один элемент, то вернётся столбец с соответствующим именем. Кстати, если передать список из нескольких элементов, то вернётся датафрейм с соответствующими столбцами:
df[['Ann', 'Bob']]
Ann | Bob | |
---|---|---|
Limits | 2.0 | 2.0 |
Derivatives | 2.3 | 1.2 |
Integrals | 2.1 | 2.3 |
Vectors | 4.0 | 2.5 |
Operators | 6.0 | 5.0 |
А если передать срез (что-то через двоеточие), то вернётся датафрейм с соответствующими строчками. (Это всё не так-то просто запомнить, увы.)
loc[]
¶Второй способ — это метод loc
. Его надо вызывать также с квадратными скобками, передавая имена. Если передать одно имя, то оно рассматривается как имя строки, а если два, то возвращается ячейка с соответствующей строкой и столбцом:
df.loc['Limits']
Ann 2 Bob 2 Claudia 3 Daniel 2 Julia 1 Name: Limits, dtype: float64
df.loc['Limits', 'Ann']
2.0
А вот так можно с помощью loc[]
получить столбец:
df.loc[:, 'Ann']
Limits 2.0 Derivatives 2.3 Integrals 2.1 Vectors 4.0 Operators 6.0 Name: Ann, dtype: float64
Здесь в качестве первого аргумента передаётся «тривиальный срез», то есть такой срез, у которого начало совпадает с началом всего массива, а конец с концом всего. Аналогично было со списками, если помните:
my_list = [4, 3, 2, 1]
other_list = my_list[:]
other_list
[4, 3, 2, 1]
iloc[]
¶Метод loc[]
работает с именами строк и столбцов, а если вы хотите использовать их номера, то вам нужен метод iloc[]
. Работает он примерно так:
df.iloc[1] # вторая строка
Ann 2.3 Bob 1.2 Claudia 3.2 Daniel 1.2 Julia 2.0 Name: Derivatives, dtype: float64
df.iloc[:, 2] # третий столбец
Limits 3.0 Derivatives 3.2 Integrals 2.3 Vectors 2.4 Operators 4.0 Name: Claudia, dtype: float64
df.iloc[1, 2] # ячейка во второй строке, третьем столбце
3.2000000000000002
at[]
и iat[]
¶Обычные квадратные скобки, а также методы loc[]
и iloc[]
должны обрабатывать разные случаи, связанные со срезами и т.д. Это делает их медленными. Если вы хотите обратиться к конкретной ячейке, можете использовать для этого методы at[]
или iat[]
.
df.at['Derivatives', 'Ann']
2.2999999999999998
df.iat[1, 2]
3.2000000000000002
Предположим, что нам нужно получить все строки, соответствующие домашкам, по которым Ann получила оценку выше тройки. Это делается так:
df[df['Ann']>3]
Ann | Bob | Claudia | Daniel | Julia | |
---|---|---|---|---|---|
Vectors | 4 | 2.5 | 2.4 | 1.2 | 4 |
Operators | 6 | 5.0 | 4.0 | 5.0 | 5 |
Этот синтаксис работает примерно так же, как аналогичный синтаксис в np.array
, обсуждавшийся на прошлой лекции. Результатом сравнения df['Ann']
с числом 3 является массив (точкее, pd.Series
), содержащий булевские элементы, являющиеся результатами попарного сравнения элементов df['Ann']
с числом 3:
df['Ann']>3
Limits False Derivatives False Integrals False Vectors True Operators True Name: Ann, dtype: bool
Если передать такой объект квадратным скобкам, то они выберут те элементы, напротив которых стоит True
.
Аналогично можно выбрать столбцы по условию, с помощью loc
.
df.loc[:, df.loc['Derivatives']>2]
# выбрать все столбцы, у которых в строке `Derivatives` оценка больше двух
Ann | Claudia | |
---|---|---|
Limits | 2.0 | 3.0 |
Derivatives | 2.3 | 3.2 |
Integrals | 2.1 | 2.3 |
Vectors | 4.0 | 2.4 |
Operators | 6.0 | 4.0 |
Допустим, мы хотим посчитать среднюю успеваемость каждого студента. Нет проблем:
df.mean()
Ann 3.28 Bob 2.60 Claudia 2.98 Daniel 2.32 Julia 3.00 dtype: float64
А может быть нас интересует средняя решаемость домашек? Можно предложить два способа её посчитать. Во-первых, можно транспонировать нашу табличку, записав строки по столбцам, и применить к ней тот же метод mean()
.
df.T
Limits | Derivatives | Integrals | Vectors | Operators | |
---|---|---|---|---|---|
Ann | 2 | 2.3 | 2.1 | 4.0 | 6 |
Bob | 2 | 1.2 | 2.3 | 2.5 | 5 |
Claudia | 3 | 3.2 | 2.3 | 2.4 | 4 |
Daniel | 2 | 1.2 | 2.2 | 1.2 | 5 |
Julia | 1 | 2.0 | 3.0 | 4.0 | 5 |
df.T.mean()
Limits 2.00 Derivatives 1.98 Integrals 2.38 Vectors 2.82 Operators 5.00 dtype: float64
Во-вторых (и это, наверное, проще), можно передать методу mean()
параметр axis
, указывающий, вдоль какой оси нужно считать. (Такое свойство есть у разных функций, работающих со строками или столбцами.)
df.mean(axis=1)
Limits 2.00 Derivatives 1.98 Integrals 2.38 Vectors 2.82 Operators 5.00 dtype: float64
df.mean(axis='columns')
# можно ещё так
Limits 2.00 Derivatives 1.98 Integrals 2.38 Vectors 2.82 Operators 5.00 dtype: float64
Конечно, есть не только среднее, но и другие распространённые функции дескриптивной статистики.
df.min()
Ann 2.0 Bob 1.2 Claudia 2.3 Daniel 1.2 Julia 1.0 dtype: float64
df.max()
Ann 6 Bob 5 Claudia 4 Daniel 5 Julia 5 dtype: float64
Рассмотрим реальный датафрейм, полученный с портала открытых данных г. Москвы. Я его скачал отсюда, но там он лежит в zip-файле, который не так-то просто разархивировать из-за кириллицы в имени файла (мне на Маке пришлось повозиться), так что я скопировал CSV'шку на наш сервер. Если попробовать её просто открыть, то появится ошибка, потому что в качестве разделителя там используется не запятая, а точка с запятой — такое часто бывает и у функции pd.read_csv()
на эту тему есть специальный параметр sep
:
df = pd.read_csv(
"http://math-info.hse.ru/f/2015-16/all-py/data/tariff2012.csv",
sep=';'
)
Таблица большая, выведем только несколько первых строк, чтобы понять, с чем мы имеем дело:
df.head()
ROWNUM | Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | город Москва (за исключением Троицкого и Новом... | Горячее водоснабжение | руб/куб.м | 01.01.2012 | 30.06.2012 | да | NaN | NaN | NaN | ОАО «МОЭК» и иные, кроме ОАО «Мосэнерго» и ОАО... | 105.45 | 19693618 | NaN |
1 | 2 | город Москва (за исключением Троицкого и Новом... | Водоотведение | руб/куб.м | 01.01.2012 | 30.06.2012 | да | NaN | NaN | NaN | МГУП «Мосводоканал» | 16.65 | 19693667 | NaN |
2 | 3 | город Москва (за исключением Троицкого и Новом... | Газ | руб/кв.м | 01.07.2012 | 31.08.2012 | нет | NaN | NaN | дома с отоплением от газовых нагревателей | ООО «Газпром межрегионгаз Москва» | 25.61 | 19693682 | NaN |
3 | 4 | город Москва (за исключением Троицкого и Новом... | Электроэнергия | руб/кВт.ч | 01.07.2012 | 31.07.2012 | да | газовая | с 07:00 по 23:00 ч (двухтарифный учет) | на бытовые нужды | ОАО «Мосэнергосбыт» | 4.03 | 19693607 | NaN |
4 | 5 | город Москва (за исключением Троицкого и Новом... | Электроэнергия | руб/кВт.ч | 01.01.2012 | 30.06.2012 | да | газовая | с 23:00 по 07:00 ч (двухтарифный учет) | на бытовые нужды | ОАО «Мосэнергосбыт» | 0.95 | 19693669 | NaN |
Кажется, в таблице есть столбец ROWNUM
, который можно было бы сделать индексом для строк, но тут не всё просто. Например:
df['ROWNUM']
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-60-2f6f79c10808> in <module>() ----> 1 df['ROWNUM'] /usr/local/lib/python3.5/site-packages/pandas/core/frame.py in __getitem__(self, key) 1912 return self._getitem_multilevel(key) 1913 else: -> 1914 return self._getitem_column(key) 1915 1916 def _getitem_column(self, key): /usr/local/lib/python3.5/site-packages/pandas/core/frame.py in _getitem_column(self, key) 1919 # get column 1920 if self.columns.is_unique: -> 1921 return self._get_item_cache(key) 1922 1923 # duplicate columns & possible reduce dimensionaility /usr/local/lib/python3.5/site-packages/pandas/core/generic.py in _get_item_cache(self, item) 1088 res = cache.get(item) 1089 if res is None: -> 1090 values = self._data.get(item) 1091 res = self._box_item_values(item, values) 1092 cache[item] = res /usr/local/lib/python3.5/site-packages/pandas/core/internals.py in get(self, item, fastpath) 3100 3101 if not isnull(item): -> 3102 loc = self.items.get_loc(item) 3103 else: 3104 indexer = np.arange(len(self.items))[isnull(self.items)] /usr/local/lib/python3.5/site-packages/pandas/core/index.py in get_loc(self, key, method, tolerance) 1690 raise ValueError('tolerance argument only valid if using pad, ' 1691 'backfill or nearest lookups') -> 1692 return self._engine.get_loc(_values_from_object(key)) 1693 1694 indexer = self.get_indexer([key], method=method, pandas/index.pyx in pandas.index.IndexEngine.get_loc (pandas/index.c:3979)() pandas/index.pyx in pandas.index.IndexEngine.get_loc (pandas/index.c:3843)() pandas/hashtable.pyx in pandas.hashtable.PyObjectHashTable.get_item (pandas/hashtable.c:12265)() pandas/hashtable.pyx in pandas.hashtable.PyObjectHashTable.get_item (pandas/hashtable.c:12216)() KeyError: 'ROWNUM'
Почему-то эта штука считает, что там нет столбца ROWNUM
, хотя мы его видим своими глазами! Давайте посмотрим повнимательнее.
df.columns
Index(['ROWNUM', 'Region', 'TariffItem', 'UnitOfMeasure', 'StartDate', 'EndDate', 'MeasureDeviceAvail', 'StoveType', 'ConsumptionTime', 'ConsumptionTarget', 'Agency', 'TariffValue', 'global_id', 'Unnamed: 13'], dtype='object')
…Вроде бы есть ROWNUM
…
list(df.columns)
['\ufeffROWNUM', 'Region', 'TariffItem', 'UnitOfMeasure', 'StartDate', 'EndDate', 'MeasureDeviceAvail', 'StoveType', 'ConsumptionTime', 'ConsumptionTarget', 'Agency', 'TariffValue', 'global_id', 'Unnamed: 13']
Вот оно! Оказывается, перед ROWNUM
есть какой-то невидимый символ, кодирующийся как \ufeff
. Это так называемый BOM — Byte Order Mark — и он вылезает иногда при чтении файлов в кодировках UTF. Он показывает, в каком порядке идут байты. Чтобы он не попал в наши данные, нужно при чтении использовать опцию encoding='utf-8-sig'
:
df = pd.read_csv(
"http://math-info.hse.ru/f/2015-16/all-py/data/tariff2012.csv",
sep=';',
encoding = 'utf-8-sig'
)
Теперь df['ROWNUM']
работает:
df['ROWNUM'].head()
0 1 1 2 2 3 3 4 4 5 Name: ROWNUM, dtype: int64
Впрочем, этот столбец нам всё равно в явном виде не понадобится — мы сделаем его индексом строк.
df = pd.read_csv(
"http://math-info.hse.ru/f/2015-16/all-py/data/tariff2012.csv",
sep=';',
encoding='utf-8-sig',
index_col=0
)
df.head()
Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROWNUM | |||||||||||||
1 | город Москва (за исключением Троицкого и Новом... | Горячее водоснабжение | руб/куб.м | 01.01.2012 | 30.06.2012 | да | NaN | NaN | NaN | ОАО «МОЭК» и иные, кроме ОАО «Мосэнерго» и ОАО... | 105.45 | 19693618 | NaN |
2 | город Москва (за исключением Троицкого и Новом... | Водоотведение | руб/куб.м | 01.01.2012 | 30.06.2012 | да | NaN | NaN | NaN | МГУП «Мосводоканал» | 16.65 | 19693667 | NaN |
3 | город Москва (за исключением Троицкого и Новом... | Газ | руб/кв.м | 01.07.2012 | 31.08.2012 | нет | NaN | NaN | дома с отоплением от газовых нагревателей | ООО «Газпром межрегионгаз Москва» | 25.61 | 19693682 | NaN |
4 | город Москва (за исключением Троицкого и Новом... | Электроэнергия | руб/кВт.ч | 01.07.2012 | 31.07.2012 | да | газовая | с 07:00 по 23:00 ч (двухтарифный учет) | на бытовые нужды | ОАО «Мосэнергосбыт» | 4.03 | 19693607 | NaN |
5 | город Москва (за исключением Троицкого и Новом... | Электроэнергия | руб/кВт.ч | 01.01.2012 | 30.06.2012 | да | газовая | с 23:00 по 07:00 ч (двухтарифный учет) | на бытовые нужды | ОАО «Мосэнергосбыт» | 0.95 | 19693669 | NaN |
Обратите внимание на NaN
— это такой специальный элемент, который указыает, что в данной ячейке нет данных (например, в исходном файле в эту ячейку ничего не было записано).
Видно, что в таблице собраны разнородные данные — тарифы на газ, электроэнергию и т.д. Прежде, чем с ними можно будет сделать что-то осмысленное, их надо как-то разделить. Это можно делать, например, с помощью условного выбора:
df_gas = df[df['TariffItem'] == 'Газ']
df_gas.head()
Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROWNUM | |||||||||||||
3 | город Москва (за исключением Троицкого и Новом... | Газ | руб/кв.м | 01.07.2012 | 31.08.2012 | нет | NaN | NaN | дома с отоплением от газовых нагревателей | ООО «Газпром межрегионгаз Москва» | 25.61 | 19693682 | NaN |
7 | город Москва | Газ | руб/куб.м | 11.01.2011 | 15.01.2012 | да | NaN | NaN | на приготовление пищи и горячее водоснабжение ... | ООО «Мосрегионгаз» | 4.09 | 19693595 | NaN |
8 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на нагрев воды с использованием газового водон... | ООО «Газпром межрегионгаз Москва» | 4.09 | 19693684 | NaN |
11 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 01.07.2012 | 31.12.2012 | да/нет | NaN | NaN | на нагрев воды с использованием газового водон... | ООО «Газпром межрегионгаз Москва» | 4.70 | 19693624 | NaN |
12 | город Москва (за исключением Троицкого и Новом... | Газ | руб/1000 куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на отопление с одновременным использованием га... | ООО «Газпром межрегионгаз Москва» | 2930.00 | 19693625 | NaN |
Теперь у нас есть табличка, в которой приведены данные только по тарифам на газ.
Другой подход состоит в том, чтобы создать сразу много табличек в зависимости от того, что записано в графе TariffItem
. Для этого в pandas есть метод groupby()
.
groups = df.groupby('TariffItem')
groups.get_group('Газ').head()
Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROWNUM | |||||||||||||
3 | город Москва (за исключением Троицкого и Новом... | Газ | руб/кв.м | 01.07.2012 | 31.08.2012 | нет | NaN | NaN | дома с отоплением от газовых нагревателей | ООО «Газпром межрегионгаз Москва» | 25.61 | 19693682 | NaN |
7 | город Москва | Газ | руб/куб.м | 11.01.2011 | 15.01.2012 | да | NaN | NaN | на приготовление пищи и горячее водоснабжение ... | ООО «Мосрегионгаз» | 4.09 | 19693595 | NaN |
8 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на нагрев воды с использованием газового водон... | ООО «Газпром межрегионгаз Москва» | 4.09 | 19693684 | NaN |
11 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 01.07.2012 | 31.12.2012 | да/нет | NaN | NaN | на нагрев воды с использованием газового водон... | ООО «Газпром межрегионгаз Москва» | 4.70 | 19693624 | NaN |
12 | город Москва (за исключением Троицкого и Новом... | Газ | руб/1000 куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на отопление с одновременным использованием га... | ООО «Газпром межрегионгаз Москва» | 2930.00 | 19693625 | NaN |
groups.get_group("Водоотведение")
Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROWNUM | |||||||||||||
2 | город Москва (за исключением Троицкого и Новом... | Водоотведение | руб/куб.м | 01.01.2012 | 30.06.2012 | да | NaN | NaN | NaN | МГУП «Мосводоканал» | 16.65 | 19693667 | NaN |
27 | город Москва (за исключением Троицкого и Новом... | Водоотведение | руб/куб.м | 01.09.2012 | 31.12.2012 | да | NaN | NaN | NaN | МГУП «Мосводоканал» | 19.00 | 19693387 | NaN |
43 | город Москва (за исключением Троицкого и Новом... | Водоотведение | руб/куб.м | 01.07.2012 | 31.08.2012 | да | NaN | NaN | NaN | МГУП «Мосводоканал» | 18.20 | 19693668 | NaN |
Список всех групп можно посмотреть так:
groups.groups.keys()
dict_keys(['Горячее водоснабжение', 'Твердое топливо', 'Электроэнергия', 'Холодное водоснабжение', 'Водоотведение', 'Отопление', 'Газ'])
Допустим, нас интересует средний тариф по каждому из типов расходов (как мы увидим ниже, для этой таблицы считать его довольно бессмысленно, но мы всё равно попробуем — просто чтобы показать, как работает pandas).
df.groupby('TariffItem').mean()
TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|
TariffItem | |||
Водоотведение | 17.950000 | 19693574 | NaN |
Газ | 651.415000 | 19693593 | NaN |
Горячее водоснабжение | 100.095000 | 19693523 | NaN |
Отопление | 1208.423333 | 19693631 | NaN |
Твердое топливо | 1126.000000 | 19693629 | NaN |
Холодное водоснабжение | 25.223333 | 19693636 | NaN |
Электроэнергия | 2.571250 | 19693585 | NaN |
Давайте рассмотрим повнимательнее данные, относящиеся к тарифам на газ. Здесь есть ещё один занятный столбец: UnitOfMeasure
— единица измерения. Посмотрим, какие значения и как часто он принимает.
df_gas['UnitOfMeasure'].value_counts()
# эта функция считает, сколько раз какое значение встретилось
руб/куб.м 8 руб/чел 6 руб/1000 куб.м 4 руб/кв.м 2 Name: UnitOfMeasure, dtype: int64
Как видимо, единицы измерения самые разные и вряд ли мы можем как-то легко перевести «руб/кв.м» (видимо, в квадратных метрах меряется всё-таки площадь квартиры, а не количество потребляемого газа) в «руб/куб.м», но по крайней мере «руб/1000 куб.м» и «руб/куб.м» — это что-то похожее. Давайте преобразуем первое во второе: для этого нам надо найти те строки, в которых единица измерения указана как «руб/1000 куб.м», взять для них столбец TariffValue
и умножить все его элементы на 1000.
Вот нужные нам строки:
unit1000 = df_gas['UnitOfMeasure'] == 'руб/1000 куб.м'
# записали в переменную unit1000 результат проверки условия о том,
# что UnitOfMeasure == 'руб/1000 куб.м'
df_gas[unit1000]
Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROWNUM | |||||||||||||
12 | город Москва (за исключением Троицкого и Новом... | Газ | руб/1000 куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на отопление с одновременным использованием га... | ООО «Газпром межрегионгаз Москва» | 2930 | 19693625 | NaN |
60 | город Москва (за исключением Троицкого и Новом... | Газ | руб/1000 куб.м | 01.07.2012 | 31.12.2012 | да/нет | NaN | NaN | на отопление с одновременным использованием га... | ООО «Газпром межрегионгаз Москва» | 3370 | 19693626 | NaN |
62 | город Москва (за исключением Троицкого и Новом... | Газ | руб/1000 куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на отопление и (или) выработку электрической э... | ООО «Газпром межрегионгаз Москва» | 2930 | 19693627 | NaN |
64 | город Москва (за исключением Троицкого и Новом... | Газ | руб/1000 куб.м | 01.07.2012 | 31.12.2012 | да/нет | NaN | NaN | на отопление и (или) выработку электрической э... | ООО «Газпром межрегионгаз Москва» | 3370 | 19693628 | NaN |
Можно было бы теперь взять от этой таблицы столбец TariffValue
и присвоить ему значение его же, разделенное на 1000. Но результат может оказаться неожиданным.
df_gas[unit1000]['TariffValue']
ROWNUM 12 2930 60 3370 62 2930 64 3370 Name: TariffValue, dtype: float64
df_gas[unit1000]['TariffValue'] = df_gas[unit1000]['TariffValue']/1000
/usr/local/lib/python3.5/site-packages/ipykernel/__main__.py:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy if __name__ == '__main__':
df_gas[unit1000]['TariffValue']
ROWNUM 12 2930 60 3370 62 2930 64 3370 Name: TariffValue, dtype: float64
Ничего не изменилось, да ещё и какая-то страшна красная штука вылезла. Почему так произошло? Дело в том, что взяв подмножество строк с помощью df_gas[unit1000]
, мы могли создать либо копию исходной таблички, либо её вид (то есть просто интерфейс). Если это был вид, то приравнивание могло сработать как надо. А если копия, то приравнивание произошло к копии, а оригинальная таблица осталась неизменной. К сожалению, заранее неизвестно, что будет возвращено квадратными скобками — вид или копия — это зависит от внутренней структуры данных. Так что нужно рассчитывать на худшее.
Чтобы избежать таких проблем, вместо применения несколько квадратных скобок подряд, нужно писать одни скобки, в них указывая, какие элементы нам нужны, указывая наборы строк и столбцов через запятую, как это обсуждалось выше. Например, вот так это сработает:
df_gas.loc[unit1000, 'TariffValue'] = df_gas.loc[unit1000, 'TariffValue']/1000
/usr/local/lib/python3.5/site-packages/pandas/core/indexing.py:420: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy self.obj[item] = s
Тоже предупреждение, но по крайней мере результат удовлетворительный:
df_gas.loc[unit1000, 'TariffValue']
ROWNUM 12 2.93 60 3.37 62 2.93 64 3.37 Name: TariffValue, dtype: float64
Предупреждение, кстати, возникло из-за того, что df_gas
сам является такой копией, полученной из df
путём выделения подмножества строк по условию. Заметим, что исходный датафрейм df
в результате не изменился — именно об этом предупреждает нас система выше.
df.loc[df['UnitOfMeasure'] == 'руб/1000 куб.м', 'TariffValue']
ROWNUM 12 2930 60 3370 62 2930 64 3370 Name: TariffValue, dtype: float64
Чтобы избежать дальнейших предпреждением, отделим df_gas
от df
окончательно:
df_gas = df_gas.copy()
Теперь нужно в тех строчках, в которых мы поменяли значение тарифа, изменить и единицу измерения, чтобы она соответствовала.
df_gas.loc[unit1000, 'UnitOfMeasure'] = "руб/куб.м"
df_gas['UnitOfMeasure'].value_counts()
руб/куб.м 12 руб/чел 6 руб/кв.м 2 Name: UnitOfMeasure, dtype: int64
Ну вот, по крайней мере 12 значений тарифа, которые можно сравнивать между собой, у нас есть. Давайте посмотрим на них повнимательнее.
df_gas_kubm = df_gas[df_gas['UnitOfMeasure'] == 'руб/куб.м']
df_gas_kubm.describe()
TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|
count | 12.000000 | 12.000000 | 0 |
mean | 3.832500 | 19693608.416667 | NaN |
std | 0.700274 | 100.067667 | NaN |
min | 2.930000 | 19693307.000000 | NaN |
25% | 3.260000 | 19693616.000000 | NaN |
50% | 4.090000 | 19693625.500000 | NaN |
75% | 4.242500 | 19693641.750000 | NaN |
max | 4.700000 | 19693685.000000 | NaN |
Конечно, осмысленный здесь только один столбец, можно его запросить отдельно:
df_gas_kubm.describe()['TariffValue']
count 12.000000 mean 3.832500 std 0.700274 min 2.930000 25% 3.260000 50% 4.090000 75% 4.242500 max 4.700000 Name: TariffValue, dtype: float64
Можно даже картинку нарисовать какую-нибудь.
%matplotlib inline
df_gas_kubm['TariffValue'].plot.hist()
<matplotlib.axes._subplots.AxesSubplot at 0x105293668>
query()
¶Допустим, мы с самого начала хотели выбрать из исходной таблицы те строки, в которых указан тариф на газ и в качестве единицы измерения указаны руб/кв.м
. Это можно сделать разными способами. Например, вот так:
df[ (df['TariffItem'] == "Газ") & (df['UnitOfMeasure'] == "руб/кв.м") ]
Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROWNUM | |||||||||||||
3 | город Москва (за исключением Троицкого и Новом... | Газ | руб/кв.м | 01.07.2012 | 31.08.2012 | нет | NaN | NaN | дома с отоплением от газовых нагревателей | ООО «Газпром межрегионгаз Москва» | 25.61 | 19693682 | NaN |
52 | город Москва (за исключением Троицкого и Новом... | Газ | руб/кв.м | 01.01.2012 | 30.06.2012 | нет | NaN | NaN | дома с отоплением от газовых нагревателей | ООО «Газпром межрегионгаз Москва» | 22.27 | 19693622 | NaN |
Обратите внимание на амперсанд &
(логическое И) и на скобки — они обязательны.
Как работает эта штука? Очень просто: df['TariffItem'] == "Газ"
— один ряд с булевскими значениями, df['UnitOfMeasure'] == "руб/кв.м"
— другой, амперсанд &
делает поэлементное «И» с этими двумя рядами — в результате получается ряд, в котором стоит True
только если выполнялись оба условия — и именно по этому ряду мы и производим выборку элементов.
Этот синтаксис не назовёшь лаконичным и разработчики pandas предложили другой — впрочем, в документации сказано, что он носит экспериментальный характер.
df.query('TariffItem == "Газ" and UnitOfMeasure == "руб/куб.м"')
Region | TariffItem | UnitOfMeasure | StartDate | EndDate | MeasureDeviceAvail | StoveType | ConsumptionTime | ConsumptionTarget | Agency | TariffValue | global_id | Unnamed: 13 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROWNUM | |||||||||||||
7 | город Москва | Газ | руб/куб.м | 11.01.2011 | 15.01.2012 | да | NaN | NaN | на приготовление пищи и горячее водоснабжение ... | ООО «Мосрегионгаз» | 4.09 | 19693595 | NaN |
8 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на нагрев воды с использованием газового водон... | ООО «Газпром межрегионгаз Москва» | 4.09 | 19693684 | NaN |
11 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 01.07.2012 | 31.12.2012 | да/нет | NaN | NaN | на нагрев воды с использованием газового водон... | ООО «Газпром межрегионгаз Москва» | 4.70 | 19693624 | NaN |
13 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 01.07.2012 | 31.12.2012 | да/нет | NaN | NaN | на приготовление пищи и нагрев воды с использо... | ООО «Газпром межрегионгаз Москва» | 4.70 | 19693623 | NaN |
14 | город Москва | Газ | руб/куб.м | 11.01.2011 | 15.01.2012 | да | NaN | NaN | на отопление жилых помещений, а также в случае... | ООО «Мосрегионгаз» | 2.93 | 19693594 | NaN |
22 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на приготовление пищи и нагрев воды с использо... | ООО «Газпром межрегионгаз Москва» | 4.09 | 19693685 | NaN |
24 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 01.07.2012 | 31.12.2012 | да/нет | NaN | NaN | на приготовление пищи и нагрев воды с использо... | ООО «Газпром межрегионгаз Москва» | 4.70 | 19693307 | NaN |
71 | город Москва (за исключением Троицкого и Новом... | Газ | руб/куб.м | 15.01.2012 | 30.06.2012 | да/нет | NaN | NaN | на приготовление пищи и нагрев воды с использо... | ООО «Газпром межрегионгаз Москва» | 4.09 | 19693683 | NaN |
r = requests.get("http://api.data.mos.ru/v1/datasets/1130/rows")
r.json()[:3]
[{'Cells': {'Agency': 'ОАО «МОЭК» и иные, кроме ОАО «Мосэнерго» и ОАО «МТК»', 'ConsumptionTarget': None, 'ConsumptionTime': None, 'EndDate': '30.06.2012', 'MeasureDeviceAvail': 'да', 'Region': 'город Москва (за исключением Троицкого и Новомосковского АО)', 'StartDate': '01.01.2012', 'StoveType': None, 'TariffItem': 'Горячее водоснабжение', 'TariffValue': 105.45, 'UnitOfMeasure': 'руб/куб.м', 'global_id': 19693618}, 'Id': '52406d3d-93dc-4def-b9c0-28df28d76054', 'Number': 1}, {'Cells': {'Agency': 'МГУП «Мосводоканал»', 'ConsumptionTarget': None, 'ConsumptionTime': None, 'EndDate': '30.06.2012', 'MeasureDeviceAvail': 'да', 'Region': 'город Москва (за исключением Троицкого и Новомосковского АО)', 'StartDate': '01.01.2012', 'StoveType': None, 'TariffItem': 'Водоотведение', 'TariffValue': 16.65, 'UnitOfMeasure': 'руб/куб.м', 'global_id': 19693667}, 'Id': '0bc84c41-b103-4db9-afa1-dbe2d9176e4f', 'Number': 2}, {'Cells': {'Agency': 'ООО «Газпром межрегионгаз Москва»', 'ConsumptionTarget': 'дома с отоплением от газовых нагревателей', 'ConsumptionTime': None, 'EndDate': '31.08.2012', 'MeasureDeviceAvail': 'нет', 'Region': 'город Москва (за исключением Троицкого и Новомосковского АО)', 'StartDate': '01.07.2012', 'StoveType': None, 'TariffItem': 'Газ', 'TariffValue': 25.61, 'UnitOfMeasure': 'руб/кв.м', 'global_id': 19693682}, 'Id': '52c1c5c5-60a7-4308-a36c-886f4c6039b1', 'Number': 3}]
Как сделать из такого JSON датафрейм? Считайте это домашним заданием!