Python Pandas: хитрости и особенности, которые вы можете не знать
Pandas - это основополагающая библиотека для аналитики, обработки данных и науки о данных. Это огромный проект с огромным количеством возможностей и глубины.
В этом руководстве будут рассмотрены некоторые менее используемые, но идиоматические возможности Pandas, которые улучшают читаемость, универсальность и скорость вашего кода,à la в списке Buzzfeed.
Если вы чувствуете себя комфортно с основными понятиями библиотеки Panda Python, надеюсь, вы найдете один или два трюка в этой статье, с которыми вы не сталкивались ранее. (Если вы только начинаете работу с библиотекой,10 Minutes to Pandas - хорошее место для начала.)
Note: Примеры в этой статье протестированы с Pandas версии 0.23.2 и Python 3.6.6. Тем не менее, они также должны быть действительными в более старых версиях.
1. Настройка параметров и настроек при запуске интерпретатора
Возможно, вы уже сталкивались с системойoptions and settings от Pandas раньше.
Это значительно экономит производительность, настраивая индивидуальные параметры Pandas при запуске интерпретатора, особенно если вы работаете в среде сценариев. Вы можете использоватьpd.set_option()
для настройки, которая вам нравится, с помощью файла запускаPython илиIPython.
В параметрах используется точечная нотация, напримерpd.set_option('display.max_colwidth', 25)
, которая хорошо подходит для вложенного словаря параметров:
import pandas as pd
def start():
options = {
'display': {
'max_columns': None,
'max_colwidth': 25,
'expand_frame_repr': False, # Don't wrap to multiple pages
'max_rows': 14,
'max_seq_items': 50, # Max length of printed sequence
'precision': 4,
'show_dimensions': False
},
'mode': {
'chained_assignment': None # Controls SettingWithCopyWarning
}
}
for category, option in options.items():
for op, value in option.items():
pd.set_option(f'{category}.{op}', value) # Python 3.6+
if __name__ == '__main__':
start()
del start # Clean up namespace in the interpreter
Если вы запустите сеанс интерпретатора, вы увидите, что все в скрипте запуска были выполнены, и Pandas автоматически импортируется для вас с набором опций:
>>>
>>> pd.__name__
'pandas'
>>> pd.get_option('display.max_rows')
14
Давайте воспользуемся некоторыми данными оabalone, размещенными в репозитории машинного обучения UCI, чтобы продемонстрировать форматирование, установленное в файле запуска. Данные будут усечены до 14 строк с точностью до 4 цифр для чисел с плавающей запятой:
>>>
>>> url = ('https://archive.ics.uci.edu/ml/'
... 'machine-learning-databases/abalone/abalone.data')
>>> cols = ['sex', 'length', 'diam', 'height', 'weight', 'rings']
>>> abalone = pd.read_csv(url, usecols=[0, 1, 2, 3, 4, 8], names=cols)
>>> abalone
sex length diam height weight rings
0 M 0.455 0.365 0.095 0.5140 15
1 M 0.350 0.265 0.090 0.2255 7
2 F 0.530 0.420 0.135 0.6770 9
3 M 0.440 0.365 0.125 0.5160 10
4 I 0.330 0.255 0.080 0.2050 7
5 I 0.425 0.300 0.095 0.3515 8
6 F 0.530 0.415 0.150 0.7775 20
# ...
4170 M 0.550 0.430 0.130 0.8395 10
4171 M 0.560 0.430 0.155 0.8675 8
4172 F 0.565 0.450 0.165 0.8870 11
4173 M 0.590 0.440 0.135 0.9660 10
4174 M 0.600 0.475 0.205 1.1760 9
4175 F 0.625 0.485 0.150 1.0945 10
4176 M 0.710 0.555 0.195 1.9485 12
Вы увидите, что этот набор данных всплывет и в других примерах позже.
2. Создание игрушечных структур данных с помощью модуля тестирования Pandas
В модулеtesting
Pandas скрыт ряд удобных функций для быстрого создания квазиреалистичных серий и фреймов данных:
>>>
>>> import pandas.util.testing as tm
>>> tm.N, tm.K = 15, 3 # Module-level default rows/columns
>>> import numpy as np
>>> np.random.seed(444)
>>> tm.makeTimeDataFrame(freq='M').head()
A B C
2000-01-31 0.3574 -0.8804 0.2669
2000-02-29 0.3775 0.1526 -0.4803
2000-03-31 1.3823 0.2503 0.3008
2000-04-30 1.1755 0.0785 -0.1791
2000-05-31 -0.9393 -0.9039 1.1837
>>> tm.makeDataFrame().head()
A B C
nTLGGTiRHF -0.6228 0.6459 0.1251
WPBRn9jtsR -0.3187 -0.8091 1.1501
7B3wWfvuDA -1.9872 -1.0795 0.2987
yJ0BTjehH1 0.8802 0.7403 -1.2154
0luaYUYvy1 -0.9320 1.2912 -0.2907
Их около 30, и вы можете увидеть полный список, вызвавdir()
для объекта модуля. Вот несколько из них:
>>>
>>> [i for i in dir(tm) if i.startswith('make')]
['makeBoolIndex',
'makeCategoricalIndex',
'makeCustomDataframe',
'makeCustomIndex',
# ...,
'makeTimeSeries',
'makeTimedeltaIndex',
'makeUIntIndex',
'makeUnicodeIndex']
Они могут быть полезны для бенчмаркинга, тестирования утверждений и экспериментов с методами Pandas, с которыми вы менее знакомы.
3. Воспользуйтесь преимуществами методов доступа
Возможно, вы слышали терминaccessor, который чем-то похож на геттер (хотя геттеры и сеттеры в Python используются нечасто). Для наших целей вы можете рассматривать аксессор Pandas как свойство, которое служит интерфейсом для дополнительных методов.
У Панд есть три из них:
>>>
>>> pd.Series._accessors
{'cat', 'str', 'dt'}
Да, приведенное выше определение является полным, поэтому давайте рассмотрим несколько примеров, прежде чем обсуждать внутреннее устройство.
.cat
- для категориальных данных,.str
- для строковых (объектных) данных, а.dt
- для данных типа datetime. Начнем с.str
: представьте, что у вас есть необработанные данные о городе / штате / ZIP в виде одного поля в серии Pandas.
Строковые методы Pandas -vectorized, что означает, что они работают со всем массивом без явного цикла for:
>>>
>>> addr = pd.Series([
... 'Washington, D.C. 20003',
... 'Brooklyn, NY 11211-1755',
... 'Omaha, NE 68154',
... 'Pittsburgh, PA 15211'
... ])
>>> addr.str.upper()
0 WASHINGTON, D.C. 20003
1 BROOKLYN, NY 11211-1755
2 OMAHA, NE 68154
3 PITTSBURGH, PA 15211
dtype: object
>>> addr.str.count(r'\d') # 5 or 9-digit zip?
0 5
1 9
2 5
3 5
dtype: int64
Для более наглядного примера предположим, что вы хотите аккуратно разделить три компонента city / state / ZIP на поля DataFrame.
Вы можете передатьregular expression в.str.extract()
, чтобы «извлечь» части каждой ячейки в Серии. В.str.extract()
.str
- это метод доступа, а.str.extract()
- метод доступа:
>>>
>>> regex = (r'(?P[A-Za-z ]+), ' # One or more letters
... r'(?P[A-Z]{2}) ' # 2 capital letters
... r'(?P\d{5}(?:-\d{4})?)') # Optional 4-digit extension
...
>>> addr.str.replace('.', '').str.extract(regex)
city state zip
0 Washington DC 20003
1 Brooklyn NY 11211-1755
2 Omaha NE 68154
3 Pittsburgh PA 15211
Это также иллюстрирует то, что известно как цепочка методов, где.str.extract(regex)
вызывается для результатаaddr.str.replace('.', '')
, что устраняет необходимость в использовании точек, чтобы получить красивое двухсимвольное сокращение состояния.
Полезно узнать немного о том, как работают эти методы доступа, как мотивирующую причину, по которой вы должны использовать их в первую очередь, а не что-то вродеaddr.apply(re.findall, ...)
.
Каждый метод доступа сам по себе является истинным классом Python:
-
.str
отображается вStringMethods
. -
.dt
отображается вCombinedDatetimelikeProperties
. -
.cat
направляется кCategoricalAccessor
.
Эти автономные классы затем «прикрепляются» к классу Series с помощьюCachedAccessor
. Когда классы оборачиваются вCachedAccessor
, происходит немного волшебства.
CachedAccessor
основан на дизайне «кэшированных свойств»: свойство вычисляется только один раз для каждого экземпляра, а затем заменяется обычным атрибутом. Это достигается путем перегрузки методаhttps://docs.python.org/reference/datamodel.html#object.get [.__get__()
], который является частью протокола дескриптора Python.
Note: если вы хотите узнать больше о том, как это работает, посмотритеPython Descriptor HOWTO иthis post в дизайне кэшированных свойств. Python 3 также представилfunctools.lru_cache()
, который предлагает аналогичные функции. Примеры этого шаблона есть повсюду, например, в пакетеaiohttp
.
Второй метод доступа,.dt
, предназначен для данных типа datetime. Технически он принадлежит Pandas ’DatetimeIndex
, и при вызове в серии сначала преобразуется вDatetimeIndex
:
>>>
>>> daterng = pd.Series(pd.date_range('2017', periods=9, freq='Q'))
>>> daterng
0 2017-03-31
1 2017-06-30
2 2017-09-30
3 2017-12-31
4 2018-03-31
5 2018-06-30
6 2018-09-30
7 2018-12-31
8 2019-03-31
dtype: datetime64[ns]
>>> daterng.dt.day_name()
0 Friday
1 Friday
2 Saturday
3 Sunday
4 Saturday
5 Saturday
6 Sunday
7 Monday
8 Sunday
dtype: object
>>> # Second-half of year only
>>> daterng[daterng.dt.quarter > 2]
2 2017-09-30
3 2017-12-31
6 2018-09-30
7 2018-12-31
dtype: datetime64[ns]
>>> daterng[daterng.dt.is_year_end]
3 2017-12-31
7 2018-12-31
dtype: datetime64[ns]
Третий метод доступа,.cat
, предназначен только для категориальных данных, который вы вскоре увидите в егоown section.
4. Создать DatetimeIndex из столбцов компонента
Говоря о данных, похожих на datetime, как вdaterng
выше, можно создать PandasDatetimeIndex
из нескольких столбцов компонентов, которые вместе образуют дату или datetime:
>>>
>>> from itertools import product
>>> datecols = ['year', 'month', 'day']
>>> df = pd.DataFrame(list(product([2017, 2016], [1, 2], [1, 2, 3])),
... columns=datecols)
>>> df['data'] = np.random.randn(len(df))
>>> df
year month day data
0 2017 1 1 -0.0767
1 2017 1 2 -1.2798
2 2017 1 3 0.4032
3 2017 2 1 1.2377
4 2017 2 2 -0.2060
5 2017 2 3 0.6187
6 2016 1 1 2.3786
7 2016 1 2 -0.4730
8 2016 1 3 -2.1505
9 2016 2 1 -0.6340
10 2016 2 2 0.7964
11 2016 2 3 0.0005
>>> df.index = pd.to_datetime(df[datecols])
>>> df.head()
year month day data
2017-01-01 2017 1 1 -0.0767
2017-01-02 2017 1 2 -1.2798
2017-01-03 2017 1 3 0.4032
2017-02-01 2017 2 1 1.2377
2017-02-02 2017 2 2 -0.2060
Наконец, вы можете удалить старые отдельные столбцы и преобразовать их в серию:
>>>
>>> df = df.drop(datecols, axis=1).squeeze()
>>> df.head()
2017-01-01 -0.0767
2017-01-02 -1.2798
2017-01-03 0.4032
2017-02-01 1.2377
2017-02-02 -0.2060
Name: data, dtype: float64
>>> df.index.dtype_str
'datetime64[ns]
Интуиция, лежащая в основе передачи DataFrame, состоит в том, что DataFrame напоминает словарь Python, где имена столбцов являются ключами, а отдельные столбцы (Series) являются значениями словаря. Вот почемуpd.to_datetime(df[datecols].to_dict(orient='list'))
также будет работать в этом случае. Это отражает конструкцию Pythondatetime.datetime
, где вы передаете аргументы ключевого слова, такие какdatetime.datetime(year=2000, month=1, day=15, hour=10)
.
5. Используйте категорические данные для экономии времени и пространства
Одной из мощных функций Pandas является его dtypeCategorical
.
Даже если вы не всегда работаете с гигабайтами данных в оперативной памяти, вы, вероятно, сталкивались со случаями, когда простые операции с большим DataFrame, похоже, зависали более чем на несколько секунд.
Pandasobject
dtype часто является отличным кандидатом для преобразования в данные категории. (object
- это контейнер для Pythonstr
, гетерогенных типов данных или «других» типов.) Строки занимают значительный объем места в памяти:
>>>
>>> colors = pd.Series([
... 'periwinkle',
... 'mint green',
... 'burnt orange',
... 'periwinkle',
... 'burnt orange',
... 'rose',
... 'rose',
... 'mint green',
... 'rose',
... 'navy'
... ])
...
>>> import sys
>>> colors.apply(sys.getsizeof)
0 59
1 59
2 61
3 59
4 61
5 53
6 53
7 59
8 53
9 53
dtype: int64
Note: Я использовалsys.getsizeof()
, чтобы показать память, занятую каждым отдельным значением в серии. Имейте в виду, что это Python-объекты, которые имеют некоторые накладные расходы. (sys.getsizeof('')
вернет 49 байтов.)
Существует такжеcolors.memory_usage()
, который суммирует использование памяти и полагается на атрибут.nbytes
базового массива NumPy. Не стоит слишком зацикливаться на этих деталях: важно то, как будет выглядеть относительное использование памяти в результате преобразования типов, как вы увидите далее.
Теперь, что если бы мы могли взять вышеупомянутые уникальные цвета и отобразить каждый из них на меньшее целое число? Вот наивная реализация этого:
>>>
>>> mapper = {v: k for k, v in enumerate(colors.unique())}
>>> mapper
{'periwinkle': 0, 'mint green': 1, 'burnt orange': 2, 'rose': 3, 'navy': 4}
>>> as_int = colors.map(mapper)
>>> as_int
0 0
1 1
2 2
3 0
4 2
5 3
6 3
7 1
8 3
9 4
dtype: int64
>>> as_int.apply(sys.getsizeof)
0 24
1 28
2 28
3 24
4 28
5 28
6 28
7 28
8 28
9 28
dtype: int64
Note: Другой способ сделать то же самое - использоватьpd.factorize(colors)
Pandas:
>>>
>>> pd.factorize(colors)[0]
array([0, 1, 2, 0, 2, 3, 3, 1, 3, 4])
В любом случае, вы кодируете объект как перечислимый тип (категориальная переменная).
Вы сразу заметите, что использование памяти почти вдвое меньше, чем при использовании полных строк с dtypeobject
.
Ранее в разделеaccessors я упоминал о (категориальном) аксессоре.cat
. Вышеупомянутое сmapper
является приблизительной иллюстрацией того, что происходит внутри Pandas с типомCategorical
dtype:
«Использование памяти
Categorical
пропорционально количеству категорий плюс длина данных. Напротив,object
dtype - это константа, умноженная на длину данных ». (Source)
Вcolors
выше у вас есть соотношение 2 значений для каждого уникального значения (категории):
>>>
>>> len(colors) / colors.nunique()
2.0
В результате экономия памяти от преобразования вCategorical
хорошая, но не большая:
>>>
>>> # Not a huge space-saver to encode as Categorical
>>> colors.memory_usage(index=False, deep=True)
650
>>> colors.astype('category').memory_usage(index=False, deep=True)
495
Однако, если вы вычеркните приведенную выше пропорцию с большим количеством данных и несколькими уникальными значениями (подумайте о данных по демографическим или буквенным оценкам теста), сокращение требуемой памяти будет более чем в 10 раз:
>>>
>>> manycolors = colors.repeat(10)
>>> len(manycolors) / manycolors.nunique() # Much greater than 2.0x
20.0
>>> manycolors.memory_usage(index=False, deep=True)
6500
>>> manycolors.astype('category').memory_usage(index=False, deep=True)
585
Бонусом является то, что вычислительная эффективность также увеличивается: для категориальногоSeries
используются строковые операцииare performed on the .cat.categories
attribute, а не для каждого исходного элементаSeries
.
Другими словами, операция выполняется один раз для каждой уникальной категории, а результаты сопоставляются со значениями. Категориальные данные имеют аксессор.cat
, который представляет собой окно с атрибутами и методами для управления категориями:
>>>
>>> ccolors = colors.astype('category')
>>> ccolors.cat.categories
Index(['burnt orange', 'mint green', 'navy', 'periwinkle', 'rose'], dtype='object')
Фактически, вы можете воспроизвести нечто похожее на пример выше, который вы делали вручную:
>>>
>>> ccolors.cat.codes
0 3
1 1
2 0
3 3
4 0
5 4
6 4
7 1
8 4
9 2
dtype: int8
Все, что вам нужно сделать, чтобы точно воспроизвести предыдущий ручной вывод, это изменить порядок кодов:
>>>
>>> ccolors.cat.reorder_categories(mapper).cat.codes
0 0
1 1
2 2
3 0
4 2
5 3
6 3
7 1
8 3
9 4
dtype: int8
Обратите внимание, что dtype - этоint8
NumPy,8-bit signed integer, который может принимать значения от -127 до 128. (Для представления значения в памяти требуется только один байт. 64-битный знаковыйints
был бы излишним с точки зрения использования памяти.) Наш грубый пример привел к даннымint64
по умолчанию, тогда как Pandas достаточно умен, чтобы понижать категориальные данные до наименьшего возможного числового dtype .
Большинство атрибутов для.cat
связаны с просмотром и манипулированием базовыми категориями:
>>>
>>> [i for i in dir(ccolors.cat) if not i.startswith('_')]
['add_categories',
'as_ordered',
'as_unordered',
'categories',
'codes',
'ordered',
'remove_categories',
'remove_unused_categories',
'rename_categories',
'reorder_categories',
'set_categories']
Однако есть несколько предостережений. Категориальные данные, как правило, менее гибкие. Например, если вы вставляете ранее невидимые значения, вам нужно сначала добавить это значение в контейнер.categories
:
>>>
>>> ccolors.iloc[5] = 'a new color'
# ...
ValueError: Cannot setitem on a Categorical with a new category,
set the categories first
>>> ccolors = ccolors.cat.add_categories(['a new color'])
>>> ccolors.iloc[5] = 'a new color' # No more ValueError
Если вы планируете устанавливать значения или изменять форму данных, а не производить новые вычисления, типыCategorical
могут быть менее гибкими.
6. Introspect Groupby объекты через итерацию
Когда вы вызываетеdf.groupby('x')
, результирующие объекты Pandasgroupby
могут быть немного непрозрачными. Этот объект лениво создается и сам по себе не имеет никакого значимого представления.
Вы можете продемонстрировать с помощью набора данных abalone изexample 1:
>>>
>>> abalone['ring_quartile'] = pd.qcut(abalone.rings, q=4, labels=range(1, 5))
>>> grouped = abalone.groupby('ring_quartile')
>>> grouped
Хорошо, теперь у вас есть объектgroupby
, но что это за штука и как ее увидеть?
Прежде чем вызывать что-то вродеgrouped.apply(func)
, вы можете воспользоваться тем фактом, что объектыgroupby
являются повторяемыми:
>>>
>>> help(grouped.__iter__)
Groupby iterator
Returns
-------
Generator yielding sequence of (name, subsetted object)
for each group
Каждая «вещь», полученнаяgrouped.__iter__()
, является кортежем(name, subsetted object)
, гдеname
- это значение столбца, по которому вы группируете, аsubsetted object
- это DataFrame, который является подмножеством исходного DataFrame на основе любого указанного вами условия группировки. То есть данные разбиваются на группы:
>>>
>>> for idx, frame in grouped:
... print(f'Ring quartile: {idx}')
... print('-' * 16)
... print(frame.nlargest(3, 'weight'), end='\n\n')
...
Ring quartile: 1
----------------
sex length diam height weight rings ring_quartile
2619 M 0.690 0.540 0.185 1.7100 8 1
1044 M 0.690 0.525 0.175 1.7005 8 1
1026 M 0.645 0.520 0.175 1.5610 8 1
Ring quartile: 2
----------------
sex length diam height weight rings ring_quartile
2811 M 0.725 0.57 0.190 2.3305 9 2
1426 F 0.745 0.57 0.215 2.2500 9 2
1821 F 0.720 0.55 0.195 2.0730 9 2
Ring quartile: 3
----------------
sex length diam height weight rings ring_quartile
1209 F 0.780 0.63 0.215 2.657 11 3
1051 F 0.735 0.60 0.220 2.555 11 3
3715 M 0.780 0.60 0.210 2.548 11 3
Ring quartile: 4
----------------
sex length diam height weight rings ring_quartile
891 M 0.730 0.595 0.23 2.8255 17 4
1763 M 0.775 0.630 0.25 2.7795 12 4
165 M 0.725 0.570 0.19 2.5500 14 4
Соответственно, объектgroupby
также имеет.groups
и получатель группы.get_group()
:
>>>
>>> grouped.groups.keys()
dict_keys([1, 2, 3, 4])
>>> grouped.get_group(2).head()
sex length diam height weight rings ring_quartile
2 F 0.530 0.420 0.135 0.6770 9 2
8 M 0.475 0.370 0.125 0.5095 9 2
19 M 0.450 0.320 0.100 0.3810 9 2
23 F 0.550 0.415 0.135 0.7635 9 2
39 M 0.355 0.290 0.090 0.3275 9 2
Это поможет вам быть более уверенным в том, что выполняемая вами операция - это то, что вам нужно:
>>>
>>> grouped['height', 'weight'].agg(['mean', 'median'])
height weight
mean median mean median
ring_quartile
1 0.1066 0.105 0.4324 0.3685
2 0.1427 0.145 0.8520 0.8440
3 0.1572 0.155 1.0669 1.0645
4 0.1648 0.165 1.1149 1.0655
Независимо от того, какие вычисления вы выполняете дляgrouped
, будь то один метод Pandas или специально созданная функция, каждый из этих «подкадров» передается один за другим в качестве аргумента вызываемой функции. Отсюда и термин «разделить-применить-объединить»: разбить данные по группам, выполнить расчеты для каждой группы и выполнить рекомбинацию в некотором агрегированном виде.
Если у вас возникли проблемы с визуальным представлением, как именно группы будут выглядеть на самом деле, просто итерируйте их и напечатайте несколько из них, что может быть чрезвычайно полезным.
7. Используйте этот картографический трюк для членства
Допустим, у вас есть Серия и соответствующая «таблица сопоставления», где каждое значение относится к группе, состоящей из нескольких членов, или к группам вообще нет:
>>>
>>> countries = pd.Series([
... 'United States',
... 'Canada',
... 'Mexico',
... 'Belgium',
... 'United Kingdom',
... 'Thailand'
... ])
...
>>> groups = {
... 'North America': ('United States', 'Canada', 'Mexico', 'Greenland'),
... 'Europe': ('France', 'Germany', 'United Kingdom', 'Belgium')
... }
Другими словами, вам нужно сопоставитьcountries
со следующим результатом:
>>>
0 North America
1 North America
2 North America
3 Europe
4 Europe
5 other
dtype: object
Здесь вам нужна функция, аналогичнаяpd.cut()
в Pandas, но для группировки на основе категориального членства. Вы можете использоватьpd.Series.map()
, который вы уже видели вexample #5, чтобы имитировать это:
from typing import Any
def membership_map(s: pd.Series, groups: dict,
fillvalue: Any=-1) -> pd.Series:
# Reverse & expand the dictionary key-value pairs
groups = {x: k for k, v in groups.items() for x in v}
return s.map(groups).fillna(fillvalue)
Это должно быть значительно быстрее, чем вложенный цикл Python черезgroups
для каждой страны вcountries
.
Вот тест-драйв:
>>>
>>> membership_map(countries, groups, fillvalue='other')
0 North America
1 North America
2 North America
3 Europe
4 Europe
5 other
dtype: object
Давайте разберемся, что здесь происходит. (Примечание: это отличное место для входа в область видимости функции с помощью отладчика Python,pdb
, чтобы проверить, какие переменные являются локальными для функции.)
Цель состоит в том, чтобы отобразить каждую группу вgroups
в целое число. ОднакоSeries.map()
не распознает'ab'
- ему нужна разбитая версия, в которой каждый символ из каждой группы сопоставлен с целым числом. Вот что делаетdictionary comprehension:
>>>
>>> groups = dict(enumerate(('ab', 'cd', 'xyz')))
>>> {x: k for k, v in groups.items() for x in v}
{'a': 0, 'b': 0, 'c': 1, 'd': 1, 'x': 2, 'y': 2, 'z': 2}
Этот словарь можно передать вs.map()
для отображения или «перевода» его значений в соответствующие индексы группы.
8. Понять, как в Pandas используются логические операторы
Возможно, вы знакомы сoperator precedence в Python, гдеand
,not
иor
имеют более низкий приоритет, чем арифметические операторы, такие как<
,<=
,>
,>=
,!=
и==
. Рассмотрим два оператора ниже, где<
и>
имеют более высокий приоритет, чем операторand
:
>>>
>>> # Evaluates to "False and True"
>>> 4 < 3 and 5 > 4
False
>>> # Evaluates to 4 < 5 > 4
>>> 4 < (3 and 5) > 4
True
Note: это не связано конкретно с Pandas, но3 and 5
оценивается как5
из-за оценки короткого замыкания:
«Возвращаемое значение оператора короткого замыкания - это последний оцененный аргумент». (Source)с
Pandas (и NumPy, на котором построен Pandas) не используетand
,or
илиnot
. Вместо этого он использует&
,|
и~
, соответственно, которые являются нормальными, добросовестными побитовыми операторами Python.
Эти операторы не «изобретены» пандами. Скорее,&
,|
и~
являются действительными встроенными операторами Python, которые имеют более высокий (а не более низкий) приоритет, чем арифметические операторы. (Pandas переопределяет такие ужасные методы, как.__ror__()
, которые сопоставляются с оператором|
.) Чтобы пожертвовать некоторыми деталями, вы можете думать о «побитовом» как об «поэлементном», поскольку это относится к Pandas и NumPy:
>>>
>>> pd.Series([True, True, False]) & pd.Series([True, False, False])
0 True
1 False
2 False
dtype: bool
Стоит полностью понять эту концепцию. Допустим, у вас есть серия, подобная диапазону:
>>>
>>> s = pd.Series(range(10))
Я предполагаю, что вы, возможно, видели это исключение в какой-то момент:
>>>
>>> s % 2 == 0 & s > 3
ValueError: The truth value of a Series is ambiguous.
Use a.empty, a.bool(), a.item(), a.any() or a.all().
Что тут происходит? Полезно постепенно связывать выражение с круглыми скобками, рассказывая, как Python шаг за шагом расширяет это выражение:
s % 2 == 0 & s > 3 # Same as above, original expression
(s % 2) == 0 & s > 3 # Modulo is most tightly binding here
(s % 2) == (0 & s) > 3 # Bitwise-and is second-most-binding
(s % 2) == (0 & s) and (0 & s) > 3 # Expand the statement
((s % 2) == (0 & s)) and ((0 & s) > 3) # The `and` operator is least-binding
Выражениеs % 2 == 0 & s > 3
эквивалентно (или рассматривается как)((s % 2) == (0 & s)) and ((0 & s) > 3)
. Это называетсяexpansion:x < y <= z
эквивалентноx < y and y <= z
.
Хорошо, теперь остановимся и давайте вернем это к разговору с Пандами. У вас есть две серии Pandas, которые мы назовемleft
иright
:
>>>
>>> left = (s % 2) == (0 & s)
>>> right = (0 & s) > 3
>>> left and right # This will raise the same ValueError
Вы знаете, что утверждение формыleft and right
проверяет истинность какleft
, так иright
, как показано ниже:
>>>
>>> bool(left) and bool(right)
Проблема заключается в том, что разработчики Pandas намеренно не устанавливают значение истинности (правдивости) для всей серии. Является ли серия правдой или ложью? Кто знает? Результат неоднозначен:
>>>
>>> bool(s)
ValueError: The truth value of a Series is ambiguous.
Use a.empty, a.bool(), a.item(), a.any() or a.all().
Единственное сравнение, которое имеет смысл, - это поэлементное сравнение. Вот почему, если задействован арифметический оператор,you’ll need parentheses:
>>>
>>> (s % 2 == 0) & (s > 3)
0 False
1 False
2 False
3 False
4 True
5 False
6 True
7 False
8 True
9 False
dtype: bool
Короче говоря, если вы видите, чтоValueError
выше всплывает с логической индексацией, первое, что вам, вероятно, следует сделать, это добавить некоторые необходимые скобки.
9. Загрузить данные из буфера обмена
Часто возникает необходимость перенести данные из такого места, как Excel илиSublime Text, в структуру данных Pandas. В идеале, вы хотите сделать это, не проходя промежуточный этап сохранения данных в файл, а затем читая файл в Pandas.
Вы можете загрузить DataFrames из буфера данных буфера обмена вашего компьютера с помощьюpd.read_clipboard()
. Его аргументы ключевого слова передаютсяpd.read_table()
.
Это позволяет вам копировать структурированный текст непосредственно в DataFrame или Series. В Excel данные будут выглядеть примерно так:
Его текстовое представление (например, в текстовом редакторе) будет выглядеть так:
a b c d 0 1 inf 1/1/00 2 7.389056099 N/A 5-Jan-13 4 54.59815003 nan 7/24/18 6 403.4287935 None NaT
Просто выделите и скопируйте простой текст выше и вызовитеpd.read_clipboard()
:
>>>
>>> df = pd.read_clipboard(na_values=[None], parse_dates=['d'])
>>> df
a b c d
0 0 1.0000 inf 2000-01-01
1 2 7.3891 NaN 2013-01-05
2 4 54.5982 NaN 2018-07-24
3 6 403.4288 NaN NaT
>>> df.dtypes
a int64
b float64
c float64
d datetime64[ns]
dtype: object
10. Напишите объекты Pandas непосредственно в сжатый формат
Этот короткий и сладкий, чтобы завершить список. Начиная с версии Pandas 0.21.0, вы можете записывать объекты Pandas непосредственно в сжатие gzip, bz2, zip или xz, вместо того, чтобы сохранять несжатый файл в памяти и преобразовывать его. Вот пример использования данныхabalone
изtrick #1:
abalone.to_json('df.json.gz', orient='records',
lines=True, compression='gzip')
В этом случае разница в размере составляет 11,6х:
>>>
>>> import os.path
>>> abalone.to_json('df.json', orient='records', lines=True)
>>> os.path.getsize('df.json') / os.path.getsize('df.json.gz')
11.603035760226396
Хотите добавить в этот список? Дайте нам знать
Надеемся, что вы смогли выбрать из этого списка несколько полезных трюков, чтобы сделать ваш код Pandas более читабельным, универсальным и быстрым.
Если у вас есть что-то в рукаве, чего здесь нет, оставьте предложение в комментариях или в видеGitHub Gist. Мы с радостью добавим в этот список и дадим кредит, где он должен.