Spec-Zone.ru › pandas 0.19

Особенности и ловушки

Использование операторов if/truth с pandas

pandas следует соглашению NumPy и генерирует ошибку при попытке преобразовать что-либо в bool. Это происходит при if или при использовании булевых операций, and, or, или not. Непонятно, каким должен быть результат

>>> if pd.Series([False, True, False]):
     ...

должен ли он быть True , потому что он не имеет длины 0? False , потому что есть False значений? Непонятно, поэтому pandas генерирует ValueError:

>>> if pd.Series([False, True, False]):
    print("I was true")
Traceback
    ...
ValueError: The truth value of an array is ambiguous. Use a.empty, a.any() or a.all().

Если вы видите это, вам нужно явно выбрать, что с этим делать (например, использовать any(), all() или empty). Или, возможно, вы захотите сравнить, является ли объект pandas None

>>> if pd.Series([False, True, False]) is not None:
       print("I was not None")
>>> I was not None

или вернуть, если значение any равно True.

>>> if pd.Series([False, True, False]).any():
       print("I am any")
>>> I am any

Для оценки одноэлементных объектов pandas в булевом контексте используйте метод .bool():

In [1]: pd.Series([True]).bool()
Out[1]: True

In [2]: pd.Series([False]).bool()
Out[2]: False

In [3]: pd.DataFrame([[True]]).bool()
Out[3]: True

In [4]: pd.DataFrame([[False]]).bool()
Out[4]: False

Битовые булевы операторы

Битовые булевы операторы, такие как == и != , вернут булево Series, что почти всегда и нужно.

>>> s = pd.Series(range(5))
>>> s == 4
0    False
1    False
2    False
3    False
4     True
dtype: bool

См. булевы сравнения для получения дополнительных примеров.

Использование оператора in

Использование оператора Python in для Series проверяет принадлежность к индексу, а не к значениям.

Если такое поведение неожиданно, помните, что при применении оператора in к словарю Python проверяются ключи, а не значения, и Series похожа на словарь. Чтобы проверить принадлежность к значениям, используйте метод isin():

Для DataFrames аналогично in применяется к оси столбцов, проверяя принадлежность к списку имён столбцов.

Пропущенные значения, целые числа и преобразование типов для пропущенных значений

Выбор представления пропущенных значений

Из-за отсутствия встроенной поддержки (отсутствующих) значений в NumPy и Python в целом, у нас был сложный выбор между:

  • Решение с маскированным массивом: массив данных и массив булевых значений, указывающих, является ли значение
  • Использование специального значения-заполнителя, битовой маски или набора значений-заполнителей для обозначения пропущенных значений через все типы данных

По многим причинам мы выбрали последний вариант. После нескольких лет использования в производственной среде он, по крайней мере, на мой взгляд, оказался лучшим решением, учитывая состояние NumPy и Python в целом. Специальное значение NaN (Не число) используется повсюду в качестве значения NA, и существуют API-функции isnull и notnull, которые можно использовать для обнаружения пропущенных значений независимо от типа данных.

Однако это сопряжено с несколькими компромиссами, которые я, безусловно, принял во внимание.

Поддержка целых пропущенных значений

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

In [5]: s = pd.Series([1, 2, 3, 4, 5], index=list('abcde'))

In [6]: s
Out[6]: 
a    1
b    2
c    3
d    4
e    5
dtype: int64

In [7]: s.dtype
Out[7]: dtype('int64')

In [8]: s2 = s.reindex(['a', 'b', 'c', 'f', 'u'])

In [9]: s2
Out[9]: 
a    1.0
b    2.0
c    3.0
f    NaN
u    NaN
dtype: float64

In [10]: s2.dtype
Out[10]: dtype('float64')

Этот компромисс в основном обусловлен соображениями памяти и производительности, а также тем, что результирующая Series продолжает быть «числовой». Одним из вариантов является использование dtype=object массивов вместо обычных.

Преобразование типов для пропущенных значений

При добавлении пропущенных значений в существующую Series или DataFrame с помощью reindex или другими методами, булевые и целочисленные типы будут преобразованы в другой тип данных для хранения пропущенных значений. Эти преобразования сведены в следующей таблице:

Тип Тип данных после преобразования для хранения пропущенных значений
floating без изменений
object без изменений
integer преобразовано в float64
boolean преобразовано в object

Хотя это может показаться серьёзным компромиссом, на практике я столкнулся с очень немногими случаями, когда это вызывало проблемы. Более подробное объяснение мотивов в следующем разделе.

Почему не сделать NumPy похожим на R?

Многие предлагали, чтобы NumPy просто эмулировал поддержку NA , которая присутствует в более специализированном языке статистического программирования R. Частично это связано с иерархией типов NumPy:

Тип Типы данных
numpy.floating float16, float32, float64, float128
numpy.integer int8, int16, int32, int64
numpy.unsignedinteger uint8, uint16, uint32, uint64
numpy.object_ object_
numpy.bool_ bool_
numpy.character string_, unicode_

В языке R, в свою очередь, всего несколько встроенных типов данных: integer, numeric (с плавающей точкой), character и boolean. Типы NA реализуются путем резервирования специальных битовых шаблонов для каждого типа в качестве значения отсутствующего значения. Хотя это можно было бы сделать и с полной иерархией типов NumPy, это было бы более существенным компромиссом (особенно для 8- и 16-битных типов данных) и более сложной задачей реализации.

Альтернативным подходом является использование маскированных массивов. Маскированный массив — это массив данных с связанной булевой маской, указывающей, должно ли каждое значение считаться NA или нет. Лично мне не нравится этот подход, так как, по моему мнению, он накладывает довольно большую нагрузку на пользователя и разработчика библиотеки. Кроме того, он влечёт довольно высокие затраты на производительность при работе с числовыми данными по сравнению с простым подходом использования NaN. Поэтому я выбрал подход Python «практичность важнее чистоты» и пожертвовал возможностью целочисленного NA для более простого подхода к использованию специального значения в массивах с плавающей точкой и объектах для обозначения NA, и преобразованию целочисленных массивов в массивы с плавающей точкой, когда необходимо ввести пропущенные значения.

Целочисленная индексация

Индексация по меткам с целочисленными метками оси — это непростая тема. Она многократно обсуждалась на списках рассылки и среди различных членов научного сообщества Python. В pandas мы придерживаемся общей точки зрения, что метки важнее целочисленных позиций. Поэтому с целочисленным индексом оси возможна только индексация по меткам с использованием стандартных инструментов, таких как .ix. Следующий код сгенерирует исключения:

s = pd.Series(range(5))
s[-1]
df = pd.DataFrame(np.random.randn(5, 4))
df
df.ix[-2:]

Это обдуманное решение было принято для предотвращения неоднозначностей и скрытых ошибок (многие пользователи сообщили об ошибках, когда API-изменения изменили «свойство» падения на позиционную индексацию).

Соглашения по срезу с индексацией по меткам

Немонотонные индексы требуют точного соответствия

Если индекс Series или DataFrame монотонно возрастает или убывает, то границы среза по меткам могут выходить за пределы индекса, аналогично индексации срезов в обычном Python list. Монотонность индекса можно проверить с помощью атрибутов is_monotonic_increasing и is_monotonic_decreasing.

In [11]: df = pd.DataFrame(index=[2,3,3,4,5], columns=['data'], data=range(5))

In [12]: df.index.is_monotonic_increasing
Out[12]: True

# no rows 0 or 1, but still returns rows 2, 3 (both of them), and 4:
In [13]: df.loc[0:4, :]
Out[13]: 
   data
2     0
3     1
3     2
4     3

# slice is are outside the index, so empty DataFrame is returned
In [14]: df.loc[13:15, :]
Out[14]: 
Empty DataFrame
Columns: [data]
Index: []

С другой стороны, если индекс не монотонный, то обе границы среза должны быть уникальными членами индекса.

In [15]: df = pd.DataFrame(index=[2,3,1,4,3,5], columns=['data'], data=range(6))

In [16]: df.index.is_monotonic_increasing
Out[16]: False

# OK because 2 and 4 are in the index
In [17]: df.loc[2:4, :]
Out[17]: 
   data
2     0
3     1
1     2
4     3
# 0 is not in the index
In [9]: df.loc[0:4, :]
KeyError: 0

# 3 is not a unique label
In [11]: df.loc[2:3, :]
KeyError: 'Cannot get right slice bound for non-unique label: 3'

Концы включены

В отличие от стандартного среза Python, где конечная точка среза не включена, индексация срезов по меткам в pandas включает обе конечные точки. Основная причина этого заключается в том, что часто невозможно легко определить «преемника» или следующий элемент после определённой метки в индексе. Например, рассмотрим следующую Series:

In [18]: s = pd.Series(np.random.randn(6), index=list('abcdef'))

In [19]: s
Out[19]: 
a    1.544821
b   -1.708552
c    1.545458
d   -0.735738
e   -0.649091
f   -0.403878
dtype: float64

Предположим, мы хотим выполнить срез от c до e, используя целые числа, это будет

In [20]: s[2:5]
Out[20]: 
c    1.545458
d   -0.735738
e   -0.649091
dtype: float64

Однако, если у вас есть только c и e, определение следующего элемента в индексе может быть довольно сложным. Например, следующее не работает:

s.ix['c':'e'+1]

Очень распространённый случай использования — ограничение временного ряда двумя конкретными датами. Для этого мы разработали дизайн, чтобы индексация срезов по меткам включала обе конечные точки:

In [21]: s.ix['c':'e']
Out[21]: 
c    1.545458
d   -0.735738
e   -0.649091
dtype: float64

Это, безусловно, подход «практичность важнее чистоты», но это важно помнить, если вы ожидаете, что индексация срезов по меткам будет вести себя точно так же, как стандартная индексация срезов Python с целыми числами.

Разные особенности индексации

Особенности reindex и ix

Многие пользователи используют возможности индексации ix в качестве компактного способа выбора данных из объекта pandas:

In [22]: df = pd.DataFrame(np.random.randn(6, 4), columns=['one', 'two', 'three', 'four'],
   ....:                   index=list('abcdef'))
   ....: 

In [23]: df
Out[23]: 
        one       two     three      four
a -2.474932  0.975891 -0.204206  0.452707
b  3.478418 -0.591538 -0.508560  0.047946
c -0.170009 -1.615606 -0.894382  1.334681
d -0.418002 -0.690649  0.128522  0.429260
e  1.207515 -1.308877 -0.548792 -1.520879
f  1.153696  0.609378 -0.825763  0.218223

In [24]: df.ix[['b', 'c', 'e']]
Out[24]: 
        one       two     three      four
b  3.478418 -0.591538 -0.508560  0.047946
c -0.170009 -1.615606 -0.894382  1.334681
e  1.207515 -1.308877 -0.548792 -1.520879

Это, конечно, полностью эквивалентно в этом случае использованию метода reindex:

In [25]: df.reindex(['b', 'c', 'e'])
Out[25]: 
        one       two     three      four
b  3.478418 -0.591538 -0.508560  0.047946
c -0.170009 -1.615606 -0.894382  1.334681
e  1.207515 -1.308877 -0.548792 -1.520879

Некоторые могут сделать вывод, что ix и reindex являются полностью эквивалентными на основе этого. Это действительно так, кроме случаев целочисленной индексации. Например, вышеупомянутую операцию можно было бы выразить как:

In [26]: df.ix[[1, 2, 4]]
Out[26]: 
        one       two     three      four
b  3.478418 -0.591538 -0.508560  0.047946
c -0.170009 -1.615606 -0.894382  1.334681
e  1.207515 -1.308877 -0.548792 -1.520879

Если вы передадите [1, 2, 4] в reindex, вы получите совершенно другое:

In [27]: df.reindex([1, 2, 4])
Out[27]: 
   one  two  three  four
1  NaN  NaN    NaN   NaN
2  NaN  NaN    NaN   NaN
4  NaN  NaN    NaN   NaN

Поэтому важно помнить, что reindex — это только строгая индексация по меткам. Это может привести к некоторым потенциально неожиданным результатам в патологических случаях, когда индекс содержит, например, как целые числа, так и строки:

In [28]: s = pd.Series([1, 2, 3], index=['a', 0, 1])

In [29]: s
Out[29]: 
a    1
0    2
1    3
dtype: int64

In [30]: s.ix[[0, 1]]
Out[30]: 
0    2
1    3
dtype: int64

In [31]: s.reindex([0, 1])
Out[31]: 
0    2
1    3
dtype: int64

Поскольку индекс в этом случае не содержит только целых чисел, ix возвращается к целочисленной индексации. В свою очередь, reindex ищет только переданные значения в индексе, таким образом, находя целые числа 0 и 1. Хотя было бы возможно добавить логику проверки, содержит ли переданная последовательность только значения из индекса, эта логика потребует очень высоких затрат при работе с большими наборами данных.

Reindex может изменить тип данных базовой Series

Использование reindex_like может потенциально изменить тип данных Series.

In [32]: series = pd.Series([1, 2, 3])

In [33]: x = pd.Series([True])

In [34]: x.dtype
Out[34]: dtype('bool')

In [35]: x = pd.Series([True]).reindex_like(series)

In [36]: x.dtype
Out[36]: dtype('O')

Это происходит потому, что reindex_like неявно вставляет NaNs, и тип данных dtype соответственно изменяется. Это может вызвать проблемы при использовании numpy ufuncs, такие как numpy.logical_and.

Более подробное обсуждение см. в данной старой проблеме.

Разбор дат из текстовых файлов

При разборе нескольких столбцов текстового файла в один столбец дат, новый столбец дат добавляется в начало данных, а затем спецификация index_col индексируется по новому набору столбцов, а не по исходным:

In [37]: print(open('tmp.csv').read())
KORD,19990127, 19:00:00, 18:56:00, 0.8100
KORD,19990127, 20:00:00, 19:56:00, 0.0100
KORD,19990127, 21:00:00, 20:56:00, -0.5900
KORD,19990127, 21:00:00, 21:18:00, -0.9900
KORD,19990127, 22:00:00, 21:56:00, -0.5900
KORD,19990127, 23:00:00, 22:56:00, -0.5900

In [38]: date_spec = {'nominal': [1, 2], 'actual': [1, 3]}

In [39]: df = pd.read_csv('tmp.csv', header=None,
   ....:                  parse_dates=date_spec,
   ....:                  keep_date_col=True,
   ....:                  index_col=0)
   ....: 

# index_col=0 refers to the combined column "nominal" and not the original
# first column of 'KORD' strings
In [40]: df
Out[40]: 
                                 actual     0         1          2          3  \
nominal                                                                         
1999-01-27 19:00:00 1999-01-27 18:56:00  KORD  19990127   19:00:00   18:56:00   
1999-01-27 20:00:00 1999-01-27 19:56:00  KORD  19990127   20:00:00   19:56:00   
1999-01-27 21:00:00 1999-01-27 20:56:00  KORD  19990127   21:00:00   20:56:00   
1999-01-27 21:00:00 1999-01-27 21:18:00  KORD  19990127   21:00:00   21:18:00   
1999-01-27 22:00:00 1999-01-27 21:56:00  KORD  19990127   22:00:00   21:56:00   
1999-01-27 23:00:00 1999-01-27 22:56:00  KORD  19990127   23:00:00   22:56:00   

                        4  
nominal                    
1999-01-27 19:00:00  0.81  
1999-01-27 20:00:00  0.01  
1999-01-27 21:00:00 -0.59  
1999-01-27 21:00:00 -0.99  
1999-01-27 22:00:00 -0.59  
1999-01-27 23:00:00 -0.59  

Отличия от NumPy

Для объектов Series и DataFrame, var нормируется по N-1, чтобы получить несмещенные оценки выборочной дисперсии, в то время как NumPy использует var нормировку по N, которая измеряет дисперсию выборки. Обратите внимание, что cov нормируется по N-1 как в pandas, так и в NumPy.

Потокобезопасность

Начиная с pandas 0.11, pandas не является на 100% потокобезопасным. Известные проблемы связаны с методом DataFrame.copy. Если вы выполняете большое количество копий объектов DataFrame, используемых несколькими потоками, рекомендуется использовать блокировки внутри потоков, где происходит копирование данных.

Дополнительную информацию см. в этой ссылке.

Обработка HTML-таблиц

Существуют некоторые проблемы с версионированием библиотек, используемых для разбора HTML-таблиц в функции pandas io верхнего уровня read_html.

Проблемы с lxml

  • Преимущества
    • lxml очень быстрый
    • lxml требует установки Cython.
  • Недостатки
    • lxml не гарантирует результаты своего разбора если не получает строго валидный разметку.
    • С учетом вышесказанного, мы предоставили вам, пользователю, возможность использовать lxml в качестве бэкенда, но этот бэкенд будет использовать html5lib, если lxml не сможет выполнить разбор.
    • Поэтому настоятельно рекомендуется установить как BeautifulSoup4, так и html5lib, чтобы получить валидный результат (при условии валидности всего остального), даже если lxml потерпит неудачу.

Проблемы с BeautifulSoup4 использованием lxml в качестве бэкенда

  • Вышеуказанные проблемы сохраняются и здесь, так как BeautifulSoup4 в основном является обертывающим элементом для бэкенда анализатора.

Проблемы с BeautifulSoup4 использованием html5lib в качестве бэкенда

  • Преимущества
    • html5lib гораздо более лоялен к lxml и, следовательно, гораздо лучше справляется с реальной разметкой, а не, например, просто удаляет элемент без уведомления.
    • html5lib автоматически генерирует валидную разметку HTML5 из невалидной разметки. Это очень важно для разбора HTML-таблиц, так как гарантирует валидный документ. Однако это не означает, что он «правильный», поскольку процесс исправления разметки не имеет единого определения.
    • html5lib написан на чистом Python и не требует дополнительных шагов сборки за пределами собственной установки.
  • Недостатки
    • Наибольшим недостатком использования html5lib является его медлительность. Однако учтите, что многие таблицы в веб-среде не настолько велики, чтобы время выполнения алгоритма разбора имело значение. Скорее всего, узким местом будет процесс чтения исходного текста из URL через веб-сервер, то есть ввод-вывод (ввод-вывод). Для очень больших таблиц это может быть не так.

Проблемы с использованием Anaconda

  • Anaconda поставляется с версией lxml 3.2.0; следующее решение для Anaconda успешно использовалось для решения проблем версионирования, связанных с lxml и BeautifulSoup4.

Примечание

Если у вас нет обоих:

  • Строгое ограничение на максимальное время выполнения некоторого кода, использующего read_html()
  • Полное знание того, что HTML, который вы будете анализировать, всегда будет на 100% валидным

то вы должны установить html5lib, и всё будет работать гладко, без необходимости возиться с conda. Если вам нужно лучшее из обоих миров, установите как html5lib, так и lxml. Если вы всё-таки установите lxml, вам нужно выполнить следующие команды, чтобы убедиться, что lxml будет работать правильно:

# remove the included version
conda remove lxml

# install the latest version of lxml
pip install 'git+git://github.com/lxml/lxml.git'

# install the latest version of beautifulsoup4
pip install 'bzr+lp:beautifulsoup'

Обратите внимание, что вам нужны bzr и git для выполнения последних двух операций.

Проблемы с порядком байтов

Иногда вам может потребоваться работать с данными, созданными на машине с другим порядком байтов, чем на той, на которой вы запускаете Python. Общим симптомом этой проблемы является ошибка типа

Traceback
    ...
ValueError: Big-endian buffer not supported on little-endian compiler

Чтобы решить эту проблему, вы должны преобразовать базовую массив NumPy в родной порядок байтов до передачи его в конструкторы Series/DataFrame/Panel, используя что-то подобное:

In [41]: x = np.array(list(range(10)), '>i4') # big endian

In [42]: newx = x.byteswap().newbyteorder() # force native byteorder

In [43]: s = pd.Series(newx)

Более подробную информацию см. в документации NumPy по порядку байтов.

© 2008–2012, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/0.19.2/gotchas.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API