如何在 Polars 中通过条件正则表达式标记字符串列中的项目

How to label items in a string column by conditional regex in Polars

提问人:blaylockbk 提问时间:11/17/2023 更新时间:11/17/2023 访问量:95

问:

在 Polars 中,我正在处理一个 DataFrame,其中列中有凌乱的字符串数据。在此示例中,该列可以包含电话号码、电子邮件地址、州、邮政编码等(我的真实数据实际上有一百个或更多不同的“模式”存储在此列中,长度为一百万行。

import polars as pl
df = pl.DataFrame(
    {
        "data": [
            "123-123-1234",
            "[email protected]",
            "345-345-3456",
            "456-456-4567",
            "12345",
            "[email protected]",
            "345-345-3456",
            "CA",
            "UT",
        ]
    }
)

我正在尝试根据正则表达式标记每个项目。例如,“如果它看起来像一个电话号码,请将其标记为 PHONE;如果它看起来像电子邮件,请将其标记为 EMAIL;等等”。结果应该看起来像这样......

shape: (9, 2)
┌───────────────────────────┬──────────┐
│ data                      ┆ label    │
│ ---                       ┆ ---      │
│ str                       ┆ str      │
╞═══════════════════════════╪══════════╡
│ 123-123-1234              ┆ PHONE    │
│ [email protected]         ┆ EMAIL    │
│ 345-345-3456              ┆ PHONE    │
│ 456-456-4567              ┆ PHONE    │
│ 12345                     ┆ ZIP CODE │
│ [email protected] ┆ EMAIL    │
│ 345-345-3456              ┆ PHONE    │
│ CA                        ┆ STATE    │
│ UT                        ┆ STATE    │
└───────────────────────────┴──────────┘




我的第一次尝试只是简单地做多个排队......str.replace

df.with_columns(
    (
        pl.col("data")
        .str.replace(".*@.*", "EMAIL")
        .str.replace(f"\d\d\d-\d\d\d-\d\d\d\d", "PHONE")
        .str.replace(f"\d\d\d\d\d", "ZIP CODE")
        .str.replace(f"^[A-Z][A-Z]$", "STATE")
    ).alias("label")
)

这是获得结果的一种方法,除非缩放到许多(我有一百多个正则表达式,我试图为超过一百万行的列标记),它非常慢,因为它多次重新运行替换方法。我希望 Polars 中有一种不同的方法可以使我还没有想到的速度更快。感谢您的任何想法。str.replace

python-极地

评论

1赞 jqurious 11/17/2023
我想到的第一种方法是: - Polars 应该并行运行它们。.with_columns(label = pl.coalesce(pl.when(pl.col("data").str.contains(pattern)).then(pl.lit(label)) for pattern, label in ...)
1赞 blaylockbk 11/17/2023
谢谢你把我介绍给 coalesce!这是处理这个问题的聪明方法。在我的真实示例中(500 万行和 200 个正则表达式模式要搜索),这需要 ~25 秒。(在我的比较中,我冗长的 str.replace 方法大约是两分钟)
0赞 jqurious 11/17/2023
不错。显然,rust regex crate 有一个 RegexSet 可以同时测试多个模式。找出最快的方法来做到这一点会很有趣。
0赞 jqurious 11/17/2023
我很好奇并测试了 RegexSet 版本。与 when/then 相比,它的运行速度要慢得多 (~10x)。(使用 200 万行,400 种模式进行测试)所以我认为这可以排除。
0赞 Dean MacGregor 11/17/2023
@jqurious你用什么作为样本数据和模式?

答:

2赞 Dean MacGregor 11/17/2023 #1

如果你有 500 万行,其中很多是重复的,那么我认为你想先制作一个具有唯一值的查找表,然后将原始值连接到查找表中。这样一来,正则表达式引擎就不必那么努力地工作了。

我不想窃取 jqurious 的答案,但使用一些随机数据合并/何时似乎比 concat/filter 更快。也就是说,我认为如果有很多重复,查找表的想法仍然有效。我的随机数据一开始是唯一的,我没有尝试重复,所以它没有说明任何关于查找表的信息。

patterns=[(".*@.*", "EMAIL"),
(f"\d\d\d-\d\d\d-\d\d\d\d", "PHONE"),
(f"\d\d\d\d\d", "ZIP CODE"),
(f"^[A-Z][A-Z]$", "STATE")]

lookups = df.unique('data').lazy()
lookups = pl.concat([
    lookups.filter(pl.col('data').str.contains(x[0])).with_columns(label=pl.lit(x[1]))
    for x in patterns
    ]).collect()
df.join(lookups, on='data')
shape: (9, 2)
┌───────────────────────────┬──────────┐
│ data                      ┆ label    │
│ ---                       ┆ ---      │
│ str                       ┆ str      │
╞═══════════════════════════╪══════════╡
│ 123-123-1234              ┆ PHONE    │
│ [email protected]         ┆ EMAIL    │
│ 345-345-3456              ┆ PHONE    │
│ 456-456-4567              ┆ PHONE    │
│ 12345                     ┆ ZIP CODE │
│ [email protected] ┆ EMAIL    │
│ 345-345-3456              ┆ PHONE    │
│ CA                        ┆ STATE    │
│ UT                        ┆ STATE    │
└───────────────────────────┴──────────┘

另一个想法是对数据进行一次(或两次)拆分,而不是将其全部推送并依赖并行性。理想情况下,您可以选择一个具有大量匹配项的标签,假设 STATE 具有最多的匹配项:

splits = (
    df.unique()
    .with_columns(is_state=pl.col('data').str.contains("^[A-Z][A-Z]$"))
    .partition_by('is_state', as_dict=True, include_key=False)
)
lookups = splits[True].with_columns(label=pl.lit('STATE'))
## Then finish the rest of the data as above
lookups = pl.concat([lookups.lazy()] + [
    splits[False].lazy()
    .filter(pl.col('data').str.contains(x[0])).with_columns(label=pl.lit(x[1]))
    for x in patterns if x[1]!='STATE'
]).collect()
df.join(lookups, on='data')

只有当总共有很多行,并且您可以挑选出覆盖总行很大一部分的模式/标签时,这种拆分方法才会有用。上面的数字是什么,你必须尝试一下。

评论

0赞 blaylockbk 11/18/2023
哇;这真是太神奇了。在我的实际示例中,我有 250 个正则表达式/标签对,“data”列中有 24,000 个唯一值,我的 df 高度几乎是 500 万行。这种“查找表/联接”方法(不拆分)花费了不到一秒钟的时间。
1赞 Dean MacGregor 11/18/2023
哇,是的,幸运地猜会有很多重复的。任何时候,只要你能把一个问题缩小200倍,它就会有很好的结果。
2赞 jqurious 11/17/2023 #2

首先想到的方法是对每个模式使用 a。pl.when().then()

Polars 并行运行它们,pl.coalesce 可以选择第一个非 null 结果。

在这些情况下,正则表达式是较长模式的“子集”是很常见的,因此您通常希望它们按长度(降序)排序。

df_re.sort(pl.col("pattern").str.len_chars()).reverse()
┌──────────┬────────────────────────┐
│ label    ┆ pattern                │
│ ---      ┆ ---                    │
│ str      ┆ str                    │
╞══════════╪════════════════════════╡
│ PHONE    ┆ \d\d\d-\d\d\d-\d\d\d\d │
│ STATE    ┆ ^[A-Z][A-Z]$           │
│ ZIP CODE ┆ \d\d\d\d\d             │
│ EMAIL    ┆ .*@.*                  │
└──────────┴────────────────────────┘
patterns = df_re.sort(pl.col("pattern").str.len_chars()).reverse().iter_rows()

choices = [
    pl.when(pl.col("data").str.contains(pattern))  
      .then(pl.lit(label))
    for label, pattern in patterns
]

df.with_columns(label = pl.coalesce(choices))
shape: (9, 2)
┌───────────────────────────┬──────────┐
│ data                      ┆ label    │
│ ---                       ┆ ---      │
│ str                       ┆ str      │
╞═══════════════════════════╪══════════╡
│ 123-123-1234              ┆ PHONE    │
│ [email protected]         ┆ EMAIL    │
│ 345-345-3456              ┆ PHONE    │
│ 456-456-4567              ┆ PHONE    │
│ 12345                     ┆ ZIP CODE │
│ [email protected] ┆ EMAIL    │
│ 345-345-3456              ┆ PHONE    │
│ CA                        ┆ STATE    │
│ UT                        ┆ STATE    │
└───────────────────────────┴──────────┘

评论

1赞 blaylockbk 11/18/2023
感谢@jquirious在问题中的原始评论之外扩展了这种方法。对正则表达式模式进行排序确实提高了我的性能。使用排序时,我的真实示例需要 13 秒,而没有排序需要 23 秒。我很高兴我现在知道coalese是做什么的。