Remove a single element from python's random.sample population

Question:

I am trying to implement a graph representation where each node of the graph has random edges to other nodes but not to itself. Therefore, I am trying to exclude the node itself from the population out of which random.sample would generate the random neighbors. However, the following code returns "TypeError: Population must be a sequence. For dicts or sets, use sorted(d)."

import random as rnd
gsize=5
graph = [rnd.sample(list(range(gsize)).remove(node), rnd.randint(0, gsize-1)) for node in range(gsize)]

I saw in this question it was suggested to create a variable first and then apply remove() to it. I tried to refactor it this way

glist = list(range(gsize))
graph = [rnd.sample(glist.remove(node), rnd.randint(0, gsize-1)) for node in range(gsize)]

but now it throws "TypeError: ‘NoneType’ object is not iterable". I think the problem is essentially the same, i.e. the population is of type NoneType. Is there a neat way of achieving what I need?

Asked By: Soheil

||

Answers:

list.remove is an in-place operation and thus returns None. What you probably want is something like:

graph = [
    rnd.sample(
       [other for other in range(gsize) if not node == other], 
       rnd.randint(0, gsize-1)
    ) for node in range(gsize)
]

Where you are building a non-inclusive set every time.

A more efficient way would be:

from collections import deque
import random as rnd

graph = []

population = deque(range(gsize))

for _ in range(gsize):
    # pop the rightmost element
    removed = population.pop()

    # build the graph with the remaining population
    graph.append(rnd.sample(population, rnd.randint(0, gsize-1)))

    # add the removed element to the leftmost side
    population.appendleft(removed)
Answered By: C.Nivs

You can use list comprehension to create the population without the current node, like this:

import random

gsize = 5
graph = [random.sample([n for n in range(gsize) if n != node], random.randint(0, gsize - 1)) for node in range(gsize)]
Answered By: zoldxk

You could take a sample from a range that is one less than gsize and add one when the value is the same or greater than the current index. This will effectively skip over the current index without having to bother with any exclusion or item removal:

import random as rnd
gsize = 5
graph = [ [n+(n>=r) for n in rnd.sample(range(gsize-1),rnd.randrange(gsize))] 
          for r in range(gsize)]

output:

print(*graph,sep="n")

[1, 2]
[4, 3]
[0, 4]
[0]
[3, 1, 0, 2]

If you want to obtain an undirected graph, then you will need these "neighbours" to be bidirectional and consistent across nodes. For this, you can use combinations from itertools to produce a list of links in one direction (leftIndex < rightIndex), select a number of these links at random according to the desired density, and then assign the references in both directions in the graph matrix:

gsize     = 5
allLinks  = gsize*(gsize-1)//2
linkCount = allLinks * 75 // 100     # 75% density

graph    = [[] for _ in range(gsize)]
from itertools import combinations
for r,c in rnd.sample([*combinations(range(gsize),2)],linkCount):
    graph[r].append(c)
    graph[c].append(r)

output:

print(*graph,sep="n")

[4, 3]
[2, 4, 3]
[1, 4]
[4, 1, 0]
[3, 0, 1, 2]

Note how node0 --> node4 and symmetrically node4 --> node0

Answered By: Alain T.
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.