Библиотека Polars для Rust: Полное руководство
Polars — это высокопроизводительная библиотека для работы с данными в формате DataFrame, написанная на Rust. Она предоставляет знакомый пользователям pandas API, но при этом значительно превосходит его по скорости и эффективности использования памяти. В отличие от pandas, который часто упирается в ограничения однопоточного выполнения и потребления оперативной памяти, Polars изначально спроектирован для работы в многопоточном режиме и использует ленивые вычисления (lazy API) для оптимизации запросов.
Основная цель Polars — предоставить инструмент для обработки больших объёмов данных (от сотен мегабайт до нескольких терабайт) на одном узле, не прибегая к распределённым системам вроде Spark. Это достигается за счёт эффективной работы с памятью (нулевое копирование, использование Apache Arrow в качестве бэкенда), векторизации операций и автоматической параллелизации. Для Rust-разработчиков Polars — это не просто обёртка над Python-библиотекой, а нативная библиотека, которая полностью использует возможности языка: безопасность памяти, систему типов и производительность.
Библиотека предлагает два API: Eager (немедленные вычисления) и Lazy (ленивые вычисления). Eager API подходит для интерактивной работы и небольших датасетов, когда нужно сразу видеть результат. Lazy API позволяет построить план запроса, который затем оптимизируется и выполняется за один проход, что критически важно для сложных трансформаций и больших данных. Polars также поддерживает работу с CSV, Parquet, JSON, IPC (Arrow) и базами данных через SQL.
Установка
Для добавления Polars в проект на Rust необходимо добавить зависимость в файл Cargo.toml:
[dependencies]
polars = "0.44.0" # Проверьте актуальную версию на crates.io
Если вам нужны дополнительные возможности (например, поддержка строковых регулярных выражений, работа с датами, или интеграция с SQL), используйте фичи:
[dependencies]
polars = { version = "0.44.0", features = ["lazy", "strings", "dtype-date", "dtype-datetime", "sql", "parquet", "csv"] }
Основные фичи:
lazy— ленивый API (обязательно для серьёзной работы)strings— методы для работы со строками (contains, replace, extract)dtype-date,dtype-datetime— поддержка типов датsql— SQL-интерфейсparquet— чтение/запись Parquetcsv— чтение/запись CSVis_in,rank,moment— дополнительные методы
После добавления зависимости выполните:
cargo build
Быстрый старт
Минимальный рабочий пример создания DataFrame и выполнения простого запроса:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Создаём DataFrame из векторов
let df = df! [
"name" => &["Alice", "Bob", "Charlie"],
"age" => &[25, 30, 35],
"salary" => &[50000, 60000, 70000]
]?;
println!("Original DataFrame:\n{:?}", df);
// Фильтрация: кто старше 28 лет
let filtered = df.clone().lazy()
.filter(col("age").gt(lit(28)))
.collect()?;
println!("Filtered (age > 28):\n{:?}", filtered);
// Агрегация: средняя зарплата
let avg_salary = df.clone().lazy()
.select([col("salary").mean().alias("avg_salary")])
.collect()?;
println!("Average salary:\n{:?}", avg_salary);
Ok(())
}
Этот код создаёт таблицу с тремя колонками, фильтрует строки по возрасту и вычисляет среднюю зарплату. Обратите внимание на использование lazy() — даже для простых операций рекомендуется использовать ленивый API, так как он позволяет строить цепочки трансформаций.
Основные методы и функции
1. df![] — макрос для создания DataFrame
Сигнатура: df![col1 => &[val1, val2, ...], col2 => &[val1, val2, ...]]
Что делает: Создаёт DataFrame из именованных колонок. Каждая колонка — это срез (&[]) или вектор. Макрос автоматически определяет типы данных.
use polars::prelude::*;
let df = df! [
"city" => &["Moscow", "SPb", "Kazan"],
"population" => &[12_000_000, 5_400_000, 1_300_000]
].unwrap();
2. CsvReader::new() — чтение CSV
Сигнатура: CsvReader::new(path: impl AsRef<Path>) -> CsvReader
Что делает: Создаёт ридер для загрузки данных из CSV-файла. Поддерживает настройку разделителя, пропуск строк, указание схемы и многое другое.
use polars::prelude::*;
let df = CsvReader::from_path("data.csv")?
.has_header(true)
.with_delimiter(b',')
.finish()?;
3. DataFrame::filter() — фильтрация строк
Сигнатура: fn filter(&self, mask: &ChunkedArray<BooleanType>) -> Result<DataFrame>
Что делает: Возвращает новый DataFrame, содержащий только строки, для которых маска (булевый массив) равна true. В lazy API используется .filter(col("col").gt(lit(10))).
// Eager API
let mask = df.column("age")?.i32()?.gt(30)?;
let filtered_df = df.filter(&mask)?;
// Lazy API (рекомендуется)
let filtered_df = df.clone().lazy()
.filter(col("age").gt(lit(30)))
.collect()?;
4. DataFrame::group_by() — группировка и агрегация
Сигнатура: fn group_by(&self, by: &[&str]) -> Result<GroupBy>
Что делает: Группирует DataFrame по указанным колонкам. После группировки можно применить агрегации: sum, mean, min, max, count, std и другие.
let grouped = df.clone().lazy()
.group_by([col("department")])
.agg([
col("salary").mean().alias("avg_salary"),
col("age").max().alias("max_age"),
col("employee_id").count().alias("employee_count")
])
.collect()?;
5. DataFrame::join() — объединение DataFrame
Сигнатура: fn join(&self, other: &DataFrame, left_on: &[&str], right_on: &[&str], how: JoinType) -> Result<DataFrame>
Что делает: Выполняет SQL-подобное объединение двух DataFrame по указанным колонкам. Поддерживаются типы: inner, left, outer, cross, anti, semi.
let joined = df_employees.clone().lazy()
.join(
df_departments.clone().lazy(),
[col("dept_id")],
[col("id")],
JoinType::Left
)
.collect()?;
6. DataFrame::with_column() — добавление/изменение колонки
Сигнатура: fn with_column(&self, column: Series) -> Result<DataFrame>
Что делает: Добавляет новую колонку в DataFrame или заменяет существующую. В lazy API используется .with_column(expr).
let df = df.clone().lazy()
.with_column(
(col("price") * col("quantity")).alias("total")
)
.collect()?;
7. DataFrame::sort() — сортировка
Сигнатура: fn sort(&self, by_column: &[&str], options: SortOptions) -> Result<DataFrame>
Что делает: Сортирует DataFrame по одной или нескольким колонкам. Можно указать порядок (возрастание/убывание) и поведение при NaN.
let sorted = df.clone().lazy()
.sort(
"salary",
SortOptions {
descending: true,
nulls_last: true,
..Default::default()
}
)
.collect()?;
8. DataFrame::unique() — удаление дубликатов
Сигнатура: fn unique(&self, subset: Option<&[&str]>, keep: UniqueKeepStrategy) -> Result<DataFrame>
Что делает: Возвращает DataFrame с уникальными строками. Можно указать подмножество колонок для проверки дубликатов и стратегию (first, last, none).
let unique_df = df.clone().lazy()
.unique(
Some(vec!["email"]),
UniqueKeepStrategy::First
)
.collect()?;
9. DataFrame::pivot() — создание сводной таблицы
Сигнатура: fn pivot(&self, values: &[&str], index: &[&str], columns: &[&str]) -> Result<DataFrame>
Что делает: Преобразует длинный формат данных в широкий (pivot). Аналог pivot_table в pandas.
let pivoted = df.clone().lazy()
.pivot(
values: ["sales"],
index: ["year"],
columns: ["product"]
)
.collect()?;
10. DataFrame::write_parquet() — запись в Parquet
Сигнатура: fn write_parquet(&self, path: impl AsRef<Path>, options: ParquetWriteOptions) -> Result<()>
Что делает: Сохраняет DataFrame в формате Apache Parquet — колоночный формат хранения, оптимизированный для аналитических нагрузок.
df.write_parquet(
"output.parquet",
ParquetWriteOptions::default()
)?;
Реальный практический пример
Рассмотрим задачу анализа продаж интернет-магазина. У нас есть два CSV-файла: orders.csv (заказы) и products.csv (товары). Нужно:
- Загрузить данные
- Объединить таблицы
- Рассчитать выручку по категориям товаров
- Найти топ-5 товаров по продажам
- Выявить сезонные тренды (по месяцам)
- Сохранить результаты в Parquet
use polars::prelude::*;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Загрузка данных
let orders = CsvReader::from_path("orders.csv")?
.has_header(true)
.with_try_parse_dates(true)
.finish()?;
let products = CsvReader::from_path("products.csv")?
.has_header(true)
.finish()?;
println!("Orders shape: {:?}", orders.shape());
println!("Products shape: {:?}", products.shape());
// 2. Очистка: удаляем заказы с нулевой ценой
let orders_clean = orders.clone().lazy()
.filter(col("price").gt(lit(0.0)))
.filter(col("quantity").gt(lit(0)))
.collect()?;
// 3. Объединение таблиц
let joined = orders_clean.clone().lazy()
.join(
products.clone().lazy(),
[col("product_id")],
[col("id")],
JoinType::Inner
)
.with_column(
(col("price") * col("quantity")).alias("revenue")
)
.collect()?;
println!("Joined data:\n{:?}", joined.head(Some(5)));
// 4. Выручка по категориям
let revenue_by_category = joined.clone().lazy()
.group_by([col("category")])
.agg([
col("revenue").sum().alias("total_revenue"),
col("order_id").count().alias("order_count"),
col("revenue").mean().alias("avg_order_value")
])
.sort(
"total_revenue",
SortOptions::default().with_descending(true)
)
.collect()?;
println!("Revenue by category:\n{:?}", revenue_by_category);
// 5. Топ-5 товаров по продажам
let top_products = joined.clone().lazy()
.group_by([col("product_name")])
.agg([
col("quantity").sum().alias("total_sold"),
col("revenue").sum().alias("total_revenue")
])
.sort(
"total_sold",
SortOptions::default().with_descending(true)
)
.limit(5)
.collect()