Spec-Zone.ru › pandas 0.18

Ограничения и подводные камни

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

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

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

должен ли он быть True, поскольку он не нулевой длины? 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 применяется к оси столбцов, проверяя членство в списке имён столбцов.

NaN, Целочисленные NA значения и NA преобразования типов

Выбор представления NA

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

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

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

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

Поддержка целочисленных NA

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

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 вместо этого.

NA преобразования типов

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

Тип Тип данных для хранения NA после преобразования
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. Таким образом, я выбрал питоновский подход «практичность важнее чистоты» и пожертвовал возможностью целочисленных NA для более простого подхода, использующего специальное значение в массивах с плавающей точкой и объектных массивах для обозначения 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 монотонно возрастает или убывает, то границы срезки по меткам могут находиться вне диапазона индекса, аналогично срезке по индексам обычного питоновского 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-таблиц в функции io pandas верхнего уровня 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.18.1/gotchas.html

Spec-Zone.ru

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