Гайд по обработке данных с помощью Pandas: часть вторая

Второй выпуск гайда по работе с библиотекой Pandas. Разбираемся, как эффективнее анализировать данные, и даём список альтернатив.

2К открытий16К показов
Гайд по обработке данных с помощью Pandas: часть вторая

В первой части гайда мы сконцентрировались на азах: разобрались, что такое Series и DataFrame, узнали, какими функциями можно выполнять чтение, запись, объединение данных. Также прошлись по индексам, групповым операциям, разобрались, как визуализировать графики и поработали с временными рядами. В этой части гайда по работе с Pandas мы разберём, как ещё эффективнее использовать библиотеку, а также какие у неё есть альтернативы.

Как сделать работу с Pandas эффективнее

Модификация данных на месте

Многие методы Pandas могут принимать флаг inplace, который в случае выставления значения True модифицирует данные на месте, а не создаёт новый объект. Если модифицировать исходные данные некритично, имеет смысл его использовать. Так не произойдёт аллокации дополнительной памяти:

			titanic2 = titanic.dropna(subset=["Age"], inplace=False)  # По умолчанию создаётся копия датафрейма
		
			titanic2 is titanic  # Действительно, у нас два разных датафрейма, то есть объём памяти удвоился
		
Гайд по обработке данных с помощью Pandas: часть вторая 1
			titanic.dropna(subset=["Age"], inplace=True)  # А так мы удаляем строки с отсутствующими данными прямо на месте
		

View vs copy

В Pandas, как и в библиотеке Numpy, особое внимание уделяется оптимизации хранения данных в памяти и минимизации лишних аллокаций. Для этого используются «представление» (view) и «копирование» (copy) данных.

Представление — это ссылка или «окно» на исходные данные, через которое можно производить чтение и изменение. Так новые массивы данных в памяти не создаются. Следовательно, модификация данных через представление приведёт к изменению исходных данных.

На контрасте с этим, копирование данных означает аллокацию нового блока памяти и создание дублирующего объекта. Изменения в такой копии не затрагивают исходные данные. Однако чем больше копий, тем ниже производительность кода. В Pandas они часто создаются при выполнении операций срезов данных.

Чтобы определить является ли объект представлением или копией, можно использовать внутреннее свойство ._is_view:

			df = pd.DataFrame(np.arange(10, 90, 10).reshape(2, 4), index=["One","Two"], columns=["A", "B", "C", "D"])
df
		
Гайд по обработке данных с помощью Pandas: часть вторая 2
			df.iloc[:, :3]
		
Гайд по обработке данных с помощью Pandas: часть вторая 3
			df.iloc[:, :3]._is_view
		
Гайд по обработке данных с помощью Pandas: часть вторая 4
			df.iloc[0:4, :]._is_view
		
Гайд по обработке данных с помощью Pandas: часть вторая 5
			df.loc[df["A"] == 10, :]
		
Гайд по обработке данных с помощью Pandas: часть вторая 6
			df.loc[df["A"] == 10, :]._is_view
		
Гайд по обработке данных с помощью Pandas: часть вторая 7

Так как сейчас в датафрейме df все колонки одного типа, Pandas оптимизирует их «под капотом», на уровне массивов Numpy. Но что будет, если изменить тип одной из колонок?

			df.dtypes  # Было
		
Гайд по обработке данных с помощью Pandas: часть вторая 8
			df["A"] = df.A.astype(np.float32)

df.dtypes  # Стало
		
Гайд по обработке данных с помощью Pandas: часть вторая 9
			df.loc[df["A"] == 10, :]._is_view  # Теперь мы получаем не view, а неявную копию данных!
		
Гайд по обработке данных с помощью Pandas: часть вторая 10
			df.iloc[:, :3]._is_view  # Аналогично. Раньше получали view, сейчас — копию.
		
Гайд по обработке данных с помощью Pandas: часть вторая 11

Можно сделать и явную копию:

			df_copy = df.copy()
		

Тема views и copies в Pandas довольно сложная и понимание приходит с опытом. Если попытаться присвоить значения копии датафрейма там, где по мнению Pandas этого быть не должно, то появится предупреждение SettingWithCopyWarning:

			df2 = df[["A"]]
df2["A"] = 0
		
Гайд по обработке данных с помощью Pandas: часть вторая 12

Чтение и обработка данных частями

Если объём данных, читаемых из файла, больше объёма памяти на компьютере, а оптимизация типов данных при чтении не помогла, то бывает эффективно читать и обрабатывать данные по частям, используя параметр chunksize.

Прочитаем CSV-файл частями по несколько строк:

			total_chunks = 0
total_rows = 0

chunk_size = 100  # Будем читать по 100 строк за цикл

for chunk in pd.read_csv("titanic.csv", chunksize=chunk_size):
    # Каждый chunk будет содержать chunk_size строк из CSV    
    total_rows += len(chunk)    
    total_chunks += 1
    print(f"Chunk {total_chunks}")
    # print(chunk.head())  # Для демонстрации можно напечатать первые 5 строк из каждого чанка    
    # print("-" * 50)

print(f"Processed {total_chunks} chunks with {total_rows} rows.")
		
Гайд по обработке данных с помощью Pandas: часть вторая 13

Также можно использовать параметр usecols, чтобы загрузить только необходимые столбцы:

			pd.read_csv("titanic.csv", usecols=["Name", "Sex", "Age", "Pclass", "Survived"], index_col="Name").head()
		
Гайд по обработке данных с помощью Pandas: часть вторая 14

Чейнинг методов

Методы Pandas по умолчанию (если inplace=False) возвращают новый объект (DataFrame или Series). Так, несколько подряд идущих методов можно объединить в цепочку вызовов. Это позволяет избежать использования промежуточных переменных и может улучшить читаемость кода, но влечёт за собой копирование данных и лишние аллокации памяти на каждом шаге. Так что чейнинг эффективен, только если скорость или память не критичны:

			result = (    
    titanic    
    .drop(columns=["PassengerId", "Survived", "Sex", "SibSp", "Parch", "Ticket", "Cabin"])  # Удалим лишние колонки    
    .dropna(subset=["Age", "Embarked"])  # Удалим строки, в которых отсутствуют возраст или город посадки    
    .loc[titanic["Fare"] > 10]  # Выберем тариф выше заданного порога    
    .groupby(["Embarked", "Pclass"])  # Сгруппируем по городу посадки и классу    
    .agg({"Age": "mean", "Fare": "mean"})  # Вычислим средний возраст и средний тариф    
    .reset_index()  # Сбросим индекс
)

result
		
Гайд по обработке данных с помощью Pandas: часть вторая 15

Использование query

Метод .query() позволяет писать выражения в упрощённом виде, ссылаться на внешние переменные, использовать альтернативные бэкенды. Так запросы выполняются быстрее и эффективнее:

			min_pclass = 2  # Переменная, на которую будем ссылаться

titanic.query("Sex == 'male' & Age > 50 & Pclass >= @min_pclass").head()
		
Гайд по обработке данных с помощью Pandas: часть вторая 16

Что почитать о работе с Pandas

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

Какие есть альтернативы

Мы уже упоминали, что Pandas не всегда может похвастаться своей эффективностью, например, в промышленных проектах. Поэтому следует присмотреться и к другим альтернативам. Вот инструменты, которые также помогают обрабатывать большие данные:

  • Pandas 2.0 — новая версия, построенная на эффективном бэкенде Apache Arrow.
  • Polars — альтернативная библиотека для более быстрой обработки данных. Написана на языке Rust и использует бэкенд Apache Arrow.
  • Dask — похож на Pandas, но разработан для параллельных вычислений и больших наборов данных.
  • Modin — использует тот же API, что и Pandas, но производительность у неё лучше за счёт параллелизма.
  • Vaex — предназначен для работы с очень большими наборами данных, которые не умещаются в оперативную память. Особенно полезен для визуализации и исследования данных.
  • PySpark DataFrame — библиотека для распределенных вычислений, часть экосистемы Apache Spark. Подходит для работы с очень большими наборами данных.
  • SQL databases — базы данных, такие как SQLite, MySQL, PostgreSQL, предоставляют мощные средства для работы с табличными данными, хотя и требуют отдельного языка запросов (SQL).

Расскажите в комментариях, какие ещё фишки и лайфхаки по работе с Pandas вы знаете и используете!

Следите за новыми постами
Следите за новыми постами по любимым темам
2К открытий16К показов