ndarray en Rust: guía completa para trabajar con arreglos N-dimensionales
1. ¿Qué es ndarray y por qué usarla?
La librería ndarray es la extensión estándar del ecosistema Rust para trabajar con arreglos multidimensionales (tensores N-dimensionales). Proporciona estructuras de datos potentes, seguras en cuanto a tipos y de alto rendimiento para cálculos numéricos, álgebra lineal, procesamiento de señales, aprendizaje automático y cálculos científicos. Si conoces NumPy en Python, ndarray es su equivalente directo en el mundo Rust, pero con las ventajas de un tipado estricto, ausencia de recolector de basura y control total sobre la memoria.
¿Por qué es necesaria? Rust no tiene soporte nativo para arreglos multidimensionales con un número arbitrario de ejes. Los vectores estándar (Vec<T>) y los slices (&[T]) solo permiten trabajar con datos unidimensionales. Para tareas que requieren álgebra matricial, convoluciones, operaciones con tensores o procesamiento de imágenes, se necesita una librería especializada. ndarray resuelve estos problemas ofreciendo:
- Arreglos N-dimensionales tipados (
ArrayBase) con un número fijo o dinámico de ejes. - Slicing eficiente, indexación e iteración.
- Operaciones aritméticas vectorizadas (broadcasting).
- Integración con BLAS/LAPACK a través de
ndarray-linalgpara cálculos de álgebra lineal de alto rendimiento. - Soporte para cálculos paralelos mediante
rayon.
La librería se utiliza activamente en proyectos como RustML, Linfa (framework de aprendizaje automático), Image (procesamiento de imágenes) y muchos otros. Es la base de la computación científica en Rust.
2. Instalación
Para instalar ndarray, usa el gestor de paquetes Cargo, que viene incluido con Rust. Añade la dependencia en tu archivo Cargo.toml:
[dependencies]
ndarray = "0.16"
Luego, ejecuta el siguiente comando en la terminal:
cargo build
Para trabajar con álgebra lineal (multiplicación de matrices, valores propios), añade ndarray-linalg y elige un backend BLAS (por ejemplo, OpenBLAS o Intel MKL):
[dependencies]
ndarray = "0.16"
ndarray-linalg = { version = "0.16", features = ["openblas"] }
Para iteraciones paralelas, usa ndarray-parallel:
[dependencies]
ndarray = "0.16"
ndarray-parallel = "0.16"
3. Inicio rápido — ejemplo mínimo funcional
Creemos un arreglo unidimensional, realicemos una suma elemento a elemento y mostremos el resultado. Guarda el siguiente código en el archivo src/main.rs:
use ndarray::Array;
fn main() {
// Creamos un arreglo unidimensional de 5 elementos
let a = Array::from_vec(vec![1, 2, 3, 4, 5]);
let b = Array::from_vec(vec![10, 20, 30, 40, 50]);
// Suma elemento a elemento
let c = &a + &b;
println!("a = {:?}", a);
println!("b = {:?}", b);
println!("a + b = {:?}", c);
}
Ejecuta el programa:
cargo run
Salida:
a = [1, 2, 3, 4, 5], shape=[5], strides=[1], layout=CFcf (0x1)
b = [10, 20, 30, 40, 50], shape=[5], strides=[1], layout=CFcf (0x1)
a + b = [11, 22, 33, 44, 55], shape=[5], strides=[1], layout=CFcf (0x1)
4. Métodos y funciones principales (mínimo 8)
4.1. Array::from_shape_vec
Firma: pub fn from_shape_vec<Sh>(shape: Sh, v: Vec<A>) -> Result<ArrayBase<OwnedRepr<A>, Dim<Sh>>, ShapeError>
Crea un arreglo a partir de un vector y una forma (shape) específica. Es útil cuando ya tienes los datos en un vector y deseas darles una estructura multidimensional.
use ndarray::Array;
let data = vec![1, 2, 3, 4, 5, 6];
let arr = Array::from_shape_vec((2, 3), data).unwrap();
println!("{:?}", arr);
// Salida: [[1, 2, 3],
// [4, 5, 6]], shape=[2, 3], strides=[3, 1]
4.2. .shape()
Firma: pub fn shape(&self) -> &IxS
Devuelve las dimensiones del arreglo como un slice. Esencial para conocer la estructura del arreglo en tiempo de ejecución.
let arr = Array::from_shape_vec((2, 3), vec![1, 2, 3, 4, 5, 6]).unwrap();
println!("Shape: {:?}", arr.shape()); // Salida: [2, 3]
4.3. .len()
Firma: pub fn len(&self) -> usize
Devuelve el número total de elementos en el arreglo.
let arr = Array::from_shape_vec((2, 3), vec![1, 2, 3, 4, 5, 6]).unwrap();
println!("Total de elementos: {}", arr.len()); // Salida: 6
4.4. .sum()
Firma: pub fn sum(&self) -> A
Calcula la suma de todos los elementos del arreglo. Muy útil para agregaciones rápidas.
let arr = Array::from_vec(vec![1, 2, 3, 4, 5]);
println!("Suma: {}", arr.sum()); // Salida: 15
4.5. .mean()
Firma: pub fn mean(&self) -> A
Calcula la media aritmética de todos los elementos. Requiere que el tipo de dato implemente el trait Float.
use ndarray::Array1;
let arr = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
println!("Media: {}", arr.mean().unwrap()); // Salida: 3.0
4.6. .dot()
Firma: pub fn dot(&self, rhs: &ArrayBase<S, Ix1>) -> A
Calcula el producto punto entre dos arreglos unidimensionales. Es la base de muchas operaciones de álgebra lineal.
use ndarray::Array1;
let a = Array1::from_vec(vec![1, 2, 3]);
let b = Array1::from_vec(vec![4, 5, 6]);
let producto = a.dot(&b);
println!("Producto punto: {}", producto); // Salida: 32
4.7. .t()
Firma: pub fn t(&self) -> ArrayBase<ViewRepr<&A>, Ix2>
Devuelve la transpuesta de un arreglo bidimensional (matriz). No modifica el arreglo original, sino que crea una vista transpuesta.
use ndarray::Array;
let arr = Array::from_shape_vec((2, 3), vec![1, 2, 3, 4, 5, 6]).unwrap();
let transpuesta = arr.t().to_owned();
println!("Original:\n{:?}", arr);
println!("Transpuesta:\n{:?}", transpuesta);
// Original: [[1, 2, 3],
// [4, 5, 6]]
// Transpuesta: [[1, 4],
// [2, 5],
// [3, 6]]
4.8. .map()
Firma: pub fn map<B, F>(&self, f: F) -> ArrayBase<OwnedRepr<B>, D> where F: FnMut(&A) -> B
Aplica una función a cada elemento del arreglo, devolviendo un nuevo arreglo con los resultados. Ideal para transformaciones elemento a elemento.
use ndarray::Array;
let arr = Array::from_vec(vec![1, 2, 3, 4, 5]);
let cuadrados = arr.map(|x| x * x);
println!("Cuadrados: {:?}", cuadrados); // Salida: [1, 4, 9, 16, 25]
4.9. .slice()
Firma: pub fn slice<I>(&self, info: I) -> ArrayBase<ViewRepr<&A>, I::Out> where I: SliceInfo<...>
Extrae una vista (sin copiar datos) de una porción del arreglo usando la sintaxis s![...]. Es fundamental para acceder a subarreglos de manera eficiente.
use ndarray::{Array, s};
let arr = Array::from_shape_vec((3, 4), (0..12).collect()).unwrap();
let sub_arr = arr.slice(s![1..3, 0..2]);
println!("Subarreglo:\n{:?}", sub_arr);
// Salida: [[4, 5],
// [8, 9]]
4.10. Array::zeros
Firma: pub fn zeros<Sh, A>(shape: Sh) -> ArrayBase<OwnedRepr<A>, Dim<Sh>> where A: Clone + Zero
Crea un arreglo lleno de ceros con la forma especificada. Muy útil para inicializar arreglos antes de llenarlos con datos.
use ndarray::Array;
let zeros = Array::zeros((3, 3));
println!("Arreglo de ceros:\n{:?}", zeros);
// Salida: [[0, 0, 0],
// [0, 0, 0],
// [0, 0, 0]]
También en la biblioteca
Envío de correo electrónico en Java: guía completa de JavaMail y Jakarta Mail
Timber para Kotlin: guía completa con ejemplos prácticos
Trabajo con CSV en C# (.NET): Guía Completa