DataFrame,数据列筛选代替遍历每一行数据去判断,大大提高数据过滤速度

文章讲述了作者如何优化Python代码,使用Pandas处理一个45M的大CSV文件,提升从特定时间段和信号范围内提取数据的效率,从最初的900s降低到30s,主要改进了数据遍历和筛选过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

运行环境

笔记本win10,i5-6200U,16G
python3.8
pandas1.3.5

程序目的

在一张接近45M的csv表格,63.7w行数据,从中提取符合时间段范围的(每天20到第二天9点的数据),并且信号数值小于一定范围的数据,接着按每天为一份,去重统计和提取这些数据的原始详细信息
在这里插入图片描述

最原始的运算方式,一行一行遍历去判断每个单元格

def date_clean(file: str, time_interval: tuple, distance: tuple):
    """
    按时间区间,信号范围来提取出数据
    :param file: 读取csv文件
    :param time_interval: 开始小时和结束小时,元组类型(20,9)20点到9点
    :param distance: 开始结束信号,元组类型(500,30000) 500到30000
    :return:
    """
    df_list = []  # 存储筛选好的数据,先存列表或字典,最后再转为DataFrame能缩短一半时间
    print(f"loading... {datetime.datetime.now()}")
    data_frame = pd.read_csv(file, encoding='utf-8', dtype="string[pyarrow]")  # 防止读取长数值时,用的是科学计数法导致精度丢失
    print(f"加载完毕... {datetime.datetime.now()}")
    start_time = datetime.time(time_interval[0], 0, 0)
    end_time = datetime.time(time_interval[1], 0, 0)
    if time_interval[0] > time_interval[1]:  # 如果时间区间是跨天

        for ind, row in data_frame.iterrows():
            if (datetime.time(time_interval[0], 0, 0) <= pd.to_datetime(row["采集时间"]).time() or
                    pd.to_datetime(row["采集时间"]).time() <= datetime.time(time_interval[1], 0, 0)):  # 只比较小时:
                if distance[0] <= int(float(row["信号"])) <= distance[1]:
                    #print(data_frame.loc[[row]])
                    #print(row)
                    df_list.append(row.to_list())
        
    else:
        pass
    df = data_frame.reset_index(drop=True)

    #df = pd.DataFrame(df_list, columns=["数据", "地点", "采集时间", "归属地", "信号"])  # to_list()后没有了表头,需要手动添加
   # .reset_index()列表里存的是Series,他还有一组与数组数据对应的标签索引。转为DataFrame后需要重设index

    #print(df)
    print(f"依据时间和信号值过滤的数据比为 {len(df)}/{len(data_frame)}")
    print(f"数据清理... {datetime.datetime.now()}")

    subsection_count_filt(df, time_interval, 28)


def subsection_count_filt(full_data: pd.DataFrame, time_interval: tuple, times: int):
    """
    按时间段划分为一段数据区间,在从去重后的唯一值列表去筛选出这段区间内出现的次数
    :param unique_data: 去重后的唯一值列表
    :param full_data: 全量数据
    :param time_interval: 时间范围区间,(开始时间,结束时间)
    :param times: 筛选出现的次数 大于等于
    :return:
    """
    full_result_list = []  # 保存统计次数的结果
    full_result_details = pd.DataFrame()   # 保存结果的详情内容
    dt = full_data["采集时间"].str.split(' ', expand=True)  # 将年月日 时分秒拆分成两列
    # print(dt)
    # full_data["年月日"] = dt[0]
    # full_data["时分秒"] = dt[1]
    day_times = dt[0].drop_duplicates().to_list()  # 获取表格中包含的日期
    day_times.sort()

    for day in day_times:  # 按时间归为一组
        print(day)
        day_time1 = datetime.datetime(int(day.split('-')[0]), int(day.split('-')[1]), int(day.split('-')[2]), time_interval[0], 0, 0)
        day_time2 = datetime.datetime(int(day.split('-')[0]), int(day.split('-')[1]), int(day.split('-')[2]), time_interval[1], 0, 0) + datetime.timedelta(days=1)
        df_day_list = []  # 分类一天的数据
        to_drop = []
        have_data = False  # 判断插入了数据
        #for row in range(0, len(full_data)):
        for index, row in full_data.iterrows():
            if day_time1 <= pd.to_datetime(row["采集时间"]) <= day_time2:
                # 一天内的数据汇总统计
                df_day_list.append(row.to_list())
                to_drop.append(index)
                have_data = True
        # 删除已不需要的后能加快速度下次遍历速度
        full_data = full_data.drop(to_drop)  # 快了1/3

        if have_data:
            df_day = pd.DataFrame(df_day_list, columns=["数据", "地点", "采集时间", "归属地", "信号"])
            one_day_counts = df_day["数据"].value_counts()  # 计数
            one_day_result = one_day_counts[one_day_counts >= times]  # 筛选出现次数大于n次的
            result = pd.DataFrame({'数据': one_day_result.index, '次数': one_day_result.values})
            # print(result)
            for ind, row in result.iterrows():

                full_result_list.append({"日期": day, "数据": row["数据"], "次数": row["次数"]})
                full_result_details = pd.concat([full_result_details, (df_day[df_day["数据"] == row["数据"]])], ignore_index=True)  # 从一天数据中检索并插入详情,concat拼接
            # print(full_result)

    full_result = pd.DataFrame(full_result_list)

    # 结果去重
    print("结果去重")
    print(full_result.drop_duplicates(subset="数据").reset_index())

运行时间 900s+,时间太长了,查资料说用pandas2.0之后的,可以用pyarrow来提高速率,但是2.0很多方法不适用了,影响到之前的一些脚本运行,后来仔细想了想,是遍历那里的时间复杂度太大了,提取出结果的数据,大概要遍历10亿次,决定从这里的循环逻辑做改善

取消遍历的方式,用DataFrame本身的数据筛选

之前一直难到的点是,觉得时间这一列要提取出来转换成time后才能作比较,现在找到直接转换成时间而不保留日期的函数了

def date_clean(file: str, time_interval: tuple, distance: tuple):
    """
    按时间区间,信号范围来提取出数据
    :param file: 读取csv文件
    :param time_interval: 开始小时和结束小时,元组类型(20,9)20点到9点
    :param distance: 开始结束信号,元组类型(500,30000) 500到30000
    :return:
    """
    df_list = []  # 存储筛选好的数据,先存列表或字典,最后再转为DataFrame能缩短一半时间
    print(f"loading... {datetime.datetime.now()}")
    data_frame = pd.read_csv(file, encoding='utf-8', dtype="string[pyarrow]")  # 防止读取长数值时,用的是科学计数法导致精度丢失
    print(f"加载完毕... {datetime.datetime.now()}")
    start_time = datetime.time(time_interval[0], 0, 0)
    end_time = datetime.time(time_interval[1], 0, 0)
    if time_interval[0] > time_interval[1]:  # 如果时间区间是跨天
        #  !!! 取消用一行行遍历的方式,来减少时间复杂度,直接根据列值来筛选后使用
        data_frame["小时间"] = pd.to_datetime(data_frame["采集时间"]).dt.time  # 只提取小时
        data_frame["信号"] = pd.to_numeric(data_frame["信号"])  # 转换数值行
        data_frame = data_frame.loc[(data_frame["小时间"] >= start_time) | (data_frame["小时间"] <= end_time)]  # 跨天时间用 或
        data_frame = data_frame.loc[(data_frame["信号"] >= float(distance[0])) & (data_frame["信号"] <= float(distance[1]))]
        data_frame.drop(['小时间'], axis=1, inplace=True)  # 删除小时

    else:
        pass
    df = data_frame.reset_index(drop=True)
    #print(df)
    print(f"依据时间和信号值过滤的数据比为 {len(df)}/{len(data_frame)}")
    print(f"数据清理... {datetime.datetime.now()}")

    # print(f"再次去重后 {len(df_unique)}/{find_num}")

    subsection_count_filt(df, time_interval, 28)


def subsection_count_filt(full_data: pd.DataFrame, time_interval: tuple, times: int):
    """
    按时间段划分为一段数据区间,在从去重后的唯一值列表去筛选出这段区间内出现的次数
    :param unique_data: 去重后的唯一值列表
    :param full_data: 全量数据
    :param time_interval: 时间范围区间,(开始时间,结束时间)
    :param times: 筛选出现的次数 大于等于
    :return:
    """
    full_result_list = []  # 保存统计次数的结果
    full_result_details = pd.DataFrame()   # 保存结果的详情内容
    dt = full_data["采集时间"].str.split(' ', expand=True)  # 将年月日 时分秒拆分成两列

    day_times = dt[0].drop_duplicates().to_list()  # 获取表格中包含的日期
    day_times.sort()

    full_data["采集时间"] = pd.to_datetime(full_data["采集时间"])  # 转换为时间格式

    for day in day_times:  # 按时间归为一组
        print(day)
        #  划分一组的开始时间和结束时间
        day_time1 = datetime.datetime(int(day.split('-')[0]), int(day.split('-')[1]), int(day.split('-')[2]), time_interval[0], 0, 0)
        day_time2 = datetime.datetime(int(day.split('-')[0]), int(day.split('-')[1]), int(day.split('-')[2]), time_interval[1], 0, 0) + datetime.timedelta(days=1)

        #  取消使用上面的遍历模式
        df_day = full_data.loc[(full_data["采集时间"] >= day_time1) & (full_data["采集时间"] <= day_time2)]  # 具体日期时间用 与

        if len(df_day) != 0:  # 有数据

            df_day = df_day.reset_index(drop=True)
            one_day_counts = df_day["数据"].value_counts()  # 计数
            one_day_result = one_day_counts[one_day_counts >= times]  # 筛选出现次数大于n次的
            result = pd.DataFrame({'数据': one_day_result.index, '次数': one_day_result.values})
            # print(result)
            for ind, row in result.iterrows():

                full_result_list.append({"日期": day, "数据": row["数据"], "次数": row["次数"]})
                full_result_details = pd.concat([full_result_details, (df_day[df_day["数据"] == row["数据"]])], ignore_index=True)  # 从一天数据中检索并插入详情,concat拼接
            # print(full_result)

    full_result = pd.DataFrame(full_result_list)

    # 结果去重
    print("结果去重")
    print(full_result.drop_duplicates(subset="数据").reset_index())

经过这种方法后,运算时间在30s+,快了30倍啊!!!,所以有时候运行慢,找到不是系统io读写文件这种问题后,考虑一下程序的时间复杂度

<think>嗯,用户给了一个Python函数merge_cells_between_indices,想要优化它的运行速度,特别是处理大规模数据的时候。现在我得先仔细看看原来的代码是怎么工作的,然后找出哪里可以改进。 首先,原函数的功能是合并DataFrame中某一(target_col)从以“1、”开头的行开始,直到下一个“1、”之前的所有行的内容。比如,假设在索引2有一个“1、...”,下一个在索引5,那么2到4行的内容会被合并到这一部分,然后替换原来的每个行内容为合并后的字符串。 接下来,原代码的步骤大概是这样的: 1. 处理NaN,转为字符串,避免后续操作出错。 2. 找到所有以“1、”开头的行的索引,排序。 3. 遍历每个起始索引,确定合并的范围,合并文本,然后批量赋值。 现在需要优化速度,特别是处理大数据的时候。那问题可能出在哪儿呢? 首先,遍历每个起始索引,然后对于每个区间,使用mask来选取行,合并字符串,然后赋值。这里可能的瓶颈是循环次数多,或者每次循环中的操作不够高效。 例如,原代码在每次循环中,对于每个current_idx和next_idx之间的行,使用mask来定位,然后合并这些行的字符串,然后将合并后的结果赋值给这些行的target_col。这一步的问题在于,如果数据量很大,每个循环内的操作可能涉及大量的数据复制或者字符串拼接,尤其是当合并的区间很大时,字符串拼接可能会很慢,因为Python的字符串是不可变的,每次拼接都会生成新对象。 那优化点可能包括: 1. 减少循环次数,或者将循环中的操作向量化。 2. 提前计算所有需要合并的区间,然后批量处理,避免逐个处理。 3. 使用更高效的方法来合并字符串,比如先将所有需要合并的部分收集起来,然后一次性更新DataFrame。 另一个可能的优化点是避免多次修改DataFrame,尤其是在循环中频繁地修改DataFrame可能会导致性能下降,因为DataFrame的修改操作通常不是原地进行的,而是生成新的副本。因此,如果能一次性生成所有需要合并的结果,然后一次性更新到DataFrame中,可能会更快。 具体来说,可以考虑以下步骤: - 预处理:填充NaN,转为字符串,这部分可能已经很快了,不需要优化。 - 找到所有起始索引,这一步也没问题。 - 生成每个区间(开始到下一个开始),然后对每个区间内的行,合并字符串。然后,将这些合并后的字符串对应到各个行中。例如,每个区间的行都对应同一个合并后的字符串,这样只需要为每个区间生成一次合并字符串,然后将该字符串赋给区间内的所有行。这应该和原来的做法类似,但可能需要优化合并的方法。 或者,可以考虑先创建一个新的Series,记录每个区间对应的合并后的字符串,然后将这个Series赋值给原始DataFrame的target_col。这可能需要使用groupby或者某种分组方法,将每个区间分组,然后计算合并后的字符串,再扩展回原来的索引。 例如,将每个区间的起始索引作为组的开始,然后给每个区间分配一个组号,之后按组进行合并。例如,创建一个组标签,每个区间的行都属于同一个组,然后对每个组计算合并后的字符串,再将这些字符串赋给对应的行。 如何创建这样的组标签呢?假设起始索引是按顺序排的,那么每个起始索引之后的行的组号应该是当前组的编号,直到遇到下一个起始索引。例如,对于起始索引表[start1, start2, start3,...],所有索引在start1到start2-1的行属于组0,start2到start3-1的行属于组1,依此类推。最后一个组则是从startN到数据末尾。 这可能需要使用pandas的groupby功能,但如何高效生成这样的组标签是关键。 举个例子,假设start_indices是排好序的表。那么对于整个DataFrame的索引,可以创建一个group,每个起始索引对应的位置为新的组,之后的行属于这个组直到下一个起始索引。比如,可以通过pandas的cut函数或者使用cumsum来生成这样的组。 比如: groups = (df.index.to_series().isin(start_indices)).cumsum() - 1 这可能不行,因为isin(start_indices)只有在那些起始索引的位置是True,其他是False。cumsum()会将连续的False合并为一个组,直到遇到下一个True。例如,假设起始索引是2和5,那么索引0、1是False,2是True(cumsum变为1),3、4是False(cumsum保持1),5是True(cumsum变为2),后面的行是False(cumsum保持2)。所以,这时候groups会是0(对于索引0、1),1(索引2、3、4),2(索引5及之后)。这可能不符合原来的分组方式,因为原函数的分组是从current_idx到next_idx-1。比如,start_indices中的第一个元素是current_idx=2,那么组应该是从2到next_idx-1=5-1=4。所以,在groups的生成中,应该将这些索引分配到同一个组中。 或者,另一种方法是,创建一个数组,其中每个元素代表该行属于哪个组。例如,初始时group = -1,然后遍历每个行索引,当遇到start_indices中的索引时,group递增。这可能更直接,但如何高效地实现? 或者,可以利用pandas的索引和start_indices来确定每个行的组号。例如,对于每个索引i,找到最大的start_idx,使得 start_idx <= i,并且start_idx在start_indices中。这类似于分段查找,可以用searchsorted来实现。 举个例子,start_indices是一个排好序的表。对于每一个索引值,可以用np.searchsorted(start_indices, i, side='right') -1,这样可以得到对应的组号。例如,假设start_indices是 [2,5], i=3,searchsorted找右边插入的位置是1,减1得到0,对应的组号是0,对应的start_indices[0]是2,即i=3属于第一个组。而i=5的话,searchsorted找到插入位置是2,减1得到1,对应的start_indices[1]=5,属于第二个组。而最后一个组的情况,当i>=最后一个start_idx时,会得到最后一个组的编号。 这可能是一个高效的方法,因为searchsorted是向量化的,可以快速处理整个索引数组。 那么,步骤如下: 1. 将start_indices排序,并转为数组。 2. 对于所有行的索引i,使用searchsorted找到对应的组号。 3. 然后按组号分组,计算每个组的合并字符串。 4. 将这些合并字符串映射回每个行。 这可能比原来的逐个循环处理更高效,尤其是当数据量大时,因为循环中的操作被向量化了。 这样,整个处理过程可以转换为向量化操作,避免显式循环,从而提升速度。 现在具体如何实施: 首先,处理后的start_indices已经排序。然后,创建一个数组group_ids,长度等于df的长度,其中每个元素表示该行所属的组号。然后,使用groupby在group_ids上,对target_col进行合并,得到每个组的merged_text。然后,将每个行的target_col替换为对应组的merged_text。 但如何生成group_ids呢? 假设df的索引是连续的整数,从0开始,或者可能不是?例如,如果原来的df索引是不连续的或者有其他形式,可能需要重置索引或者假设索引是单调递增的。 不过原代码中,start_indices是直接获取符合条件的行的索引,然后排序。假设这些索引是单调递增的,因为DataFrame的索引默认是0-based的连续整数,除非用户重置过。但原代码处理的是原始索引,所以可能存在非连续的情况? 不过假设索引是单调递增的,或者至少start_indices是排好序的,那么我们可以利用searchsorted。 例如: import numpy as np # start_indices是已经排序好的表 start_indices_arr = np.array(start_indices) # 所有行的索引转为数组 all_indices = df.index.values # 对于每个索引i,找到在start_indices_arr中最后一个小于等于i的位置 group_indices = np.searchsorted(start_indices_arr, all_indices, side='right') - 1 这样,group_indices中的每个元素就是对应的组号。例如,start_indices_arr为[2,5],对于i=0,得到-1,但需要处理这种情况。因为可能有些行的索引在第一个start_idx之前,比如索引0和1,这时候group_indices会是-1。而根据原函数逻辑,这些行不会被合并,因为原代码的循环是从第一个start_idx开始的。所以,如何处理这部分? 原函数中,在第一个start_idx之前的行不会被处理。例如,假设start_indices是[2,5],那么循环中的i从0开始,处理current_idx=2,mask是索引>=2且<5的行。而索引0和1的行不会被处理。所以在新的方法中,group_indices为-1的行应该保持原来的值,而group_indices>=0的行被替换为合并后的字符串。 所以,可以将group_indices中为-1的情况排除,或者设置这些组号为-1,在后续处理中忽略。 但如何生成正确的group_ids?可能需要调整,因为原函数中的处理是,每个区间从current_idx到next_idx-1(如果有下一个的话),否则到末尾。例如,在最后一个start_idx之后的所有行都属于最后一个组。 例如,start_indices是 [2,5], 所以: 组0:索引2,3,4 组1:索引5,6,7,... 原函数中的处理是,当i是最后一个start_index时,mask是 >= current_idx,也就是从5开始的所有行都属于组1。 而用searchsorted得到的group_indices对于索引5,得到的是1(因为 start_indices_arr中第二个元素是5,所以searchsorted(5, side='right')返回2,减1得到1,所以索引5属于组1。 索引6,searchsorted得到插入位置是2,减1得到1,所以也属于组1,正确。 对于索引0,得到-1,属于无效组。 所以,接下来,我们可以将group_ids设置为 group_indices,其中 >=0的部分。然后,按group_ids分组,计算每个组的合并字符串,然后将这些字符串赋给对应的行。 但如何处理无效的组(group_indices为-1的行)?这些行应该保持原来的值。所以,在后续处理中,我们可以将group_ids中的-1替换为NaN,然后在分组时忽略这些行,或者直接在合并时处理。 例如: # 创建group_ids数组 group_ids = np.searchsorted(start_indices_arr, all_indices, side='right') - 1 valid_groups_mask = group_ids >=0 group_ids_valid = group_ids[valid_groups_mask] 然后,对于有效的group_ids,分组并计算合并后的字符串: merged_series = df.loc[valid_groups_mask, target_col].groupby(group_ids_valid).agg(''.join) 然后,将这个merged_series映射回原始数据。例如: df.loc[valid_groups_mask, target_col] = df.loc[valid_groups_mask, 'group_id'].map(merged_series) 但这里可能存在索引的问题,因为group_ids_valid是数组形式,而groupby需要按这些值来分组。或者,可能需要将group_ids附加到DataFrame中,然后进行groupby。 或者,可以这样做: 在df中创建一个临时,表示group_id: df['group_id'] = group_ids 然后,对于group_id >=0的行,将它们的target_col按group_id分组,合并字符串: merged = df[df['group_id'] >=0].groupby('group_id')[target_col].agg(''.join) 然后,将merged合并回原始df: df[target_col] = df['group_id'].map(merged).fillna(df[target_col]) 这样,原来的无效组(group_id <0)的值会被保留,而有效的组则被替换为合并后的字符串。 然后,删除临时'group_id'。 这种方法的好处是,将整个合并操作转换为向量化操作,避免了循环,从而可能显著提高速度,特别是在大数据集的情况下。 但需要注意,这样的处理是否正确。例如,原函数中合并后的字符串被赋值给该区间的每一行,即每个区间的所有行都会被替换为合并后的字符串。而使用groupby后的合并,每个组的字符串会被应用到该组的所有行,这与原函数的行为是一致的。 所以,这样的优化方法应该是可行的,并且更高效。 此外,原函数在合并字符串时,使用''.join(...),所以不需要任何分隔符。这和使用groupby后的agg(''.join)是一致的。 现在,总结优化步骤: 1. 预处理target_col,填充NaN并转为字符串。 2. 找到所有以“1、”开头的行的索引,排序得到start_indices。 3. 如果start_indices为空,直接返回df。 4. 否则,将start_indices转为numpy数组。 5. 计算每个行的group_id,使用searchsorted。 6. 创建group_id。 7. 对group_id >=0的行,按group_id分组,合并target_col。 8. 将合并后的结果映射回原df的target_col。 9. 删除group_id。 这样的方法避免了显式的循环,转而使用向量化操作和groupby,应该会更快。 另一个可能的优化点是在处理字符串时,原代码在每次循环中合并字符串,而这里直接使用groupby的agg,可能会更高效,因为agg是在底层用C实现的,速度更快。 此外,原函数在循环中对每个区间进行合并,然后逐个赋值,而优化后的方法是一次性处理所有区间,减少了Python层面的循环次数。 现在,测试一下这种方法的正确性: 假设有如下df: 索引 | target_col 0 | a 1 | b 2 | 1、start 3 | c 4 | d 5 | 1、next 6 | e 处理后的结果应该是: 索引0: a(未处理) 索引1: b(未处理) 索引2: 1、startcd (合并索引2到4) 索引3: 1、startcd 索引4: 1、startcd 索引5: 1、nexte (合并索引5到6) 索引6: 1、nexte 原来的代码中,在第一个循环i=0,current_idx=2,next_idx=5。mask是索引>=2且<5,即2,3,4。合并后的字符串是'1、start'+'c'+'d' = '1、startcd',然后这些行都被赋值为该字符串。 第二个循环i=1,current_idx=5,没有下一个索引,mask是>=5。合并索引5和6的字符串为'1、next'+'e'='1、nexte',赋给这两行。 而优化后的方法,group_id的计算: start_indices = [2,5] 对于每个索引: 索引0: searchsorted(0) → 插入位置0(因为0 <2),返回0,减1 → -1 索引1: 同样 → -1 索引2: searchsorted(2) →找到位置1(因为start_indices_arr是[2,5],2等于第一个元素,插入位置1(side='right'的话,会找到第一个大于2的位置?比如,假设start_indices_arr是[2,5],当查询2时,side='right'返回插入位置1,因为2在数组中存在,side='right'会将插入点放在现有元素的右边。所以,searchsorted(2, side='right')返回1,减1得到0。所以,索引2的group_id是0。 索引3: searchsorted(3) → 插入位置1(3在2和5之间,side='right'的插入位置是1),减1得到0 → group_id 0. 索引4: 同样得到0. 索引5: searchsorted(5) → 插入位置2,减1得到1 → group_id 1. 索引6: searchsorted(6) →插入位置2,减1得到1 → group_id1. 所以,group_id是: 索引0: -1 索引1: -1 索引2:0 索引3:0 索引4:0 索引5:1 索引6:1 然后,分组后的合并结果是: 组0: 索引2,3,4的target_col合并 → "1、start"+"c"+"d" →"1、startcd" 组1: 索引5,6的target_col合并 →"1、next"+"e"→"1、nexte" 所以,映射后,这些行的target_col被替换为对应的合并字符串,而索引0和1保持原样。这与原函数的结果一致。 因此,这种方法是正确的。 接下来,如何实现这一优化? 现在,原函数中的start_indices是排好序的,所以无需处理。然后,将start_indices转为numpy数组: start_indices_arr = np.array(start_indices) 然后,计算group_ids: group_ids = np.searchsorted(start_indices_arr, df.index.values, side='right') -1 注意,这里使用的是df.index.values,即所有行的索引数组。然后,得到的group_ids可能包含-1(当索引小于第一个start_idx时),以及正确的组号。 然后,将group_ids添加到df中: df['group_id'] = group_ids 然后,对group_id >=0的行进行分组: merged = df[df['group_id'] >=0].groupby('group_id')[target_col].agg(''.join) 然后,将merged映射到原df的target_col: df[target_col] = df['group_id'].map(merged).fillna(df[target_col]) 最后,删除group_id: df.drop(columns=['group_id'], inplace=True) 这样,整个操作就完成了。不需要循环,因此速度应该更快,尤其是在处理大规模数据时。 此外,原函数中的.fillna('').astype(str)是必要的,但可能可以优化为更高效的方式。例如,使用fillna(''),然后将转换为str类型,或者直接使用astype(str),因为NaN转换为字符串会变成'nan'?需要确认原函数的意图。原函数中,fillna('')是为了将NaN转为空字符串,然后再转为字符串类型。因此,正确的处理应该是: df[target_col] = df[target_col].fillna('').astype(str) 这一步是必须的,否则,如果有NaN,后续的startswith会出错。 现在,综合这些步骤,优化后的函数可能如下: def merge_cells_between_indices_optimized(df, target_col): # 处理NaN并转为字符串 df[target_col] = df[target_col].fillna('').astype(str) # 获取所有以'1、'开头的行索引并排序 start_indices = df[df[target_col].str.startswith('1、', na=False)].index.tolist() start_indices = sorted(start_indices) if not start_indices: return df start_indices_arr = np.array(start_indices) all_indices = df.index.values group_ids = np.searchsorted(start_indices_arr, all_indices, side='right') - 1 # 创建临时group_id df['group_id'] = group_ids # 过滤有效组并合并文本 valid_mask = df['group_id'] >= 0 merged = df.loc[valid_mask].groupby('group_id')[target_col].agg(''.join) # 更新目标 df.loc[valid_mask, target_col] = df.loc[valid_mask, 'group_id'].map(merged) # 删除临时 df.drop(columns=['group_id'], inplace=True) return df 这样,整个函数避免了显式的Python循环,转而使用NumPy和pandas的向量化操作,应该在大规模数据下运行更快。 需要测试该函数是否处理了原函数的所有情况,比如当没有start_indices时,直接返回df。原函数在这种情况下也会返回df,但合并操作不会进行。优化后的函数在start_indices为空时直接返回,是正确的。 此外,关于性能测试,假设数据量很大,例如有百万行,使用原函数的循环方法可能需要O(N)次循环(假设每个区间包含多个行),而优化后的方法则通过向量化和groupby操作,将复杂度降低到接近O(1)的操作,或者至少更高效的低层级实现。 另一个可能的优化点是使用transform方法,而不是map。例如,在分组后使用transform,直接得到一个与原df长度相同的Series,其中每个元素是该组的合并字符串: merged_series = df[valid_mask].groupby('group_id')[target_col].transform(''.join) 然后,直接赋值: df.loc[valid_mask, target_col] = merged_series 这可能更简洁,并且不需要创建merged的映射。例如: def merge_cells_between_indices_optimized(df, target_col): df[target_col] = df[target_col].fillna('').astype(str) start_indices = df[df[target_col].str.startswith('1、', na=False)].index.tolist() start_indices = sorted(start_indices) if not start_indices: return df start_indices_arr = np.array(start_indices) group_ids = np.searchsorted(start_indices_arr, df.index, side='right') -1 df['group_id'] = group_ids valid_mask = df['group_id'] >=0 df.loc[valid_mask, target_col] = ( df[valid_mask].groupby('group_id')[target_col] .transform(lambda x: ''.join(x)) ) df.drop(columns=['group_id'], inplace=True) return df 使用transform可能更高效,因为它直接在分组后生成对应每个元素的合并字符串。这样,代码更简洁,且可能更高效,因为transform返回的是一个与原数据长度相同的Series,对齐索引。 但需要注意,transform应用于groupby后的对象时,返回的Series的索引与原数据相同,因此可以直接赋值。 这可能比先agg再map更高效,因为它避免了map的步骤,而是直接通过groupby+transform得到结果。 测试一下: 对于之前的例子,groupby后的transform会将每个组的合并字符串赋给每个成员。例如,组0的成员会得到"1、startcd",组1的成员得到"1、nexte"。这样,结果与原函数一致。 因此,使用transform可能更高效,代码更简洁。 因此,最终的优化函数可能采用这种方式。 另一个考虑因素是,在pandas中,groupby后的transform和agg的性能差异。对于大分组,transform可能需要更多的内存,因为它为每个元素生成结果,而agg则是每个组生成一个结果。然而,在这个场景中,由于每个组的结果需要被多次赋值到对应的行,使用transform可能更直接,且可能更快,因为它内部处理了如何将分组结果扩展回原索引。 因此,优化后的函数可以这样写: import numpy as np def merge_cells_between_indices_optimized(df, target_col): # 处理NaN并转为字符串 df[target_col] = df[target_col].fillna('').astype(str) # 获取所有以'1、'开头的行索引并排序 start_mask = df[target_col].str.startswith('1、', na=False) start_indices = df[start_mask].index.tolist() start_indices = sorted(start_indices) if not start_indices: return df # 使用numpy计算分组标识 start_indices_arr = np.array(start_indices) group_ids = np.searchsorted(start_indices_arr, df.index.values, side='right') - 1 # 创建有效掩码(排除不属于任何组的情况) valid_mask = group_ids >= 0 # 使用transform进行合并操作 df.loc[valid_mask, target_col] = ( df.loc[valid_mask, target_col] .groupby(group_ids[valid_mask]) .transform(''.join) ) return df 这里,不再需要创建group_id,而是直接在groupby时使用group_ids[valid_mask]数组。这可以节省内存,因为不需要在DataFrame中添加临时。 例如,groupby的参数可以是数组,因此可以将group_ids[valid_mask]作为分组依据。同时,df.loc[valid_mask, target_col]是目标的有效部分,而groupby的参数是group_ids[valid_mask],即每个有效行对应的组号。 这样,transform会将每个组的字符串合并,然后将结果赋回给df的相应位置。 这可能更高效,因为避免了创建和删除临时,同时groupby的参数直接使用数组,而不需要的存在。 此外,start_mask的生成可能需要优化。原代码中,df[target_col].str.startswith('1、', na=False)生成一个布尔Series,用于筛选起始行。这里,.str.startswith的参数na=False会将NaN视为False,所以fillna('')已经处理了NaN的情况,所以startswith的na参数可能无关紧要,但为了安全,保留na=False。 这样,整个函数的步骤是: 1. 将target_col填充空字符串并转为字符串类型。 2. 找到所有以“1、”开头的行的索引,排序。 3. 如果没有这样的索引,直接返回df。 4. 使用numpy的searchsorted计算每个行的组号。 5. 确定有效掩码(组号>=0)。 6. 对有效部分,按组号分组,使用transform将每个行的target_col替换为合并后的字符串。 7. 返回修改后的df。 这应该比原函数更高效,因为它避免了显式的Python循环,转而使用向量化的操作和groupby的底层优化。 可能的性能提升点: - 使用向量化的searchsorted代替显式循环遍历每个起始索引。 - 使用groupby+transform,内部由pandas的C扩展处理,而不是Python层级的循环和字符串拼接。 - 减少对DataFrame的多次赋值操作,改为一次或少量次数的赋值。 因此,这样的优化应该能显著提升函数在大规模数据上的运行速度。</think>以下是优化后的函数及分步说明: ```python import numpy as np def merge_cells_between_indices_optimized(df, target_col): # 预处理:转换NaN并确保字符串类型 df[target_col] = df[target_col].fillna('').astype(str) # 获取所有符合条件的起始索引 start_mask = df[target_col].str.startswith('1、', na=False) start_indices = df[start_mask].index.tolist() # 如果没有起始点直接返回 if not start_indices: return df # 核心优化部分 start_indices = sorted(start_indices) start_indices_arr = np.array(start_indices) all_indices = df.index.values # 计算每个行所属的组号 (向量化操作) group_ids = np.searchsorted(start_indices_arr, all_indices, side='right') - 1 valid_mask = group_ids >= 0 # 标识需要处理的行 # 分组合并文本 (使用pandas原生优化) df.loc[valid_mask, target_col] = ( df.loc[valid_mask, target_col] .groupby(group_ids[valid_mask]) .transform(''.join) ) return df ``` ### 优化原理分步说明 1. **向量化索引计算** 使用`np.searchsorted`替代循环判断区间范围,通过二分查找算法快速确定每个行所属的合并组。时间复杂度从O(N*M)降低到O(N log M)(N总行数,M起始点数) 2. **内存预分配优化** - 将起始点索引转换为NumPy数组 `start_indices_arr` - 一次性获取所有索引值 `all_indices = df.index.values` - 避免在循环中重复生成中间变量 3. **原生分组操作** 使用`gr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值