Rust

Rust para Data Science — Processamento de Dados com Polars e ndarray Já leu

16 min de leitura

Rust para Data Science — Processamento de Dados com Polars e ndarray
Rust — Artigo #41 Rust para Data Science — Processamento de Dados com Polars e ndarray Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano Python domi

Rust — Artigo #41

Rust para Data Science — Processamento de Dados com Polars e ndarray

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


Python domina o data science há uma década — NumPy, Pandas, scikit-learn formam um ecossistema maduro e poderoso. Mas há um custo: performance. Análises sobre datasets grandes em Pandas podem demorar minutos onde uma implementação otimizada demoraria segundos.

Rust está mudando essa equação. Polars — escrito em Rust — é frequentemente 5 a 20 vezes mais rápido que Pandas em operações equivalentes. ndarray oferece arrays N-dimensionais com semântica similar ao NumPy mas com segurança de tipos em tempo de compilação. E o melhor: você pode chamar código Rust a partir do Python via PyO3, combinando a conveniência do Python com a performance de Rust.


Dependências

[package]
name = "data_science_rs"
version = "0.1.0"
edition = "2021"

[dependencies]
# DataFrame — similar ao Pandas mas muito mais rápido
polars = { version = "0.39", features = [
    "lazy",          # API lazy (mais eficiente)
    "csv",           # leitura/escrita CSV
    "json",          # leitura/escrita JSON
    "parquet",       # formato Parquet (colunar, comprimido)
    "temporal",      # tipos de data/hora
    "strings",       # operações em strings
    "dtype-categorical",
    "describe",      # estatísticas descritivas
    "rolling_window",# janelas deslizantes
    "groupby_list",
    "pivot",
]}

# Arrays N-dimensionais — similar ao NumPy
ndarray = { version = "0.15", features = ["rayon"] }
ndarray-stats = "0.5"
ndarray-rand = "0.14"

# Álgebra linear
nalgebra = "0.32"

# Geração de números aleatórios
rand = "0.8"
rand_distr = "0.4"

# Visualização (output de texto)
textplots = "0.8"

# Serialização
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Erros
anyhow = "1"

# Paralelismo
rayon = "1"

ndarray — Arrays N-dimensionais

use ndarray::{Array1, Array2, Axis, arr1, arr2, s};
use ndarray_rand::RandomExt;
use ndarray_rand::rand_distr::{Normal, Uniform};
use ndarray_stats::QuantileExt;

fn demonstrar_ndarray() {
    println!("══ ndarray: Arrays N-dimensionais ══
");

    // Array 1D
    let v: Array1<f64> = arr1(&[1.0, 2.0, 3.0, 4.0, 5.0]);
    println!("Vetor: {v}");
    println!("Soma: {}", v.sum());
    println!("Média: {}", v.mean().unwrap());

    // Operações vetorizadas — sem loop explícito
    let dobrado = &v * 2.0;
    let quadrado = v.mapv(|x| x * x);
    println!("Dobrado: {dobrado}");
    println!("Quadrado: {quadrado}");

    // Array 2D (matriz)
    let m: Array2<f64> = arr2(&[
        [1.0, 2.0, 3.0],
        [4.0, 5.0, 6.0],
        [7.0, 8.0, 9.0],
    ]);

    println!("
Matriz 3x3:
{m}");
    println!("Shape: {:?}", m.shape());
    println!("Transposta:
{}", m.t());

    // Soma por eixos
    println!("Soma por coluna: {}", m.sum_axis(Axis(0)));
    println!("Soma por linha:  {}", m.sum_axis(Axis(1)));

    // Slicing — sem cópia
    let submatriz = m.slice(s![0..2, 1..3]);
    println!("
Submatriz [0..2, 1..3]:
{submatriz}");

    // Produto de matrizes
    let a: Array2<f64> = arr2(&[[1.0, 2.0], [3.0, 4.0]]);
    let b: Array2<f64> = arr2(&[[5.0, 6.0], [7.0, 8.0]]);
    let produto = a.dot(&b);
    println!("
A × B =
{produto}");

    // Arrays aleatórios
    let normal = Array1::random(1000, Normal::new(0.0, 1.0).unwrap());
    println!("
Amostra N(0,1): n={}", normal.len());
    println!("Média amostral: {:.4}", normal.mean().unwrap());
    println!("Desvio padrão:  {:.4}", normal.std(0.0));
    println!("Min: {:.4}", *normal.min().unwrap());
    println!("Max: {:.4}", *normal.max().unwrap());
}

// Regressão linear com ndarray
fn regressao_linear_ols(
    x: &Array2<f64>,  // matriz de features (n_amostras × n_features)
    y: &Array1<f64>,  // vetor de targets
) -> Array1<f64> {
    // β = (XᵀX)⁻¹Xᵀy — mínimos quadrados ordinários

    let xt = x.t();
    let xtx = xt.dot(x);

    // Inversão via decomposição LU (simplificada)
    // Em produção use nalgebra ou linfa
    let xty = xt.dot(y);

    // Para regressão simples: β = Σ(xi*yi) / Σ(xi²)
    // Versão completa requereria inversão de matriz
    xty
}

fn main() {
    demonstrar_ndarray();
}

Estatísticas descritivas com ndarray

use ndarray::{Array1, Array2};
use ndarray_stats::QuantileExt;
use rand::SeedableRng;
use rand_distr::{Distribution, Normal, Uniform};

struct EstatisticasDescritivas {
    n: usize,
    media: f64,
    mediana: f64,
    desvio_padrao: f64,
    variancia: f64,
    minimo: f64,
    maximo: f64,
    q1: f64,
    q3: f64,
    assimetria: f64,
    curtose: f64,
}

impl EstatisticasDescritivas {
    fn calcular(dados: &Array1<f64>) -> Self {
        let n = dados.len();
        let media = dados.mean().unwrap();
        let variancia = dados.var(0.0); // divisão por n
        let desvio_padrao = variancia.sqrt();

        // Centraliza os dados
        let centralizado = dados.mapv(|x| x - media);

        // Assimetria (skewness)
        let assimetria = centralizado.mapv(|x| x.powi(3)).mean().unwrap()
            / desvio_padrao.powi(3);

        // Curtose (kurtosis) — excesso em relação à normal
        let curtose = centralizado.mapv(|x| x.powi(4)).mean().unwrap()
            / desvio_padrao.powi(4) - 3.0;

        // Ordenar para percentis
        let mut ordenado: Vec<f64> = dados.to_vec();
        ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let arr_ord = Array1::from(ordenado.clone());

        let percentil = |p: f64| -> f64 {
            let idx = p / 100.0 * (n - 1) as f64;
            let i = idx.floor() as usize;
            let f = idx - i as f64;
            if i + 1 < n {
                arr_ord[i] * (1.0 - f) + arr_ord[i + 1] * f
            } else {
                arr_ord[n - 1]
            }
        };

        EstatisticasDescritivas {
            n,
            media,
            mediana: percentil(50.0),
            desvio_padrao,
            variancia,
            minimo: ordenado[0],
            maximo: *ordenado.last().unwrap(),
            q1: percentil(25.0),
            q3: percentil(75.0),
            assimetria,
            curtose,
        }
    }

    fn exibir(&self) {
        println!("┌─────────────────────────────────────┐");
        println!("│       Estatísticas Descritivas       │");
        println!("├─────────────────────────────────────┤");
        println!("│  n              : {:>14}    │", self.n);
        println!("│  Média          : {:>14.4}    │", self.media);
        println!("│  Mediana        : {:>14.4}    │", self.mediana);
        println!("│  Desvio Padrão  : {:>14.4}    │", self.desvio_padrao);
        println!("│  Variância      : {:>14.4}    │", self.variancia);
        println!("├─────────────────────────────────────┤");
        println!("│  Mínimo         : {:>14.4}    │", self.minimo);
        println!("│  Q1             : {:>14.4}    │", self.q1);
        println!("│  Mediana        : {:>14.4}    │", self.mediana);
        println!("│  Q3             : {:>14.4}    │", self.q3);
        println!("│  Máximo         : {:>14.4}    │", self.maximo);
        println!("├─────────────────────────────────────┤");
        println!("│  IQR            : {:>14.4}    │", self.q3 - self.q1);
        println!("│  Assimetria     : {:>14.4}    │", self.assimetria);
        println!("│  Curtose        : {:>14.4}    │", self.curtose);
        println!("└─────────────────────────────────────┘");
    }
}

fn histograma_texto(dados: &Array1<f64>, n_bins: usize) {
    let min = dados.iter().cloned().fold(f64::INFINITY, f64::min);
    let max = dados.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let largura = (max - min) / n_bins as f64;

    let mut contagens = vec![0usize; n_bins];
    for &x in dados.iter() {
        let idx = ((x - min) / largura).floor() as usize;
        let idx = idx.min(n_bins - 1);
        contagens[idx] += 1;
    }

    let max_contagem = *contagens.iter().max().unwrap();
    let largura_barra = 40;

    println!("
Histograma:");
    for (i, &contagem) in contagens.iter().enumerate() {
        let inicio = min + i as f64 * largura;
        let fim = inicio + largura;
        let tamanho = contagem * largura_barra / max_contagem;
        let barra = "█".repeat(tamanho);
        println!("{:6.2}–{:6.2} │{:<40}│ {}", inicio, fim, barra, contagem);
    }
}

fn main() {
    let mut rng = rand::rngs::SmallRng::seed_from_u64(42);
    let normal = Normal::new(170.0, 10.0).unwrap(); // altura em cm

    let alturas: Array1<f64> = Array1::from(
        (0..1000).map(|_| normal.sample(&mut rng)).collect::<Vec<_>>()
    );

    println!("Dados: alturas de 1000 pessoas (simulado)
");

    let stats = EstatisticasDescritivas::calcular(&alturas);
    stats.exibir();

    histograma_texto(&alturas, 12);
}

Polars — DataFrames de alta performance

use polars::prelude::*;
use std::fs;

fn demonstrar_polars() -> PolarsResult<()> {
    println!("══ Polars: DataFrames ══
");

    // Criando DataFrame manualmente
    let df = DataFrame::new(vec![
        Series::new("nome",   &["Alice", "Bob", "Carlos", "Diana", "Eduardo"]),
        Series::new("idade",  &[28i32, 34, 45, 29, 52]),
        Series::new("salario",&[5500.0f64, 8200.0, 12000.0, 6800.0, 15000.0]),
        Series::new("depto",  &["TI", "TI", "RH", "TI", "RH"]),
        Series::new("ativo",  &[true, true, false, true, true]),
    ])?;

    println!("DataFrame original:");
    println!("{df}
");

    // Informações básicas
    println!("Shape: {:?}", df.shape());
    println!("Colunas: {:?}", df.get_column_names());
    println!("Schema:
{:?}
", df.schema());

    // Seleção de colunas
    let cols_selecionadas = df.select(["nome", "salario"])?;
    println!("Colunas selecionadas:
{cols_selecionadas}
");

    // Filtro
    let ti_ativos = df.filter(
        &df["depto"].equal("TI")?.bitand(&df["ativo"].bool()?.clone())
    )?;
    println!("TI ativos:
{ti_ativos}
");

    // Ordenação
    let por_salario = df.sort(["salario"], SortMultipleOptions::default()
        .with_order_descending(true))?;
    println!("Por salário (desc):
{por_salario}
");

    // Estatísticas descritivas
    println!("Describe:
{}
", df.describe(None)?);

    Ok(())
}

API Lazy do Polars — plano de execução otimizado

use polars::prelude::*;

fn analise_lazy() -> PolarsResult<()> {
    println!("══ Polars Lazy API ══
");

    // Lazy frames — operações não são executadas imediatamente
    // O Polars otimiza o plano antes de executar
    let lf = DataFrame::new(vec![
        Series::new("produto",    &["A","B","C","D","E","A","B","C"]),
        Series::new("categoria",  &["X","X","Y","Y","X","Y","X","Y"]),
        Series::new("vendas",     &[100i32,200,150,300,120,180,220,90]),
        Series::new("mes",        &[1i32,1,1,1,2,2,2,2]),
    ])?.lazy();

    // Encadeamento de operações — só executa no .collect()
    let resultado = lf
        .filter(col("vendas").gt(lit(100)))
        .group_by([col("categoria"), col("mes")])
        .agg([
            col("vendas").sum().alias("total_vendas"),
            col("vendas").mean().alias("media_vendas"),
            col("produto").count().alias("num_produtos"),
        ])
        .sort(["categoria", "mes"], SortMultipleOptions::default())
        .collect()?;

    println!("Análise por categoria e mês:
{resultado}
");

    // Expressões customizadas
    let df = DataFrame::new(vec![
        Series::new("valor", &[10.0f64, 20.0, 30.0, 40.0, 50.0]),
    ])?;

    let com_derivadas = df.lazy()
        .with_columns([
            col("valor").alias("original"),
            (col("valor") * lit(2.0)).alias("dobrado"),
            (col("valor") - col("valor").mean()).alias("centralizado"),
            col("valor").rank(RankOptions::default(), None).alias("rank"),
        ])
        .collect()?;

    println!("DataFrame com colunas derivadas:
{com_derivadas}
");

    Ok(())
}

Leitura e escrita de arquivos

use polars::prelude::*;
use std::io::Cursor;

fn io_polars() -> PolarsResult<()> {
    // ── CSV ─────────────────────────────────────────

    // Dados CSV em memória para demonstração
    let csv_dados = r#"nome,idade,salario,cidade
Alice,28,5500.0,São Paulo
Bob,34,8200.0,Rio de Janeiro
Carlos,45,12000.0,Belo Horizonte
Diana,29,6800.0,São Paulo
Eduardo,52,15000.0,Curitiba
Fernanda,31,7200.0,São Paulo
"#;

    // Leitura de CSV
    let df = CsvReader::new(Cursor::new(csv_dados))
        .has_header(true)
        .with_try_parse_dates(true)
        .finish()?;

    println!("CSV lido:
{df}
");

    // Escrita em CSV
    let mut buffer = Vec::new();
    CsvWriter::new(&mut buffer)
        .finish(&mut df.clone())?;
    println!("CSV escrito: {} bytes
", buffer.len());

    // ── JSON ─────────────────────────────────────────

    let mut json_buf = Vec::new();
    JsonWriter::new(&mut json_buf)
        .with_json_format(JsonFormat::Json)
        .finish(&mut df.clone())?;

    println!("JSON (primeiros 200 chars):
{}
",
        &String::from_utf8_lossy(&json_buf)[..200.min(json_buf.len())]);

    // ── Parquet ──────────────────────────────────────

    // Parquet é o formato preferido para análise — colunar e comprimido
    let parquet_path = "/tmp/dados_teste.parquet";

    let mut file = std::fs::File::create(parquet_path)?;
    ParquetWriter::new(&mut file)
        .with_compression(ParquetCompression::Snappy)
        .finish(&mut df.clone())?;

    let file_leitura = std::fs::File::open(parquet_path)?;
    let df_parquet = ParquetReader::new(file_leitura).finish()?;

    println!("Parquet lido de volta ({} linhas):
{df_parquet}
",
        df_parquet.height());

    std::fs::remove_file(parquet_path)?;

    Ok(())
}

Análise exploratória completa

use polars::prelude::*;
use std::io::Cursor;

fn analise_exploratoria() -> PolarsResult<()> {
    println!("══ Análise Exploratória de Dados ══
");

    // Dataset simulado de vendas
    let csv = r#"data,produto,categoria,quantidade,preco_unit,vendedor,regiao
2024-01-05,Notebook,Eletrônicos,2,3500.0,Ana,Sul
2024-01-08,Mouse,Periféricos,15,85.0,Bob,Norte
2024-01-12,Teclado,Periféricos,8,200.0,Ana,Sul
2024-01-15,Monitor,Eletrônicos,3,1200.0,Carlos,Sudeste
2024-02-02,Notebook,Eletrônicos,1,3500.0,Bob,Norte
2024-02-10,Headset,Periféricos,12,350.0,Ana,Sul
2024-02-18,Monitor,Eletrônicos,5,1200.0,Ana,Sul
2024-02-22,Mouse,Periféricos,20,85.0,Carlos,Sudeste
2024-03-05,Notebook,Eletrônicos,4,3800.0,Carlos,Sudeste
2024-03-14,Teclado,Periféricos,6,200.0,Bob,Norte
2024-03-20,Headset,Periféricos,8,350.0,Carlos,Sudeste
2024-03-28,Monitor,Eletrônicos,2,1200.0,Bob,Norte
"#;

    let df = CsvReader::new(Cursor::new(csv))
        .has_header(true)
        .finish()?;

    println!("Dataset de vendas:
{df}
");

    // Coluna derivada: receita total
    let df = df.lazy()
        .with_column(
            (col("quantidade") * col("preco_unit"))
                .alias("receita")
        )
        .collect()?;

    // 1. Resumo geral
    println!("── Resumo Geral ──");
    let total_receita = df["receita"].sum::<f64>().unwrap();
    let total_vendas  = df["quantidade"].sum::<i64>().unwrap();
    println!("Receita total: R${total_receita:.2}");
    println!("Unidades vendidas: {total_vendas}");
    println!("Ticket médio: R${:.2}
", total_receita / df.height() as f64);

    // 2. Por categoria
    println!("── Receita por Categoria ──");
    let por_categoria = df.clone().lazy()
        .group_by([col("categoria")])
        .agg([
            col("receita").sum().alias("receita_total"),
            col("quantidade").sum().alias("unidades"),
            col("receita").mean().alias("ticket_medio"),
        ])
        .sort(["receita_total"], SortMultipleOptions::default()
            .with_order_descending(true))
        .collect()?;
    println!("{por_categoria}
");

    // 3. Por vendedor
    println!("── Performance por Vendedor ──");
    let por_vendedor = df.clone().lazy()
        .group_by([col("vendedor")])
        .agg([
            col("receita").sum().alias("receita_total"),
            col("receita").count().alias("num_vendas"),
            col("receita").max().alias("maior_venda"),
        ])
        .sort(["receita_total"], SortMultipleOptions::default()
            .with_order_descending(true))
        .collect()?;
    println!("{por_vendedor}
");

    // 4. Por região
    println!("── Receita por Região ──");
    let por_regiao = df.clone().lazy()
        .group_by([col("regiao")])
        .agg([col("receita").sum().alias("total")])
        .sort(["total"], SortMultipleOptions::default()
            .with_order_descending(true))
        .collect()?;
    println!("{por_regiao}
");

    // 5. Top produtos
    println!("── Top Produtos ──");
    let top_produtos = df.clone().lazy()
        .group_by([col("produto")])
        .agg([
            col("receita").sum().alias("receita_total"),
            col("quantidade").sum().alias("unidades"),
        ])
        .sort(["receita_total"], SortMultipleOptions::default()
            .with_order_descending(true))
        .collect()?;
    println!("{top_produtos}
");

    Ok(())
}

fn main() -> anyhow::Result<()> {
    demonstrar_ndarray();
    println!("
{}
", "═".repeat(50));

    analise_lazy()?;
    println!("{}
", "═".repeat(50));

    io_polars()?;
    println!("{}
", "═".repeat(50));

    analise_exploratoria()?;

    Ok(())
}

Regressão Linear com nalgebra

use nalgebra::{DMatrix, DVector};

struct RegressaoLinear {
    coeficientes: DVector<f64>,
    intercepto: f64,
}

impl RegressaoLinear {
    /// Ajusta o modelo por mínimos quadrados ordinários.
    /// β = (XᵀX)⁻¹Xᵀy
    fn ajustar(x: &DMatrix<f64>, y: &DVector<f64>) -> Option<Self> {
        let n = x.nrows();
        let p = x.ncols();

        // Adiciona coluna de uns para o intercepto
        let mut x_aug = DMatrix::zeros(n, p + 1);
        for i in 0..n {
            x_aug[(i, 0)] = 1.0; // intercepto
            for j in 0..p {
                x_aug[(i, j + 1)] = x[(i, j)];
            }
        }

        // β = (XᵀX)⁻¹Xᵀy
        let xt = x_aug.transpose();
        let xtx = &xt * &x_aug;
        let xty = &xt * y;

        let beta = xtx.try_inverse()?.mul(&xty);

        Some(RegressaoLinear {
            intercepto: beta[0],
            coeficientes: beta.rows(1, p).into_owned(),
        })
    }

    fn prever(&self, x: &DMatrix<f64>) -> DVector<f64> {
        let previsoes = x * &self.coeficientes;
        previsoes.map(|v| v + self.intercepto)
    }

    fn r_quadrado(&self, x: &DMatrix<f64>, y: &DVector<f64>) -> f64 {
        let previsoes = self.prever(x);
        let media_y = y.mean();

        let ss_res: f64 = y.iter().zip(previsoes.iter())
            .map(|(yi, fi)| (yi - fi).powi(2))
            .sum();

        let ss_tot: f64 = y.iter()
            .map(|yi| (yi - media_y).powi(2))
            .sum();

        1.0 - ss_res / ss_tot
    }

    fn rmse(&self, x: &DMatrix<f64>, y: &DVector<f64>) -> f64 {
        let previsoes = self.prever(x);
        let mse: f64 = y.iter().zip(previsoes.iter())
            .map(|(yi, fi)| (yi - fi).powi(2))
            .sum::<f64>() / y.len() as f64;
        mse.sqrt()
    }
}

fn demonstrar_regressao() {
    println!("══ Regressão Linear com nalgebra ══
");

    // Dataset: área (m²) e quartos → preço do imóvel
    let x_dados = vec![
        50.0, 2.0,
        65.0, 2.0,
        80.0, 3.0,
        90.0, 3.0,
        110.0, 4.0,
        120.0, 4.0,
        140.0, 5.0,
        160.0, 5.0,
    ];

    let y_dados = vec![
        280_000.0,
        350_000.0,
        420_000.0,
        480_000.0,
        580_000.0,
        630_000.0,
        720_000.0,
        850_000.0,
    ];

    let n = y_dados.len();
    let x = DMatrix::from_row_slice(n, 2, &x_dados);
    let y = DVector::from_vec(y_dados.clone());

    match RegressaoLinear::ajustar(&x, &y) {
        Some(modelo) => {
            println!("Coeficientes:");
            println!("  Intercepto : R${:.2}", modelo.intercepto);
            println!("  Área (m²)  : R${:.2} por m²", modelo.coeficientes[0]);
            println!("  Quartos    : R${:.2} por quarto", modelo.coeficientes[1]);

            println!("
Métricas:");
            println!("  R²   : {:.4}", modelo.r_quadrado(&x, &y));
            println!("  RMSE : R${:.2}", modelo.rmse(&x, &y));

            println!("
Previsões vs Real:");
            let previsoes = modelo.prever(&x);
            println!("{:>10} {:>14} {:>10}", "Real", "Previsto", "Erro%");
            println!("{}", "─".repeat(36));
            for (real, prev) in y_dados.iter().zip(previsoes.iter()) {
                let erro = (prev - real) / real * 100.0;
                println!("R${:>10.0} R${:>12.0} {:>9.1}%", real, prev, erro);
            }

            // Previsão para novo imóvel
            let novo = DMatrix::from_row_slice(1, 2, &[100.0, 3.0]);
            let preco = modelo.prever(&novo)[0];
            println!("
Previsão: 100m² / 3 quartos → R${preco:.2}");
        }
        None => eprintln!("Falha ao ajustar o modelo"),
    }
}

fn main() {
    demonstrar_regressao();
}

Integração com Python via PyO3

Uma das aplicações mais poderosas: expor funções de análise Rust para uso em notebooks Python:

# Cargo.toml para extensão Python
[lib]
name = "rust_data"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.21", features = ["extension-module"] }
ndarray = "0.15"
polars = { version = "0.39", features = ["lazy"] }
numpy = "0.21"  # integração ndarray ↔ numpy
use pyo3::prelude::*;
use numpy::{PyArray1, PyReadonlyArray1};
use ndarray::Array1;

/// Calcula estatísticas descritivas de um array NumPy.
///
/// Retorna dicionário com: média, dp, min, max, q1, mediana, q3.
#[pyfunction]
fn estatisticas<'py>(
    py: Python<'py>,
    dados: PyReadonlyArray1<f64>,
) -> PyResult<PyObject> {
    let arr: Array1<f64> = dados.as_array().to_owned();

    if arr.is_empty() {
        return Err(pyo3::exceptions::PyValueError::new_err("Array vazio"));
    }

    let n = arr.len() as f64;
    let media = arr.sum() / n;
    let variancia = arr.mapv(|x| (x - media).powi(2)).sum() / n;
    let dp = variancia.sqrt();

    let mut ordenado: Vec<f64> = arr.to_vec();
    ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());

    let percentil = |p: f64| -> f64 {
        let idx = p / 100.0 * (ordenado.len() - 1) as f64;
        let i = idx.floor() as usize;
        let f = idx - i as f64;
        if i + 1 < ordenado.len() {
            ordenado[i] * (1.0 - f) + ordenado[i + 1] * f
        } else {
            *ordenado.last().unwrap()
        }
    };

    let resultado = pyo3::types::PyDict::new(py);
    resultado.set_item("n", arr.len())?;
    resultado.set_item("media", media)?;
    resultado.set_item("dp", dp)?;
    resultado.set_item("min", ordenado[0])?;
    resultado.set_item("q1", percentil(25.0))?;
    resultado.set_item("mediana", percentil(50.0))?;
    resultado.set_item("q3", percentil(75.0))?;
    resultado.set_item("max", *ordenado.last().unwrap())?;

    Ok(resultado.into())
}

/// Normalização Z-score: (x - μ) / σ
#[pyfunction]
fn normalizar_zscore<'py>(
    py: Python<'py>,
    dados: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let arr: Array1<f64> = dados.as_array().to_owned();
    let media = arr.sum() / arr.len() as f64;
    let dp = (arr.mapv(|x| (x - media).powi(2)).sum() / arr.len() as f64).sqrt();

    let normalizado = arr.mapv(|x| (x - media) / dp);
    Ok(PyArray1::from_array(py, &normalizado))
}

/// Detecta outliers pelo método IQR.
/// Retorna índices dos outliers.
#[pyfunction]
fn detectar_outliers<'py>(
    py: Python<'py>,
    dados: PyReadonlyArray1<f64>,
    fator_iqr: f64,
) -> PyResult<&'py PyArray1<i64>> {
    let arr: Array1<f64> = dados.as_array().to_owned();
    let mut ordenado: Vec<f64> = arr.to_vec();
    ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());

    let n = ordenado.len();
    let q1 = ordenado[n / 4];
    let q3 = ordenado[3 * n / 4];
    let iqr = q3 - q1;

    let limite_inf = q1 - fator_iqr * iqr;
    let limite_sup = q3 + fator_iqr * iqr;

    let indices: Vec<i64> = arr.iter()
        .enumerate()
        .filter(|(_, &x)| x < limite_inf || x > limite_sup)
        .map(|(i, _)| i as i64)
        .collect();

    Ok(PyArray1::from_vec(py, indices))
}

#[pymodule]
fn rust_data(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(estatisticas, m)?)?;
    m.add_function(wrap_pyfunction!(normalizar_zscore, m)?)?;
    m.add_function(wrap_pyfunction!(detectar_outliers, m)?)?;
    Ok(())
}

Uso em Python/Jupyter:

import rust_data
import numpy as np

dados = np.random.normal(loc=170, scale=10, size=1000)
# Insere outliers
dados[0] = 250.0
dados[1] = 50.0

stats = rust_data.estatisticas(dados)
print(f"Média: {stats['media']:.2f}")
print(f"DP:    {stats['dp']:.2f}")

normalizado = rust_data.normalizar_zscore(dados)
print(f"Z-score média: {normalizado.mean():.6f}")

outliers = rust_data.detectar_outliers(dados, fator_iqr=1.5)
print(f"Outliers encontrados: {len(outliers)}")
print(f"Índices: {outliers}")

Fontes e leituras recomendadas

  • Polars User Guide — documentação completa — https://docs.pola.rs
  • ndarray documentation — https://docs.rs/ndarray
  • ndarray-stats crate — estatísticas para ndarray — https://docs.rs/ndarray-stats
  • nalgebra documentation — álgebra linear — https://docs.rs/nalgebra
  • "Polars vs Pandas benchmarks" — https://pola.rs/posts/benchmarks/
  • linfa crate — machine learning em Rust — https://docs.rs/linfa
  • candle crate — deep learning em Rust (Hugging Face) — https://github.com/huggingface/candle
  • burn crate — framework de deep learning — https://burn.dev

Artigo #41 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #42: Rust para GameDev — Motores de jogo, ECS e Bevy


Comentários

Mais em Rust

Iteradores e Closures — O Estilo Funcional de Rust
Iteradores e Closures — O Estilo Funcional de Rust

&nbsp; Nos artigos anteriores usamos&nbsp;for para percorrer cole&ccedil;&ot...

Testes Avançados — Propriedades, Mocks e Benchmarks
Testes Avançados — Propriedades, Mocks e Benchmarks

No Artigo #16 cobrimos os fundamentos de testes em Rust — testes unitários, d...

Traits — Definindo Comportamento Compartilhado
Traits — Definindo Comportamento Compartilhado

&nbsp; Nos artigos anteriores criamos structs e enums para modelar dados. Ma...