polars для Rust: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Библиотека 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 — чтение/запись Parquet
  • csv — чтение/запись CSV
  • is_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 (товары). Нужно:

  1. Загрузить данные
  2. Объединить таблицы
  3. Рассчитать выручку по категориям товаров
  4. Найти топ-5 товаров по продажам
  5. Выявить сезонные тренды (по месяцам)
  6. Сохранить результаты в 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()                    

Рекомендаций