【问题标题】:go through every rows of a dataframe without iteration在不迭代的情况下遍历数据帧的每一行
【发布时间】:2022-08-17 18:27:14
【问题描述】:

这是我的样本数据:

存货是基于一个产品

  Customer  Product  Quantity   Inventory    
  1           A         100        800      
  2           A         1000       800  
  3           A         700        800  
  4           A         50         800   
  5           B         20         100  
  6           B         50         100  
  7           B         40         100  
  8           B         30         100  

代码需要创建此数据:

data = {
    \'Customer\':[1,2,3,4,5,6,7,8],
    \'Product\':[\'A\',\'A\',\'A\',\'A\',\'B\',\'B\',\'B\',\'B\'],
    \'Quantity\':[100,1000,700,50,20,50,40,30],
    \'Inventory\':[800,800,800,800,100,100,100,100]
}
df = pd.DataFrame(data)

我需要得到一个已知的新列可承诺这是通过从先前可用于承诺的数量中减去数量来计算的,并且仅当先前可用的库存大于订单数量时才会进行计算。

这是我的预期输出:

Customer  Product  Quantity Inventory   Available to Promise 
  1           A         100        800   700                (800-100 = 700)
  2           A         1000       800   700                (1000 greater than 700 so same value)
  3           A         700        800   0                  (700-700 = 0)
  4           A         50         800   0                  (50 greater than 0)
  5           B         20         100   80                 (100-20 = 80)
  6           B         50         100   30                 (80-50 = 30)
  7           B         40         100   30                 (40 greater than 30)
  8           B         30         100   0                  (30 - 30 = 0)

我已经在 python pandas 中使用 for 循环和 itterows 实现了这一点

这是我的代码:

master_df = df[[\'Product\',\'Inventory\']].drop_duplicates()
master_df[\'free\'] = df[\'Inventory\']
df[\'available_to_promise\']=np.NaN
for i,row in df.iterrows():
    if i%1000==0:

        print(i)
    try:
        available = master_df[row[\'Product\']==master_df[\'Product\']][\'free\'].reset_index(drop=True).iloc[0]
        if available-row[\'Quantity\']>=0:
            df.at[i,\'available_to_promise\']=available-row[\'Quantity\']
            a = master_df.loc[row[\'Product\']==master_df[\'Product\']].reset_index()[\'index\'].iloc[0]
            master_df.at[a,\'free\'] = available-row[\'Quantity\']
        else:
            df.at[i,\'available_to_promise\']=available
    except Exception as e:
         print(i)
         print(e)
print((df.columns))
df = df.fillna(0)

由于为了python中的循环太慢了,当有大量数据输入时,这个循环需要很长时间才能执行,因此我的aws lambda函数失败了

你们可以通过引入一个可以在几秒钟内执行的循环的更好替代方案来帮助我优化这段代码吗?

  • @norok2 我已经用代码编辑了问题以创建数据框
  • @norok2 是的,你是对的。我已经改变了它。

标签: python pandas dataframe numpy vectorization


【解决方案1】:

我不确定编写复制所需逻辑的矢量化和高性能代码是否简单。

但是,用 Numba 加速的方式编写它相对简单。

首先,让我们将您的代码编写为数据帧的(纯)函数,返回值以最终放入df["Available to Promise"]。 最终,很容易将其结果整合到原始数据框中:

df["Available to Promise"] = calc_avail_OP(df)

OP 的代码,除了异常处理和打印(以及刚刚讨论的并入原始数据帧)等价于以下内容:

import numpy as np
import pandas as pd


def calc_avail_OP(df):
    temp_df = df[["Product", "Inventory"]].drop_duplicates()
    temp_df["free"] = df["Inventory"]
    result = np.zeros(len(df), dtype=df["Inventory"].dtype)
    for i, row in df.iterrows():
        available = (
            temp_df[row["Product"] == temp_df["Product"]]["free"]
            .reset_index(drop=True)
            .iloc[0]
        )
        if available - row["Quantity"] >= 0:
            result[i] = available - row["Quantity"]
            a = (
                temp_df.loc[row["Product"] == temp_df["Product"]]
                .reset_index()["index"]
                .iloc[0]
            )
            temp_df.at[a, "free"] = available - row["Quantity"]
        else:
            result[i] = available
    return result

现在,如果对输入进行排序以使唯一产品连续出现,则可以在本机 NumPy 对象上使用一些标量临时变量来实现相同的目的,这可以是有效使用 Numba 加速:

import numba as nb


@nb.njit
def _calc_avail_nb(products, quantities, stocks):
    n = len(products)
    avails = np.empty(n, dtype=stocks.dtype)
    last_product = products[0]
    avail = stocks[0]
    for i in range(n):
        if products[i] != last_product:
            last_product = products[i]
            avail = stocks[i]
        qty = quantities[i]
        if avail >= qty:
            avail -= qty
        avails[i] = avail
    return avails
            

def calc_avail_nb(df):            
    return _calc_avail_nb(
        df["Product"].to_numpy(dtype="U"),
        df["Quantity"].to_numpy(),
        df["Inventory"].to_numpy()
    )

在测试数据帧上,他们得到相同的结果:

data = {
    'Customer':[1,2,3,4,5,6,7,8],
    'Product':['A','A','A','A','B','B','B','B'],
    'Quantity':[100,1000,700,50,20,50,40,30],
    'Inventory':[800,800,800,800,100,100,100,100]
}
df = pd.DataFrame(data)


res_OP = calc_avail_OP(df)
res_nb = calc_avail_nb(df)
print(np.allclose(res_OP, res_nb))
# True

但速度提高了 200 倍:

%timeit -n 16 -r 16 calc_avail_OP(df)
# 16 loops, best of 16: 11.7 ms per loop
%timeit -n 16 -r 16 calc_avail_nb(df)
# 16 loops, best of 16: 56.8 µs per loop

【讨论】:

  • 根据您的算法判断,您假设数据事先按Product 排序,对吗?如果是这样,那么 IMO 将在您的模型中包含排序时间是公平的。需要明确的是,在我的实践中,我从来没有按照我想要的方式对数据进行排序。因此,如果我想对自己诚实地谈论实现目标的全部时间,我还必须计算准备时间。但是,是的,对于已排序的数据,您的解决方案很好。顺便说一句,OP 是否声明数据已排序?
  • 当在没有太多上下文或足够通用的测试用例的情况下提出问题时,总会有很多推断。我不同意应该有任何排序或事先分组,因为我在被认为适合该任务的 OP 代码中看不到它。
  • 在 OP 中,我可以看到一个类似字典的支持表 master_df 来跟踪单元格 free 中每个 Product 的当前可用 Inventory 数量。此代码可以在不假设 df 已排序的情况下工作。
  • “我不同意应该有任何排序或事先分组”- 不确定我的英语是否足以理解这个想法。请帮我理解你。您是否声明您的代码(带有numba 的代码)正在正确处理未排序的数据?
  • 不,我的意思是我可能会假设排序存在,除非另有明确说明。当然,如果输入未排序,OP 的代码和此代码的工作方式会有所不同,值得一提。但是输入可能会以许多不同的方式出现偏差,因此除非明确提及,否则我会说提供一种适用于所提供输入的算法是公平的。
【解决方案2】:

如何使用生成器将具有中间状态的函数应用于 pandas 数据帧

def stock(val):
    s = val
    q = yield 
    while True:
        q = yield (s:=s-q) if s >= q else s

def exaust_stock(df):
    st = stock(df.iloc[0]['Inventory']).send
    st(None)
    return df['Quantity'].apply(st)

df['Stock'] = (
    df
    .groupby('Product')
    .apply(exaust_stock)
    .reset_index(level=0, drop=True)
)

【讨论】:

    【解决方案3】:

    您正在对您拥有的两个数据帧进行大量操作,我认为这可能是速度问题的原因。

    我会使用字典来跟踪可用库存。

    我真的很好奇如果你在一个大的数据帧上应用这个速度比较......(请参阅下面的我的编辑)

    import pandas as pd
    
    
    data = {
        'Customer':[1,2,3,4,5,6,7,8],
        'Product':['A','A','A','A','B','B','B','B'],
        'Quantity':[100,1000,700,50,20,50,40,30],
        'Inventory':[800,800,800,800,100,100,100,100]
    }
    df = pd.DataFrame(data)
    df["Available to Promise"] = 0
    # create availability tracking
    available = {k: None for k in set(df.Product)}
    
    
    for idx, row in df.iterrows():
        if available[row.Product] == None:
            if row.Quantity <= row.Inventory:
                available[row.Product] = row.Inventory - row.Quantity
                df.at[idx, "Available to Promise"] = available[row.Product]
            else:
                df.at[idx, "Available to Promise"] = row.Inventory
                available[row.Product] = 0
            
        elif available[row.Product] > 0:
            if row.Quantity <= available[row.Product]:
                available[row.Product] = available[row.Product] - row.Quantity
                df.at[idx, "Available to Promise"] = available[row.Product] 
            else:
                df.at[idx, "Available to Promise"] = available[row.Product]
                available[row.Product] = 0
        
    
    print(df)
    

    输出

       Customer Product  Quantity  Inventory  Available to Promise
    0         1       A       100        800                   700
    1         2       A      1000        800                   700
    2         3       A       700        800                     0
    3         4       A        50        800                     0
    4         5       B        20        100                    80
    5         6       B        50        100                    30
    6         7       B        40        100                    30
    7         8       B        30        100                     0
    

    编辑:

    在norok2下面的评论之后,我做了一个速度比较。

    包含 timeit 的调整代码

    import pandas as pd
    data = {
        'Customer':[1,2,3,4,5,6,7,8],
        'Product':['A','A','A','A','B','B','B','B'],
        'Quantity':[100,1000,700,50,20,50,40,30],
        'Inventory':[800,800,800,800,100,100,100,100]
    }
    df = pd.DataFrame(data)
    df["Available to Promise"] = 0
    
    def do_stuff(df):
        available = {k: None for k in set(df.Product)}
        for idx, row in df.iterrows():
            if available[row.Product] == None:
                if row.Quantity <= row.Inventory:
                    available[row.Product] = row.Inventory - row.Quantity
                    df.at[idx, "Available to Promise"] = available[row.Product]
                else:
                    df.at[idx, "Available to Promise"] = row.Inventory
                    available[row.Product] = 0
            elif available[row.Product] > 0:
                if row.Quantity <= available[row.Product]:
                    available[row.Product] = available[row.Product] - row.Quantity
                    df.at[idx, "Available to Promise"] = available[row.Product] 
                else:
                    df.at[idx, "Available to Promise"] = available[row.Product]
                    available[row.Product] = 0
    
    import timeit
    import statistics
    timings=[]
    for _ in range(1000):
        timings.append(timeit.timeit("do_stuff(df)", setup="from __main__ import do_stuff, df", number=1))
    print(f"Mine:\n  Mean: {statistics.mean(timings)}\n  Min:  {min(timings)}\n  Max:  {max(timings)}")
    

    然后,我使用了 norok2 创建的函数calc_avail_OP(df, label="Avail"),并以与我相同的方式对其进行计时,使用以下代码:

    import timeit
    import statistics
    timings=[]
    for _ in range(1000):
        timings.append(timeit.timeit("calc_avail_OP(df)", setup="from __main__ import calc_avail_OP, df", number=1))
    print(f"OP's:\n  Mean: {statistics.mean(timings)}\n  Min:  {min(timings)}\n  Max:  {max(timings)}")
    

    两者的输出

    Mine:
      Mean: 0.0003488006000061432
      Min:  0.0003338999995321501
      Max:  0.001021500000206288
    OP's:
      Mean: 0.0037762733999825286
      Min:  0.003618599999754224
      Max:  0.005391000000599888
    

    所以,使用 %timeit 我得到了这个结果:

    %timeit  -n 16 -r 16 do_stuff(df)
    365 µs ± 19.5 µs per loop (mean ± std. dev. of 16 runs, 16 loops each)
    
    %timeit  -n 16 -r 16 calc_avail_nb(df)
    30 µs ± 13.2 µs per loop (mean ± std. dev. of 16 runs, 16 loops each)
    
    %timeit  -n 16 -r 16 calc_avail_OP(df)
    3.95 ms ± 258 µs per loop (mean ± std. dev. of 16 runs, 16 loops each)
    

    norok2 仍然是最快的,在更大的 df 上差异变得非常明显

    使用 100k 行数据框:

    %timeit  -n 16 -r 16 do_stuff(df)
    3.26 s ± 153 ms per loop (mean ± std. dev. of 16 runs, 16 loops each)
    
    %timeit  -n 16 -r 16 calc_avail_nb(df)
    82.3 ms ± 15.9 ms per loop (mean ± std. dev. of 16 runs, 16 loops each)
    
    %timeit  -n 16 -r 16 calc_avail_OP(df)
    39.3 s ± 3.01 s per loop (mean ± std. dev. of 16 runs, 16 loops each)
    

    【讨论】:

    • 您是否将其与 OP 的方法进行了比较?虽然逻辑可能更易于阅读,但我不确定它会更快。
    • 似乎是这样,虽然远不及你得到的速度@norok2
    • dict(zip(list(product_set), [None for _ in range(len(product_set))]))?
    • dicts 很快,我将值设置为 None 所以我可以第一次将值设置为 Inventory
    • IMO 施工过于复杂。我可以建议{k:None for k in product_set}defaultdict(lambda:None) 吗?
    【解决方案4】:

    我有一个解决方案,它不是非常强大,因为它仍然使用循环,但它具有更简单和易于优化的优点。

    import pandas as pd
    import numpy as np
    
    def func_no_jit(quant, inv):
        stock = inv[0]
        n = len(quant)
        out = np.zeros((n,), dtype=np.int64)
        for i in range(n):
            if stock > 0 and quant[i] <= stock:
                stock -= quant[i]
                out[i] = stock
            else:
                out[i] = stock
        return out
    
    
    res = (
        df.groupby('Product')
        .apply(lambda x: func(x['Quantity'].values, x['Inventory'].values))
        .explode()
    )
    
    df["Promise"] = res
    

    一个可能的解决方案是使用numba,当我使用它时,我可以将处理时间缩短一半,对于 100_000 个元素的数据帧,它对小数据帧没有实际影响。

    from numba import njit
    
    @njit
    def func(quant, inv):
        stock = inv[0]
        n = len(quant)
        out = np.zeros((n,), dtype=np.int64)
        for i in range(n):
            if stock > 0 and quant[i] <= stock:
                stock -= quant[i]
                out[i] = stock
            else:
                out[i] = stock
        return out
    

    在此处查看结果:

    In [11]: big_df
    Out[11]: 
           Customer Product  Quantity  Inventory
    0             0       I       328        282
    1             1       A       668        874
    2             2       H        51        496
    3             3       A       561        526
    4             4       H       143        421
    ...         ...     ...       ...        ...
    99995     99995       D        43        392
    99996     99996       F       162        540
    99997     99997       C       565        902
    99998     99998       H       633        936
    99999     99999       A       731        810
    
    [100000 rows x 4 columns]
    
    big_df.sort_values('Product', inplace=True) # Sort to keep track of indices
    
    In [12]: %timeit big_df.groupby('Product').apply(lambda x : func_no_jit(x["Quantity"].values
        ...: ,x["Inventory"].values)).explode()
    33.3 ms ± 102 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    
    In [13]: %timeit big_df.groupby('Product').apply(lambda x : func(x["Quantity"].values,x["Inv
        ...: entory"].values)).explode()
    12.5 ms ± 21.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

    OP 对 100_000 个元素数据框的解决方案:

    product_set = set(big_df.Product)
    available = dict(zip(list(product_set), [None for _ in range(len(product_set))]))
    
    
    def op_func():
        big_df['Available to Promise'] = 0
        for idx, row in big_df.iterrows():
            if available[row.Product] == None:
                if row.Quantity <= row.Inventory:
                    available[row.Product] = row.Inventory - row.Quantity
                    big_df.at[idx, "Available to Promise"] = available[row.Product]
                else:
                    big_df.at[idx, "Available to Promise"] = row.Inventory
                    available[row.Product] = 0
    
            elif available[row.Product] > 0:
                if row.Quantity <= available[row.Product]:
                    available[row.Product] = available[row.Product] - row.Quantity
                    big_df.at[idx, "Available to Promise"] = available[row.Product]
                else:
                    big_df.at[idx, "Available to Promise"] = available[row.Product]
                    available[row.Product] = 0
    
    In [11]: %timeit op_func()
    3.53 s ± 433 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    【讨论】:

    • 这与 OP 的方法相比如何?我觉得这可能会更慢。
    • @norok2 一点也不,OP 解决方案是最慢的解决方案之一。在 100 000 个元素的 DF 上,我的速度提高了 100 到 200 倍。
    • @norok2 这种方法比其他方法更快。它在速度上与通过生成器的方法相当(其基本逻辑相同)。但它确实有一个陷阱——这个由numpy.arrays 制作的算法不保留索引。 NathanFurnal,你认为如何将数据连接到主库?您以某种方式恢复了索引,这需要额外的时间。
    • @Vitalizzare如果索引是一个问题,您可以简单地预先在您使用分组的键上对数据进行排序。因此,如果您想对产品进行分组,请事先对其进行排序,然后使用我的程序。
    猜你喜欢
    • 1970-01-01
    • 2020-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-10
    • 1970-01-01
    • 2020-09-07
    相关资源
    最近更新 更多