Трансформация данных в pandas ч.1 / pd 11

526

Процесс подготовки данных для анализа включает сборку данных в Dataframe с возможными добавлениями из других объектов и удалением ненужных частей. Следующий этап — трансформация. После того как данные внутри структуры организованы, нужно преобразовать ее значения. Этот раздел будет посвящен распространенным проблемам и процессам, которые требуются для их решения с помощью функций библиотеки pandas.

Среди них удаление элементов с повторяющимися значениями, изменение индексов, обработка числовых значений данных и строк.

Удаление повторов

Дубликаты строк могут присутствовать в Dataframe по разным причинам. И в объектах особо крупного размера их может быть сложно обнаружить. Для этого в pandas есть инструменты анализа повторяющихся данных для крупных структур.

Для начала создадим простой Dataframe с повторяющимися строками.

>>> dframe = pd.DataFrame({'color': ['white','white','red','red','white'],
... 			   'value': [2,1,3,3,2]})
>>> dframe

|   | color | value |
|---|-------|-------|
| 0 | white | 2     |
| 1 | white | 1     |
| 2 | red   | 3     |
| 3 | red   | 3     |
| 4 | white | 2     |

Функция duplicated() способна обнаружить дубликаты. Она вернет объект Series, состоящий из булевых значений, где каждый элемент соответствует строке. Их значения равны True, если строка является дубликатом (все повторения за исключением первого) и False, если повторов этого элемента не было.

>>> dframe.duplicated()
0    False
1    False
2    False
3     True
4     True
dtype: bool

Объект с булевыми элементами может быть особенно полезен, например, для фильтрации. Так, чтобы увидеть строки-дубликаты, нужно просто написать следующее:

>>> dframe[dframe.duplicated()]

|   | color | value |
|---|-------|-------|
| 3 | red   | 3     |
| 4 | white | 2     |

Обычно повторяющиеся строки удаляются. Для этого в pandas есть функция drop_duplicates(), которая возвращает Dataframe без дубликатов.

>>> dframe = dframe.drop_duplicates()
>>> dframe

|   | color | value |
|---|-------|-------|
| 0 | white | 2     |
| 2 | red   | 3     |

Маппинг

Библиотека pandas предоставляет набор функций, использующих маппинг для выполнения определенных операций. Маппинг — это всего лишь создание списка совпадений двух разных значений, что позволяет привязывать значение определенной метке или строке.

Для определения маппинга лучше всего подходит объект dict:

map = {
 'label1' : 'value1,
 'label2' : 'value2,
 ...
}

Функции, которые будут дальше встречаться в этом разделе, выполняют конкретные операции, но всегда принимают объект dict.

replace() — Заменяет значения
map() — Создает новый столбец
rename() — Заменяет значения индекса

Замена значений с помощью маппинга

Часто бывает так, что в готовой структуре данных присутствуют значения, не соответствующие конкретным требованиям. Например, текст может быть написан на другом языке, являться синонимом или, например, выраженным в другом виде. В таких случаях используется операция замены разных значений.

Для примера определим Dataframe с разными объектами и их цветами, включая два названия цветов не на английском.

>>> frame = pd.DataFrame({'item':['ball','mug','pen','pencil','ashtray'],
... 			  'color':['white','rosso','verde','black','yellow'],
...			  'price':[5.56,4.20,1.30,0.56,2.75]})
>>> frame

|   | color  | item    | price |
|---|--------|---------|-------|
| 0 | white  | ball    | 5.56  |
| 1 | rosso  | mug     | 4.20  |
| 2 | verde  | pen     | 1.30  |
| 3 | black  | pencil  | 0.56  |
| 4 | yellow | ashtray | 2.75  |

Для замены некорректных значений новыми нужно определить маппинг соответствий, где ключами будут выступать новые значения.

>>> newcolors = {
... 	'rosso': 'red',
... 	'verde': 'green'
... }

Теперь осталось использовать функцию replace(), задав маппинг в качестве аргумента.

>>> frame.replace(newcolors)

|   | color  | item    | price |
|---|--------|---------|-------|
| 0 | white  | ball    | 5.56  |
| 1 | red    | mug     | 4.20  |
| 2 | green  | pen     | 1.30  |
| 3 | black  | pencil  | 0.56  |
| 4 | yellow | ashtray | 2.75  |

Как видно выше, два цвета были заменены на корректные значения в Dataframe. Распространенный пример — замена значений NaN на другие, например, на 0. Функция replace() отлично справляется и с этим.

>>> ser = pd.Series([1,3,np.nan,4,6,np.nan,3])
>>> ser
0    1.0
1    3.0
2    NaN
3    4.0
4    6.0
5    NaN
6    3.0
dtype: float64

>>> ser.replace(np.nan,0)
0    1.0
1    3.0
2    0.0
3    4.0
4    6.0
5    0.0
6    3.0
dtype: float64

Добавление значений с помощью маппинга

В предыдущем примере вы узнали, как менять значения с помощью маппинга. Теперь попробуем использовать маппинг на другом примере — для добавления новых значений в колонку на основе значений в другой. Маппинг всегда определяется отдельно.

>>> frame = pd.DataFrame({'item':['ball','mug','pen','pencil','ashtray'],
... 			  'color':['white','red','green','black','yellow']})
>>> frame

|   | color  | item    |
|---|--------|---------|
| 0 | white  | ball    |
| 1 | red    | mug     |
| 2 | green  | pen     |
| 3 | black  | pencil  |
| 4 | yellow | ashtray |

Предположим, что нужно добавить колонку с ценой вещи из объекта. Также предположим, что имеется список цен. Определим его в виде объекта dict с ценами для каждого типа объекта.

>>> prices = {
... 	'ball' : 5.56,
... 	'mug' : 4.20,
... 	'bottle' : 1.30,
... 	'scissors' : 3.41,
... 	'pen' : 1.30,
... 	'pencil' : 0.56,
... 	'ashtray' : 2.75
... }

Функция map(), примененная к Series или колонке объекта Dataframe принимает функцию или объект с маппингом dict. В этому случае можно применить маппинг цен для элементов колонки, добавив еще одну колонку price в Dataframe.

>>> frame['price'] = frame['item'].map(prices)
>>> frame

|   | color  | item    | price |
|---|--------|---------|-------|
| 0 | white  | ball    | 5.56  |
| 1 | red    | mug     | 4.20  |
| 2 | green  | pen     | 1.30  |
| 3 | black  | pencil  | 0.56  |
| 4 | yellow | ashtray | 2.75  |

Переименование индексов осей

По примеру того, как работает изменение значений в Series и Dataframe, можно трансформировать метки оси с помощью маппинга. Для замены меток индексов в pandas есть функция rename(), которая принимает маппинг (объект dict) в качестве аргумента.

>>> frame

|   | color  | item    | price |
|---|--------|---------|-------|
| 0 | white  | ball    | 5.56  |
| 1 | red    | mug     | 4.20  |
| 2 | green  | pen     | 1.30  |
| 3 | black  | pencil  | 0.56  |
| 4 | yellow | ashtray | 2.75  |

>>> reindex = {
... 	0: 'first',
... 	1: 'second',
... 	2: 'third',
... 	3: 'fourth',
... 	4: 'fifth'}
>>> frame.rename(reindex)

|        | color  | item    | price |
|--------|--------|---------|-------|
| first  | white  | ball    | 5.56  |
| second | red    | mug     | 4.20  |
| third  | green  | pen     | 1.30  |
| fourth | black  | pencil  | 0.56  |
| fifth  | yellow | ashtray | 2.75  |

По умолчанию переименовываются индексы. Если же нужно поменять названия колонок, то используется параметр columns. В следующем примере присвоим несколько маппингов двум индексам с параметром columns.

>>> recolumn = {
... 	'item':'object',
... 	'price': 'value'}
>>> frame.rename(index=reindex, columns=recolumn)

|        | color  | object  | price |
|--------|--------|---------|-------|
| first  | white  | ball    | 5.56  |
| second | red    | mug     | 4.20  |
| third  | green  | pen     | 1.30  |
| fourth | black  | pencil  | 0.56  |
| fifth  | yellow | ashtray | 2.75  |

В тех случаях когда заменить нужно только одно значение, все можно и не писать.

>>> frame.rename(index={1:'first'}, columns={'item':'object'})

|       | color  | object  | price |
|-------|--------|---------|-------|
| 0     | white  | ball    | 5.56  |
| first | red    | mug     | 4.20  |
| 2     | green  | pen     | 1.30  |
| 3     | black  | pencil  | 0.56  |
| 4     | yellow | ashtray | 2.75  |

Пока что функция rename() возвращала объект Dataframe с изменениями, не трогая оригинальный объект. Но если нужно поменять его, то необходимо передать значение True параметру inplace.

>>> frame.rename(columns={'item':'object'}, inplace=True)
>>> frame

|   | color  | object  | price |
|---|--------|---------|-------|
| 0 | white  | ball    | 5.56  |
| 1 | red    | mug     | 4.20  |
| 2 | green  | pen     | 1.30  |
| 3 | black  | pencil  | 0.56  |
| 4 | yellow | ashtray | 2.75  |

Дискретизация и биннинг

Более сложный процесс преобразования называется дискретизацией. Он используется для обработки большим объемов данных. Для анализа их необходимо разделять на дискретные категории, например, распределив диапазон значений на меньшие интервалы и посчитав статистику для каждого. Еще один пример — большое количество образцов. Даже здесь необходимо разделять весь диапазон по категориям и внутри них считать вхождения и статистику.

В следующем случае, например, нужно работать с экспериментальными значениями, лежащими в диапазоне от 0 до 100. Эти данные собраны в список.

>>> results = [12,34,67,55,28,90,99,12,3,56,74,44,87,23,49,89,87]

Вы знаете, что все значения лежат в диапазоне от 0 до 100, а это значит, что их можно разделить на 4 одинаковых части, бины. В первом будут элементы от 0 до 25, во втором — от 26 до 50, в третьем — от 51 до 75, а в последнем — от 75 до 100.

Для этого в pandas сначала нужно определить массив со значениями разделения:

>>> bins = [0,25,50,75,100]

Затем используется специальная функция cut(), которая применяется к массиву. В нее нужно добавить и бины.

>>> cat = pd.cut(results, bins)
>>> cat
 (0, 25]
 (25, 50]
 (50, 75]
 (50, 75]
 (25, 50]
 (75, 100]
 (75, 100]
 (0, 25]
 (0, 25]
 (50, 75]
 (50, 75]
 (25, 50]
 (75, 100]
 (0, 25]
 (25, 50]
 (75, 100]
 (75, 100]
Levels (4): Index(['(0, 25]', '(25, 50]', '(50, 75]', '(75, 100]'],
dtype=object)

Функция cut() возвращает специальный объект типа Categorical. Его можно считать массивом строк с названиями бинов. Внутри каждая содержит массив categories, включающий названия разных внутренних категорий и массив codes со списком чисел, равных элементам results. Число соответствует бину, которому был присвоен соответствующий элемент results.

>>> cat.categories
IntervalIndex([0, 25], (25, 50], (50, 75], (75, 100]]
	      closed='right'
	      dtype='interval[int64]')
>>> cat.codes
array([0, 1, 2, 2, 1, 3, 3, 0, 0, 2, 2, 1, 3, 0, 1, 3, 3], dtype=int8)

Чтобы узнать число вхождений каждого бина, то есть, результаты для всех категорий, нужно использовать функцию value_counts().

>>> pd.value_counts(cat)
(75, 100]    5
(50, 75]     4
(25, 50]     4
(0, 25]      4
dtype: int64

У каждого класса есть нижний предел с круглой скобкой и верхний — с квадратной. Такая запись соответствует математической, используемой для записи интервалов. Если скобка квадратная, то число лежит в диапазоне, а если круглая — то нет.

Бинам можно задавать имена, передав их в массив строк, а затем присвоив его параметру labels в функции cut(), которая используется для создания объекта Categorical.

>>> bin_names = ['unlikely','less likely','likely','highly likely']
>>> pd.cut(results, bins, labels=bin_names)
 unlikely
 less likely
 likely
 likely
 less likely
 highly likely
 highly likely
 unlikely
 unlikely
 likely
 likely
 less likely
 highly likely
 unlikely
 less likely
 highly likely
 highly likely
Levels (4): Index(['unlikely', 'less likely', 'likely', 'highly likely'],
dtype=object)

Если функции cut() передать в качестве аргумента целое число, а не границы бина, то диапазон значений будет разделен на указанное количество интервалов.

Пределы будут основаны на минимуме и максимуме данных.

>>> pd.cut(results, 5)
 (2.904, 22.2]
 (22.2, 41.4]
 (60.6, 79.8]
 (41.4, 60.6]
 (22.2, 41.4]
 (79.8, 99]
 (79.8, 99]
 (2.904, 22.2]
 (2.904, 22.2]
 (41.4, 60.6]
 (60.6, 79.8]
 (41.4, 60.6]
 (79.8, 99]
 (22.2, 41.4]
 (41.4, 60.6]
 (79.8, 99]
 (79.8, 99]
Levels (5): Index(['(2.904, 22.2]', '(22.2, 41.4]', '(41.4, 60.6]',
 '(60.6, 79.8]', '(79.8, 99]'], dtype=object)

Также в pandas есть еще одна функция для биннинга, qcut(). Она делит весь набор на квантили. Так, в зависимости от имеющихся данных cut() обеспечит разное количество данных для каждого бина. А qcut() позаботится о том, чтобы количество вхождений было одинаковым. Могут отличаться только границы.

>>> quintiles = pd.qcut(results, 5)
>>> quintiles
 [3, 24]
 (24, 46]
 (62.6, 87]
 (46, 62.6]
 (24, 46]
 (87, 99]
 (87, 99]
 [3, 24]
 [3, 24]
 (46, 62.6]
 (62.6, 87]
 (24, 46]
 (62.6, 87]
 [3, 24]
 (46, 62.6]
 (87, 99]
 (62.6, 87]
Levels (5): Index(['[3, 24]', '(24, 46]', '(46, 62.6]', '(62.6, 87]',
 '(87, 99]'], dtype=object)

>>> pd.value_counts(quintiles)
[3, 24] 4
(62.6, 87] 4
(87, 99] 3
(46, 62.6] 3
(24, 46] 3
dtype: int64

В этом примере видно, что интервалы отличаются от тех, что получились в результате использования функции cut(). Также можно обратить внимание на то, что qcut() попыталась стандартизировать вхождения для каждого бина, поэтому в первых двух больше вхождений. Это связано с тем, что количество объектов не делится на 5.

Определение и фильтрация лишних данных

При анализе данных часто приходится находить аномальные значения в структуре данных. Для примера создайте Dataframe с тремя колонками целиком случайных чисел.

>>> randframe = pd.DataFrame(np.random.randn(1000,3))

С помощью функции describe() можно увидеть статистику для каждой колонки.

>>> randframe.describe()

|       | 0           | 1           | 2           |
|-------|-------------|-------------|-------------|
| count | 1000.000000 | 1000.000000 | 1000.000000 |
| mean  | -0.036163   | 0.037203    | 0.018722    |
| std   | 1.038703    | 0.986338    | 1.011587    |
| min   | -3.591217   | -3.816239   | -3.586733   |
| 25%   | -0.729458   | -0.581396   | -0.665261   |
| 50%   | -0.025864   | 0.005155    | -0.002774   |
| 75%   | 0.674396    | 0.706958    | 0.731404    |
| max   | 3.115554    | 2.899073    | 3.425400    |

Лишними можно считать значения, которые более чем в три раза больше стандартного отклонения. Чтобы оставить только подходящие, нужно использовать функцию std().

>>> randframe.std()
0    1.038703
1    0.986338
2    1.011587
dtype: float64

Теперь используйте фильтр для всех значений Dataframe, применив соответствующее стандартное отклонение для каждой колонки. Функция any() позволит использовать фильтр для каждой колонки.

>>> randframe[(np.abs(randframe) > (3*randframe.std())).any(1)]

|     | 0         | 1         | 2         |
|-----|-----------|-----------|-----------|
| 87  | -2.106846 | -3.408329 | -0.067435 |
| 129 | -3.591217 | 0.791474  | 0.243038  |
| 133 | 1.149396  | -3.816239 | 0.328653  |
| 717 | 0.434665  | -1.248411 | -3.586733 |
| 726 | 1.682330  | 1.252479  | -3.090042 |
| 955 | 0.272374  | 2.224856  | 3.425400  |

Перестановка

Операции перестановки (случайного изменения порядка) в объекте Series или строках Dataframe можно выполнить с помощью функции numpy.random.permutation().

Для этого примера создайте Dataframe с числами в порядке возрастания.

>>> nframe = pd.DataFrame(np.arange(25).reshape(5,5))
>>> nframe

|   | 0  | 1  | 2  | 3  | 4  |
|---|----|----|----|----|----|
| 0 | 0  | 1  | 2  | 3  | 4  |
| 1 | 5  | 6  | 7  | 8  | 9  |
| 2 | 10 | 11 | 12 | 13 | 14 |
| 3 | 15 | 16 | 17 | 18 | 19 |
| 4 | 20 | 21 | 22 | 23 | 24 |

Теперь создайте массив из пяти чисел от 0 до 4 в случайном порядке с функцией permutation(). Этот массив будет новым порядком, в котором потребуется разместить и значения строк из Dataframe.

>>> new_order = np.random.permutation(5)
>>> new_order
array([2, 3, 0, 1, 4])

Теперь примените его ко всем строкам Dataframe с помощью функции take().

>>> nframe.take(new_order)

|   | 0  | 1  | 2  | 3  | 4  |
|---|----|----|----|----|----|
| 2 | 10 | 11 | 12 | 13 | 14 |
| 3 | 15 | 16 | 17 | 18 | 19 |
| 0 | 0  | 1  | 2  | 3  | 4  |
| 4 | 20 | 21 | 22 | 23 | 24 |
| 1 | 5  | 6  | 7  | 8  | 9  |

Как видите, порядок строк поменялся, а индексы соответствуют порядку в массиве new_order.

Перестановку можно произвести и для отдельной части Dataframe. Это сгенерирует массив с последовательностью, ограниченной конкретным диапазоном, например, от 2 до 4.

>>> new_order = [3,4,2]
>>> nframe.take(new_order)

|   | 0  | 1  | 2  | 3  | 4  |
|---|----|----|----|----|----|
| 3 | 15 | 16 | 17 | 18 | 19 |
| 4 | 20 | 21 | 22 | 23 | 24 |
| 2 | 10 | 11 | 12 | 13 | 14 |

Случайная выборка

Вы уже знаете, как доставать отдельные части Dataframe для последующей перестановки. Но иногда ее потребуется отобрать случайным образом. Проще всего сделать это с помощью функции np.random.randint().

>>> sample = np.random.randint(0, len(nframe), size=3)
>>> sample
array([1, 4, 4])
>>> nframe.take(sample)

|   | 0  | 1  | 2  | 3  | 4  |
|---|----|----|----|----|----|
| 3 | 15 | 16 | 17 | 18 | 19 |
| 3 | 15 | 16 | 17 | 18 | 19 |
| 3 | 15 | 16 | 17 | 18 | 19 |

В этом случае один и тот же участок попадается даже чаще.

Тест на знание python

Какой ввод НЕ приведет к ошибке?
Какой будет результат выполнения этого кода?
Что делает функция re.match()?
Какой будет результат выполнения кода — print(abc) ?
Как узнать длину списка?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ. Пишу на популярные темы: веб-разработка, работа с базами данных, data sciense и другие...