We start by grouping the df by order_id, and within each group calculate all possible pairs. Note we sort first by product_id so the same pairs in different groups are always in the same order
import itertools
all_pairs = []
for _, group in df.sort_values('product_id').groupby('order_id'):
all_pairs += list(itertools.combinations(group['product_id'],2))
all_pairs
we get a list of all pairs from all orders
[('3333', '365'),
('3333', '48750'),
('3333', '9877'),
('365', '48750'),
('365', '9877'),
('48750', '9877'),
('32001', '3333'),
('32001', '48750'),
('3333', '48750'),
('11202', '3333'),
('11202', '365'),
('11202', '365'),
('3333', '365'),
('3333', '365'),
('365', '365')]
Now we count duplicates
from collections import Counter
count_dict = dict(Counter(all_pairs))
count_dict
so we get the count of each pair, basically what you are after
{('3333', '365'): 3,
('3333', '48750'): 2,
('3333', '9877'): 1,
('365', '48750'): 1,
('365', '9877'): 1,
('48750', '9877'): 1,
('32001', '3333'): 1,
('32001', '48750'): 1,
('11202', '3333'): 1,
('11202', '365'): 2,
('365', '365'): 1}
Putting this back into a cross-product table is a bit of work, the key bit is spliitng the tuples into columns by calling .apply(pd.Series)
and eventually moving one of the columns to the column names via unstack
:
(pd.DataFrame.from_dict(count_dict, orient='index')
.reset_index(0)
.set_index(0)['index']
.apply(pd.Series)
.rename(columns = {0:'pid1',1:'pid2'})
.reset_index()
.rename(columns = {0:'count'})
.set_index(['pid1', 'pid2'] )
.unstack()
.fillna(0))
this produces a 'compact' form of the table you are after that only includes products that appeared in at least one pair
count
pid2 3333 365 48750 9877
pid1
11202 1.0 2.0 0.0 0.0
32001 1.0 0.0 1.0 0.0
3333 0.0 3.0 2.0 1.0
365 0.0 1.0 1.0 1.0
48750 0.0 0.0 0.0 1.0
UPDATE
Here is a rather simplified version of the above, following various discussions in the comments
import numpy as np
import pandas as pd
from collections import Counter
# we start as in the original solution but use permutations not combinations
all_pairs = []
for _, group in df.sort_values('product_id').groupby('order_id'):
all_pairs += list(itertools.permutations(group['product_id'],2))
count_dict = dict(Counter(all_pairs))
# We create permutations for _all_ product_ids ... note we use unique() but also product(..) to allow for (365,265) combinations
total_pairs = list(itertools.product(df['product_id'].unique(),repeat = 2))
# pull out first and second elements separately
pid1 = [p[0] for p in total_pairs]
pid2 = [p[1] for p in total_pairs]
# and get the count for those permutations that exist from count_dict. Use 0
# for those that do not
count = [count_dict.get(p,0) for p in total_pairs]
# Now a bit of dataFrame magic
df_cross = pd.DataFrame({'pid1':pid1, 'pid2':pid2, 'count':count})
df_cross.set_index(['pid1','pid2']).unstack()
and we are done. df_cross
below
count
pid2 11202 32001 3333 365 48750 9877
pid1
11202 0 0 1 2 0 0
32001 0 0 1 0 1 0
3333 1 1 0 3 2 1
365 2 0 3 2 1 1
48750 0 1 2 1 0 1
9877 0 0 1 1 1 0