在 Python 中根据条件添加前导零

3

我有一个包含500万行数据的数据框。假设数据框长这样:

>>> df = pd.DataFrame(data={"Random": "86 7639103627 96 32 1469476501".split()})
>>> df
       Random
0          86
1  7639103627
2          96
3          32
4  1469476501

请注意,Random列存储为字符串。
如果Random列中的数字少于9位数,我希望添加前导零以使其达到9位数。如果该数字具有9位或更多位数,则希望添加前导零以使其达到20位数。
我的做法是:
for i in range(0,len(df['Random'])):
      if len(df['Random'][i]) < 9:
          df['Random'][i]=df['Random'][i].zfill(9)
      else:
           df['Random'][i]=df['Random'][i].zfill(20)

由于行数超过500万,执行这个过程需要很长时间!(性能为5it/秒。使用tqdm测试,预计完成时间需要几天!)

有没有更简单、更快捷的执行此任务的方法呢?

5个回答

3

让我们结合使用 np.wherezfill,或者您可以使用 str.pad 进行检查。

df.Random=np.where(df.Random.str.len()<9,df.Random.str.zfill(9),df.Random.str.zfill(20))
df
Out[9]: 
                 Random
0             000000086
1  00000000007639103627
2             000000096
3             000000032
4  00000000001469476501

1
我可以看到两个问题:你在做重复的工作,所有行不考虑长度都被填充为9和20个字符,然后np.where()从这两组行中进行选择,长度为9的列被填充到长度为20。后者也是OP发布的代码中的缺陷。 - Martijn Pieters

3

我使用了下面写的fill_zeros函数结合'apply'方法,使得在100万行的数据框上运行时间为603毫秒。

data = {
    'Random': [str(randint(0, 100_000_000)) for i in range(0, 1_000_000)]
}

df = pd.DataFrame(data)

def fill_zeros(x):
    if len(x) < 9:
        return x.zfill(9)
    else:
        return x.zfill(20)

%timeit df['Random'].apply(fill_zeros)

603 ms ± 1.23 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

相比之下:

%timeit np.where(df.Random.str.len()<9,df.Random.str.zfill(9),df.Random.str.zfill(20))
1.57 s ± 6.57 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

1
我可以确认这确实(令人惊讶地)更快。事实上,快了两倍。 - Martijn Pieters
1
你也可以使用 return x.zfill(9 if len(x) < 9 else 20) - Martijn Pieters
1
为了真正挤出纯Python函数的最后一滴性能,避免全局和属性查找,可以将它们缓存为参数:def fill_zeros(x, _len=len, _zfill=str.zfill): return _zfill(x, 9 if _len(x) else 20) - Martijn Pieters

2
由于您关心效率,字符串操作是Pandas中常见的“陷阱”之一,因为虽然它们是矢量化的(可以一次性应用于整个Series),但这并不意味着它们比循环更有效率。这是一个例子,在这种情况下,循环实际上会比使用字符串访问器更快,后者往往更多地用于方便而非速度。
如果不确定,请确保在实际数据上计时函数,因为您认为笨拙缓慢的东西可能比看起来干净的东西更快!
我将提出一个非常基本的循环函数,我认为它将击败任何使用字符串访问器的方法。
def loopy(series):
    return pd.Series(
        (
            el.zfill(9) if len(el) < 9 else el.zfill(20)
            for el in series
        ),
        name=series.name,
    )

# to compare more fairly with the apply version
def cache_loopy(series, _len=len, _zfill=str.zfill):
    return pd.Series(
      (_zfill(el, 9 if _len(el) < 9 else 20) for el in series), name=series.name)

现在让我们使用Martijn提供的代码和simple_benchmark来检查时间。

函数

def loopy(series):
    series.copy()    # not necessary but just to make timings fair
    return pd.Series(
        (
            el.zfill(9) if len(el) < 9 else el.zfill(20)
            for el in series
        ),
        name=series.name,
    )

def str_accessor(series):
    target = series.copy()
    mask = series.str.len() < 9
    unmask = ~mask
    target[mask] = target[mask].str.zfill(9)
    target[unmask] = target[unmask].str.zfill(20)
    return target

def np_where_str_accessor(series):
    target = series.copy()
    return np.where(target.str.len()<9,target.str.zfill(9),target.str.zfill(20))

def fill_zeros(x, _len=len, _zfill=str.zfill):
    # len() and str.zfill() are cached as parameters for performance
    return _zfill(x, 9 if _len(x) < 9 else 20)

def apply_fill(series):
    series = series.copy()
    return series.apply(fill_zeros)

def cache_loopy(series, _len=len, _zfill=str.zfill):
    series.copy()
    return pd.Series(
      (_zfill(el, 9 if _len(el) < 9 else 20) for el in series), name=series.name)

设置

import pandas as pd
import numpy as np
from random import choices, randrange
from simple_benchmark import benchmark

def randvalue(chars="0123456789", _c=choices, _r=randrange):
    return "".join(_c(chars, k=randrange(5, 30))).lstrip("0")

fns = [loopy, str_accessor, np_where_str_accessor, apply_fill, cache_loopy]
args = { 2**i: pd.Series([randvalue() for _ in range(2**i)]) for i in range(14, 21)}

b = benchmark(fns, args, 'Series Length')

b.plot()

enter image description here


1
你需要将其向量化;使用布尔索引选择列,并在结果子集上使用.str.zfill()
# select the right rows to avoid wasting time operating on longer strings
shorter = df.Random.str.len() < 9
longer = ~shorter
df.Random[shorter] = df.Random[shorter].str.zfill(9)
df.Random[longer] = df.Random[longer].str.zfill(20)

注意:我没有使用np.where(),因为我们不想做重复的工作。向量化的df.Random.str.zfill()比循环行更快,但是对于每组行进行两次仍然比只进行一次需要更多的时间。
在具有随机长度值(从5个字符到30个字符)的100万行字符串上进行速度比较:
In [1]: import numpy as np, pandas as pd

In [2]: import platform; print(platform.python_version_tuple(), platform.platform(), pd.__version__, np.__version__, sep="\n")
('3', '7', '3')
Darwin-17.7.0-x86_64-i386-64bit
0.24.2
1.16.4

In [3]: !sysctl -n machdep.cpu.brand_string
Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz

In [4]: from random import choices, randrange

In [5]: def randvalue(chars="0123456789", _c=choices, _r=randrange):
   ...:     return "".join(_c(chars, k=randrange(5, 30))).lstrip("0")
   ...:

In [6]: df = pd.DataFrame(data={"Random": [randvalue() for _ in range(10**6)]})

In [7]: %%timeit
   ...: target = df.copy()
   ...: shorter = target.Random.str.len() < 9
   ...: longer = ~shorter
   ...: target.Random[shorter] = target.Random[shorter].str.zfill(9)
   ...: target.Random[longer] = target.Random[longer].str.zfill(20)
   ...:
   ...:
825 ms ± 22.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [8]: %%timeit
   ...: target = df.copy()
   ...: target.Random = np.where(target.Random.str.len()<9,target.Random.str.zfill(9),target.Random.str.zfill(20))
   ...:
   ...:
929 ms ± 69.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

这一行代码 target = df.copy() 的作用是确保每次重复的测试运行都与之前的隔离.

结论: 在100万行数据中,使用np.where() 大约慢了10%.

但是,使用 df.Row.apply()正如jackbicknell14所提出的, 比任何一种方法都要快得多:

In [9]: def fill_zeros(x, _len=len, _zfill=str.zfill):
   ...:     # len() and str.zfill() are cached as parameters for performance
   ...:     return _zfill(x, 9 if _len(x) < 9 else 20)

In [10]: %%timeit
    ...: target = df.copy()
    ...: target.Random = target.Random.apply(fill_zeros)
    ...:
    ...:
299 ms ± 2.55 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

那大约快了3倍!


在你的longer条件的第一个子句中否定较短的字符串会比再次使用缓慢的字符串访问器更快。 - user3483203
@user3483203:不行,因为这样会包括 len() == 9。这会导致只填充到长度为9的数字被填充到20位。 - Martijn Pieters
在进行第一次赋值之前,请先计算掩码。 - user3483203
@user3483203:这仍然不能排除已经以9位数字开头的行。 - Martijn Pieters
他已经澄清了他确实想要将长度精确填充为9到20。即使不是这种情况,如果字符串长度小于等于9(除非存在大量恰好长度为9的字符串),仍然会更快地进行一些额外的工作,以使第二个掩码计算更快。 - user3483203

1
df.Random.str.zfill(9).where(df.Random.str.len() < 9, df.Random.str.zfill(20))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接