Делаем добро вместе Детальнее

it Новости 6 неявных ошибок новичков в Pandas
6 неявных ошибок новичков в Pandas

6 неявных ошибок новичков в Pandas

23 644
15 декабря 2021 в 17:16

Все мы привыкли к ярким и заметным сообщениям об ошибках, которые маячат у нас перед глазами, пока мы пишем код. Но что делать с теми ошибками, что не видны сразу?

Введение

К счастью, обычные пользователи чаще всего их не видят, ведь мы успеваем вовремя все исправить. Но как быть с ошибками, которые не выдают предупреждений? А они как раз самые опасные… и за них приходится краснеть перед более опытными коллегами.


Эти ошибки не связаны с API или синтаксисом инструментов и красноречиво говорят о вашем теоретическом и практическом опыте. Сегодня мы поговорим о 6 неявных ошибках, с которыми сталкиваются начинающие пользователи Pandas, и научимся их исправлять.


1. Использование Pandas 

Весьма иронично то, что первая ошибка как раз-таки связана с непосредственным использованием Pandas для решения определенных задач. Современные табличные наборы данных просто огромны. И прочитывать их в среде Pandas – колоссальная ошибка.


Почему? Да просто Pandas ужасно медленная! В примере ниже мы загрузили набор данных TPS October с 1 млн строк и ~300 функциями, что заняло целых 2,2ГБ пространства на диске.

import pandas as pd
%%time

tps_october = pd.read_csv("data/train.csv")
Wall time: 21.8 s

На обработку потребовалось ~22 секунды. Не так уж много, скажете вы… но подумайте вот, о чем. На разных стадиях в пределах одного проекта вы проводите множество экспериментов. Скорее всего, вы также создадите отдельные скрипты или блокноты для очистки, проектирования функций, выбора модели и других задач. 


А ожидание в 20 секунд для каждой загрузки данных способно пошатнуть любую нервную систему. Скорее всего, ваш реальный набор данных будет еще больше. Так как найти более быстрое решение?


Давайте забудем о Pandas и подберем альтернативные варианты, написанные специально для ввода-вывода. Мой фаворит – datatable, но можете присмотреться к Dask, Vaex, cuDF и т.д. Вот, сколько времени уйдет на загрузку того же набора данных через datatable:

import datatable as dt # pip install datatble

%%time

tps_dt_october = dt.fread("data/train.csv").to_pandas()

------------------------------------------------------------

Wall time: 2 s 

Всего 2 секунды!


2. Нет векторам?

Одно из самых безумных правил в функциональном программировании гласит: никогда не пользоваться циклами (а еще есть правило «никаких переменных»). Казалось бы, соблюдение вышеобозначенного правила при работе в Pandas – это лучшее, что можно сделать для ускорения вычислений.


Функциональное программирование заменяет циклы рекурсиями. К счастью, здесь есть небольшие поблажки, ведь мы можем просто прибегнуть к векторизации!


Векторизация, которая лежит в самом сердце Pandas и NumPy, выполняет математические операции над целым массивом, а не отдельными скалярами. А самое приятное в том, что в Pandas уже заложен расширенный набор векторных функций, и нам не придется заново изобретать велосипед.


При работе с Pandas Series и DataFrames все арифметические операторы в Python (+, -, *, /, **) можно использовать в качестве векторов. А все прочие математические функции, которые вы видите в Pandas или NumPy, уже векторизованы.


Для увеличения скорости мы воспользуемся big_function, которое принимает 3 столбца в качестве входного значения и выполняет бесполезную арифметику:

def big_function(col1, col2, col3):
	return np.log(col1 ** 10 / col2 ** 9 + np.sqrt(col3 ** 3))
Сначала опробуем эту функцию с самым быстрым итератором в Pandas – apply:
%time tps_october['f1000'] = tps_october.apply(
		lambda row: big_function(row['f0'], row['f1'], row['f2']), axis=1
	)

-------------------------------------------------

Wall time: 20.1 s

Оператору потребовалось 20 секунд. Давайте повторим то же самое с использованием массивов NumPy в векторном виде:

%time tps_october['f1001'] = big_function(tps_october['f0'].values, 
				tps_october['f1'].values, 
				tps_october['f2'].values)

------------------------------------------------------------------

Wall time: 82 ms

Теперь на это ушло 82 миллисекунды. То есть мы получили результат в 250 раз быстрее. 

И все-таки вы не можете полностью отказаться от циклов. В конце концов, не все операторы для манипуляций с данными – математические. Но если вас вдруг потянуло на циклические функции (apply, applymap или itertuples), остановитесь и подумайте: смогу ли я векторизовать то, что мне нужно?


3. Типы данных, dtypes, types!

Нет, это не то, что вам рассказывали на уроке «Как изменить стандартные типы данных в столбцах Pandas» в средней школе. Здесь мы копнем гораздо глубже, а именно обсудим типы данных, в зависимости от  потребляемого ими объема памяти.


Самый ужасный и затратный тип данных – это object, который также ограничивает некоторые функции Pandas. Далее идут целые числа (integer) и числа с плавающей запятой (float). Я не очень хочу перечислять все типы данных в Pandas, поэтому давайте просто рассмотрим таблицу:


Источник: http://pbpython.com/pandas_dtypes.html


После названия типа данных идут разные числа. Они показывают, сколько битов памяти потребляет каждое число данного типа. Итак, наша задача заключается в том, чтобы привести каждый столбец из набора данных к наименьшему из возможных подтипов. Но как же его выбрать? Здесь нам поможет другая таблица:


Источник: https://docs.scipy.org/doc/numpy-1.13.0/user/basics.types.html


В принципе, вам можно взять за образец таблицу выше и привести числа с плавающей запятой из своего набора к float16/32 , а столбцы с положительными и отрицательными целыми числами свести к int8/16/32. Чтобы еще больше сократить потребление памяти, можно привести логические значения (boolean) и положительные целые числа (positive-only integers) к uint8.

Ниже демонстрируется полезная, но долгая функция, которая берет за основу логику таблицы выше и разделяет целые числа и числа с плавающей запятой на малые подтипы:

def reduce_memory_usage(df, verbose=True):
	numerics = ["int8", "int16", "int32", "int64", "float16", "float32", "float64"]
	start_mem = df.memory_usage().sum() / 1024 ** 2
	for col in df.columns:
		col_type = df[col].dtypes
		if col_type in numerics:
			c_min = df[col].min()
			c_max = df[col].max()
			if str(col_type)[:3] == "int":
				if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
					df[col] = df[col].astype(np.int8)
				elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
					df[col] = df[col].astype(np.int16)
				elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
					df[col] = df[col].astype(np.int32)
				elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
					df[col] = df[col].astype(np.int64)
				else:
					if (
						c_min > np.finfo(np.float16).min
							and c_max < np.finfo(np.float16).max
					):
						df[col] = df[col].astype(np.float16)
					elif (
						c_min > np.finfo(np.float32).min
							and c_max < np.finfo(np.float32).max
					):
						df[col] = df[col].astype(np.float32)
					else:
						df[col] = df[col].astype(np.float64)
	end_mem = df.memory_usage().sum() / 1024 ** 2
	if verbose:
		print(
			"Mem. usage decreased to {:.2f} Mb ({:.1f}% reduction)".format(
					end_mem, 100 * (start_mem - end_mem) / start_mem
			)
		)
	return df

Давайте воспользуемся ей на данных TPS October и посмотрим, сколько памяти удастся сэкономить:

>>> reduce_memory_usage(tps_october)
Mem. usage decreased to 509.26 Mb (76.9% reduction) 



Мы сжали набор данных до 510 МБ (вместо исходных 2,2 ГБ). К несчастью для нас, вся экономия сходит на нет, стоит только сохранить фрейм данных в файл.


В чем мы в этот раз ошиблись? Потребление оперативной памяти играет важную роль при работе с наборами данных в крупных моделях машинного обучения. Несколько ошибок OutOfMemory помогут вам быстрее разобраться в теме и научиться парочке трюков (по типу этого), чтобы осчастливить свой компьютер.


4. Нет стилям?

Одна из лучших возможностей Pandas – ее способность отображать стилизованные фреймы данных. Необработанные данные отображаются в виде HTML-таблиц с толикой CSS в блокноте Jupyter.


Для людей со вкусом и тех, кто готов на многое, чтобы сделать блокноты красивее и ярче, Pandas продумала добавление стилей в DataFrames через атрибут style.

tps_october.sample(20, axis=1).describe().T.style.bar(
	subset=["mean"], color="#205ff2"
).background_gradient(subset=["std"], cmap="Reds").background_gradient(
	subset=["50%"], cmap="coolwarm"
)

Выше мы в случайном порядке выбрали 20 столбцов, создали для них сводку из 5 чисел, транспонировали результат, и выделили цветом столбцы mean, std и median (по их величине).

Такие изменения позволяют быстрее находить закономерности в «сырых» числах, не обращаясь к библиотекам визуализации. Здесь можно почитать подробнее о стилизации фреймов данных.


А вообще, нет ничего плохого в неиспользовании стилей в Dataframes. В то же время, это настолько классная штука, что просто грех ей не воспользоваться.


5. Сохранение в CSV

Чтение CSV-файлов – процесс небыстрый, равно как и сохранение данных в этот формат. Вот, сколько времени потребуется, чтобы сохранить данные из TPS October в CSV:

%%time

tps_october.to_csv("data/copy.csv")

------------------------------------------

Wall time: 2min 43s

Почти 3 минуты. Цените свое и чужое время и сохраняйте DataFrames в более легкие форматы: feather и parquet.

%%time

tps_october.to_feather("data/copy.feather")

Wall time: 1.05 s

--------------------------------------------------------------------------------

%%time

tps_october.to_parquet("data/copy.parquet")

Wall time: 7.84 s 

Как вы видите, сохранение фрейма данных в формат feather происходит в 160 раз быстрее. Кроме того, оба этих формата занимают гораздо меньше места на диске.


6. Надо было читать мануал!

На самом деле, самая грубая ошибка в этом списке – пренебрежение руководством для пользователей и документацией Pandas.


Я понимаю. Все мы примерно одинаково относимся к документации. Нам проще часами искать ответ в интернете, чем прочитать мануал.


И все-таки с Pandas ситуация немного иная. Для нее написано шикарное руководство для пользователей, в котором отражены все возможные темы – от самых основ до участия в улучшении Pandas.


К слову сказать, обо всех ошибках из этой статьи вы могли бы узнать в мануале для пользователей. А в разделе о чтении больших наборов данных четко сказано: не лезьте в Pandas, выбирайте другие пакеты (например, Dask). Будь у меня достаточно свободного времени для прочтения всего мануала, я бы привел вам еще 50 ошибок начинающего программиста. Так что, дело за вами.


Заключение

Сегодня мы обсудили 6 популярных ошибок новичков в Pandas.

Еще раз подчеркну: ошибками они считаются только при гигабайтных наборах данных. При работе с малыми наборами можете просто забыть о прочитанном, ведь ощутимой разницы вы не заметите.


Но по мере того, как вы нарабатываете опыт и переходите на реальные наборы данных, информация выше убережет вас от многих проблем.

Больше интересных новостей

Комментарии для сайта Cackle