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