Melt dataframe based on condition

Viewed 249
d = {'key': [1,2,3], 'a': [True,True, False], 'b': [False,False,True]}
df = pd.DataFrame(d)

Current melt function is:

df2 = df.melt(id_vars=['key'], var_name = 'letter', value_name = 'Bool')
df2 = df2.query('Bool == True')

Is there a way to incorporate that 'True' condition in the melt function. As I continue to add entries to my df and I have hundreds of columns, I assume it's much less costly to pull only the values I need instead of melting the entire df and then filtering. Any ideas?

4 Answers

Use pd.melt instead. Factor in replacement of False with NaN and dropna() eventually.

pd.melt(df.replace(False, np.nan), id_vars=['key'],var_name = 'letter', value_name = 'Bool').dropna()



    key letter  Bool
0    1      a  True
1    2      a  True
5    3      b  True

You can filter the non key cols first, melt the results and concat the melted rows back. See the following;

import pandas as pd
import numpy as np
import time

d = {'key': [1,2,3], 'a': [True,True, False], 'b': [False,False,True]}
df = pd.DataFrame(d)

start_time = time.time()
key_column_name = 'key'
key_column_loc = list(df.columns).index(key_column_name)
filtered_frame = None
for letter in [s for s in list(df.columns) if s != key_column_name]:
  true_booleans = np.nonzero(df[letter].values)[0]
  melted_df = df.iloc[true_booleans][[key_column_name, letter]].reset_index(drop=True).melt(id_vars=[key_column_name], var_name = 'letter', value_name = 'Bool')
  if filtered_frame is None:
    filtered_frame = melted_df
  else:
    filtered_frame = pd.concat((filtered_frame, melted_df), axis = 0)
end_time = time.time()
print(filtered_frame, '\n\n', end_time - start_time, 'seconds!')

Output

   key letter  Bool
0    1      a  True
1    2      a  True
0    3      b  True 

 0.011133432388305664 seconds!

Compared to your code, it is slower (your score is 0.008090734481811523 seconds!), however as the rows increase, I would expect that above way of doing it will be more efficient. Looking forward for the results.

Regarding the discussion on speed (Benchmarks)

import pandas as pd
import numpy as np
import time
import matplotlib.pyplot as plt

# Benchmark Tests 
d = {'key': [1,2,3], 'a': [True,True, False], 'b': [False,False,True]}
df_initial = pd.DataFrame(d)

data_size = [10, 100, 10000, 50000, 100000, 500000, 1000000, 5000000, 10000000, 50000000]
scores_current = []
scores_golden_lion  = []
scores_sammywemmy = []
scores_wwnde = []
scores_slybot = []

for n_rows in data_size:

  df = df_initial.sample(n=n_rows, replace=True).reset_index(drop=True)

  ## @Current method 
  start_time = time.time()
  df_current = df.melt(id_vars=['key'], var_name = 'letter', value_name = 'Bool')
  df_current = df_current.query('Bool == True')
  end_time = time.time()
  scores_current.append(end_time-start_time)

  ## @Golden Lion
  start_time = time.time()
  df_golden_lion = df.melt(id_vars=['key'], var_name = 'letter', value_name = 'Boolean')
  df_golden_lion= df_golden_lion.drop(df_golden_lion.index[df_golden_lion['Boolean'] == False])
  end_time = time.time()
  scores_golden_lion.append(end_time-start_time)

  ## @sammywemmy
  start_time = time.time()
  box = df.iloc[:, 1:]
  len_df = len(df)
  letters = np.tile(box.columns, (len_df,1))[box]
  df_sammywemmy = pd.DataFrame({'key':df.key.array, 
                                'letter' : letters, 
                                'Bool' : [True]*len_df})
  end_time = time.time()
  scores_sammywemmy.append(end_time-start_time)

  ## @wwnde 
  start_time = time.time()
  df_wwnde = pd.melt(df.replace(False, np.nan), id_vars=['key'],var_name = 'letter', value_name = 'Bool').dropna()
  end_time = time.time()
  scores_wwnde.append(end_time-start_time)

  ## @Slybot 
  start_time = time.time()
  key_column_name = 'key'
  key_column_loc = list(df.columns).index(key_column_name)
  filtered_frame = None
  for letter in [s for s in list(df.columns) if s != key_column_name]:
    true_booleans = np.nonzero(df[letter].values)[0]
    melted_df = df.iloc[true_booleans][[key_column_name, letter]].melt(id_vars=[key_column_name], var_name = 'letter', value_name = 'Bool')
    if filtered_frame is None:
      filtered_frame = melted_df
    else:
      filtered_frame = pd.concat((filtered_frame, melted_df), axis = 0)
  end_time = time.time()
  scores_slybot.append(end_time-start_time)

plt.plot(data_size, scores_current, label = "Current method")
plt.plot(data_size, scores_golden_lion, label = "Golden Lion")
plt.plot(data_size, scores_sammywemmy, label = "sammywemmy")
plt.plot(data_size, scores_wwnde, label = "wwnde")
plt.plot(data_size, scores_slybot, label = "Slybot")
plt.legend()
plt.show()

Benchmarks

Interesting to see that none of the other answers can beat the originally suggested method with a dataset of 500,000 rows! Until 200,000 rows sammywemmy's method is a clear winner though.

The melt and filter step is efficient though, I'd probably stick with loc instead of query, especially if your data is not that large (<200_000 rows)

Another option is to skip melt, use numpy, and build a new dataframe:

box = df.iloc[:, 1:]
len_df = len(df)
letters = np.tile(box.columns, (len_df,1))[box]
pd.DataFrame({'key':df.key.array, 
              'letter' : letters, 
              'Bool' : [True]*len_df})

   key letter  Bool
0    1      a  True
1    2      a  True
2    3      b  True

melt moves column data and stacks it vertically resulting in two columns: the variable name of the column being stacked and the value column name.

d = {'key': [1,2,3], 'a': [True,True, False], 'b': [False,False,True],'c':['Batchelor','Masters','Doctorate']}
df = pd.DataFrame(d)


df2 = df.melt(id_vars=['key'], var_name = 'letter', value_name = 'Boolean')

df2=df2.drop(df2.index[df2['Boolean'] == False])

print(df2)

output

  key letter    Boolean
0    1      a       True
1    2      a       True
5    3      b       True
6    1      c  Batchelor
7    2      c    Masters
8    3      c  Doctorate
​
Related