Особенности и ловушки
Использование операторов 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 в качестве бэкенда, но этот бэкенд будет использовать 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