You can solve your problem with the help of graph theory. In your case you are looking for complete bipartite graphs of type K3,3 (the middle one in the picture below).

You can use this answer to find all maximal complete bipartite graphs with the help of maximal cliques and then filter out K3,3 graphs.
Example:
I used networkx
package and jupyter notebook
.
import networkx as nx
from networkx.algorithms import bipartite
from itertools import combinations, chain
Let’s create a random bipartite graph (like your CSV data).
B = bipartite.random_graph(n=8, m=8, p=0.5, seed=3)
print(B.edges)
Prints:
[(0, 8), (0, 10), (0, 11), (0, 13), (0, 15), (1, 8), (1, 9), (1, 12), (1, 13), (1, 14), (2, 14), (2, 15), (3, 10), (3, 11), (3, 13), (3, 14), (4, 8), (4, 11), (4, 13), (4, 15), (5, 9), (5, 10), (5, 13), (5, 15), (6, 8), (6, 9), (6, 12), (6, 13), (6, 15), (7, 11), (7, 13)]
I used this answer to create a function to draw nice bipartite graphs.
def draw_bipart(graph, set_X, set_Y):
pos = dict()
pos.update( (n, (1, i)) for i, n in enumerate(set_X) ) # put nodes from X at x=1
pos.update( (n, (2, i)) for i, n in enumerate(set_Y) ) # put nodes from Y at x=2
nx.draw(graph, pos=pos, with_labels=True)
X, Y = bipartite.sets(B) #two sets of data (in your case user_id and item_id)
%matplotlib inline
draw_bipart(B, X, Y)

Let’s create a copy of the graph B and connect all vertices in each set (X and Y) so we can search for cliques.
connect_B = B.copy()
edges_X = combinations(X, 2)
edges_Y = combinations(Y, 2)
connect_B.add_edges_from(chain(edges_X, edges_Y))
draw_bipart(connect_B, X, Y)
Now each pair of vertices from the set X (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) is connected by an edge (not all visible due to the overlapping). The same goes for the Y set.

Now let’s search for maximal cliques:
cliques = list(nx.find_cliques(connect_B))
print(cliques)
Prints all maximal cliques:
[[2, 0, 4, 5, 6, 1, 3, 7], [2, 0, 4, 5, 6, 15], [2, 14, 1, 3], [2, 14, 15], [13, 0, 10, 11, 8, 15], [13, 0, 10, 11, 3], [13, 0, 10, 5, 3], [13, 0, 10, 5, 15], [13, 0, 4, 11, 8, 15], [13, 0, 4, 11, 3, 7], [13, 0, 4, 6, 1, 8], [13, 0, 4, 6, 1, 3, 5, 7], [13, 0, 4, 6, 15, 8], [13, 0, 4, 6, 15, 5], [13, 9, 8, 12, 6, 1], [13, 9, 8, 12, 6, 15], [13, 9, 8, 12, 14, 1], [13, 9, 8, 12, 14, 10, 11, 15], [13, 9, 5, 10, 15], [13, 9, 5, 6, 1], [13, 9, 5, 6, 15], [13, 14, 3, 1], [13, 14, 3, 10, 11]]
Now we have to filter K3,3 graphs. I use here two conditions: the K3,3 graph should have 6 vertices and 3 of them should belong to one set.
cliques_K33 = [c for c in cliques if len(c) == 6 and len(X.intersection(c)) == 3]
print(cliques_K33)
Prints:
[[13, 0, 4, 6, 15, 8]]
Finally we draw a subgraph of the graph B induced by vertices from the found K3,3 clique:
draw_bipart(B.subgraph(cliques_K33[0]), X, Y)
