1
\$\begingroup\$

I am sampling from m normal distributions n times each, and storing the samples in a 2-dimensional ndarray. I then need to turn this into a polars dataframe as I need to add some string columns and do a lot of querying/aggregating.

I think where I convert the array to a dataframe is inefficient. The Polars docs did not seem to have any examples of doing this, every example I saw uses small amounts of data typed out by hand, whereas I have 10,000 samples each from 100,000 normal distributions.

Here is the function that samples:

use rand_distr::{Normal, Distribution};
use polars::prelude::*;
use rayon::prelude::*;
use ndarray::Array2;
use cute::c;
#[macro_use]
extern crate fstrings;

fn multi_rnorm(n: usize, means: Vec<f64>, sds: Vec<f64>) -> Array2<f64> {

    let mut preds: Array2<f64> = Array2::zeros((means.len(), n));

    preds.axis_iter_mut(ndarray::Axis(0)).into_par_iter().enumerate().for_each(|(i, mut row)| {

        let mut rng = rand::thread_rng();
        (0..n).into_iter().for_each(|j| {
            let normal = Normal::new(means[i], sds[i]).unwrap();
            row[j as usize] = normal.sample(&mut rng);
        })
    });
    preds
}

And here is how I am creating the data frame:

let mut df: DataFrame = DataFrame::new(
    c![Series::new(
                &f!("{tuple.0}"), 
                tuple.1.to_vec()), for tuple in preds.axis_iter(ndarray::Axis(1))
                    .into_iter()
                    .enumerate()
                    .collect::<Vec<_>>()])
                    .unwrap();

Main looks like this:

fn main() {

    let means = vec![0.0; 99_128];
    let sds = vec![1.0; 99_128];

    let preds = rprednorm(10000, means, sds);

    let mut df: DataFrame = DataFrame::new(
        c![Series::new(
                    &f!("{tuple.0}"), 
                    tuple.1.to_vec()), for tuple in preds.axis_iter(ndarray::Axis(1))
                        .into_iter()
                        .enumerate()
                        .collect::<Vec<_>>()])
                        .unwrap();
}

I am using the c! macro from the cute crate to emulate python dictionary comprehensions, as well as the f!() macro from fstrings to perform string interpolation.

I have tried substituting into_par_iter() for into_iter(), but there is no speedup.

I was very surprised to not see any examples of how to do this in Polars, I figured there would be some implementation of FromNdarray so you could just call DataFrame::new(the_ndarray) or something.

Does my implementation look any good, or can it be faster?

\$\endgroup\$
4
  • \$\begingroup\$ Rather than initializing rng on every row, surely you could hoist that out of the loop, right? And rather than a single normal draw, maybe you could request a bunch of draws in a single call? Also, feel free to share profiler or godbolt.org details. \$\endgroup\$
    – J_H
    Feb 15 at 0:15
  • \$\begingroup\$ @J_H I tried with different types of RNGs, but this was the only one I could get working, and because it is a thread rng it needed to be inside the for_each so each thread gets its own one. I may be able to iterate over only the rows though and get all n samples for row i at once. \$\endgroup\$
    – Jage
    Feb 15 at 0:42
  • \$\begingroup\$ Ok, fair enough, I understand. Maybe a thread pool of 8 on an 8 core machine, with 8 rng's? Also, the key thing to understand is "where did the time go?" Which line(s) are troublesome? I guess, put another way, is there some "simple" (non-polars) code that goes head-to-head with this which achieves better timing figures? \$\endgroup\$
    – J_H
    Feb 15 at 0:58
  • \$\begingroup\$ @J_H TO answer "where did the time go?" it is definitely the creation of the dataframe. I can get all 1 billion samples in about 350ms, but taking the ndarray and turning it into the dataframe takes 11s. I haven't found better examples, Polars only shows examples where they create data frames by manually typing out the columns for small datasets with 3-8 rows and 2-3 columns. \$\endgroup\$
    – Jage
    Feb 15 at 16:05

1 Answer 1

0
\$\begingroup\$

It is possible to optimize the creation of the DataFrame from the Array2 by 5x by parallelizing it.

let mut df: DataFrame = DataFrame::new(
    preds.axis_iter(ndarray::Axis(1))
        .into_par_iter()
        .enumerate()
        .map(|(i, col)| {
            Series::new(
                &f!("{i}"),
                col.to_vec()
            )
        })
        .collect::<Vec<Series>>()
    ).unwrap();
\$\endgroup\$
1
  • \$\begingroup\$ I changed your answer a little bit to make an observation. A good answer on Code Review contains at least one insightful observation about the code. Alternate code only solutions are considered poor answers and may be down voted or deleted by the community. Please read How do I write a good answer. \$\endgroup\$
    – pacmaninbw
    Feb 16 at 0:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.