Python для сбора данных

Алла Тамбовцева, НИУ ВШЭ

Работа с датафреймами Pandas: часть 2

Часть 2 включает:

  • Выбор строк по условию.
  • Выбор строк и столбцов по названию и по номеру.
  • Добавление новых столбцов.
  • Удаление пропущенных значений.

Загрузим все тот же файл с данными вымышленного опроса посетителей елочного базара:

In [1]:
import pandas as pd
tree = pd.read_csv("firtree.csv")

Выбор строк по условию

Часто при работе с датафреймом нас не интересует выбор отдельных строк по названию или номеру, а интересует фильтрация наблюдений – выбор строк датафрейма, которые удовлетворяют определенному условию. Для этого интересующее нас условие необходимо указать в квадратных скобках. Например, выберем только те строки, которые соответствуют людям, готовым отдать более 1500 рублей за елку:

In [2]:
tree[tree["expenses"] > 1500]
Out[2]:
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? Давайте напишем, и посмотрим, что получится:

In [3]:
tree["expenses"] > 1500
Out[3]:
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 рублей и которые респонденты хотели бы приобрести себе:

In [4]:
tree[(tree["expenses"] > 1500) & (tree["wish"] == "да")]
Out[4]:
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

Все сосны, либо сосны Крым, либо датские сосны:

In [5]:
tree[(tree["ftype"] == "сосна Крым" ) | (tree["ftype"] == "сосна датская")]
Out[5]:
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 – в противном случае.

In [6]:
tree[tree["ftype"].str.contains("сосна")]
Out[6]:
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:

In [7]:
tree[tree["ftype"].str.contains("сосна") == False]
Out[7]:
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

А можно воспользоваться оператором ~ для отрицания и поставить его перед всем условием в скобках:

In [8]:
tree[~tree["ftype"].str.contains("сосна")]
Out[8]:
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 по пробелу:

In [9]:
tree["ftype"].str.split()
Out[9]:
0          [пихта, Нобилис]
1          [пихта, Нобилис]
2             [сосна, Крым]
3             [сосна, Крым]
4             [сосна, Крым]
               ...         
1195    [ель, обыкновенная]
1196       [пихта, Нобилис]
1197          [сосна, Крым]
1198       [сосна, датская]
1199       [сосна, датская]
Name: ftype, Length: 1200, dtype: object

Чтобы создать новые столбцы вместо списков внутри одного, можно добавить аргумент expand=True.

In [10]:
tree["ftype"].str.split(expand= True)
Out[10]:
0 1
0 пихта Нобилис
1 пихта Нобилис
2 сосна Крым
3 сосна Крым
4 сосна Крым
... ... ...
1195 ель обыкновенная
1196 пихта Нобилис
1197 сосна Крым
1198 сосна датская
1199 сосна датская

1200 rows × 2 columns

Код выше вернул нам новый датафрейм. Мы можем сохранить его в переменную tree2, а потом объединить tree и tree2 с помощью функции concat():

In [11]:
tree2 = tree["ftype"].str.split(expand= True)
pd.concat([tree, tree2], axis = 1)
Out[11]:
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 снизу.

Выбор строк и столбцов по названию и по номеру

Отдельный столбец по названию мы уже выбрали:

In [12]:
tree["wish"]
Out[12]:
0        да
1       нет
2        да
3        да
4       нет
       ... 
1195    нет
1196     да
1197    нет
1198     да
1199    нет
Name: wish, Length: 1200, dtype: object

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

In [13]:
tree[["ftype", "score"]]
Out[13]:
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:

In [14]:
tree.loc[:, "ftype":"score"] 
Out[14]:
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:

In [15]:
tree.loc[0:12, "ftype":"score"]
Out[15]:
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 и столбцы со второго по третий:

In [16]:
tree.iloc[0:12, 2:4]
Out[16]:
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 вписать только одно число, по умолчанию будет выдана строка с таким номером:

In [17]:
tree.iloc[2]
Out[17]:
Unnamed: 0             3
gender            female
ftype         сосна Крым
height               248
score                  4
expenses             655
wish                  да
Name: 2, dtype: object

Это будет объект типа pandas Series:

In [18]:
type(tree.iloc[2]) 
Out[18]:
pandas.core.series.Series

Переименование столбцов

Посмотрим на список названий всех столбцов (точнее, это будет объект специального типа Index, который внутри очень похож на массив):

In [19]:
tree.columns
Out[19]:
Index(['Unnamed: 0', 'gender', 'ftype', 'height', 'score', 'expenses', 'wish'], dtype='object')

Аналогичным образом посмотрим на названия строк:

In [20]:
tree.index
Out[20]:
RangeIndex(start=0, stop=1200, step=1)

Строки не имеют специально заданных текстовых названий, поэтому они автоматически названы по промежутку из целых чисел RangeIndex от 0 до 1200.

Теперь попробуем переименовать столбец Unnamed: 0 и дать ему более симпатичное название. Так как это по сути номер респондента, назовем его id. Воспользуемся методом .rename():

In [21]:
tree.rename(columns = {"Unnamed: 0": "id"})  
Out[21]:
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:

In [22]:
tree.head() 
Out[22]:
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 (записать изменения «на место» старых данных):

In [23]:
# теперь все ок
tree.rename(columns = {"Unnamed: 0": "id"}, inplace = True)  
tree.head()
Out[23]:
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:

In [24]:
tree["height"] / 100
Out[24]:
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:

In [25]:
tree["height_m"] = tree["height"] / 100
In [26]:
tree.head() 
Out[26]:
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 возвращать текстовый комментарий:

In [27]:
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:

In [28]:
tree["comment"] = tree["score"].apply(trans_comm)
tree.head()
Out[28]:
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.

Удалим строки с пропущенными значениями из датафрейма совсем:

In [29]:
# inplace = True – сохраняем значения
tree.dropna(inplace=True)