【问题标题】:How to improve for-loop on DataFrame with row-wise calculation depending on previous row?如何根据前一行通过逐行计算来改进 DataFrame 上的 for 循环?
【发布时间】:2022-01-20 10:21:10
【问题描述】:

我有一个带有 for 循环的工作代码,我想在速度方面进行优化(该项目已被拒绝,因为它很慢)。

这是我的情况:我有一个 Excel 文件,我从该文件中将一个包含数千行和几列的表导入到 Pandas DataFrame 中。第一列是单调递增的时间戳序列,频率为 15 分钟。

我必须使用这个表来计算一些额外的列,将它们附加到原始表中,并将生成的 DataFrame 保存到一个新的 Excel 文件中。

使代码变慢的是额外列的核心计算。这是工作代码的sn-p:

import pandas as pd
from datetime import timedelta as td

cons_prod = pd.read_csv("sample.csv", index_col=0, parse_dates=True)

soc_dct = {}  # State of charge (kW)
charge_dct = {}  # Charge (kW)
discharge_dct = {}  # Discharge (kW)
acc_dct = {}  # Auto-consumption NEW (kW)
lst_dct = {}  # Lost injection due to battery efficiency (kW)
inj_dct = {}  # Injection NEW (kW)
gridcons_dct = {}  # Grid Consumption NEW (kW)
agg_dct = {}  # Additional Auto-consumption through battery (kW)

battery_parameters = {
    "power": 50,
    "energy": 130,
    "efficiency": 0.9,
    "minsoc": 0.1,
    "soct0": 65.0,
}

bp_energy = battery_parameters["energy"]
bp_power = battery_parameters["power"]
soct0 = 0.5 * bp_energy

for t in cons_prod.index:
    L = cons_prod.loc[t, "Injection (kW)"]
    m = cons_prod.loc[t, "Grid Consumption (kW)"]
    k = cons_prod.loc[t, "Auto-consumption (kW)"]
    f = cons_prod.loc[t, "Consumption (kW)"]
    if t == cons_prod.index[0]:
        # State of charge (kW)
        soc_dct.setdefault(t, soct0)

        # Charge (kW)
        charge_dct.setdefault(
            t,
            min(
                L,
                (bp_energy - soc_dct[t]) * 4,
                bp_power,
            )
            * battery_parameters["efficiency"]
            if L >= 0
            else 0,
        )

        # Discharge (kW)
        discharge_dct.setdefault(
            t,
            -min(
                m,
                4 * soc_dct[t] - 4 * (battery_parameters["minsoc"] * bp_energy),
                bp_power,
            )
            if m >= 0
            else 0,
        )

        # Auto-consumption NEW (kW)
        acc_dct.setdefault(t, k - discharge_dct[t])

        # Lost injection due to battery efficiency (kW)
        lst_dct.setdefault(
            t,
            (charge_dct[t] / battery_parameters["efficiency"]) - charge_dct[t],
        )

        # Injection NEW (kW)
        inj_dct.setdefault(t, L - charge_dct[t] - lst_dct[t])

        # Grid Consumption NEW (kW)
        gridcons_dct.setdefault(t, f - acc_dct[t])

        # Additional Auto-consumption through battery (kW)
        agg_dct.setdefault(t, acc_dct[t] - k)
    else:
        # State of charge (kW)
        soc_dct.setdefault(
            t,
            soc_dct[t - td(minutes=15)]
            + (charge_dct[t - td(minutes=15)] + discharge_dct[t - td(minutes=15)]) / 4,
        )

        # Charge (kW)
        charge_dct.setdefault(
            t,
            min(
                L,
                (bp_energy - soc_dct[t]) * 4,
                bp_power,
            )
            * battery_parameters["efficiency"]
            if L >= 0
            else 0,
        )

        # Discharge (kW)
        discharge_dct.setdefault(
            t,
            -min(
                m,
                4 * soc_dct[t] - 4 * (battery_parameters["minsoc"] * bp_energy),
                bp_power,
            )
            if m >= 0
            else 0,
        )

        # Auto-consumption NEW (kW)
        acc_dct.setdefault(t, k - discharge_dct[t])

        # Lost injection due to battery efficiency (kW)
        lst_dct.setdefault(
            t, charge_dct[t] / battery_parameters["efficiency"] - charge_dct[t]
        )

        # Injection NEW (kW)
        inj_dct.setdefault(t, L - charge_dct[t] - lst_dct[t])

        # Grid Consumption NEW (kW)
        gridcons_dct.setdefault(t, f - acc_dct[t])

        # Additional Auto-consumption through battery (kW)
        agg_dct.setdefault(t, acc_dct[t] - k)
# Creating a DataFrame with all the values
output_df = pd.DataFrame(
    data=[
        soc_dct,
        charge_dct,
        discharge_dct,
        acc_dct,
        lst_dct,
        inj_dct,
        gridcons_dct,
        agg_dct,
    ]
).T
output_df.columns = [
    "State of charge (kW)",
    "Charge (kW)",
    "Discharge (kW)",
    "Auto-consumption NEW (kW)",
    "Lost injection due to battery efficiency (kW)",
    "Injection NEW (kW)",
    "Grid Consumption NEW (kW)",
    "Additional Auto-consumption through battery (kW)",
]
charge_dct = {}  # Charge (kW)
discharge_dct = {}  # Discharge (kW)
acc_dct = {}  # Auto-consumption NEW (kW)
lst_dct = {}  # Lost injection due to battery efficiency (kW)
inj_dct = {}  # Injection NEW (kW)
gridcons_dct = {}  # Grid Consumption NEW (kW)
agg_dct = {}  # Additional Auto-consumption through battery (kW)

for t in cons_prod.index:
    L = cons_prod.loc[t, "Injection (kW)"]
    m = cons_prod.loc[t, "Grid Consumption (kW)"]
    k = cons_prod.loc[t, "Auto-consumption (kW)"]
    f = cons_prod.loc[t, "Consumption (kW)"]
    if t == cons_prod.index[0]:
        # State of charge (kW)
        soc_dct.setdefault(t, soct0)

        # Charge (kW)
        charge_dct.setdefault(
            t,
            min(
                L,
                (bp_energy - soc_dct[t]) * 4,
                bp_power,
            )
            * battery_parameters["efficiency"]
            if L >= 0
            else 0,
        )

        # Discharge (kW)
        discharge_dct.setdefault(
            t,
            -min(
                m,
                4 * soc_dct[t] - 4 * (battery_parameters["minsoc"] * bp_energy),
                bp_power,
            )
            if m >= 0
            else 0,
        )

        # Auto-consumption NEW (kW)
        acc_dct.setdefault(t, k - discharge_dct[t])

        # Lost injection due to battery efficiency (kW)
        lst_dct.setdefault(
            t,
            (charge_dct[t] / battery_parameters["efficiency"]) - charge_dct[t],
        )

        # Injection NEW (kW)
        inj_dct.setdefault(t, L - charge_dct[t] - lst_dct[t])

        # Grid Consumption NEW (kW)
        gridcons_dct.setdefault(t, f - acc_dct[t])

        # Additional Auto-consumption through battery (kW)
        agg_dct.setdefault(t, acc_dct[t] - k)
    else:
        # State of charge (kW)
        soc_dct.setdefault(
            t,
            soc_dct[t - td(minutes=15)]
            + (charge_dct[t - td(minutes=15)] + discharge_dct[t - td(minutes=15)]) / 4,
        )

        # Charge (kW)
        charge_dct.setdefault(
            t,
            min(
                L,
                (bp_energy - soc_dct[t]) * 4,
                bp_power,
            )
            * battery_parameters["efficiency"]
            if L >= 0
            else 0,
        )

        # Discharge (kW)
        discharge_dct.setdefault(
            t,
            -min(
                m,
                4 * soc_dct[t] - 4 * (battery_parameters["minsoc"] * bp_energy),
                bp_power,
            )
            if m >= 0
            else 0,
        )

        # Auto-consumption NEW (kW)
        acc_dct.setdefault(t, k - discharge_dct[t])

        # Lost injection due to battery efficiency (kW)
        lst_dct.setdefault(
            t, charge_dct[t] / battery_parameters["efficiency"] - charge_dct[t]
        )

        # Injection NEW (kW)
        inj_dct.setdefault(t, L - charge_dct[t] - lst_dct[t])

        # Grid Consumption NEW (kW)
        gridcons_dct.setdefault(t, f - acc_dct[t])

        # Additional Auto-consumption through battery (kW)
        agg_dct.setdefault(t, acc_dct[t] - k)
# Creating a DataFrame with all the values
output_df = pd.DataFrame(
    data=[
        soc_dct,
        charge_dct,
        discharge_dct,
        acc_dct,
        lst_dct,
        inj_dct,
        gridcons_dct,
        agg_dct,
    ]
).T
output_df.columns = [
    "State of charge (kW)",
    "Charge (kW)",
    "Discharge (kW)",
    "Auto-consumption NEW (kW)",
    "Lost injection due to battery efficiency (kW)",
    "Injection NEW (kW)",
    "Grid Consumption NEW (kW)",
    "Additional Auto-consumption through battery (kW)",
]

cons_prod是导入DataFrame的表。

如您所见,我们有两种情况:当t == cons_prod.index[0](即时间戳的第一项)时,计算使用相同t处的值。但是,从第二个时间戳开始,一些计算引用了之前的值(这里指的是使用索引t - td(minutes=15) 之前的 15 分钟)。

这些是我努力摆脱 for 循环的原因。

对可能的问题的一些解释

  • 问:你为什么使用字典? 答:因为我发现它们比其他数据类型填充得更快,以后我可以使用它们来创建 DataFrame。
  • 问:时间戳是否一致,例如它们有缺失值吗? 答:没有缺失值,因为我之前编写了一个函数来确保完全填充时间戳。
  • 问:计算当前不引用 DataFrame 中的前一行,而是引用字典中的行!为什么会有误导性的标题? 答:这是迄今为止我能想到的最好的解决方案,但我想知道我对 Pandas 的不完全了解是否隐藏了一个更简单、更快的解决方案。

希望框架清晰。

提前谢谢你!

编辑:根据要求,添加cons_prod100-lines sample 并修改之前的代码以满足MRE 的要求。

编辑 2:我尝试从字典转移到 Pandas 查找,尝试尽可能优化。这是我想出的代码:

from time import time as tt

cp = cons_prod.copy(deep=True)

# Initialise the columns filling them with zeroes
cp["State of charge (kW)"] = 0
cp["Charge (kW)"] = 0
cp["Discharge (kW)"] = 0

# Storing the position of the columns in variables
cp_soc = cp.columns.get_loc("State of charge (kW)")
cp_charge = cp.columns.get_loc("Charge (kW)")
cp_discharge = cp.columns.get_loc("Discharge (kW)")
cp_inj = cp.columns.get_loc("Injection (kW)")
cp_gridcons = cp.columns.get_loc("Grid Consumption (kW)")

# Storing the values of the battery dictionary lookups in variables
bp_energy = dct_bp["energy"]
bp_power = dct_bp["power"]
bp_efficiency = dct_bp["efficiency"]
bp_soct0 = dct_bp["soct0"]
bp_minsoc = dct_bp["minsoc"]

start1 = tt()  # Measuring time
for row in cp.itertuples(name=None):  # Using itertuples to gain some speed
    L = cp.loc[row[0], "Injection (kW)"]
    m = cp.loc[row[0], "Grid Consumption (kW)"]
    k = cp.loc[row[0], "Auto-consumption (kW)"]
    f = cp.loc[row[0], "Consumption (kW)"]
    if row[0] == cp.index[0]:
        cp.iloc[0, cp_soc] = bp_soct0
        cp.iloc[0, cp_charge] = float(
            min(L, (bp_energy - bp_soct0) * 4, bp_power) * bp_efficiency
            if L >= 0
            else 0,
        )
        cp.iloc[0, cp_discharge] = float(
            -min(
                m,
                4 * bp_soct0 - 4 * (bp_minsoc * bp_energy),
                bp_power,
            )
            if m >= 0
            else 0
        )
    else:
        t = pd.Index(cp.index).get_loc(row[0])
        cp.iloc[t, cp_soc] = float(
            cp.iloc[t - 1, cp_soc]
            + (cp.iloc[t - 1, cp_charge] + cp.iloc[t - 1, cp_discharge]) / 4
        )
        cp.iloc[t, cp_charge] = float(
            min(L, (bp_energy - cp.iloc[t, cp_soc]) * 4, bp_power) * bp_efficiency
            if L >= 0
            else 0,
        )
        cp.iloc[t, cp_discharge] = float(
            -min(
                m,
                4 * cp.iloc[t, cp_soc] - 4 * (dct_bp["minsoc"] * bp_energy),
                bp_power,
            )
            if m >= 0
            else 0
        )
end1 = tt() - start1
print(f"Pandas lookup took {end1:.2f} seconds")

使用这段代码,我平均每完成一项任务需要 42 秒,而我过去使用字典的时间不到 20 秒。

【问题讨论】:

  • Ciao Filippo,您介意添加minimal reproducible example 吗?特别是在cons_prod中有一个数据样本会很棒
  • 您好@rpanai,感谢您的回答!我对代码进行了更改,只是为了复制粘贴和使用。至于sample.csv,使用我附上的Pastebin中的数据。
  • 考虑在数据框中创建空列(例如:soc_dct),然后逐行递增地填充它们。在这种情况下,使用 dicts 实际上可能会更慢。 (1) 我不知道说字典填得更快是什么意思。 dict 的使用需要散列,然后(可能)线性搜索,所有这些都可能比在 pandas 中按索引访问数组要慢,(2)将 dict 转换为 df 需要时间,(3)t - td(minutes=15) 总是指前一行df 因为数据以 15 分钟为增量,这意味着通过 t - td(minutes=15) 的完整时间戳查找字典是浪费的。
  • 感谢@Ankur 的评论。我想指出我不访问字典,但我只是在每个循环中用一个新的键值对填充它们。然后使用 dicts 在原始数据框中添加列,由于它们的 O(1) 时间复杂度,我选择了它们而不是列表(或类似的)。尽管如此,我会尝试你创建空列并填充它们的方法,我会告诉你的。
  • @Ankur,添加了我直接使用 Pandas 查找的尝试。它比使用字典要慢(除非我仍然可以优化它?)。

标签: python pandas dataframe for-loop optimization


【解决方案1】:

从 Python 的角度来看,访问 dict 的速度很快,但对于 Pandas 的角度来说,并不完全正确。 Pandas 的最佳特性之一是矢量化,这使得 Pandas 在大型数据集上表现得非常高效。

什么是矢量化,为什么快? => What is "vectorization"?

另外,我想留下这个参考。在处理 Pandas 时,请尝试按照链接中提到的顺序查找操作以优化流程。

https://stackoverflow.com/a/55557758/2956135

回到您的案例,您对大多数参数的计算仅取决于同一行的数据。这是您可以轻松进行矢量化的完美案例。

这 3 个参数(“充电状态”、“充电”和“放电”)相互依赖,并取决于上一行的计算结果。这部分我没有找到矢量化的方法。希望,有人可以进一步优化。

首先我取出常量字典并将其保持为基本常量。这可能没有太大区别,但简单的引用比额外的步骤来访问字典中的常量要好。如果您必须将多个常量传递给一个函数,则放入字典并将其作为字典传递是有意义的,但这里并非如此。

power = 50
efficiency = 0.9
minsoc = 0.1
soct0 = 65.0
bp_energy = 130
bp_power = 50
soct0 = 0.5 * bp_energy

然后,定义计算3个参数的函数,取出其他参数的任何计算。

def _calc_soc_discharge(prev, L, m):
    if prev is None:
        soc = 65.0

        # Charge (kW)
        charge = min(
            L,
            (bp_energy - soc) * 4,
            bp_power,
        ) * efficiency if L >= 0 else 0

        # Discharge (kW)
        discharge =  -min(
            m,
            4 * soc - 4 * (minsoc * bp_energy),
            bp_power,
        ) if m >= 0 else 0
        return [soc, charge, discharge]
    else:
        soc = prev[0] + (prev[1] + prev[2]) / 4

        # Charge (kW)
        charge = min(
            L,
            (bp_energy - soc) * 4,
            bp_power,
        ) * efficiency if L >= 0 else 0

        # Discharge (kW)
        discharge = -min(
            m,
            4 * soc - 4 * (minsoc * bp_energy),
            bp_power,
        ) if m >= 0 else 0
            
        return [soc, charge, discharge]

迭代调用此函数并将结果附加回数据帧。

scd = []
for i, row in cons_prod[['Injection (kW)', 'Grid Consumption (kW)']].iterrows():
    scd.append(_calc_soc_discharge(None if len(scd) == 0 else scd[-1], row['Injection (kW)'], row['Grid Consumption (kW)']))

cons_prod = pd.concat([cons_prod, pd.DataFrame(scd, columns=["State of charge (kW)", "Charge (kW)", "Discharge (kW)"], index=cons_prod.index)], axis=1)

现在,此数据框包含计算其他参数所需的所有数据。这里我们使用向量化。这部分应该真正优化。 Pandas 系列调用的数学运算是矢量化的一部分。

cons_prod['Auto-consumption NEW (kW)'] = cons_prod['Auto-consumption (kW)'] - cons_prod['Discharge (kW)']
cons_prod['Lost injection due to battery efficiency (kW)'] = cons_prod['Charge (kW)'] / efficiency - cons_prod['Charge (kW)']
cons_prod['Injection NEW (kW)'] = cons_prod['Injection (kW)'] - cons_prod['Charge (kW)'] - cons_prod['Lost injection due to battery efficiency (kW)']
cons_prod['Grid Consumption NEW (kW)'] = cons_prod['Consumption (kW)'] - cons_prod['Auto-consumption NEW (kW)']
cons_prod['Additional Auto-consumption through battery (kW)'] = cons_prod['Auto-consumption NEW (kW)'] - cons_prod['Auto-consumption (kW)']

基准测试

在我的笔记本电脑上使用示例数据。

Original solution: 48.2 ms ± 6.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
With vectorization: 13.8 ms ± 2.13 ms per loop (mean ± std. dev. of 7 runs, 100 loops each) 

【讨论】:

  • 这正是我一直在寻找的优化!我一直在问自己“我怎么能在这里引入矢量化?”,但我错过了显而易见的事情。谢谢!
【解决方案2】:

由于我没有要测试的数据集,而且我不确定您需要多快运行它,我想我会从一些优化和建议开始回答并添加到它,直到它涵盖所有内容关闭。

您正在使用文本标签来识别循环中的列,该循环遍历您的所有数据:

L = cp.loc[row[0], "Injection (kW)"]
m = cp.loc[row[0], "Grid Consumption (kW)"]
k = cp.loc[row[0], "Auto-consumption (kW)"]
f = cp.loc[row[0], "Consumption (kW)"]

这些标签可能缓存在字典中的某个位置,但您应该能够完全避免这种查找 using iloc instead of loc。只需在开始循环之前识别正确的列,然后使用整数索引。

您有一个从循环内部提取的常量字典:

battery_parameters = {
    "power": 50,
    "energy": 130,
    "efficiency": 0.9,
    "minsoc": 0.1,
    "soct0": 65.0,
}

每次您在循环中使用这些值之一时,您都会无缘无故地进行一次额外的字典查找。为什么不bp_power=50 bp_energy=130

您的循环中有一个 if 语句来检查每个循环都被评估的第一行。只需将第一行的代码拉出循环并在循环之前执行,您就可以跳过所有其他行的此评估。

我真的不明白您为什么要使用字典和 setdefault 来计算要计算的其他行。除非我遗漏了一些东西,否则您应该使用列表,因为无论如何您的所有键都只是整数,并且 setdefault 检查字典中不存在的先前值,这会浪费时间。如果您执行以下操作:

soc_dct = [0] * num_rows

您将拥有一个预先分配的 0 列表。预分配将节省时间,但最大的胜利是更快的查找和插入,并且不需要附加到列表,因为所有索引都已经存在。因此,您只需使用 soc_dct[index] = newValue 而不是 soc_dct.setdefault(index,newValue)

我觉得这会让你达到你满意的表现水平。看起来你只是在循环数据并做简单的数学运算。简单的数学在 python 中非常慢,但在这里可能不是问题。如果您仍然想要更快的速度,您可以使用 Cython 将这个模块编译成 C 模块,只需使用 cythonize 命令,如果这还不够,您可能必须进入并开始进行类型注释。

【讨论】:

  • 感谢您的回复,大卫。首先,如果您想尝试使用数据集,您可以在 OP(即 Pastebin 链接)中找到它。然后,我看到您使用的是第二个代码示例而不是第一个:请注意,我这样做只是为了测试 @Ankur 的方式。尽管如此,我将测试删除行检查和额外的字典查找。
  • 我从来没有真正使用过 pandas,我确实希望有一种更好的方法可以随时将列添加到数据集中我非常有信心不必要的字典查找会拖慢你的速度。同样,由于我不熟悉 pandas,我不能确定它是如何工作的,但我希望你的主循环使用 enumerate 和 uppacking for 语句中的行会更好,特别是如果你事先知道各种整数索引列。关键是不要做不必要的工作,尤其是在大循环中。
  • 哦,现在看看 Ankur 的评论,在我看来,他实际上是在推荐与我的列表推荐相同的东西,但是以熊猫的方式。从代码的角度来看,这样做肯定会更好,而且可能更快(尽管可能不会更快,具体取决于 pandas 在后台所做的事情)。这部分是最有意义的优化,因为它消除了许多不必要的字典插入。
【解决方案3】:

您可以使用 Numba 让这段代码变得更快。 Numba 使用即时编译器生成非常快速的本机代码。 Numba 不支持 Pandas,但列可以转换为 Numpy 视图,Numba 能够非常快速地处理它们。

假设日期时间索引在输入数据帧中正确排序(这似乎是关于 Pandas 代码的情况),您可以编写以下 Numba 函数:

import numba as nb

# Note: if you know that there is no NaN & Inf values then you can 
# use the additional flag fastmath=True so to generate a faster code.
# Check the results with this flag to be safe.
@nb.njit('void(float64[:], float64[:], float64[:], int64[:], float64[:], float64[:], float64[:])')
def compute(L, m, k, f, soc, charge, discharge):
    n = L.shape[0]
    assert m.size == n and k.size == n and f.size == n
    assert soc.size == n and charge.size == n and discharge.size == n

    for i in range(n):
        if i == 0:
            soc[i] = bp_soct0
            charge[i] = min(L[i], (bp_energy - bp_soct0) * 4.0, bp_power) * bp_efficiency if L[i] >= 0.0 else 0.0
            discharge[i] = -min(m[i], 4.0 * bp_soct0 - 4.0 * (bp_minsoc * bp_energy), bp_power) if m[i] >= 0.0 else 0.0
        else:
            soc[i] = soc[i-1] + (charge[i-1] + discharge[i-1]) * 0.25
            charge[i] = min(L[i], (bp_energy - soc[i]) * 4.0, bp_power) * bp_efficiency if L[i] >= 0.0 else 0.0
            discharge[i] = -min(m[i], 4.0 * soc[i] - 4.0 * (bp_minsoc * bp_energy), bp_power) if m[i] >= 0.0 else 0.0

@nb.njit 是一个将 Python 函数编译成快速原生代码的装饰器。它的字符串参数是函数的签名,指定参数类型和函数的返回类型。您的机器上的类型可能不同。请注意,如果需要,最好通过手动转换 Pandas 列来处理定义明确的类型。它不仅速度快,而且占用更少的内存空间并消除可能的意外问题(例如浮点舍入)。

Numba 代码适用于名为 cp 的复制的 Pandas 数据帧的每一列的视图。包装代码如下:

from time import time as tt

cp = cons_prod.copy(deep=True)

# Initialise the columns filling them with zeroes
cp["State of charge (kW)"] = 0.0
cp["Charge (kW)"] = 0.0
cp["Discharge (kW)"] = 0.0

dct_bp = {
    "power": 50,
    "energy": 130,
    "efficiency": 0.9,
    "minsoc": 0.1,
    "soct0": 65.0,
}

# Storing the values of the battery dictionary lookups in variables
bp_energy = dct_bp["energy"]
bp_power = dct_bp["power"]
bp_efficiency = dct_bp["efficiency"]
bp_soct0 = dct_bp["soct0"]
bp_minsoc = dct_bp["minsoc"]

start1 = tt()  # Measuring time

# Extract Numpy views from the Pandas dataframe (20 us)
L = cp["Injection (kW)"].to_numpy()
m = cp["Grid Consumption (kW)"].to_numpy()
k = cp["Auto-consumption (kW)"].to_numpy()
f = cp["Consumption (kW)"].to_numpy()
soc = cp["State of charge (kW)"].to_numpy()
charge = cp["Charge (kW)"].to_numpy()
discharge = cp["Discharge (kW)"].to_numpy()

# Mutate/Fill the columns (2 us)
compute(L, m, k, f, soc, charge, discharge)

end1 = tt() - start1
print(f"Numba took {end1:.2f} seconds")

基准测试

在我的机器上,初始 Pandas 代码(EDIT 2)需要 70 毫秒。 Numba compute 函数只需要 0.002 毫秒,而调用 to_numpy 的包装代码只需要 0.020 毫秒!这意味着当前 Numba 代码在提供的小示例 Pandas 数据帧上大约快 3200 倍!此外,包装代码应该花费与输入大小无关的恒定时间。这意味着只有compute 函数的时间应该在更大的数据帧的测量部分中花费大量时间。

事实上,我尝试创建一个大 100 倍的输入数据框,其中小的值重复了 3 个月,以下是我机器上的性能结果:

Initial code in ("EDIT 2"):      6830   ms
This Numba code:                    0.1 ms

因此,在更大的数据集上,Numba 快了 68300 倍

【讨论】:

  • 感谢 Jérôme 的解决方案!不幸的是,我不能使用 numba,因为它必须被冻结并用 pyinstaller 打包,而后者仍然存在 numba 问题(这是一个已知的错误)。让我说我会立即使用您的解决方案!
  • 好吧,伤心。请注意,Numba 是 AOT compiler。或者,您可以使用 Cython。代码应该非常相似,当然也非常快(至少>1000x)。 Cython seems to be supported by PyInstaller.
  • 我会试试 Cython,谢谢! :)
【解决方案4】:

Cython 可以解决循环缓慢的问题,它非常适合您需要的任何类型的计算。 Cython 允许您在 Python 中编写 c 风格的程序:https://cython.readthedocs.io/en/latest/src/quickstart/overview.html 为了执行 cython 函数,您需要创建一个文件,编写 cython 函数并编译该文件。该文档可帮助您更好地理解语法。 另一种解决方案是按照以下步骤在 Jupyter 笔记本中尝试代码:

  1. 在控制台/终端中执行pip install Cython
  2. 在 Jupyter notebook 中,编写并执行%load_ext Cython
  3. 在第一行是魔术关键字%%cython的单元格中创建您的函数

我还注意到一些重复的代码;我尽量简化了我的理解

这是我的解决方案,在 Jupyter 笔记本中运行: 我想每列的类型是float32/int32,而不是float64/int64;应该是合理的选择

cons_prod['Consumption (kW)'] = cons_prod['Consumption (kW)'].astype(np.int32)
cons_prod['Renewable Production (kW)'] = cons_prod['Renewable Production (kW)'].astype(np.float32)
cons_prod['Auto-consumption (kW)']= cons_prod['Auto-consumption (kW)'].astype(np.float32)
cons_prod['Injection (kW)']= cons_prod['Injection (kW)'].astype(np.float32)
cons_prod['Grid Consumption (kW)']= cons_prod['Grid Consumption (kW)'].astype(np.float32)

cons_prod.dtypes

要提高 cython 的性能,显式键入至关重要

%%cython
import cython
cimport cython
cimport numpy as np
import numpy as np

ctypedef np.float32_t float32_t
ctypedef np.int32_t int32_t


def func(
    float32_t[:] injection,
    float32_t[:] grid_consumption,
    float32_t[:] auto_consumption,
    int32_t[:] consumption,
    battery_parameters
    ):
    
    cdef int n_rows = len(injection)
    cdef int bp_energy = battery_parameters["energy"]
    cdef int bp_power = battery_parameters["power"]
    cdef float soct0 = 0.5 * bp_energy
    
    soc_dct = np.zeros(n_rows, dtype=np.float32)
    charge_dct = np.zeros(n_rows, dtype=np.float32)
    discharge_dct = np.zeros(n_rows, dtype=np.float32)
    acc_dct = np.zeros(n_rows, dtype=np.float32)
    lst_dct = np.zeros(n_rows, dtype=np.float32)
    inj_dct = np.zeros(n_rows, dtype=np.float32)
    gridcons_dct = np.zeros(n_rows, dtype=np.float32)
    agg_dct = np.zeros(n_rows, dtype=np.float32)
    
    cdef float32_t[:] soc_view = soc_dct
    cdef float32_t[:] charge_view = charge_dct
    cdef float32_t[:] discharge_view = discharge_dct
    cdef float32_t[:] acc_view = acc_dct
    cdef float32_t[:] lst_view = lst_dct
    cdef float32_t[:] inj_view = inj_dct
    cdef float32_t[:] gridcons_view = gridcons_dct
    cdef float32_t[:] agg_view = agg_dct
    
    cdef int i
    
    # L = injection[i]
    # m = grid_consumption[i]
    # k = auto_consumption[i]
    # f = consumption[i]

    for i in range(0, n_rows):
        if i == 0:
            # State of charge (kW)
            soc_view[i] = soct0
        else:
            # State of charge (kW)
            soc_view[i] = soc_view[i-1] + (charge_view[i-1] + discharge_view[i-1]) / 4
        
        # Charge (kW)
        charge_view[i] = min(
            injection[i], (bp_energy - soc_view[i]) * 4,
            bp_power
        ) * battery_parameters['efficiency'] if injection[i]>=0 else 0
        
        # Discharge (kW)
        discharge_view[i] = -min(
            grid_consumption[i],
            4*soc_view[i] - 4*(battery_parameters['minsoc']*bp_energy),
            bp_power
        ) if grid_consumption[i] >= 0 else 0
    
        # Auto-consumption NEW (kW)
        auto_consumption[i] - discharge_view[i]
        
        # Lost injection due to battery efficiency (kW)
        lst_view[i] = (charge_view[i] / battery_parameters["efficiency"]) - charge_view[i]
        
        # Injection NEW (kW)
        inj_view[i] = injection[i] - charge_view[i] - lst_view[i]
        
        # Grid Consumption NEW (kW)
        gridcons_view[i] = consumption[i] - acc_view[i]
        
        # Additional Auto-consumption through battery (kW)
        agg_view[i] = acc_view[i] - auto_consumption[i]
        
    return soc_dct, charge_dct, discharge_dct, acc_dct, lst_dct, inj_dct, gridcons_dct,agg_dct

基准测试

初始代码:15.6ms

Cython 代码:349 µs

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-05-08
    • 1970-01-01
    • 2015-02-11
    • 2021-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多