优化Pandas Series相邻值平均迭代逻辑,解决大数据量性能问题
Hey there! Let's break down why your reshape_arr function is taking 7-10 minutes with 100k+ rows, and fix it with way faster, more efficient approaches.
First: Why Your Current Code Is So Slow
Your function has three critical performance bottlenecks that add up to those brutal runtimes:
- Repeated sorting & deduplication: Every loop calls
sort_values().unique()—an expensive O(n log n) operation. Doing this over and over wastes massive amounts of time. - Multiple global replacements:
a.replace()scans the entire Series every time you call it. When you do this multiple times per loop, you’re looking at O(k*n) time (k = number of iterations), which is devastating for large datasets. - Inefficient iterative logic: You re-scan the entire array every loop to catch new merges from averages. This leads to redundant work, as you’re re-checking pairs you’ve already processed.
Optimized Approach 1: Single-Pass Group Merging (No Multiprocessing)
The core idea here is to group all consecutive values within close distance in one pass, compute their averages, then map those averages back to the original Series in a single vectorized operation. This eliminates repeated loops and global replacements entirely.
Here's the optimized code:
import pandas as pd import numpy as np def reshape_arr_optimized(a, close): """Merge values within 'close' distance into their average, returns unique values""" # Get sorted unique values once (no repeated sorting!) sorted_unique = np.sort(a.unique()) n = len(sorted_unique) if n <= 1: return sorted_unique # Group consecutive values that are within 'close' range groups = [] current_group = [sorted_unique[0]] for num in sorted_unique[1:]: if num - current_group[-1] < close: current_group.append(num) else: groups.append(current_group) current_group = [num] groups.append(current_group) # Create a mapping from original values to their group average value_map = {} for group in groups: avg = np.mean(group) for num in group: value_map[num] = avg # Replace all values in one vectorized pass (way faster than multiple replaces!) updated_a = a.map(value_map) return updated_a.unique()
Why This Works Faster
- Single sort/deduplication: We only sort and deduplicate once, not every loop iteration.
- Vectorized mapping:
a.map()scans the Series once, instead of multiplereplace()calls that each scan the whole Series. - No repeated loops: We catch all mergable groups in one pass, so we don’t need to keep looping until no changes happen.
For 100k+ rows, this should drop your runtime from minutes to seconds.
Optimized Approach 2: Multiprocessing with Chunk Overlap (For Extra Large Datasets)
If you’re dealing with 200k+ rows and want to leverage multiple CPU cores, you can split the data into chunks—but you need to handle chunk boundaries to avoid missing merges between chunks.
The fix is to add overlap to each chunk (keeping values near the boundary that might merge with adjacent chunks), process chunks in parallel, then do a final global merge to clean up any cross-chunk merges.
Here's the code:
from multiprocessing import Pool import pandas as pd import numpy as np def process_chunk(chunk, close): """Process a single chunk using the optimized group merging logic""" sorted_unique = np.sort(chunk.unique()) n = len(sorted_unique) if n <= 1: return chunk.map({sorted_unique[0]: sorted_unique[0]}) if n ==1 else chunk groups = [] current_group = [sorted_unique[0]] for num in sorted_unique[1:]: if num - current_group[-1] < close: current_group.append(num) else: groups.append(current_group) current_group = [num] groups.append(current_group) value_map = {} for group in groups: avg = np.mean(group) for num in group: value_map[num] = avg return chunk.map(value_map) def reshape_arr_multiprocess(a, close, num_processes=4): """Multiprocess version of the optimized function, handles chunk boundaries""" # Sort the Series first to make chunk overlap meaningful sorted_a = a.sort_values() total_len = len(sorted_a) chunk_size = total_len // num_processes # Create chunks with overlap (keep values within 'close' range of boundaries) chunks = [] for i in range(num_processes): # Start a bit early to capture values that might merge with the previous chunk start = max(0, i * chunk_size - close) # End a bit late to capture values that might merge with the next chunk end = min(total_len, (i + 1) * chunk_size + close) chunks.append(sorted_a.iloc[start:end]) # Process chunks in parallel with Pool(num_processes) as pool: processed_chunks = pool.starmap(process_chunk, [(chunk, close) for chunk in chunks]) # Combine chunks and do a final global merge to fix cross-chunk merges combined_values = pd.concat(processed_chunks).unique() sorted_combined = np.sort(combined_values) n = len(sorted_combined) if n <=1: return sorted_combined # Final grouping to catch any merges between chunk boundaries groups = [] current_group = [sorted_combined[0]] for num in sorted_combined[1:]: if num - current_group[-1] < close: current_group.append(num) else: groups.append(current_group) current_group = [num] groups.append(current_group) # Return the final unique averages return np.array([np.mean(group) for group in groups])
Key Fix for Chunk Boundaries
By adding overlap to each chunk (extending start/end by close), we ensure that values near chunk edges aren’t cut off from potential merges with adjacent chunks. The final global merge cleans up any remaining merges that span chunk boundaries.
内容的提问来源于stack exchange,提问作者Keven Scharaswak

