12

I have a dataframe of format: (example data)

      Metric1  Metric2  Metric3  Metric4  Metric5
ID    
1     0.5      0.3      0.2      0.8      0.7    
2     0.1      0.8      0.5      0.2      0.4    
3     0.3      0.1      0.7      0.4      0.2    
4     0.9      0.4      0.8      0.5      0.2    

where score range between [0,1] and I wish to generate a function that, for each id (row), calculates the top n metrics, where n is an input of the function along with the original dataframe.

My ideal output would be:(for eg. n = 3)

      Top_1     Top_2     Top_3
ID    
1     Metric4   Metric5   Metric1    
2     Metric2   Metric3   Metric5    
3     Metric3   Metric4   Metric1    
4     Metric1   Metric3   Metric4  

Now I have written a function that does work:

def top_n_partners(scores,top_n=3):
metrics = np.array(scores.columns)
records=[]
for rec in scores.to_records():
    rec = list(rec)
    ID = rec[0]
    score_vals = rec[1:]
    inds = np.argsort(score_vals)
    top_metrics = metrics[inds][::-1]
    dic = {
        'top_score_%s' % (i+1):top_metrics[i]
        for i in range(top_n)
    }
    dic['ID'] = ID
    records.append(dic)
top_n_df = pd.DataFrame(records)
top_n_df.set_index('ID',inplace=True)
return top_n_df

However it seems rather inefficient/slow especially for the volume of data I'd be running this over (dataframe with millions of rows) and I was wondering if there was a smarter way to go about this?

smci
  • 32,567
  • 20
  • 113
  • 146
tfcoe
  • 391
  • 4
  • 13

2 Answers2

9

You can use numpy.argsort:

print (np.argsort(-df.values, axis=1)[:,:3])
[[3 4 0]
 [1 2 4]
 [2 3 0]
 [0 2 3]]

print (df.columns[np.argsort(-df.values, axis=1)[:,:3]])

Index([['Metric4', 'Metric5', 'Metric1'], ['Metric2', 'Metric3', 'Metric5'],
       ['Metric3', 'Metric4', 'Metric1'], ['Metric1', 'Metric3', 'Metric4']],
      dtype='object')

df = pd.DataFrame(df.columns[np.argsort(-df.values, axis=1)[:,:3]], 
                               index=df.index)
df = df.rename(columns = lambda x: 'Top_{}'.format(x + 1))
print (df)
      Top_1    Top_2    Top_3
ID                           
1   Metric4  Metric5  Metric1
2   Metric2  Metric3  Metric5
3   Metric3  Metric4  Metric1
4   Metric1  Metric3  Metric4 

Thank you Divakar for improving:

n = 3
df = pd.DataFrame(df.columns[df.values.argsort(1)[:,-n+2:1:-1]], 
                               index=df.index)

df = df.rename(columns = lambda x: 'Top_{}'.format(x + 1))
print (df)
      Top_1    Top_2    Top_3
ID                           
1   Metric4  Metric5  Metric1
2   Metric2  Metric3  Metric5
3   Metric3  Metric4  Metric1
4   Metric1  Metric3  Metric4                
jezrael
  • 822,522
  • 95
  • 1,334
  • 1,252
  • 4
    A bit more of performance could be gained with : `df.values.argsort(1)[:,-n+2:1:-1]`. – Divakar Jun 01 '17 at 14:11
  • @jezrael I am getting warnings using above syntax: `FutureWarning: Support for multi-dimensional indexing (e.g. obj[:, None]) is deprecated and will be removed in a future version. Convert to a numpy array before indexing instead.` – piyush-balwani Mar 01 '22 at 18:22
3

A different way using Pandas reshaping:

df.set_index('ID', inplace=True)
df_out = df.rank(axis=1, ascending=False).astype(int).reset_index().melt(id_vars='ID').query('value <= 3').pivot(index='ID',columns='value')
df_out.columns = df_out.columns.droplevel().astype(str)
df_out = df_out.add_prefix('Top_')
print(df_out)

Output:

value    Top_1    Top_2    Top_3
ID                              
1      Metric4  Metric5  Metric1
2      Metric2  Metric3  Metric5
3      Metric3  Metric4  Metric1
4      Metric1  Metric3  Metric4
Scott Boston
  • 147,308
  • 15
  • 139
  • 187