Изучение фреймов данных Polars Rust, функций агрегации и не только

TLDR;

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

Эта рецензия служит продолжением этой серии, в которой мы демистифицируем мир полярников. В первой части этой серии мы узнали об объекте серии Rust Polars, вариантах его использования и многом другом. В этой части серии мы рассмотрим другую фундаментальную структуру данных Polars, а именно объект DataFrame. С помощью практических упражнений и фрагментов кода вы приобретете важные навыки, такие как выполнение различных операций с DataFrames среди прочего.

Примечание. В этой статье предполагается, что у вас есть базовые знания языка программирования Rust.

Блокнот под названием 4-polars-tutorial-part-2.ipynbбылразработан для этой статьи, которую можно найти в следующем репозитории:



Содержание (TOC)

Объект DataFrame
Индексирование и нарезка
Очистка данных
Показатели центральной тенденции
Ndarray
Агрегация Функции
Объединение фреймов данных
Заключение
Заключение
Ресурсы

Объект DataFrame

В основе библиотеки Polars лежит важный компонент, служащий ее основой; Структура DataFrame. Это гениальное представление двумерных данных организовано в виде строк и столбцов, аналогично объекту серии, но с дополнительными измерениями.

Инициализация кадра данных

В Polars инициализировать фрейм данных так же просто, как использовать мощную структуру DataFrame. Чтобы проиллюстрировать невероятную простоту инициализации DataFrame, давайте рассмотрим следующий фрагмент кода для создания пустого фрейма данных:

let df = DataFrame::default();

Теперь давайте подробно изучим гибкость библиотеки Polars. Взгляните на этот фрагмент кода, в котором ряды легко преобразуются в двухмерные кадры данных.

let s1 = Series::new("Name", &["Mahmoud", "Ali"]);
let s2 = Series::new("Age", &[23, 27]);
let s3 = Series::new("Height", &[1.84, 1.78]);
let df: PolarsResult<DataFrame> = DataFrame::new(vec![s1, s2, s3])?;
println!("{:?}", df.unwrap());

// Output:

// shape: (2, 3)
// ┌─────────┬─────┬────────┐
// │ Name    ┆ Age ┆ Height │
// │ ---     ┆ --- ┆ ---    │
// │ str     ┆ i32 ┆ f64    │
// ╞═════════╪═════╪════════╡
// │ Mahmoud ┆ 23  ┆ 1.84   │
// │ Ali     ┆ 27  ┆ 1.78   │
// └─────────┴─────┴────────┘

Процесс инициализации Polars DataFrame прост, как видно из его легкой реализации. Кроме того, макрос df! позволяет легко создавать фреймы данных. Возьмите следующее в качестве примера использования этого макроса:

let df: PolarsResult<DataFrame> = df!("Name" => &["Mahmoud", "Ali"],
                                      "Age" => &[23, 27],
                                      "Height" => &[1.84, 1.78]);

Описывать

Метод describe в Polars — это широко используемый метод, который обеспечивает обзор статистических показателей для наборов данных. Этот метод создает исчерпывающую таблицу, состоящую из числа, среднего, стандартного отклонения, минимума и максимума. и от 25-го процентиля до 75-го процентиля диапазона ( медиана) для каждого столбца в наборе данных. Используя этот метод, вы можете получить ценную информацию о характеристиках ваших данных, например, выявить потенциальные выбросы и эффективно понять схему их распределения.

let df1: DataFrame = df!("categorical" => &["d","e","f"],
                         "numeric" => &[1, 2, 3],
                         "object" => &["a", "b", "c"]).unwrap();
println!("{}", df1);

// Output:

// shape: (3, 3)
// ┌─────────────┬─────────┬────────┐
// │ categorical ┆ numeric ┆ object │
// │ ---         ┆ ---     ┆ ---    │
// │ str         ┆ i32     ┆ str    │
// ╞═════════════╪═════════╪════════╡
// │ d           ┆ 1       ┆ a      │
// │ e           ┆ 2       ┆ b      │
// │ f           ┆ 3       ┆ c      │
// └─────────────┴─────────┴────────┘

let df2: DataFrame = df1.describe(None).unwrap();
println!("{}", df2);

// Output:

// shape: (9, 4)
// ┌────────────┬─────────────┬─────────┬────────┐
// │ describe   ┆ categorical ┆ numeric ┆ object │
// │ ---        ┆ ---         ┆ ---     ┆ ---    │
// │ str        ┆ str         ┆ f64     ┆ str    │
// ╞════════════╪═════════════╪═════════╪════════╡
// │ count      ┆ 3           ┆ 3.0     ┆ 3      │
// │ null_count ┆ 0           ┆ 0.0     ┆ 0      │
// │ mean       ┆ null        ┆ 2.0     ┆ null   │
// │ std        ┆ null        ┆ 1.0     ┆ null   │
// │ …          ┆ …           ┆ …       ┆ …      │
// │ 25%        ┆ null        ┆ 1.5     ┆ null   │
// │ 50%        ┆ null        ┆ 2.0     ┆ null   │
// │ 75%        ┆ null        ┆ 2.5     ┆ null   │
// │ max        ┆ f           ┆ 3.0     ┆ c      │
// └────────────┴─────────────┴─────────┴────────┘

Голова

Как и объект серии, метод head позволяет нам быстро просмотреть первые несколько строк объекта DataFrame. Этот метод экономит время и усилия, поскольку устраняет необходимость прокручивать многочисленные записи, что может быть утомительным и утомительным. При вызове эта функция возвращает новый DataFrame, содержащий n строк из исходного набора данных на основе пользовательских параметров. По умолчанию десять (10) строк отображаются, когда None передается в этот метод. Рассмотрим следующий пример:

let df: DataFrame = df!("Name" => &["Mahmoud", "Bob"],
                        "Age" => &[23, 27],
                        "Height" => &[1.84, 1.78]).unwrap();
println!("{}", df.head(None));

// Output:

// shape: (2, 3)
// ┌─────────┬─────┬────────┐
// │ Name    ┆ Age ┆ Height │
// │ ---     ┆ --- ┆ ---    │
// │ str     ┆ i32 ┆ f64    │
// ╞═════════╪═════╪════════╡
// │ Mahmoud ┆ 23  ┆ 1.84   │
// │ Bob     ┆ 27  ┆ 1.78   │
// └─────────┴─────┴────────┘

По умолчанию метод head отображает первые десять строк, но его можно настроить для отображения любого числа с помощью аргумента. Например, df.head(Some(3)) вернет только первые три строки данных. Эта функция позволяет нам проверять имена и содержимое столбцов, предоставляя обзор того, что внутри, прежде чем углубляться в анализ.

Хвост

Так же, как и ряды, функция tail в Polars — это мощный метод, который позволяет просматривать последние несколько строк любого объекта DataFrame. Например, если ваш DataFrame содержит информацию о сотрудниках, такую ​​как их имена, возраст и рост; использование этого метода позволит вам быстро проверить данные и структуру столбца.

let df: DataFrame = df!("Name" => &["Mahmoud", "Bob"],
                        "Age" => &[23, 27],
                        "Height" => &[1.84, 1.78]).unwrap();
println!("{}", df.tail(None));

// Output:

// shape: (2, 3)
// ┌─────────┬─────┬────────┐
// │ Name    ┆ Age ┆ Height │
// │ ---     ┆ --- ┆ ---    │
// │ str     ┆ i32 ┆ f64    │
// ╞═════════╪═════╪════════╡
// │ Mahmoud ┆ 23  ┆ 1.84   │
// │ Bob     ┆ 27  ┆ 1.78   │
// └─────────┴─────┴────────┘

По умолчанию метод tail отображает последние десять строк вашего набора данных, но его можно настроить, указав аргумент, указывающий, сколько строк должно отображаться вместо этого. Чтобы проиллюстрировать далее: df.tail(Some(3)) будет отображать только последние три строки из нашего примера фрейма данных сотрудников.

По сути, использование tail в DataFrames помогает сэкономить время при проверке содержимого или получении информации о его общей структуре. Он предоставляет ценную информацию с первого взгляда без необходимости просматривать каждую строку вручную!

Индексирование и нарезка

В отличие от серий, объект DataFrame можно индексировать с помощью квадратных скобок []:

// Create a sample DataFrame
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 29],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[50000, 60000, 250000]).unwrap();

// Indexing using brackets
// Select a single column by name

let name_col = &df["Name"];
// Or
let name_col1 = &df[0];
println!("{:?}", name_col);
println!("{:?}", name_col1);

// Select a single column by name

// Output:

// shape: (3,)
// Series: 'Name' [str]
// [
//     "Mahmoud"
//     "Ali"
//     "ThePrimeagen"
// ]

// Select multiple columns by slicing

subset = &df[..2];
println!("{:?}", subset);

// Output:

// [shape: (3,)
// Series: 'Name' [str]
// [
//     "Mahmoud"
//     "Ali"
//     "ThePrimeagen"
// ],shape: (3,)
// Series: 'Age' [i32]
// [
//     22
//     30
//     29
// ]]

В этом примере мы создали фрейм данных, состоящий из четырех столбцов: «Имя», «Возраст», «Пол» и «Зарплата». Затем мы продемонстрировали различные методы индексации фрейма данных с помощью скобок. Чтобы извлечь один столбец на основе его имени, мы использовали df[‘Name’]. Этот метод возвращает серию Polars, состоящую из всех значений, принадлежащих указанному столбцу — в нашем случае это был столбец «Имя». Использование таких методов очень полезно, когда требуется конкретная информация из соответствующих кадров данных.

Впоследствии путем нарезки с помощью df[..2] мы выбрали только определенные подмножества столбцов, что привело к созданию еще одного нового DataFrame, содержащий только первые два столбца: `Имя` и `Возраст`. Такие быстрые, но эффективные способы идеально подходят для простого выбора нескольких требуемых атрибутов из любого заданного фрейма данных. Точно так же мы можем выбрать подмножество столбцов с помощью метода select, и, например, вызов df.select([“Name”, “Age”]) вернет только столбцы Имя и Пол.

let name_age_cols = df.select(["Name", "Age"]).unwrap();
println!("{:?}", name_age_cols);

// Output:

// shape: (3, 2)
// ┌──────────────┬─────┐
// │ Name         ┆ Age │
// │ ---          ┆ --- │
// │ str          ┆ i32 │
// ╞══════════════╪═════╡
// │ Mahmoud      ┆ 22  │
// │ Ali          ┆ 25  │
// │ ThePrimeagen ┆ 29  │
// └──────────────┴─────┘

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

let name_col = df.column("Name");
println!("{:?}", name_col);

// Output:

// shape: (3,)
// Series: 'Name' [str]
// [
//     "Mahmoud"
//     "Ali"
//     "ThePrimeagen"
// ]

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

// Create a sample DataFrame
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[50000, 60000, 250000]).unwrap();

let mask = df.column("Age").expect("Age must exist!").gt(25).unwrap();
let filtered_data = df.filter(&mask).unwrap();

println!("{:?}", filtered_data);

// Output:

// shape: (1, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name         ┆ Age ┆ Gender ┆ Salary │
// │ ---          ┆ --- ┆ ---    ┆ ---    │
// │ str          ┆ i32 ┆ str    ┆ i32    │
// ╞══════════════╪═════╪════════╪════════╡
// │ ThePrimeagen ┆ 36  ┆ M      ┆ 250000 │
// └──────────────┴─────┴────────┴────────┘

Кроме того, метод slice позволяет нам выбирать определенные подмножества строк и столбцов из объекта фрейма данных. Например, если мы используем df.slice(2,3), будут возвращены три строки, начиная с индекса два (с использованием индексации с отсчетом от нуля). Кроме того, этот выбор будет включать все столбцы, что приведет к совершенно новому фрейму данных, состоящему из трех строк, если он существует, и четырех столбцов.

println!("{:?}", df.slice(2, 3));

// Output:

// shape: (1, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name         ┆ Age ┆ Gender ┆ Salary │
// │ ---          ┆ --- ┆ ---    ┆ ---    │
// │ str          ┆ i32 ┆ str    ┆ i32    │
// ╞══════════════╪═════╪════════╪════════╡
// │ ThePrimeagen ┆ 36  ┆ M      ┆ 250000 │
// └──────────────┴─────┴────────┴────────┘

Альтернативой является использование функции transpose, которая переворачивает строки и столбцы матрицы. Это позволяет нам получить доступ к одной строке как к серии посредством индексации ее транспонированной формы.

// Create a sample DataFrame
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[50000, 60000, 250000]).unwrap();

println!("{:?}", df.transpose().unwrap()[0]);

// Output:

// shape: (4,)
// Series: 'column_0' [str]
// [
//     "Mahmoud"
//     "22"
//     "M"
//     "50000"
// ]

Обратите внимание, что это очень дорогая операция, как указано в документации.

Очистка данных

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

Количество нулей

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

// Create a sample DataFrame
let df = df!("Name" => &[Some("Mahmoud"),  None, None],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[50000, 60000, 250000]).unwrap();

println!("{:?}", df.null_count());

// Output:

// shape: (1, 4)
// ┌──────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ ---  ┆ --- ┆ ---    ┆ ---    │
// │ u32  ┆ u32 ┆ u32    ┆ u32    │
// ╞══════╪═════╪════════╪════════╡
// │ 2    ┆ 0   ┆ 0      ┆ 0      │
// └──────┴─────┴────────┴────────┘

Дубликаты

Используя этот метод, можно получить логическую маску, которая указывает все реплицированные строки в вашем DataFrame. Эта конкретная маска служит эффективным инструментом для фильтрации этих дубликатов и точного получения нового фрейма данных. Чтобы использовать функцию is_duplicated, вызовите ее в своем DataFrame и назначьте полученную маску новой переменной. Впоследствии примените этот же фильтр к исходному фрейму данных, чтобы удалить эти реплики.

let df = df!("Name" => &["Mahmoud",  "Mahmoud", "ThePrimeagen"],
             "Age" => &[22, 22, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[50000, 50000, 250000]).unwrap();
let mask = df.is_duplicated().unwrap();
let filtered_data = df.filter(&mask).unwrap();
println!("{:?}", filtered_data);

// Output:

// shape: (2, 4)
// ┌─────────┬─────┬────────┬────────┐
// │ Name    ┆ Age ┆ Gender ┆ Salary │
// │ ---     ┆ --- ┆ ---    ┆ ---    │
// │ str     ┆ i32 ┆ str    ┆ i32    │
// ╞═════════╪═════╪════════╪════════╡
// │ Mahmoud ┆ 22  ┆ M      ┆ 50000  │
// │ Mahmoud ┆ 22  ┆ M      ┆ 50000  │
// └─────────┴─────┴────────┴────────┘

Уникальные ценности

Метод is_unique позволяет определить, содержит ли каждая строка в вашем DataFrame уникальные значения. Этот метод позволяет вам получить маску всех отдельных строк, присутствующих в вашем наборе данных, что может быть особенно полезно при работе с обширными данными или выполнении над ними сложных операций.

Чтобы применить этот метод, просто вызовите функцию is_unique для объекта DataFrame. Он сгенерирует логический массив, который выделяет те строки, которые содержат уникальные элементы. Затем вы можете использовать этот массив в качестве механизма фильтрации для эффективного извлечения только уникальных строк из исходного DataFrame.

let df = df!("Name" => &["Mahmoud",  "Mahmoud", "ThePrimeagen"],
             "Age" => &[22, 22, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[50000, 50000, 250000]).unwrap();
let mask = df.is_unique().unwrap();
let filtered_data = df.filter(&mask).unwrap();
println!("{:?}", filtered_data);

// Output:

// shape: (1, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name         ┆ Age ┆ Gender ┆ Salary │
// │ ---          ┆ --- ┆ ---    ┆ ---    │
// │ str          ┆ i32 ┆ str    ┆ i32    │
// ╞══════════════╪═════╪════════╪════════╡
// │ ThePrimeagen ┆ 36  ┆ M      ┆ 250000 │
// └──────────────┴─────┴────────┴────────┘

Уронить

Удаление ненужной информации из фрейма или ряда данных имеет важное значение при анализе данных. К счастью, Polars предлагает несколько эффективных способов сделать это. Один из таких методов включает использование функции drop, которая позволяет исключить определенные строки или столбцы.

Чтобы использовать этот метод, укажите имя/метку целевого столбца в качестве аргумента для метода drop. Стоит отметить, что по умолчанию эта функция возвращает новый объект DataFrame с удалением только указанных строк, оставляя исходный DataFrame нетронутым. Это может быть особенно полезно для новичков, которые могут ожидать, что их исходный набор данных изменится навсегда после запуска определенных функций. Например, давайте возьмем пример объекта Fruit и Color на основе DataFrame, где столбец «Color» больше не требуется в дальнейших анализах:

let df: DataFrame = df!("Fruit" => &["Apple", "Apple", "Pear"],
                        "Color" => &["Red", "Yellow", "Green"]).unwrap();

Мы можем использовать функцию drop, чтобы удалить столбец с меткой «Color» из этого фрейма данных:

let df_remain = df.drop("Color").unwrap(); 
println!("{}", df_remain);

// Output:

// shape: (3, 1)
// ┌───────┐
// │ Fruit │
// │ ---   │
// │ str   │
// ╞═══════╡
// │ Apple │
// │ Apple │
// │ Pear  │
// └───────┘

Новый объект DataFrame, df_remain, теперь содержит данные, идентичные исходным, за исключением столбца «Color». После проверки исходного фрейма данных мы можем подтвердить, что его информация остается неизменной.

println!("{}", df); // the original DataFrame

// Output:

// shape: (3, 2)
// ┌───────┬────────┐
// │ Fruit ┆ Color  │
// │ ---   ┆ ---    │
// │ str   ┆ str    │
// ╞═══════╪════════╡
// │ Apple ┆ Red    │
// │ Apple ┆ Yellow │
// │ Pear  ┆ Green  │
// └───────┴────────┘

Если вы хотите внести изменения непосредственно в исходный DataFrame, рассмотрите возможность использования функции drop_in_place вместо drop. Этот метод работает аналогично drop, но изменяет фрейм данных без создания нового объекта.

let mut df: DataFrame = df!("Fruit" => &["Apple", "Apple", "Pear"],
                            "Color" => &["Red", "Yellow", "Green"]).unwrap();
df.drop_in_place("Color"); // remove the row with index 1 ("Color") from df
println!("{:?}", df);

// Output:

// shape: (3, 1)
// ┌───────┐
// │ Fruit │
// │ ---   │
// │ str   │
// ╞═══════╡
// │ Apple │
// │ Apple │
// │ Pear  │
// └───────┘

Кроме того, вы также можете удалить несколько столбцов, указав их имена в качестве аргументов для функции drop_many:

let df_dropped_col = df.drop_many(&["Color", ""]);
println!("{:?}", df_dropped_col);

// Output:

// shape: (3, 1)
// ┌───────┐
// │ Fruit │
// │ ---   │
// │ str   │
// ╞═══════╡
// │ Apple │
// │ Apple │
// │ Pear  │
// └───────┘

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

let df: DataFrame = df!("Fruit" => &["Apple", "Apple", "Pear"],
                        "Color" => &[Some("Red"), None, None]).unwrap();
let df_clean = df.drop_nulls::<String>(None).unwrap();
println!("{:?}", df_clean);

// Output:

// shape: (1, 2)
// ┌───────┬───────┐
// │ Fruit ┆ Color │
// │ ---   ┆ ---   │
// │ str   ┆ str   │
// ╞═══════╪═══════╡
// │ Apple ┆ Red   │
// └───────┴───────┘

Используя метод is_not_null, мы можем создать ненулевую маску для любого столбца в нашем DataFrame. Этот метод возвращает логическую маску, которая различает значения, содержащие null, и значения без них. После применения к определенному столбцу создается фильтр, в котором каждое значение соответствует статусу соответствующей строки: null или not null. Используя этот эффективный метод для извлечения только строк, соответствующих определенным критериям, мы можем легко удалить все экземпляры отсутствующих данных из нашего нового DataFrame. Например, чтобы создать нулевую маску для столбца «Зарплата» в DataFrame, мы можем использовать следующий код:

let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[Some(50000), Some(60000), None]).unwrap();
let mask = df.column("Salary").expect("Salary must exist!").is_not_null();
println!("{:?}", mask.head(None));

// Output:

// shape: (3,)
// ChunkedArray: 'Age' [bool]
// [
//     true
//     true
//     false
// ]

Фрагмент кода создает пустую маску для столбца «Зарплата» в объекте df DataFrame. Он также отображает некоторые начальные значения булевой маски, созданной в качестве вывода. Этот фильтр можно применять для извлечения данных только из строк, в которых есть ненулевые записи в столбце «Зарплата».

let filtered_data = df.filter(&mask).unwrap();
println!("{:?}", filtered_data);

// Output:

// shape: (2, 4)
// ┌─────────┬─────┬────────┬────────┐
// │ Name    ┆ Age ┆ Gender ┆ Salary │
// │ ---     ┆ --- ┆ ---    ┆ ---    │
// │ str     ┆ i32 ┆ str    ┆ i32    │
// ╞═════════╪═════╪════════╪════════╡
// │ Mahmoud ┆ 22  ┆ M      ┆ 50000  │
// │ Ali     ┆ 25  ┆ M      ┆ 60000  │
// └─────────┴─────┴────────┴────────┘

Использование нулевой маски обеспечивает большую точность в управлении процессом фильтрации. Этот метод оказывается полезным, когда мы стремимся фильтровать на основе различных условий или комбинаций нулевых значений в разных столбцах. Тем не менее, это влечет за собой написание большего количества кода, чем простое использование функции drop_nulls, и может быть не столь эффективным для больших наборов данных.

Подводя итог этому разделу, можно сказать, что удаление строк — это распространенная и удобная операция для удаления строк или столбцов из фрейма данных в Polars. Существует несколько вариантов изменения исходных данных или удаления нулевых значений.

Наполнять

Polars представляет ценный метод обработки отсутствующих данных — метод fill_null. Эта функция позволяет заменять нулевые или отсутствующие значения в объекте DataFrame или Series назначенным методом или значением. Частое применение fill_null заключается в замене всех ошибочных записей в пределах DataFrame или Серии одним единственным значением. Этого можно добиться, передав скалярный параметр в fill_null. Например, если вы хотите заменить каждую отсутствующую запись в вашем DataFrame предыдущими значениями, просто используйте fill_null, как показано ниже:

let mut df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[Some(50000), Some(60000), None]).unwrap();

let filtered_nulls = df.fill_null(FillNullStrategy::Forward(None)).unwrap();
    
println!("{:?}", filtered_nulls);

// Output:

// shape: (3, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name         ┆ Age ┆ Gender ┆ Salary │
// │ ---          ┆ --- ┆ ---    ┆ ---    │
// │ str          ┆ i32 ┆ str    ┆ i32    │
// ╞══════════════╪═════╪════════╪════════╡
// │ Mahmoud      ┆ 22  ┆ M      ┆ 50000  │
// │ Ali          ┆ 25  ┆ M      ┆ 60000  │
// │ ThePrimeagen ┆ 36  ┆ M      ┆ 60000  │
// └──────────────┴─────┴────────┴────────┘

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

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

Меры центральной тенденции

Иметь в виду

Как и в случае с Series, мы можем вычислить среднее значение каждого отдельного столбца в заданном фрейме данных.

let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[Some(50000), Some(60000), None]).unwrap();

println!("{:?}", df.mean());

// Output:

// shape: (1, 4)
// ┌──────┬───────────┬────────┬─────────┐
// │ Name ┆ Age       ┆ Gender ┆ Salary  │
// │ ---  ┆ ---       ┆ ---    ┆ ---     │
// │ str  ┆ f64       ┆ str    ┆ f64     │
// ╞══════╪═══════════╪════════╪═════════╡
// │ null ┆ 27.666667 ┆ null   ┆ 55000.0 │
// └──────┴───────────┴────────┴─────────┘

медиана

Мы также можем вычислить медиану каждого отдельного столбца в заданном фрейме данных.

let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[Some(50000), Some(60000), None]).unwrap();

println!("{:?}", df.median());

// Output:

// shape: (1, 4)
// ┌──────┬──────┬────────┬─────────┐
// │ Name ┆ Age  ┆ Gender ┆ Salary  │
// │ ---  ┆ ---  ┆ ---    ┆ ---     │
// │ str  ┆ f64  ┆ str    ┆ f64     │
// ╞══════╪══════╪════════╪═════════╡
// │ null ┆ 25.0 ┆ null   ┆ 55000.0 │
// └──────┴──────┴────────┴─────────┘

Меры распространения

стандарт

let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[Some(50000), Some(60000), None]).unwrap();

println!("{:?}", df.std(1));

// Output:

// shape: (1, 4)
// ┌──────┬──────────┬────────┬─────────────┐
// │ Name ┆ Age      ┆ Gender ┆ Salary      │
// │ ---  ┆ ---      ┆ ---    ┆ ---         │
// │ str  ┆ f64      ┆ str    ┆ f64         │
// ╞══════╪══════════╪════════╪═════════════╡
// │ null ┆ 7.371115 ┆ null   ┆ 7071.067812 │
// └──────┴──────────┴────────┴─────────────┘

вар

let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[Some(50000), Some(60000), None]).unwrap();

println!("{:?}", df.var(1));

// Output:

// shape: (1, 4)
// ┌──────┬───────────┬────────┬────────┐
// │ Name ┆ Age       ┆ Gender ┆ Salary │
// │ ---  ┆ ---       ┆ ---    ┆ ---    │
// │ str  ┆ f64       ┆ str    ┆ f64    │
// ╞══════╪═══════════╪════════╪════════╡
// │ null ┆ 54.333333 ┆ null   ┆ 5e7    │
// └──────┴───────────┴────────┴────────┘

Ндаррай

Вы можете преобразовать фрейм данных в ndarray, как мы видели в первой статьеэтой серии. Этот метод создает 2D-объект ndarray::Array из объекта DataFrame. Это требует, чтобы все столбцы в DataFrame были ненулевыми и числовыми. Они будут преобразованы в один и тот же тип данных (если это еще не сделано). Он неявно преобразует None в NaN без сбоев для данных с плавающей запятой.

let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
             "Age" => &[22, 25, 36],
             "Gender" => &["M", "M", "M"],
             "Salary" => &[Some(50000), Some(60000), None]).unwrap();

println!("{:?}", df.to_ndarray::<Float64Type>().unwrap());

// Output:

// [[NaN, 22.0, NaN, 50000.0],
//  [NaN, 25.0, NaN, 60000.0],
//  [NaN, 36.0, NaN, NaN]], shape=[3, 4], strides=[1, 3], layout=Ff (0xa), const ndim=2

Теперь вы можете применять к этому массиву различные операции, описанные в предыдущей статье под названием The Ultimate Ndarray Handbook: Mastering the Art of Scientific Computing with Rust.

Функции агрегации

При работе с большими объемами данных очень важно классифицировать и понимать агрегированные данные на групповом уровне. К счастью, Polars предлагает отличное решение благодаря функции groupby. Этот метод разбивает фрейм данных на фрагменты на основе определенных значений ключа перед применением вычислений и объединением результатов обратно в другой фрейм данных — известный как шаблон разделить-применить-объединить.

С помощью функций агрегирования мы можем быстро выполнять различные операции, такие как счет, сумма или среднее внутри этих групп; это значительно ускоряет время вычислений при эффективной работе с массивными наборами данных. Другими распространенными примерами функций агрегирования являются дисперсия и стандартная. Вот несколько примеров использования .groupby:

  • Розничная компания использует метод groupby для анализа данных о продажах по регионам и категориям товаров. Этот анализ позволяет им определить, какие продукты хорошо продаются в каких регионах, и принимать более обоснованные решения относительно управления запасами и продвижения товаров.
let sales_revenue_df: DataFrame = sales_df.groupby(["Region", "Product_Category"]).expect("Columns must exist!").select(["Sales Revenue"]).sum().unwrap();
  • Используя метод groupby, организация здравоохранения может анализировать данные о пациентах в зависимости от возрастной группы и состояния здоровья.
let patient_by_age_condition_df: DataFrame = patients_data.groupby(["Age_Group", "Condition"]).expect("Columns must exist!").select(["Patient ID", "Length of Stay"]).count().unwrap();
  • Транспортное предприятие может использовать метод groupby для анализа расхода топлива своим транспортным средством на основе водителя и категории транспортного средства. Этот анализ позволяет им выявлять неэффективность в потреблении газа, что позволяет принимать оперативные корректирующие меры, повышающие эффективность использования топлива.
let average_fuel_consumption_df: DataFrame = fuel_data.groupby(["Vehicle_Type", "Driver"]).expect("Columns must exist!").select(["Fuel Consumption"]).mean().unwrap();
  • Используя метод groupby, страховая компания может эффективно анализировать данные о претензиях на основе типа полиса и демографических данных клиентов. Этот анализ позволяет им выявлять клиентов с высоким уровнем риска и разрабатывать политики, отвечающие их индивидуальным требованиям.
let claims_amount_df: DataFrame = claims_data.groupby(["Policy_Type", "Customer_Demographics"]).expect("Columns must exist!").select(["Claims Amount"]).sum().unwrap();

Таким образом, метод .groupby — это мощный инструмент для анализа данных, который позволяет вам группировать ваши данные любым мыслимым образом и применять любой тип функции независимо к каждой группе, прежде чем возвращать один набор данных.

Пример агрегации

Давайте углубимся в набор данных о рейсах, размещенный на Kaggle, и выполним базовую агрегацию, которая включает только один столбец группировки, один столбец агрегации и отдельную функцию агрегации. Наша цель — определить среднюю задержку прибытия для каждой авиакомпании. Polars предлагает различные варианты синтаксиса для создания такой агрегации, которые мы рассмотрим в этом разделе.

use std::path::Path;
use polars::prelude::*;

fn read_data_frame_from_csv(
    csv_file_path: &Path,
) -> DataFrame {
    CsvReader::from_path(csv_file_path)
        .expect("Cannot open file.")
        .has_header(true)
        .finish()
        .unwrap()
}

let flights_file_path: &Path = Path::new("/path/to/Flight_on_time_HIX.csv");
let columns = ["Airline", "Origin_Airport", "Destination_Airport", "Departure_Delay_Minutes", "Arrival_Delay_Minutes"];
let flights_df: DataFrame = read_data_frame_from_csv(flights_file_path).select(columns).unwrap();
println!("{:?}", flights_df.head(Some(5)));

// Output:

// shape: (5, 5)
// ┌─────────┬────────────────┬─────────────────────┬─────────────────────────┬───────────────────────┐
// │ Airline ┆ Origin_Airport ┆ Destination_Airport ┆ Departure_Delay_Minutes ┆ Arrival_Delay_Minutes │
// │ ---     ┆ ---            ┆ ---                 ┆ ---                     ┆ ---                   │
// │ str     ┆ str            ┆ str                 ┆ i64                     ┆ i64                   │
// ╞═════════╪════════════════╪═════════════════════╪═════════════════════════╪═══════════════════════╡
// │ TR      ┆ IYF            ┆ HIX                 ┆ 62                      ┆ 52                    │
// │ TR      ┆ HEN            ┆ HIX                 ┆ 15                      ┆ 8                     │
// │ RO      ┆ HIX            ┆ IZN                 ┆ 0                       ┆ 0                     │
// │ XM      ┆ HIX            ┆ IZU                 ┆ 34                      ┆ 44                    │
// │ XM      ┆ HIX            ┆ LKF                 ┆ 144                     ┆ 146                   │
// └─────────┴────────────────┴─────────────────────┴─────────────────────────┴───────────────────────┘

Чтобы эффективно сгруппировать данные в DataFrame, важно определить столбцы группировки, такие как Airline, и выбрать функцию агрегирования, такую ​​как mean для столбца Arrival_Delay_Minutes. Как только это будет сделано, просто поместите столбец группировки в метод groupby и выберите нужный столбец отображения, прежде чем применять к нему функцию агрегирования. Это создаст новый DataFrame.

let arr_delay_mean_df: DataFrame = flights_df.groupby(["Airline"]).expect("Airline Column must exist!").select(["Arrival_Delay_Minutes"]).mean().unwrap();
println!("{:?}", arr_delay_mean_df.head(Some(5)));
// Output:

// shape: (5, 2)
// ┌─────────┬────────────────────────────┐
// │ Airline ┆ Arrival_Delay_Minutes_mean │
// │ ---     ┆ ---                        │
// │ str     ┆ f64                        │
// ╞═════════╪════════════════════════════╡
// │ UG      ┆ 34.374332                  │
// │ WC      ┆ 158.221406                 │
// │ TR      ┆ 281.309919                 │
// │ TO      ┆ 24.833333                  │
// │ YJ      ┆ 11.839243                  │
// └─────────┴────────────────────────────┘

С несколькими столбцами возможны группировка и агрегация. Однако синтаксис отличается от синтаксиса операции с одним столбцом. Чтобы обеспечить бесперебойное выполнение любого типа процесса групповой функции, важно определить три ключевых компонента: функции агрегации, группирующие столбцы и агрегированные столбцы. Например; в этом примере мы рассчитываем среднюю задержку вылета по авиакомпаниям в аэропорт отправления.

let dep_delay_mean_def: DataFrame = flights_df.groupby(["Airline", "Origin_Airport"]).expect("Airline and Origin_Airport Columns must exist!").select(["Departure_Delay_Minutes"]).mean().unwrap();
println!("{:?}", dep_delay_mean_def.head(Some(5)));
// Output:

// shape: (5, 3)
// ┌─────────┬────────────────┬──────────────────────────────┐
// │ Airline ┆ Origin_Airport ┆ Departure_Delay_Minutes_mean │
// │ ---     ┆ ---            ┆ ---                          │
// │ str     ┆ str            ┆ f64                          │
// ╞═════════╪════════════════╪══════════════════════════════╡
// │ TR      ┆ ERM            ┆ 9.7890625                    │
// │ NR      ┆ ULZ            ┆ 29.857143                    │
// │ RO      ┆ VYM            ┆ 10.722222                    │
// │ TJ      ┆ ERR            ┆ 20.290323                    │
// │ NR      ┆ XNL            ┆ 16.351064                    │
// └─────────┴────────────────┴──────────────────────────────┘

Если вы знакомы с пандами Python, то использование groupby приведет к созданию объекта MultiIndex. Наличие мультииндексов можно найти как в самом индексе, так и в самих столбцах. Однако Polars полностью устраняет эту проблему, не требуя никаких подобных операций от разработчиков, что делает его более выгодной альтернативой Pandas для целей обработки данных.

Слияние фреймов данных

Polars предлагает набор инструментов для обработки данных для выполнения таких задач, как слияние наборов данных. Одним из таких инструментов является метод соединения, который упрощает объединение различных объектов DataFrame. Чтобы выполнить эту операцию, вам нужно вызвать функцию join на любом из DataFrames и указать вместе с ней другие параметры. Чтобы лучше понять, как это работает на практике, рассмотрим следующий пример кода.

let df3: DataFrame = df1.join(other=&df2, left_on=["variable1"], right_on=["variable2"], how=JoinType::Inner, suffix=None).unwrap();

При выполнении операции слияния параметр how играет важную роль в определении типа слияния. У вас есть несколько вариантов на выбор, включая внутреннее, левое, правое и внешнее соединения.

Чтобы точно определить переменные, которые будут служить ключами соединения для каждого кадра данных, используйте параметры left_on и right_on соответственно. Эти конкретные значения упрощают связывание соответствующих строк из обоих фреймов данных.

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

Наконец, если два столбца имеют одинаковые имена в разных наборах данных, которые объединяются вместе, то с помощью суффиксов можно легко различать их, добавляя уникальные строки в конце соответствующих заголовков столбцов. Вот пример использования функции join:

let df1: DataFrame = df!("Carrier" => &["HA", "EV", "VX", "DL"],
                         "ArrDelay" => &[-3, 28, 0, 1]).unwrap();
let df2: DataFrame = df!("Airline" => &["HA", "EV", "OO", "VX"],
                         "DepDelay" => &[21, -8, 11, -4]).unwrap();

let df3: DataFrame = df1.join(&df2, ["Carrier"], ["Airline"], JoinType::Inner, None).unwrap();
// or: let df3: DataFrame = df1.inner_join(&df2, ["Carrier"], ["Airline"]).unwrap();
println!("{:?}", df3.head(Some(5)));

// Output:

// shape: (3, 3)
// ┌─────────┬──────────┬──────────┐
// │ Carrier ┆ ArrDelay ┆ DepDelay │
// │ ---     ┆ ---      ┆ ---      │
// │ str     ┆ i32      ┆ i32      │
// ╞═════════╪══════════╪══════════╡
// │ HA      ┆ -3       ┆ 21       │
// │ EV      ┆ 28       ┆ -8       │
// │ VX      ┆ 0        ┆ -4       │
// └─────────┴──────────┴──────────┘

При объединении DataFrames в Polars важно понимать различные доступные типы соединений. Внутренние соединения сохраняют только те строки, которые общие между обоими фреймами данных, в то время как левое и правое соединения сохраняют все строки из одного фрейма данных и добавить соответствующие данные из другого на основе совпадающих значений.

let df1: DataFrame = df!("Carrier" => &["HA", "EV", "VX", "DL"],
                         "ArrDelay" => &[-3, 28, 0, 1]).unwrap();
let df2: DataFrame = df!("Airline" => &["HA", "EV", "OO", "VX"],
                         "DepDelay" => &[21, -8, 11, -4]).unwrap();

// left join
let df3: DataFrame = df1.left_join(&df2, ["Carrier"], ["Airline"]).unwrap();
println!("{:?}", df3.head(Some(5)));

// Right join
let df4: DataFrame = df2.left_join(&df1, ["Airline"], ["Carrier"]).unwrap();
println!("{:?}", df4.head(Some(5)));

let df5: DataFrame = df1.outer_join(&df2, ["Carrier"], ["Airline"]).unwrap();
println!("{:?}", df5.head(Some(5)));

// Output:

// Left Join
// shape: (4, 3)
// ┌─────────┬──────────┬──────────┐
// │ Carrier ┆ ArrDelay ┆ DepDelay │
// │ ---     ┆ ---      ┆ ---      │
// │ str     ┆ i32      ┆ i32      │
// ╞═════════╪══════════╪══════════╡
// │ HA      ┆ -3       ┆ 21       │
// │ EV      ┆ 28       ┆ -8       │
// │ VX      ┆ 0        ┆ -4       │
// │ DL      ┆ 1        ┆ null     │
// └─────────┴──────────┴──────────┘



// Right Join
// shape: (4, 3)
// ┌─────────┬──────────┬──────────┐
// │ Airline ┆ DepDelay ┆ ArrDelay │
// │ ---     ┆ ---      ┆ ---      │
// │ str     ┆ i32      ┆ i32      │
// ╞═════════╪══════════╪══════════╡
// │ HA      ┆ 21       ┆ -3       │
// │ EV      ┆ -8       ┆ 28       │
// │ OO      ┆ 11       ┆ null     │
// │ VX      ┆ -4       ┆ 0        │
// └─────────┴──────────┴──────────┘


// Outer Join
// shape: (5, 3)
// ┌─────────┬──────────┬──────────┐
// │ Carrier ┆ ArrDelay ┆ DepDelay │
// │ ---     ┆ ---      ┆ ---      │
// │ str     ┆ i32      ┆ i32      │
// ╞═════════╪══════════╪══════════╡
// │ HA      ┆ -3       ┆ 21       │
// │ EV      ┆ 28       ┆ -8       │
// │ OO      ┆ null     ┆ 11       │
// │ VX      ┆ 0        ┆ -4       │
// │ DL      ┆ 1        ┆ null     │
// └─────────┴──────────┴──────────┘

Если нет соответствующих данных для строки в DataFrame, нулевые или отсутствующие значения будут заполнены соответственно. Правое соединение работает аналогично, но вместо этого сохраняет все строки из правого DataFrame.

Объединение фреймов данных может значительно повысить эффективность анализа данных с помощью удобного метода join от Polars.

Заключение

В этой статье вы познакомились с фундаментальной структурой данных в Polars — DataFrame. Кроме того, мы изучили основные концепции запроса, изменения и объединения фреймов данных в Polars. В результате это должно придать вам уверенности при работе с DataFrames в будущем. Это будет неотъемлемым компонентом всей оставшейся части этой серии статей.

В этой статье мы рассмотрели следующие темы:

  • Объект Polars DataFrame.
  • Изучение различных функций агрегирования, доступных в Polars.
  • Как объединить DataFrames в Polars и чем это отличается от Pandas.

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

Заключительное примечание

Завершая этот урок, я хотел бы выразить искреннюю признательность всем тем, кто посвятил свое время и энергию его завершению. Было очень приятно продемонстрировать с вами исключительные возможности языка программирования Rust.

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

Спасибо!

Ресурсы