Optimising itertools combination with grouped DataFrame and post filter

Viewed 72

I have a DataFrame as

Locality     money
1            3
1            4
1            10
1            12
1            15
2            16
2            18

I have to do a combination with replacement of money column with a groupby view on Locality and a filter on the money difference. The target must be like

Locality     money1     money2
1            3          3 
1            3          4
1            4          4
1            10         10
1            10         12
1            10         15
1            12         12
1            12         15
1            15         15
2            16         16
2            16         18
2            18         18

Note that the combination is applied for values on the same Locality and values which have a difference less than 6.

My current code is

from itertools import combinations_with_replacement
import numpy as np
import panda as pd

def generate_graph(input_series, out_cols):
    return pd.DataFrame(list(combinations_with_replacement(input_series, r=2)), columns=out_cols)

df = (
    df.groupby(['Locality'])['money'].apply(
        lambda x: generate_graph(x, out_cols=['money1', 'money2'])
    ).reset_index().drop(columns=['level_1'], errors='ignore')
)

# Ensure the Distance between money is within the permissible limit
df = df.loc[(
    df['money2'] - df['money1'] < 6
)]

The issue is, I have a DataFrame with 100000 rows which takes almost 33 seconds to process my code. I need to optimize the time taken by my code probably using numpy. I am looking for optimizing the groupby and the post-filter which takes extra space and time. For sample data, you can use this code to generate the DataFrame.

# Generate dummy data
t1 = list(range(0, 100000))
b = np.random.randint(100, 10000, 100000)
a = (b/100).astype(int)

df = pd.DataFrame({'Locality': a, 'money': t1})
df = df.sort_values(by=['Locality', 'money'])
1 Answers

To gain both running time speedup and reduce space consumption:

Instead of post-filtering - apply an extended function (say combine_values) that generates dataframe on a generator expression yielding already filtered (by condition) combinations.

(factor below is a default argument that indicates to the mentioned permissible limit)

In [48]: def combine_values(values, out_cols, factor=6): 
    ...:     return pd.DataFrame(((m1, m2) for m1, m2 in combinations_with_replacement(values, r=2)  
    ...:                         if m2 - m1 < factor), columns=out_cols) 
    ...:                                                                                                                                         

In [49]: df_result = ( 
    ...:     df.groupby(['Locality'])['money'].apply( 
    ...:         lambda x: combine_values(x, out_cols=['money1', 'money2']) 
    ...:     ).reset_index().drop(columns=['level_1'], errors='ignore') 
    ...: ) 

Execution time performance:

In [50]: %time df.groupby(['Locality'])['money'].apply(lambda x: combine_values(x, out_cols=['money1', 'money2'])).reset_index().drop(columns=['l
    ...: evel_1'], errors='ignore')                                                                                                              
CPU times: user 2.42 s, sys: 1.64 ms, total: 2.42 s
Wall time: 2.42 s
Out[50]: 
        Locality  money1  money2
0              1      34      34
1              1     106     106
2              1     123     123
3              1     483     483
4              1     822     822
...          ...     ...     ...
105143        99   99732   99732
105144        99   99872   99872
105145        99   99889   99889
105146        99   99913   99913
105147        99   99981   99981

[105148 rows x 3 columns]
Related