通过< Utf8>Rust polars中的自定义函数将Utf8系列转换为列表系列

2ul0zpep  于 2023-02-08  发布在  其他
关注(0)|答案(1)|浏览(212)

我的DataFrame中有一个Utf8列,我想从该列创建一个List<Utf8>列。
具体来说,对于每一行,我都将获取HTML文档的文本,并使用soup解析出<p>类的所有段落,并将每个单独段落的文本集合存储为Vec<String>Vec<&str>

fn parse_paragraph(s: &str) -> Vec<&str> {

    let soup = Soup::new(s);
    
    soup.tag(p).find_all().iter().map(|&p| p.text()).collect()

}

在尝试修改Rust polars中应用自定义函数的几个可用示例时,我似乎无法编译转换。
以这个MVP为例,使用一个更简单的字符串到字符串向量的例子,借用了文档中的迭代器例子:

use polars::prelude::*;

fn vector_split(text: &str) -> Vec<&str> {

    text.split(' ').collect()
    
}

fn vector_split_series(s: &Series) -> PolarsResult<Series> {

    let output : Series = s.utf8()
        .expect("Text data")
        .into_iter()
        .map(|t| t.map(vector_split))
        .collect();

    Ok(output)
    
}

fn main() {

    let df = df! [
        "text" => ["a cat on the mat", "a bat on the hat", "a gnat on the rat"]
    ].unwrap();

    df.clone().lazy()
        .select([
            col("text").apply(|s| vector_split_series(&s), GetOutput::default())
                .alias("words")
        ])
        .collect();
    
}

(Note:我知道utf8系列有一个内置的split函数,但我需要一个比解析HTML更简单的示例)
我从cargo check得到以下错误:

error[E0277]: a value of type `polars::prelude::Series` cannot be built from an iterator over elements of type `Option<Vec<&str>>`
    --> src/main.rs:11:27
     |
11   |       let output : Series = s.utf8()
     |  ___________________________^
12   | |         .expect("Text data")
13   | |         .into_iter()
14   | |         .map(|t| t.map(vector_split))
     | |_____________________________________^ value of type `polars::prelude::Series` cannot be built from `std::iter::Iterator<Item=Option<Vec<&str>>>`
15   |           .collect();
     |            ------- required by a bound introduced by this call
     |
     = help: the trait `FromIterator<Option<Vec<&str>>>` is not implemented for `polars::prelude::Series`
     = help: the following other types implement trait `FromIterator<A>`:
               <polars::prelude::Series as FromIterator<&'a bool>>
               <polars::prelude::Series as FromIterator<&'a f32>>
               <polars::prelude::Series as FromIterator<&'a f64>>
               <polars::prelude::Series as FromIterator<&'a i32>>
               <polars::prelude::Series as FromIterator<&'a i64>>
               <polars::prelude::Series as FromIterator<&'a str>>
               <polars::prelude::Series as FromIterator<&'a u32>>
               <polars::prelude::Series as FromIterator<&'a u64>>
             and 15 others
note: required by a bound in `std::iter::Iterator::collect`

这种过程的正确用法是什么?有没有更简单的方法来应用函数?

j5fpnvbx

j5fpnvbx1#

对于未来的研究者,我将解释一般的解决方案,然后解释使这个例子工作的具体代码,我还将指出这个具体例子的一些陷阱。

解释

如果需要使用自定义函数而不是方便的Expr表达式,在它的核心,你需要创建一个函数,将输入列的Series转换成一个Series,并由一个正确输出类型的ChunkedArray支持,这个函数就是你在mainselect语句中给map的函数。ChunkedArray的类型是您提供的GetOutput类型。
问题中vector_split_series内部的代码适用于标准数值类型或数值类型列表的转换函数。它不会自动适用于Utf8字符串的Lists,例如,因为它们被特殊处理用于ChunkedArrays。这是出于性能原因。您需要通过正确的类型构建器显式构建Series
在这个问题中,我们需要使用一个ListUtf8ChunkedBuilder,它将创建一个List<Utf8>ChunkedArray
所以一般来说,这个问题的代码适用于数值或数值列表的转换输出,但是对于字符串列表,需要使用ListUtf8ChunkedBuilder

正确代码

问题示例的正确代码如下所示:

use polars::prelude::*;

fn vector_split(text: &str) -> Vec<String> {

    text.split(' ').map(|x| x.to_owned()).collect()
    
}

fn vector_split_series(s: Series) -> PolarsResult<Series> {

    let ca = s.utf8()?;

    let mut builder = ListUtf8ChunkedBuilder::new("words", s.len(), ca.get_values_size());

    ca.into_iter()
        .for_each(|opt_s| match opt_s {
            None => builder.append_null(),
            Some(s) => {
                builder.append_series(
                    &Series::new("words", vector_split(s).into_iter() )
                )
            }});

    Ok(builder.finish().into_series())
    
}

fn main() {

    let df = df! [
        "text" => ["a cat on the mat", "a bat on the hat", "a gnat on the rat"]
    ].unwrap();

    let df2 = df.clone().lazy()
        .select([
            col("text")
                .apply(|s| vector_split_series(s), GetOutput::from_type(DataType::List(Box::new(DataType::Utf8))))

                // Can instead use default if the compiler can determine the types
                //.apply(|s| vector_split_series(s), GetOutput::default())
                .alias("words")
        ])
        .collect()
        .unwrap();

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

核心在vector_split_series中,它具有在map中使用的函数定义。
match语句是必需的,因为Series可以有null条目,并且为了保持Series的长度,你需要传递null。我们在这里使用构建器,以便它附加适当的null。
对于非空条目,构建器需要追加Series。通常可以追加append_from_iter,但是(从polars 0.26.1开始)没有针对Iterator<Item=Vec<T>>的FromIterator实现。因此,需要将集合转换为值的迭代器,并将该迭代器转换为新的Series
一旦构建了更大的ChunkedArray(类型为ListUtf8ChunkedArray),就可以将其转换为PolarsResult<Series>以返回到map

抓住你了

在上面的例子中,vector_split可以返回Vec<String>Vec<&str>,这是因为split以一种很好的方式创建了&str的迭代器。
如果你使用的是更复杂的东西--就像我最初通过Soup查询提取文本的例子一样--如果它们输出&str的迭代器,那么引用可能被认为是由临时变量拥有的,然后你就会遇到返回临时变量的引用的问题。
这就是为什么在工作代码中,我将Vec<String>传递回构建器,尽管它不是严格要求的。

相关问题