Skip to content

Commit 8278b5a

Browse files
committed
for mallows model
1 parent c20a6b3 commit 8278b5a

File tree

1 file changed

+376
-0
lines changed

1 file changed

+376
-0
lines changed

preflibgenprofiles.py

+376
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
'''
2+
File: generate_profiles.py
3+
Author: Nicholas Mattei ([email protected])
4+
Date: Sept 11, 2013
5+
November 6th, 2013
6+
7+
* Copyright (c) 2014, Nicholas Mattei and NICTA
8+
* All rights reserved.
9+
*
10+
* Developed by: Nicholas Mattei
11+
* NICTA
12+
* http://www.nickmattei.net
13+
* http://www.preflib.org
14+
*
15+
* Redistribution and use in source and binary forms, with or without
16+
* modification, are permitted provided that the following conditions are met:
17+
* * Redistributions of source code must retain the above copyright
18+
* notice, this list of conditions and the following disclaimer.
19+
* * Redistributions in binary form must reproduce the above copyright
20+
* notice, this list of conditions and the following disclaimer in the
21+
* documentation and/or other materials provided with the distribution.
22+
* * Neither the name of NICTA nor the
23+
* names of its contributors may be used to endorse or promote products
24+
* derived from this software without specific prior written permission.
25+
*
26+
* THIS SOFTWARE IS PROVIDED BY NICTA ''AS IS'' AND ANY
27+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
28+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29+
* DISCLAIMED. IN NO EVENT SHALL NICTA BE LIABLE FOR ANY
30+
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
31+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
32+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
33+
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36+
37+
38+
About
39+
--------------------
40+
This file generates voting profiles according to a given distribution.
41+
It requires io to work properly.
42+
43+
'''
44+
import random
45+
import itertools
46+
import math
47+
import copy
48+
import argparse
49+
import sys
50+
51+
# from preflibtools import io
52+
53+
# Generator Functions
54+
55+
# Generate a generically labeled candidate map from
56+
# a number of alternatives.
57+
def gen_cand_map(nalts):
58+
candmap = {}
59+
for i in range(1, nalts+1):
60+
candmap[i] = "Candidate " + str(i)
61+
return candmap
62+
63+
# Generate an Impartial Culture profile
64+
# that adheres to the format above given a candidate map.
65+
def gen_impartial_culture_strict(nvotes, candmap):
66+
rankmapcounts = []
67+
rankmap = []
68+
voteset = gen_urn(nvotes, 0, candmap.keys())
69+
return voteset_to_rankmap(voteset, candmap)
70+
71+
# Generate an Impartial Anonymous Culture profile
72+
# that adheres to the format above.
73+
def gen_impartial_aynonmous_culture_strict(nvotes, candmap):
74+
rankmapcounts = []
75+
rankmap = []
76+
#Use the existing functions.
77+
voteset = gen_urn(nvotes, 1, candmap.keys())
78+
return voteset_to_rankmap(voteset, candmap)
79+
80+
# Generate an Urn Culture with Replacement = replace profile
81+
# that adheres to the format above.
82+
def gen_urn_culture_strict(nvotes, replace, candmap):
83+
rankmapcounts = []
84+
rankmap = []
85+
#Use the existing functions.
86+
voteset = gen_urn(nvotes, replace, candmap.keys())
87+
return voteset_to_rankmap(voteset, candmap)
88+
89+
# Generate an SinglePeakedImpartialCulture vote set.
90+
def gen_single_peaked_impartial_culture_strict(nvotes, candmap):
91+
voteset = {}
92+
for i in range(nvotes):
93+
tvote = gen_icsp_single_vote(list(candmap.keys()))
94+
voteset[tvote] = voteset.get(tvote, 0) + 1
95+
return voteset_to_rankmap(voteset, candmap)
96+
97+
# Generate strict Urn
98+
99+
def gen_urn_strict(nvotes, replace, candmap):
100+
rankmapcounts = []
101+
rankmap = []
102+
#Use the existing functions.
103+
voteset = gen_urn(nvotes, replace, candmap.keys())
104+
return voteset_to_rankmap(voteset, candmap)
105+
106+
# Generate a Mallows model with the various mixing parameters passed in
107+
# nvoters is the number of votes we need
108+
# candmap is a candidate map
109+
# mix is an array such that sum(mix) == 1 and describes the distro over the models
110+
# phis is an array len(phis) = len(mix) = len(refs) that is the phi for the particular model
111+
# refs is an array of dicts that describe the reference ranking for the set.
112+
def gen_mallows(nvoters, candmap, mix, phis, refs):
113+
114+
if len(mix) != len(phis) or len(phis) != len(refs):
115+
print("Mix != Phis != Refs")
116+
exit()
117+
118+
#Precompute the distros for each Phi and Ref.
119+
#Turn each ref into an order for ease of use...
120+
m_insert_dists = []
121+
for i in range(len(mix)):
122+
m_insert_dists.append(compute_mallows_insertvec_dist(len(candmap), phis[i]))
123+
124+
#Now, generate votes...
125+
votemap = {}
126+
for cvoter in range(nvoters):
127+
#print("drawing")
128+
cmodel = draw(list(range(len(mix))), mix)
129+
#print(cmodel)
130+
#print(refs[cmodel])
131+
#Generate a vote for the selected model
132+
insvec = [0] * len(candmap)
133+
for i in range(1, len(insvec)+1):
134+
#options are 1...max
135+
#print("Options: " + str(list(range(1, i+1))))
136+
#print("Dist: " + str(insertvec_dist[i]))
137+
#print("Drawing on model " + str(cmodel))
138+
insvec[i-1] = draw(list(range(1, i+1)), m_insert_dists[cmodel][i])
139+
vote = []
140+
for i in range(len(refs[cmodel])):
141+
vote.insert(insvec[i]-1, refs[cmodel][i])
142+
#print("mallows vote: " + str(vote))
143+
tvote = tuple(vote)
144+
votemap[tuple(vote)] = votemap.get(tuple(vote), 0) + 1
145+
146+
return votemap
147+
148+
# Generate Mallows with a particular number of reference rankings and phi's drawn iid.
149+
def gen_mallows_mix(nvoters, candmap, nref):
150+
#Generate the requisite number of reference rankings and phis
151+
#Mix should be a random number over each...
152+
mix = []
153+
phis = []
154+
refs = []
155+
for i in range(nref):
156+
refm, refc = gen_impartial_culture_strict(1, candmap);
157+
refs.append(io.rankmap_to_order(refm[0]))
158+
phis.append(round(random.random(), 5))
159+
mix.append(random.randint(1,100))
160+
smix = sum(mix)
161+
mix = [float(i) / float(smix) for i in mix]
162+
163+
164+
return gen_mallows(nvoters, candmap, mix, phis, refs)
165+
166+
167+
# Helper Functions -- Actual Generators -- Don't call these directly.
168+
169+
# Return a value drawn from a particular distribution.
170+
def draw(values, distro):
171+
#Return a value randomly from a given discrete distribution.
172+
#This is a bit hacked together -- only need that the distribution
173+
#sums to 1.0 within 5 digits of rounding.
174+
if round(sum(distro),5) != 1.0:
175+
print("Input Distro is not a Distro...")
176+
print(str(distro) + " Sum: " + str(sum(distro)))
177+
exit()
178+
if len(distro) != len(values):
179+
print("Values and Distro have different length")
180+
181+
cv = 0
182+
draw = random.random() - distro[cv]
183+
while draw > 0.0:
184+
cv+= 1
185+
draw -= distro[cv]
186+
return values[cv]
187+
# For Phi and a given number of candidates, compute the
188+
# insertion probability vectors.
189+
def compute_mallows_insertvec_dist(ncand, phi):
190+
#Compute the Various Mallows Probability Distros
191+
vec_dist = {}
192+
for i in range(1, ncand+1):
193+
#Start with an empty distro of length i
194+
dist = [0] * i
195+
#compute the denom = phi^0 + phi^1 + ... phi^(i-1)
196+
denom = sum([pow(phi,k) for k in range(i)])
197+
#Fill each element of the distro with phi^i-j / denom
198+
for j in range(1, i+1):
199+
dist[j-1] = pow(phi, i - j) / denom
200+
#print(str(dist) + "total: " + str(sum(dist)))
201+
vec_dist[i] = dist
202+
return vec_dist
203+
# Convert a votemap to a rankmap and rankmapcounts....
204+
def voteset_to_rankmap(votemap, candmap):
205+
rmapcount =[]
206+
rmap = []
207+
#Votemaps are tuple --> count, so it's strict and easy...
208+
for order in votemap.keys():
209+
rmapcount.append(votemap[order])
210+
cmap = {}
211+
for crank in range(1, len(order)+1):
212+
cmap[order[crank-1]] = crank
213+
rmap.append(cmap)
214+
return rmap, rmapcount
215+
# Given a rankmap and counts, return a voteset for writing.
216+
def rankmap_to_voteset(rankmaps, rankmapcounts):
217+
#convert these
218+
votemap = {}
219+
for n in range(len(rankmaps)):
220+
cmap = rankmaps[n]
221+
#Decompose the rankmap into a string.
222+
#Get number of elements
223+
lenrank = max(cmap.values())
224+
strlist = ['']*lenrank
225+
#place the candidates in their buckets
226+
for i in sorted(cmap.keys()):
227+
strlist[cmap[i]-1] += str(i) + ","
228+
#Strip off unecessary commas.
229+
strlist = [i[:len(i)-1] for i in strlist]
230+
#Make the correct string.
231+
votestr = ""
232+
for i in strlist:
233+
if i.find(",") == -1:
234+
votestr += i + ","
235+
else:
236+
votestr += "{" + i + "},"
237+
#Trim
238+
votestr = votestr[:len(votestr)-1].strip()
239+
#insert into the map.
240+
votemap[votestr] = votemap.get(votestr, 0) + rankmapcounts[n]
241+
return votemap
242+
243+
# Return a Tuple for a IC-Single Peaked... with alternatives in range 1....range.
244+
def gen_icsp_single_vote(alts):
245+
a = 0
246+
b = len(alts)-1
247+
temp = []
248+
while a != b:
249+
if random.randint(0,1) == 1:
250+
temp.append(alts[a])
251+
a += 1
252+
else:
253+
temp.append(alts[b])
254+
b -= 1
255+
temp.append(alts[a])
256+
return tuple(temp[::-1]) # reverse
257+
258+
# Generate votes based on the URN Model..
259+
# we need numvotes with replace replacements.
260+
def gen_urn(numvotes, replace, alts):
261+
voteMap = {}
262+
ReplaceVotes = {}
263+
264+
ICsize = math.factorial(len(alts))
265+
ReplaceSize = 0
266+
267+
for x in range(numvotes):
268+
flip = random.randint(1, ICsize+ReplaceSize)
269+
if flip <= ICsize:
270+
#generate an IC vote and make a suitable number of replacements...
271+
tvote = gen_ic_vote(alts)
272+
voteMap[tvote] = (voteMap.get(tvote, 0) + 1)
273+
ReplaceVotes[tvote] = (ReplaceVotes.get(tvote, 0) + replace)
274+
ReplaceSize += replace
275+
#print("made " + str(tvote))
276+
277+
else:
278+
#iterate over replacement hash and select proper vote.
279+
flip = flip - ICsize
280+
for vote in ReplaceVotes.keys():
281+
flip = flip - ReplaceVotes[vote]
282+
if flip <= 0:
283+
voteMap[vote] = (voteMap.get(vote, 0) + 1)
284+
ReplaceVotes[vote] = (ReplaceVotes.get(vote, 0) + replace)
285+
ReplaceSize += replace
286+
break
287+
else:
288+
print("We Have a problem... replace fell through....")
289+
exit()
290+
291+
return voteMap
292+
# Return a TUPLE! IC vote given a vector of alternatives. Basically remove one
293+
def gen_ic_vote(alts):
294+
options = list(alts)
295+
vote = []
296+
while(len(options) > 0):
297+
#randomly select an option
298+
vote.append(options.pop(random.randint(0,len(options)-1)))
299+
return tuple(vote)
300+
301+
302+
# Below is a template Main which shows off some of the
303+
# features of this library.
304+
if __name__ == '__main__':
305+
306+
if len(sys.argv) < 2:
307+
print("Run " + sys.argv[0] + " -h for help.")
308+
309+
parser = argparse.ArgumentParser(description='Prefence Profile Generator for PrefLib Tools.\n\n Can be run in interactive mode or from the command line to generate preferenes from a fixed set of statistical cultures.')
310+
parser.add_argument('-i', '--interactive', dest='interactive', action='store_true', help='Run in Interactive Mode.')
311+
parser.add_argument('-n', '--voters', type=int, dest='nvoter', metavar='nvoter', help='Number of voters in profiles.')
312+
parser.add_argument('-m', '--candidates', type=int, dest='ncand', metavar='ncand', help='Number of candidates in profiles.')
313+
parser.add_argument('-t', '--modeltype', type=int, dest='model', metavar='model', default="1", help='Model to generate the profile: (1) Impartial Culture (2) Single Peaked Impartial Culture (3) Impartial Anonymous Culture (4) Mallows with 5 Reference Orders (5) Mallows with 1 Reference Order (6) Urn with 50%% Replacement.')
314+
parser.add_argument('-c', '--numinstances', type=int, dest='ninst', metavar='ninst', help='Number of instanes to generate.')
315+
parser.add_argument('-o', '--outpath', dest='outpath', metavar='path', help='Path to save output.')
316+
317+
results = parser.parse_args()
318+
319+
320+
if results.interactive:
321+
# Generate a file in Preflib format with a specified number
322+
# of candidates and options
323+
print("Preference Profile Generator for PrefLib Tools. \nCan be run in interactive mode or from the command line to generate preferenes from a fixed set of statistical cultures. \n\nRun with -h to see help and command line options. \n\n")
324+
ncand = int(input("Enter a number of candidates: "))
325+
nvoter = int(input("Enter a number of voters: "))
326+
327+
print('''Please select from the following: \n 1) Impartial Culture \n 2) Single Peaked Impartial Culture \n 3) Impartial Anonymous Culture \n 4) Mallows with 5 Reference Orders \n 5) Mallows with 1 Reference Order \n 6) Urn with 50% Replacement \n''')
328+
model = int(input("Selection >> "))
329+
ninst = 1
330+
else:
331+
ncand = results.ncand if results.ncand != None else 1
332+
nvoter = results.nvoter if results.nvoter != None else 1
333+
model = results.model if results.model != None else 1
334+
ninst = results.ninst if results.ninst != None else 1
335+
base_file_name = "GenModel_"
336+
base_path = results.outpath if results.outpath != None else "./"
337+
338+
cmap = gen_cand_map(ncand)
339+
for i in range(ninst):
340+
if model == 1:
341+
# Generate an instance of Impartial Culture
342+
rmaps, rmapscounts = gen_impartial_culture_strict(nvoter, cmap)
343+
elif model == 2:
344+
# Generate an instance of Single Peaked Impartial Culture
345+
rmaps, rmapscounts = gen_single_peaked_impartial_culture_strict(nvoter, cmap)
346+
elif model == 3:
347+
# Generate an instance of Impartial Aynonmous Culture
348+
rmaps, rmapscounts = gen_impartial_aynonmous_culture_strict(nvoter, cmap)
349+
elif model == 4:
350+
# Generate a Mallows Mixture with 5 random reference orders.
351+
rmaps, rmapscounts = gen_mallows_mix(nvoter, cmap, 5)
352+
elif model == 5:
353+
# Generate a Mallows Mixture with 1 reference.
354+
rmaps, rmapscounts = gen_mallows_mix(nvoter, cmap, 1)
355+
elif model == 6:
356+
#We can also do replacement rates, recall that there are items! orders, so
357+
#if we want a 50% chance the second preference is like the first, then
358+
#we set replacement to items!
359+
rmaps, rmapscounts = gen_urn_strict(nvoter, math.factorial(ncand), cmap)
360+
else:
361+
print("Not a valid model")
362+
exit()
363+
364+
if results.interactive:
365+
#Print the result to the screen
366+
io.pp_profile_toscreen(cmap, rmaps, rmapscounts)
367+
368+
#Write it out.
369+
fname = str(input("\nWhere should I save the file: "))
370+
outf = open(fname, 'w')
371+
io.write_map(cmap, nvoter, rankmap_to_voteset(rmaps, rmapscounts),outf)
372+
outf.close()
373+
else:
374+
outf = open(base_path + base_file_name + str(i) + ".soc", 'w')
375+
io.write_map(cmap, nvoter, rankmap_to_voteset(rmaps, rmapscounts),outf)
376+
outf.close()

0 commit comments

Comments
 (0)