Алла Тамбовцева, НИУ ВШЭ
Часть 2 включает:
Загрузим все тот же файл с данными вымышленного опроса посетителей елочного базара:
import pandas as pd
tree = pd.read_csv("firtree.csv")
Часто при работе с датафреймом нас не интересует выбор отдельных строк по названию или номеру, а интересует фильтрация наблюдений – выбор строк датафрейма, которые удовлетворяют определенному условию. Для этого интересующее нас условие необходимо указать в квадратных скобках. Например, выберем только те строки, которые соответствуют людям, готовым отдать более 1500 рублей за елку:
tree[tree["expenses"] > 1500]
Unnamed: 0 | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да |
5 | 6 | male | сосна Крым | 91 | 3 | 2139 | да |
7 | 8 | female | ель обыкновенная | 94 | 2 | 2707 | нет |
9 | 10 | male | сосна датская | 221 | 4 | 1521 | нет |
... | ... | ... | ... | ... | ... | ... | ... |
1192 | 1193 | male | сосна датская | 131 | 5 | 2683 | нет |
1194 | 1195 | female | ель обыкновенная | 127 | 4 | 2932 | нет |
1197 | 1198 | male | сосна Крым | 220 | 5 | 1591 | нет |
1198 | 1199 | male | сосна датская | 94 | 1 | 1966 | да |
1199 | 1200 | male | сосна датская | 105 | 5 | 2204 | нет |
652 rows × 7 columns
Почему нельзя было написать проще, то есть tree["expenses"] > 1500
? Давайте напишем, и посмотрим, что получится:
tree["expenses"] > 1500
0 False 1 True 2 False 3 True 4 False ... 1195 False 1196 False 1197 True 1198 True 1199 True Name: expenses, Length: 1200, dtype: bool
Что мы увидели? Просто результат проверки условия, набор из True
и False
. Когда мы подставляем это выражение в квадратные скобки, Python выбирает из tree
те строки, где выражение принимает значение True
.
Все операторы для проверки и объединения условий работают как обычно. Например, два условия одновременно: строки, соответствующие елкам, которые оценили дороже 1500 рублей и которые респонденты хотели бы приобрести себе:
tree[(tree["expenses"] > 1500) & (tree["wish"] == "да")]
Unnamed: 0 | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да |
5 | 6 | male | сосна Крым | 91 | 3 | 2139 | да |
17 | 18 | male | сосна датская | 151 | 5 | 2715 | да |
18 | 19 | male | сосна Крым | 227 | 3 | 2771 | да |
22 | 23 | female | пихта Нобилис | 128 | 4 | 2424 | да |
... | ... | ... | ... | ... | ... | ... | ... |
1186 | 1187 | female | ель обыкновенная | 246 | 2 | 2861 | да |
1188 | 1189 | female | пихта Нобилис | 103 | 5 | 2647 | да |
1189 | 1190 | female | ель обыкновенная | 226 | 5 | 2990 | да |
1191 | 1192 | male | пихта Нобилис | 158 | 4 | 2715 | да |
1198 | 1199 | male | сосна датская | 94 | 1 | 1966 | да |
354 rows × 7 columns
Все сосны, либо сосны Крым, либо датские сосны:
tree[(tree["ftype"] == "сосна Крым" ) | (tree["ftype"] == "сосна датская")]
Unnamed: 0 | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет |
5 | 6 | male | сосна Крым | 91 | 3 | 2139 | да |
9 | 10 | male | сосна датская | 221 | 4 | 1521 | нет |
... | ... | ... | ... | ... | ... | ... | ... |
1192 | 1193 | male | сосна датская | 131 | 5 | 2683 | нет |
1193 | 1194 | male | сосна Крым | 138 | 4 | 304 | да |
1197 | 1198 | male | сосна Крым | 220 | 5 | 1591 | нет |
1198 | 1199 | male | сосна датская | 94 | 1 | 1966 | да |
1199 | 1200 | male | сосна датская | 105 | 5 | 2204 | нет |
616 rows × 7 columns
Если бы типов сосен было много, было бы неудобно прописывать через |
условия для каждого типа. Тогда логично было бы воспользоваться методом, который позволяет выбрать все строки, где в ячейке с текстом встречается слово «сосна». Такой метод есть – это метод на строках .contains()
, который возвращает True
если некоторая подстрока (набор символов) входит в строку, и False
– в противном случае.
tree[tree["ftype"].str.contains("сосна")]
Unnamed: 0 | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет |
5 | 6 | male | сосна Крым | 91 | 3 | 2139 | да |
9 | 10 | male | сосна датская | 221 | 4 | 1521 | нет |
... | ... | ... | ... | ... | ... | ... | ... |
1192 | 1193 | male | сосна датская | 131 | 5 | 2683 | нет |
1193 | 1194 | male | сосна Крым | 138 | 4 | 304 | да |
1197 | 1198 | male | сосна Крым | 220 | 5 | 1591 | нет |
1198 | 1199 | male | сосна датская | 94 | 1 | 1966 | да |
1199 | 1200 | male | сосна датская | 105 | 5 | 2204 | нет |
616 rows × 7 columns
А если наоборот, нам нужно отрицание – все строки, которые относятся к чему угодно, только не к соснам? Можно проверить равенство False
:
tree[tree["ftype"].str.contains("сосна") == False]
Unnamed: 0 | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет |
6 | 7 | male | ель обыкновенная | 151 | 5 | 702 | да |
7 | 8 | female | ель обыкновенная | 94 | 2 | 2707 | нет |
8 | 9 | female | ель обыкновенная | 138 | 5 | 713 | нет |
... | ... | ... | ... | ... | ... | ... | ... |
1189 | 1190 | female | ель обыкновенная | 226 | 5 | 2990 | да |
1191 | 1192 | male | пихта Нобилис | 158 | 4 | 2715 | да |
1194 | 1195 | female | ель обыкновенная | 127 | 4 | 2932 | нет |
1195 | 1196 | male | ель обыкновенная | 137 | 2 | 1298 | нет |
1196 | 1197 | female | пихта Нобилис | 141 | 3 | 906 | да |
584 rows × 7 columns
А можно воспользоваться оператором ~
для отрицания и поставить его перед всем условием в скобках:
tree[~tree["ftype"].str.contains("сосна")]
Unnamed: 0 | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет |
6 | 7 | male | ель обыкновенная | 151 | 5 | 702 | да |
7 | 8 | female | ель обыкновенная | 94 | 2 | 2707 | нет |
8 | 9 | female | ель обыкновенная | 138 | 5 | 713 | нет |
... | ... | ... | ... | ... | ... | ... | ... |
1189 | 1190 | female | ель обыкновенная | 226 | 5 | 2990 | да |
1191 | 1192 | male | пихта Нобилис | 158 | 4 | 2715 | да |
1194 | 1195 | female | ель обыкновенная | 127 | 4 | 2932 | нет |
1195 | 1196 | male | ель обыкновенная | 137 | 2 | 1298 | нет |
1196 | 1197 | female | пихта Нобилис | 141 | 3 | 906 | да |
584 rows × 7 columns
В str
хранится множество удобных методов, которые во многом повторяют методы на строках, с которыми мы уже знакомы. Например, метод .split()
. Разобьем все строки в ячейках столбца ftype
по пробелу:
tree["ftype"].str.split()
0 [пихта, Нобилис] 1 [пихта, Нобилис] 2 [сосна, Крым] 3 [сосна, Крым] 4 [сосна, Крым] ... 1195 [ель, обыкновенная] 1196 [пихта, Нобилис] 1197 [сосна, Крым] 1198 [сосна, датская] 1199 [сосна, датская] Name: ftype, Length: 1200, dtype: object
Чтобы создать новые столбцы вместо списков внутри одного, можно добавить аргумент expand=True
.
tree["ftype"].str.split(expand= True)
0 | 1 | |
---|---|---|
0 | пихта | Нобилис |
1 | пихта | Нобилис |
2 | сосна | Крым |
3 | сосна | Крым |
4 | сосна | Крым |
... | ... | ... |
1195 | ель | обыкновенная |
1196 | пихта | Нобилис |
1197 | сосна | Крым |
1198 | сосна | датская |
1199 | сосна | датская |
1200 rows × 2 columns
Код выше вернул нам новый датафрейм. Мы можем сохранить его в переменную tree2
, а потом объединить tree
и tree2
с помощью функции concat()
:
tree2 = tree["ftype"].str.split(expand= True)
pd.concat([tree, tree2], axis = 1)
Unnamed: 0 | gender | ftype | height | score | expenses | wish | 0 | 1 | |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да | пихта | Нобилис |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет | пихта | Нобилис |
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да | сосна | Крым |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да | сосна | Крым |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет | сосна | Крым |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
1195 | 1196 | male | ель обыкновенная | 137 | 2 | 1298 | нет | ель | обыкновенная |
1196 | 1197 | female | пихта Нобилис | 141 | 3 | 906 | да | пихта | Нобилис |
1197 | 1198 | male | сосна Крым | 220 | 5 | 1591 | нет | сосна | Крым |
1198 | 1199 | male | сосна датская | 94 | 1 | 1966 | да | сосна | датская |
1199 | 1200 | male | сосна датская | 105 | 5 | 2204 | нет | сосна | датская |
1200 rows × 9 columns
Аргумент axis=1
нужен для того, чтобы датафреймы склеивались по столбцам, то есть, чтобы датафрейм tree2
разместился справа от tree
. По умолчанию axis=0
, поэтому, если аргумент axis
не изменить, строки из tree2
будут приклеены к строкам из tree
снизу.
Отдельный столбец по названию мы уже выбрали:
tree["wish"]
0 да 1 нет 2 да 3 да 4 нет ... 1195 нет 1196 да 1197 нет 1198 да 1199 нет Name: wish, Length: 1200, dtype: object
Если нам нужно сразу несколько столбцов (маленький датафрейм на основе старого), то названия столбцов необходимо оформить в виде списка и указать его в квадратных скобках:
tree[["ftype", "score"]]
ftype | score | |
---|---|---|
0 | пихта Нобилис | 3 |
1 | пихта Нобилис | 3 |
2 | сосна Крым | 4 |
3 | сосна Крым | 1 |
4 | сосна Крым | 3 |
... | ... | ... |
1195 | ель обыкновенная | 2 |
1196 | пихта Нобилис | 3 |
1197 | сосна Крым | 5 |
1198 | сосна датская | 1 |
1199 | сосна датская | 5 |
1200 rows × 2 columns
Если нам нужно несколько столбцов подряд, начиная с одного названия и заканчивая другим, можно воспользоваться методом .loc
:
tree.loc[:, "ftype":"score"]
ftype | height | score | |
---|---|---|---|
0 | пихта Нобилис | 190 | 3 |
1 | пихта Нобилис | 174 | 3 |
2 | сосна Крым | 248 | 4 |
3 | сосна Крым | 191 | 1 |
4 | сосна Крым | 147 | 3 |
... | ... | ... | ... |
1195 | ель обыкновенная | 137 | 2 |
1196 | пихта Нобилис | 141 | 3 |
1197 | сосна Крым | 220 | 5 |
1198 | сосна датская | 94 | 1 |
1199 | сосна датская | 105 | 5 |
1200 rows × 3 columns
Метод .loc
используется для выбора определенных строк и столбцов, поэтому в квадратных скобках образуется запись через запятую: на первом месте условия для строк, на втором – для столбцов. Здесь нас интересуют все строки (полный срез через :
) и конкретные столбцы, с ftype
по score
включительно.
Если бы мы хотели выбрать строки с 0 по 12 и столбцы с ftype
по score
, тоже бы пригодился метод .loc
:
tree.loc[0:12, "ftype":"score"]
ftype | height | score | |
---|---|---|---|
0 | пихта Нобилис | 190 | 3 |
1 | пихта Нобилис | 174 | 3 |
2 | сосна Крым | 248 | 4 |
3 | сосна Крым | 191 | 1 |
4 | сосна Крым | 147 | 3 |
5 | сосна Крым | 91 | 3 |
6 | ель обыкновенная | 151 | 5 |
7 | ель обыкновенная | 94 | 2 |
8 | ель обыкновенная | 138 | 5 |
9 | сосна датская | 221 | 4 |
10 | сосна Крым | 162 | 1 |
11 | ель обыкновенная | 149 | 5 |
12 | ель обыкновенная | 160 | 2 |
Внимание: хотя в .loc
мы задействуем обычные питоновские срезы, внутри этого метода срезы включают как левый, так и правый конец среза. Так, в примере выше были выбраны строки по 12-ую включительно и столбец score
так же был включен.
Иногда может возникнуть необходимость выбрать столбец по его порядковому номеру. Например, когда названий столбцов нет как таковых или когда названия слишком длинные, а переименовывать их нежелательно. Сделать это можно с помощью метода .iloc
(i
– от index). Выберем строки с 0 по 11 и столбцы со второго по третий:
tree.iloc[0:12, 2:4]
ftype | height | |
---|---|---|
0 | пихта Нобилис | 190 |
1 | пихта Нобилис | 174 |
2 | сосна Крым | 248 |
3 | сосна Крым | 191 |
4 | сосна Крым | 147 |
5 | сосна Крым | 91 |
6 | ель обыкновенная | 151 |
7 | ель обыкновенная | 94 |
8 | ель обыкновенная | 138 |
9 | сосна датская | 221 |
10 | сосна Крым | 162 |
11 | ель обыкновенная | 149 |
Внимание: в методе .iloc
, поскольку работа идет с обычными числовыми индексами (как в списках и кортежах), правый конец среза исключается. Поэтому в примере выше 12-я строка и 4-ый столбец показаны не были.
Если в .iloc
вписать только одно число, по умолчанию будет выдана строка с таким номером:
tree.iloc[2]
Unnamed: 0 3 gender female ftype сосна Крым height 248 score 4 expenses 655 wish да Name: 2, dtype: object
Это будет объект типа pandas Series:
type(tree.iloc[2])
pandas.core.series.Series
Посмотрим на список названий всех столбцов (точнее, это будет объект специального типа Index
, который внутри очень похож на массив):
tree.columns
Index(['Unnamed: 0', 'gender', 'ftype', 'height', 'score', 'expenses', 'wish'], dtype='object')
Аналогичным образом посмотрим на названия строк:
tree.index
RangeIndex(start=0, stop=1200, step=1)
Строки не имеют специально заданных текстовых названий, поэтому они автоматически названы по промежутку из целых чисел RangeIndex
от 0 до 1200.
Теперь попробуем переименовать столбец Unnamed: 0
и дать ему более симпатичное название. Так как это по сути номер респондента, назовем его id
. Воспользуемся методом .rename()
:
tree.rename(columns = {"Unnamed: 0": "id"})
id | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет |
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет |
... | ... | ... | ... | ... | ... | ... | ... |
1195 | 1196 | male | ель обыкновенная | 137 | 2 | 1298 | нет |
1196 | 1197 | female | пихта Нобилис | 141 | 3 | 906 | да |
1197 | 1198 | male | сосна Крым | 220 | 5 | 1591 | нет |
1198 | 1199 | male | сосна датская | 94 | 1 | 1966 | да |
1199 | 1200 | male | сосна датская | 105 | 5 | 2204 | нет |
1200 rows × 7 columns
Метод .rename()
по умолчанию работает со строками, поэтому необходимо явно указать, что изменения применяются к столбцам – аргумент columns
. Далее в качестве значения этого аргумента запишем словарь, где ключом будет старое название столбца, а значением – новое название столбца.
Посмотрим теперь на первые несколько строк в tree
:
tree.head()
Unnamed: 0 | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет |
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет |
Столбец не переименовался! Почему? Потому что многие методы в pandas
не сохраняют изменения в исходном датафрейме, а возвращают копию датафрейма с внесенными изменениями, чтобы пользователь не мог случайно «испортить» датафрейм. Чтобы сохранить изменения, нужно дописать опцию inplace=True
(записать изменения «на место» старых данных):
# теперь все ок
tree.rename(columns = {"Unnamed: 0": "id"}, inplace = True)
tree.head()
id | gender | ftype | height | score | expenses | wish | |
---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет |
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет |
Внимание: датафрейм является изменяемым типом данных (как и список). То есть, если понадобится создать копию датафрейма для тестирования всякого рода изменений, ее нужно будет создавать через метод .copy()
. Запись вида df2 = df1
создаст не копию датафрейма df1
, а лишь ссылку на него, поэтому при изменении df2
датафрейм df1
тоже изменится.
Так как отдельный столбец датафрейма является объектом типа pandas Series, который наследует свойства массива, выполнять операции над столбцами довольно просто. Например, мы хотим добавить в tree
столбец с высотой елки в метрах. Для этого достаточно выбрать столбец height
и поделить все его значения на 100:
tree["height"] / 100
0 1.90 1 1.74 2 2.48 3 1.91 4 1.47 ... 1195 1.37 1196 1.41 1197 2.20 1198 0.94 1199 1.05 Name: height, Length: 1200, dtype: float64
Теперь запишем полученный результат в новый столбец height_m
датафрейма tree
:
tree["height_m"] = tree["height"] / 100
tree.head()
id | gender | ftype | height | score | expenses | wish | height_m | |
---|---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да | 1.90 |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет | 1.74 |
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да | 2.48 |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да | 1.91 |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет | 1.47 |
По умолчанию новые столбцы записываются в конец датафрейма, но при желании столбцы можно упорядочить по своему желанию.
Пример: в некотором датафрейме df
есть столбцы a
, b
, c
, мы хотим, поменять их местами так, чтобы сначала был c
, потом a
, а потом b
:
cols = ['c', 'a', 'b']
df = df[cols]
Теперь рассмотрим случай посложнее. Допустим, мы хотим добавить новый столбец comment
, который будет содержать текстовые комментарии на каждое значение из score
.
Напишем функцию trans_comm()
, которая будет на каждое значение score
возвращать текстовый комментарий:
def trans_comm(x):
if x == 5:
r = "excellent"
elif x == 4:
r = "good"
elif x == 3:
r = "not bad"
elif x == 2:
r = "bad"
elif x == 1:
r = "really firtree?"
else:
r = None
return r
Теперь применим эту функцию к столбцу score
. Метод .apply()
работает так: пишем функцию как будто бы для значения в одной ячейке столбца, а потом применяем ее ко всему столбцу. Применяем и добавляем новый столбец comment
:
tree["comment"] = tree["score"].apply(trans_comm)
tree.head()
id | gender | ftype | height | score | expenses | wish | height_m | comment | |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | female | пихта Нобилис | 190 | 3 | 1051 | да | 1.90 | not bad |
1 | 2 | male | пихта Нобилис | 174 | 3 | 2378 | нет | 1.74 | not bad |
2 | 3 | female | сосна Крым | 248 | 4 | 655 | да | 2.48 | good |
3 | 4 | female | сосна Крым | 191 | 1 | 2934 | да | 1.91 | really firtree? |
4 | 5 | female | сосна Крым | 147 | 3 | 1198 | нет | 1.47 | not bad |
Мы уже видели, что в данном датафрейме есть строки (и столбцы) с пропущенными значениями (NaN
).
Полезное примечание: Из-за наличия этих таких значений содержащие их столбцы, даже если остальные значения являются целыми, имеют тип float
.
Удалим строки с пропущенными значениями из датафрейма совсем:
# inplace = True – сохраняем значения
tree.dropna(inplace=True)