Reshape a DataFrame based on column value, and pad missing slices with zeros

Viewed 123

I have a Pandas DataFrame which looks like:

ID order other_column_1 other_column_x
A 0 10 20
A 1 11 21
A 2 12 22
B 0 31 41
B 2 33 43

I want to reshape it to a 3D matrix with shape (#IDs, #order, #other columns). For the example above, it should be of shape (2, 3, 2).

The order column holds the order of the 2nd dimension, so slice ['A', 0, :] should be [10, 20] and ['A', 1, :] [11, 21] etc. The values of order are identical for all ID (0, 1, 2 in this case).

Trouble is, sometimes a slice is missing e.g. for 'B', the slice (order) '1' is missing, which I want to make it a slice pad with all 0's, to keep the shape consistent.

I think of pre-sorting the whole DataFrame by ID and order, loop over each ID , insert missing slices, and stack them together. However, the DataFrame is huge so I try to avoid global sort and loop if possible.

2 Answers

I came up with a way to do it (if you have enough pc memory to allocate) where you dont have to loop the whole dataframe although I coudn't test it with 10M rows because of memory allocation. I tested it with 5M rows by 300 columns and I will show the results at the end of the answer.

The idea is to get all the combinations of the unique values of the first 2 columns as an index to build the first 2 dimensions of the 3D array.

After that you can merge the original dataframe with the dataframe containing index combinations to then fill all the missing values with 0.

Once the data is complete you can pass it to numpy and reshape it in 3 dimensions.

Code without comments:

# df = orginal dataframe
d1 = df.ID.unique()
d2 = df.order.unique()
df3 = pd.MultiIndex.from_product((d1, d2), names=['ID', 'order'])\
    .to_frame().reset_index(drop=True)\
    .merge(df, on=['ID', 'order'], how='left')\
    .fillna(0)
np_3d_array = df3[df3.columns[2:]].to_numpy().reshape(d1.shape[0], d2.shape[0], df.columns[2:].shape[0])

Code with comments:

# df = orginal dataframe

# Get unique id for 1st dimension
d1 = df.ID.unique()

# Get unique order fpr 2nd dimension
d2 = df.order.unique()

# Get complete DF
df3 = pd.MultiIndex.from_product((d1, d2), names=['ID', 'order'])\ # Get missing values from 1st and 2nd dimensions as index
    .to_frame().reset_index(drop=True)\ # Get Dataframe from multiindex and reset index
    .merge(df, on=['ID', 'order'], how='left')\ # Merge the complete dimensions with the original values
    .fillna(0) # fill missing values with 0

# get complete data as 2D array and reshape as 3D array
np_3d_array = df3[df3.columns[2:]].to_numpy().reshape(d1.shape[0], d2.shape[0], df.columns[2:].shape[0])

Test:

First I tried to test with 10M rows but I could not allocate the memory needed for that.

To test the code I created a a dataframe with 6M rows x 300 columns (random float numbers) and dropped 1M rows to simulate the missing values.

Here is the code I used to test and the results.

Test code:

import random
import time
import pandas as pd
import numpy as np

# 100000 diff. ID and 60 diff. order
df_test = pd.MultiIndex.from_product((range(100000), range(60)), names=['ID', 'order'])\
    .to_frame().reset_index(drop=True)\
    .drop(random.sample(range(6_000_000), k=1_000_000))\ # Drop 1M rows to simulate missing rows
    .reset_index(drop=True)

# 5M rows random data by 298 columns
df_test2 = pd.DataFrame(np.random.random(size=(5_000_000, 298)))

df = df_test.merge(df_test2, left_index=True, right_index=True)

start = time.time()

d1 = df.ID.unique()
print(f'time 1st Dimension: {round(time.time()-start, 3)}')

d2 = df.order.unique()
print(f'time 2nd Dimension: {round(time.time()-start, 3)}')

df3 = pd.MultiIndex.from_product((d1, d2), names=['ID', 'order'])\
    .to_frame().reset_index(drop=True)\
    .merge(df, on=['ID', 'order'], how='left').fillna(0)
print(f'time merge: {round(time.time()-start, 3)}')

np_3d_array = df3[df3.columns[2:]].to_numpy().reshape(d1.shape[0], d2.shape[0], df.columns[2:].shape[0])
print(f'time ndarray: {round(time.time()-start, 3)}')
print(f'array shape: {np_3d_array.shape}')
print(f'array type: {type(np_3d_array)}')

Test Results:

time 1st Dimension: 0.035
time 2nd Dimension: 0.063
time merge: 47.202
time ndarray: 49.441
array shape: (100000, 60, 298)
array type: <class 'numpy.ndarray'>
ids = df.ID.unique()
orders = df.order.unique()
ar = (df.set_index(['ID','order'])
        .reindex(pd.MultiIndex.from_product((ids, orders)))
        .fillna(0)
        .to_numpy()
        .reshape(len(ids), len(orders), len(df.columns[2:])))
print(ar)
print(ar.shape)

Output:

[[[10. 20.]
  [11. 21.]
  [12. 22.]]

 [[31. 41.]
  [ 0.  0.]
  [33. 43.]]]

(2, 3, 2)
Related