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?
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)
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)]
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
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?
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)
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)]
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