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
ndarraydocumentation — https://docs.rs/ndarrayndarray-statscrate — estatísticas para ndarray — https://docs.rs/ndarray-statsnalgebradocumentation — álgebra linear — https://docs.rs/nalgebra- "Polars vs Pandas benchmarks" — https://pola.rs/posts/benchmarks/
linfacrate — machine learning em Rust — https://docs.rs/linfacandlecrate — deep learning em Rust (Hugging Face) — https://github.com/huggingface/candleburncrate — 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