提问人:blaylockbk 提问时间:11/17/2023 更新时间:11/17/2023 访问量:95
如何在 Polars 中通过条件正则表达式标记字符串列中的项目
How to label items in a string column by conditional regex in Polars
问:
在 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
答:
如果你有 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')
只有当总共有很多行,并且您可以挑选出覆盖总行很大一部分的模式/标签时,这种拆分方法才会有用。上面的数字是什么,你必须尝试一下。
评论
首先想到的方法是对每个模式使用 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 │
└───────────────────────────┴──────────┘
评论
.with_columns(label = pl.coalesce(pl.when(pl.col("data").str.contains(pattern)).then(pl.lit(label)) for pattern, label in ...)