Если вы работаете с данными в Python, то, скорее всего, хорошо знакомы с библиотекой Pandas. Уже очень долго она является стандартом в data science. Да, Pandas популярен, очень хорош для множества задач, но когда дело доходит до действительно больших объемов данных или сложных, многошаговых вычислений, его производительность и аппетиты к оперативной памяти могут стать узким местом.
Есть ли у нас альтернатива, спроектированная с нуля с упором на максимальную производительность? Встречайте Polars — относительно новую библиотеку для работы с датафреймами, написанную на Rust и быстро набирающую популярность.
pip install polars
pip install polars[all]
import polars as pl
# Попытка прочитать реальный файл
try:
df_sales = pl.read_csv(
"sales_data.csv",
try_parse_dates=True, # Пытаемся распознать даты
dtypes={"quantity": pl.Int32} # Явно указываем тип для quantity
)
print("Данные успешно загружены из 'sales_data.csv'")
except FileNotFoundError:
# Если файла нет, создадим набор данных для примеров
print("Файл 'sales_data.csv' не найден. Создаем демонстрационный DataFrame.")
df_sales = pl.DataFrame({
"order_id": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115],
"customer_name": ["Анна", "Борис", "Анна", "Виктор", "Борис", "Дарья", "Анна", "Борис", "Виктор", "Анна", "Дарья", "Борис", "Виктор", "Анна", "Дарья"],
"product_sku": ["АРТ-А1", "АРТ-Б2", "АРТ-В1", "АРТ-А1", "АРТ-Б2", "АРТ-Г3", "АРТ-А1", "АРТ-В1", "АРТ-Б2", "АРТ-В1", "АРТ-Г3", "АРТ-А1", "АРТ-Г3", "АРТ-Б2", "АРТ-А1"],
"quantity": pl.Series([10, 5, 8, 12, 6, 15, 7, 9, 11, 4, 18, 10, 20, 6, 9], dtype=pl.Int32), # Явно задаем тип Int32
"price": [1550.0, 2200.0, 875.5, 1550.0, 2200.0, 3100.0, 1550.0, 875.5, 2200.0, 875.5, 3100.0, 1550.0, 3100.0, 2200.0, 1550.0],
"order_date": [
"2025-01-15", "2025-01-20", "2025-02-10", "2025-02-12", "2025-02-18",
"2025-02-25", "2025-03-05", "2025-03-11", "2025-03-15", "2025-03-22",
"2025-04-01", "2025-04-02", "2025-04-10", "2025-04-15", "2025-04-20"
]
# Важный шаг: преобразуем строки с датами в тип Date
}).with_columns(pl.col("order_date").str.strptime(pl.Date, "%Y-%m-%d"))
# Посмотрим на результат
print(df_sales)
print(f"\nРазмер датафрейма: {df_sales.shape}")
# Вывод: Размер DataFrame: (15, 6)
print("\nПервые 5 строк:")
display(df_sales.head())
print("\nПоследние 3 строки:")
display(df_sales.tail(3))
print("\nОписательная статистика:")
display(df_sales.describe())
print("\nСхема датафрейма:")
print(df_sales.schema)
Схема датафрейма:
Schema({'order_id': Int64, 'customer_name': String, 'product_sku': String, 'quantity': Int32, 'price': Float64, 'order_date': Date})
# Выбираем только имя клиента, артикул и дату заказа
df_selected = df_sales.select(
pl.col("customer_name"),
pl.col("product_sku"),
pl.col("order_date")
)
print("\nВыборка трех столбцов:")
display(df_selected.head(3))
# Выборка с переименованием
df_renamed = df_sales.select(
pl.col("customer_name").alias("Клиент"),
pl.col("product_sku").alias("Артикул"),
pl.col("quantity").alias("Количество")
)
print("\nВыборка с переименованием:")
display(df_renamed.head(3))
# Найти заказы с количеством больше 10
df_filtered_qty = df_sales.filter(
pl.col("quantity") > 10
)
print("\nЗаказы с количеством > 10:")
display(df_filtered_qty.head(3))
# Найти заказы клиента "Анна"
df_filtered_anna = df_sales.filter(
pl.col("customer_name") == "Анна"
)
print("\nЗаказы клиента Анна:")
display(df_filtered_anna.head(3))
# Найти заказы Бориса ИЛИ Виктора (ИЛИ: |), сделанные ПОСЛЕ 1 марта 2025 (И: &)
df_complex_filter = df_sales.filter(
(pl.col("customer_name").is_in(["Борис", "Виктор"])) &
(pl.col("order_date") > pl.date(2025, 3, 1)) # Сравнение с датой
)
print("\nЗаказы Бориса или Виктора после 2025-03-01:")
display(df_complex_filter)
df_new_cols = df_sales.with_columns(
# 1. Создаем столбец 'total_price' (Общая стоимость = количество * цена)
(pl.col("quantity") * pl.col("price")).alias("total_price"),
# 2. Извлекаем месяц из даты заказа
pl.col("order_date").dt.month().alias("order_month"),
# 3. Добавляем категорию товара на основе SKU
pl.when(pl.col("product_sku").str.starts_with("АРТ-А"))
.then(pl.lit("Категория А")) # pl.lit() создает литеральное (константное) значение
.when(pl.col("product_sku").str.starts_with("АРТ-Б"))
.then(pl.lit("Категория Б"))
.when(pl.col("product_sku").str.starts_with("АРТ-В"))
.then(pl.lit("Категория В"))
.otherwise(pl.lit("Другая Категория")) # Для всех остальных случаев (АРТ-Г3)
.alias("sku_category"),
# 4. Добавляем флаг "Крупный заказ" (количество >= 10)
(pl.col("quantity") >= 10).alias("is_large_order") # Результат будет булевым (True/False)
)
print("\nДатафрейм с новыми столбцами:")
display(df_new_cols.head())
print("\nСхема обновленного датафрейма:")
print(df_new_cols.schema)
Схема обновленного DataFrame:
Schema({'order_id': Int64, 'customer_name': String, 'product_sku': String, 'quantity': Int32, 'price': Float64, 'order_date': Date, 'total_price': Float64, 'order_month': Int8, 'sku_category': String, 'is_large_order': Boolean})
# Используем датафрейм с новыми столбцами из предыдущего шага
df_grouped = df_new_cols.group_by("customer_name").agg(
pl.sum("quantity").alias("total_quantity_per_customer"), # Суммарное кол-во товаров
pl.sum("total_price").alias("total_value_per_customer"), # Общая сумма заказов
pl.mean("price").alias("average_item_price_per_customer"), # Средняя цена купленного товара
pl.len().alias("order_count_per_customer"), # Количество заказов (строк) в группе
pl.n_unique("product_sku").alias("unique_skus_per_customer"), # Кол-во уникальных артикулов
pl.sum("is_large_order").alias("large_orders_count") # Считаем кол-во крупных заказов (True=1, False=0)
).sort("total_value_per_customer", descending=True) # Сортируем результат по сумме
print("\nСгруппированные данные по покупателям (отсортировано по сумме):")
display(df_grouped)
report = (
df_new_cols
.filter( # Шаг 1: Фильтруем по категории и месяцу
(pl.col("sku_category").is_in(["Категория А", "Категория Б"])) &
(pl.col("order_month") == 3) # Март
)
.group_by("customer_name", "sku_category") # Шаг 2: Группируем по покупателю И категории
.agg( # Шаг 3: Считаем суммарную стоимость и количество
pl.sum("total_price").alias("total_value"),
pl.sum("quantity").alias("total_quantity")
)
.sort("customer_name", "sku_category") # Шаг 4: Сортируем для наглядности
)
print("\nОтчет: Стоимость заказов категорий А и Б за март 2025 по покупателям:")
display(report)
# Возьмем наш датафрейм с добавленными столбцами из предыдущего раздела
lazy_df_sales = df_new_cols.lazy()
print(type(lazy_df_sales))
# <class 'polars.lazyframe.frame.LazyFrame'>
print(lazy_df_sales)
# naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
# DF ["order_id", "customer_name", "product_sku", "quantity", ...]; PROJECT */10 COLUMNS
lazy_report_query = (
lazy_df_sales # Начинаем с LazyFrame
.filter(
(pl.col("sku_category").is_in(["Категория А", "Категория Б"])) &
(pl.col("order_month") == 3)
)
.group_by("customer_name", "sku_category")
.agg(
pl.sum("total_price").alias("total_value"),
pl.sum("quantity").alias("total_quantity")
)
.sort("customer_name", "sku_category")
)
print("\nТип объекта запроса:")
print(type(lazy_report_query))
# <class 'polars.lazyframe.frame.LazyFrame'>
print("\nСам объект запроса (всё ещё ленивый):")
print(lazy_report_query)
# naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
print("\nОптимизированный план запроса (текст):")
print(lazy_report_query.explain())
Оптимизированный план запроса (текст):
SORT BY [col("customer_name"), col("sku_category")]
AGGREGATE
[col("total_price").sum().alias("total_value"), col("quantity").sum().alias("total_quantity")] BY [col("customer_name"), col("sku_category")]
FROM
FILTER [(col("sku_category").is_in([Series])) & ([(col("order_month")) == (3)])]
FROM
DF ["order_id", "customer_name", "product_sku", "quantity", ...]; PROJECT["total_price", "quantity", "customer_name", "sku_category", ...] 5/10 COLUMNS
# Выполняем ленивый запрос
report_final = lazy_report_query.collect()
print("\nТип результата после .collect():")
print(type(report_final))
# <class 'polars.dataframe.frame.DataFrame'>
print("\nИтоговый DataFrame:")
display(report_final)
import polars as pl
url = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet"
# Создаем LazyFrame, читая только метаданные Parquet файла по URL.
lazy_taxi_data = pl.scan_parquet(url)
# Определяем агрегацию: средняя длительность и сумма поездки
# для поездок с > 1 пассажиром, сгруппировав по зоне посадки.
lazy_query = (
lazy_taxi_data
.filter(pl.col("passenger_count") > 1)
.group_by("PULocationID")
.agg(
(pl.col("tpep_dropoff_datetime") - pl.col("tpep_pickup_datetime"))
.mean()
.dt.total_seconds()
.alias("avg_duration_seconds"),
pl.mean("total_amount").alias("avg_total_amount"),
pl.len().alias("num_trips")
)
.filter(pl.col("num_trips") > 100)
.sort("num_trips", descending=True)
)
# Polars скачивает данные по частям с URL, выполняет фильтры,
# агрегации и возвращает итоговый DataFrame.
final_report = lazy_query.collect() # Запрос выполняется здесь
# Результат - обычный датафрейм
print("\nИтоговый отчет (первые 5 строк):")
display(final_report.head())
import pandas as pd
import numpy as np
# Создадим небольшой DataFrame для демонстрации записи
# (В реальном коде здесь мог бы быть результат предыдущих шагов, как final_report)
write_example_df = pl.DataFrame({
"PULocationID": [1, 2, 3],
"avg_duration_seconds": [600.5, 750.2, 800.0],
"avg_total_amount": [25.5, 30.0, 35.8],
"num_trips": [150, 200, 120]
})
print("--- Примеры записи данных ---")
print("Используем DataFrame:")
print(write_example_df)
# --- Запись данных ---
csv_path = "report_example.csv"
write_example_df.write_csv(csv_path)
print(f"\nСохранено в CSV: {csv_path}")
parquet_path = "report_example.parquet"
write_example_df.write_parquet(parquet_path)
print(f"Сохранено в Parquet: {parquet_path}")
json_path = "report_example.ndjson"
write_example_df.write_ndjson(json_path)
print(f"Сохранено в JSON Lines: {json_path}")
# Запись в Excel (требует установки xlsxwriter: pip install xlsxwriter)
excel_path = "report_example.xlsx"
write_example_df.write_excel(
excel_path,
table_style="Table Style Medium 2",
autofit=True
)
print(f"Сохранено в Excel: {excel_path}")
# --- Чтение данных (жадное) ---
print("\n--- Примеры чтения данных ---")
df_from_csv = pl.read_csv(csv_path)
print(f"\nПрочитано из CSV ({df_from_csv.shape}):\n{df_from_csv.head(3)}")
df_from_parquet = pl.read_parquet(parquet_path)
print(f"\nПрочитано из Parquet ({df_from_parquet.shape}):\n{df_from_parquet.head(3)}")
df_from_json = pl.read_ndjson(json_path)
print(f"\nПрочитано из JSON Lines ({df_from_json.shape}):\n{df_from_json.head(3)}")
# --- Ленивое сканирование Parquet файла ---
print("\n--- Пример ленивого сканирования Parquet файла ---")
lazy_df = pl.scan_parquet(parquet_path)
print(f"Создан LazyFrame из {parquet_path}")
print(f"Результат collect():\n{lazy_df.collect().head(3)}")
# Используем DataFrame из предыдущего примера чтения
polars_df_to_convert = df_from_parquet
# --- Polars -> Pandas ---
df_pandas = polars_df_to_convert.to_pandas()
# --- Polars -> NumPy ---
numpy_array = polars_df_to_convert.to_numpy()
# --- Pandas -> Polars ---
pandas_example = pd.DataFrame({
'col_a': [10, 20, 30],
'col_b': ['x', 'y', 'z']
})
print(pandas_example)
df_polars_from_pandas = pl.from_pandas(pandas_example)
# --- NumPy -> Polars ---
numpy_example = np.array([[1.1, 2.2], [3.3, 4.4], [5.5, 6.6]])
print(numpy_example)
# ВАЖНО: При конвертации из NumPy нужно указать схему (имена и типы)
df_polars_from_numpy = pl.from_numpy(
numpy_example,
schema={"val1": pl.Float64, "val2": pl.Float64}
)