Процесс подготовки данных для анализа включает сборку данных в 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 |
В этом случае один и тот же участок попадается даже чаще.