Revisiting xmas kindle draw

Posted on Mon 18 November 2019 in analysis

Using jupyter notebook, to figure out how to draw our next christmas kindle.

A very simple kindle draw

Revisiting the simple kindle draw. And getting utterly confused.

In [1]:
import random

def simple_secret_santa(names,printit=False):
    n = len(names)
    s = random.sample(names,k=n)
    out = []
    for i,name in enumerate(s):
        if i == len(s)-1:
            to = s[0]
        else:
            to = s[i+1]
        out.append(name+'-'+to)
        if printit:
            print(str(i+1) + "   "+name + ' gives to ' + to)
    return out
In [2]:
names = ['Ro','Pa','Ao','HaM',
        'Fi', 'Ci',
        'Te','Ja','Ch','HaC',
        'Co','An','It',
        'To',
        'Et',
        'Ro', 'Li']
In [3]:
s = simple_secret_santa(names,printit=True)
1   Te gives to Ro
2   Ro gives to Ci
3   Ci gives to Ch
4   Ch gives to HaM
5   HaM gives to Co
6   Co gives to HaC
7   HaC gives to Pa
8   Pa gives to To
9   To gives to Ao
10   Ao gives to Ro
11   Ro gives to Li
12   Li gives to Fi
13   Fi gives to It
14   It gives to Et
15   Et gives to Ja
16   Ja gives to An
17   An gives to Te

How random is this ?

First lets import matplotlib package so we can do some vizualisation.

In [4]:
%matplotlib inline
import matplotlib.pyplot as plt 
import matplotlib.gridspec as gridspec 
import numpy as np
import itertools
In [5]:
def monte_carlo(names,n=1000):
    comb = {name1+'-'+name2:0 for name1,name2 in itertools.permutations(names,2)}
    for i in range(n):
        for couple in simple_secret_santa(names):
            comb[couple] += 1
    fig1 = plt.figure()
    plt.plot([comb[k] for k in comb.keys()])
    plt.xlabel('Iterations')
    plt.ylabel('Count each combo')
    #plt.axhline(y=n, color='r', linestyle='-')
    plt.title('Xmas Kindle')
    plt.show()
    return comb
In [6]:
c = monte_carlo(names,n=1000000)
No description has been provided for this image

That is very odd, I would have expected each combination to appear roughly the same number of times, it looks like some combination are twice as frequent ? ... Let's try with a smaller set.

In [7]:
names2 = ['Jolene','Niamh','Sophie','Lorraine','Paschal']
#only 5 people in the draw, which should make 20 possible donor combinations
comb2 = {name1+'-'+name2:0 for name1,name2 in itertools.permutations(names2,2)}
print(len(comb2))
20
In [8]:
c2 = monte_carlo(names2,n=50000)
No description has been provided for this image

This looks about right.Let's used a reduced inital set.

In [9]:
c_Reduced = monte_carlo(names[:14],n=1000000)
No description has been provided for this image

Again this looks more as expected, after inspecting the initial names selection, I actually realized the name were not uniques.

In [10]:
duplicates = [names.count(n)>=2 for n in names]
print(duplicates)
[True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False]
In [11]:
# Replicating the issue with duplicate names
names3 = ['Jolene','Niamh','Sophie','Lorraine','Jolene']
#only 5 people in the draw, which should make 20 possible donor combinations, with a duplicate
comb3 = {name1+'-'+name2:0 for name1,name2 in itertools.permutations(names3,2)}
print(len(comb2))
c3 = monte_carlo(names3,n=50000)
20
No description has been provided for this image
In [12]:
#This explain the disruption in randomness so we can easily fix that by adding a check for name uniqueness
def isThereDuplicates(names):
    dups = [names.count(n)>=2 for n in names]
    if any(dups):
        print("Duplicate names")
        print(names[dups.index(True)])
        return True
    else:
        return False

isThereDuplicates(names)
Duplicate names
Ro
Out[12]:
True
In [13]:
# Fixing the duplicates

def simple_secret_santa(names,printit=False):
    n = len(names)
    s = random.sample(names,k=n)
    out = []
    if not isThereDuplicates(names):
        for i,name in enumerate(s):
            if i == len(s)-1:
                to = s[0]
            else:
                to = s[i+1]
            out.append(name+'-'+to)
            if printit:
                print(str(i+1) + "   "+name + ' gives to ' + to)
    return out

Family draws

However for a family draw, you may want to avoid gift between siblings ( my particular family case ) or between partners (not considered here), so only gift to cousin are allowed, to do this I am introducing a dictionary, with the name of the parent as value and the kid name as key.

In [14]:
def simple_secret_santa_avoid_siblings(names):
    givers = list(names.keys())
    recipients = []
    s = random.sample(givers,k=len(givers))
    out = []
    if not isThereDuplicates(list(givers)):
        for i,name in enumerate(s):
            possible = [r for r in names.keys() if (names[r] != names[name] and r not in recipients)]
            recipient = random.sample(possible,k=1)[0]
            recipients.append(recipient)
            print(str(i+1) + ': ' + name + ' gives to ' + recipient + ' ::: '+ str(len(recipients))+':'+str(len(possible)))
            out.append(name+'-'+recipient)
        #Check
        print('Recipients: '+str(len(recipients))+':: names:'+str(len(names.keys())))
In [15]:
names_siblings = {'Child1':'Parent1','Child2':'Parent1','Child3':'Parent1','Child4':'Parent1',
        'Child5':'Parent2', 'Child6':'Parent2',
        'Child7':'Parent3','Child8':'Parent3','Child9':'Parent3','Child10':'Parent3',
        'Child11':'Parent4','Child12':'Parent4','Child13':'Parent4',
        'Child14':'Parent5',
        'Child15':'Parent6',
        'Child16':'Parent7', 'Child17':'Parent7'}
In [16]:
simple_secret_santa_avoid_siblings(names_siblings)
1: Child11 gives to Child14 ::: 1:14
2: Child1 gives to Child11 ::: 2:12
3: Child9 gives to Child6 ::: 3:11
4: Child14 gives to Child3 ::: 4:14
5: Child3 gives to Child5 ::: 5:10
6: Child6 gives to Child9 ::: 6:12
7: Child12 gives to Child10 ::: 7:9
8: Child8 gives to Child17 ::: 8:8
9: Child7 gives to Child15 ::: 9:7
10: Child5 gives to Child8 ::: 10:8
11: Child4 gives to Child12 ::: 11:4
12: Child17 gives to Child13 ::: 12:5
13: Child10 gives to Child1 ::: 13:4
14: Child16 gives to Child4 ::: 14:3
15: Child2 gives to Child7 ::: 15:2
16: Child15 gives to Child2 ::: 16:2
17: Child13 gives to Child16 ::: 17:1
Recipients: 17:: names:17
In [ ]: