Python Pandas: хитрости и особенности, которые вы можете не знать

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:

Эти автономные классы затем «прикрепляются» к классу 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 данные будут выглядеть примерно так:

Excel Clipboard Data

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

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. Мы с радостью добавим в этот список и дадим кредит, где он должен.