question.rb
8.73 KB
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
class Question < ActiveRecord::Base
require 'set'
extend ActiveSupport::Memoizable
belongs_to :creator, :class_name => "Visitor", :foreign_key => "creator_id"
belongs_to :site, :class_name => "User", :foreign_key => "site_id"
has_many :choices, :order => 'score DESC'
has_many :prompts do
def pick(algorithm = nil)
logger.info( "inside Question#prompts#pick - never called?")
if algorithm
algorithm.pick_from(self) #todo
else
lambda {prompts[rand(prompts.size-1)]}.call
end
end
end
has_many :votes
has_many :densities
has_many :appearances
#comment out to run bt import script!
after_save :ensure_at_least_two_choices
attr_accessor :ideas
def item_count
choices_count
end
#TODO: generalize for prompts of rank > 2
#TODO: add index for rapid finding
def picked_prompt(rank = 2)
logger.info "inside Question#picked_prompt"
raise NotImplementedError.new("Sorry, we currently only support pairwise prompts. Rank of the prompt must be 2.") unless rank == 2
begin
choice_id_array = distinct_array_of_choice_ids(rank)
@p = prompts.find_or_create_by_left_choice_id_and_right_choice_id(choice_id_array[0], choice_id_array[1], :include => [{ :left_choice => :item }, { :right_choice => :item }])
logger.info "#{@p.inspect} is active? #{@p.active?}"
end until @p.active?
return @p
end
# adapted from ruby cookbook(2006): section 5-11
def catchup_choose_prompt
weighted = catchup_prompts_weights
# Rand returns a number from 0 - 1, so weighted needs to be normalized
prompt = nil
until prompt && prompt.active?
target = rand
prompt_id = nil
weighted.each do |item, weight|
if target <= weight
prompt_id = item
break
end
target -= weight
end
prompt = Prompt.find(prompt_id, :include => ['left_choice', 'right_choice'])
end
# check if prompt has two active choices here, maybe we can set this on the prompt level too?
prompt
end
# TODO Add index for question id on prompts table
def catchup_prompts_weights
weights = Hash.new(0)
throttle_min = 0.05
#assuming all prompts exist
#the_prompts = prompts.find(:all, :select => 'id, votes_count')
#We don't really need to instantiate all the objects
the_prompts = ActiveRecord::Base.connection.select_all("SELECT id, votes_count from prompts where question_id =#{self.id}")
the_prompts.each do |p|
weights[p["id"].to_i] = [(1.0/ (p["votes_count"].to_i + 1).to_f).to_f, throttle_min].min
end
normalize!(weights)
weights
end
def normalize!(weighted)
if weighted.instance_of?(Hash)
sum = weighted.inject(0) do |sum, item_and_weight|
sum += item_and_weight[1]
end
sum = sum.to_f
weighted.each do |item, weight|
weighted[item] = weight/sum
weighted[item] = 0.0 unless weighted[item].finite?
end
elsif weighted.instance_of?(Array)
sum = weighted.inject(0) {|sum, item| sum += item}
weighted.each_with_index do |item, i|
weighted[i] = item/sum
weighted[i] = 0.0 unless weighted[i].finite?
end
end
end
def bradley_terry_probs
probs = []
prev_probs = []
fuzz = 0.001
# What ordering key we use is unimportant, just need a consistent way to link index of prob to id
the_choices = self.choices.sort{|x,y| x.id<=>y.id}
# This hash is keyed by pairs of choices - 'LC.id, RC.id'
the_prompts = prompts_hash_by_choice_ids
# Initial probabilities chosen at random
the_choices.size.times do
probs << rand
prev_probs << rand
end
t=0
probs_size = probs.size
difference = 1
# probably want to add a fuzz here to account for floating rounding
while difference > fuzz do
s = t % probs_size
prev_probs = probs.dup
choice = the_choices[s]
numerator = choice.wins.to_f
denominator = 0.0
the_choices.each_with_index do |c, index|
if(index == s)
next
end
wins_and_losses = the_prompts["#{choice.id}, #{c.id}"].votes.size + the_prompts["#{c.id}, #{choice.id}"].votes.size
denominator+= (wins_and_losses).to_f / (prev_probs[s] + prev_probs[index])
end
probs[s] = numerator / denominator
# avoid divide by zero NaN
probs[s] = 0.0 unless probs[s].finite?
normalize!(probs)
t+=1
difference = 0
probs.each_with_index do |curr, index|
difference += (curr - prev_probs[index]).abs
end
puts difference
end
probs_hash = {}
probs.each_with_index do |item, index|
probs_hash[the_choices[index].id] = item
end
probs_hash
end
def all_bt_scores
btprobs = bradley_terry_probs
btprobs.each do |key, value|
c = Choice.find(key)
puts "#{c.id}: #{c.votes.size} #{c.compute_bt_score(btprobs)}"
end
end
def prompts_hash_by_choice_ids
the_prompts = {}
self.prompts.each do |p|
the_prompts["#{p.left_choice_id}, #{p.right_choice_id}"] = p
end
the_prompts
end
def distinct_array_of_choice_ids(rank = 2, only_active = true)
@choice_ids = choice_ids
@s = @choice_ids.size
begin
index_list = (0...@s).sort_by{rand}
first_one, second_one = index_list.first, index_list.second
@the_choice_ids = @choice_ids.values_at(first_one, second_one)
# @the_choice_ids << choices.active.first(:order => 'RAND()', :select => 'id').id
# @the_choice_ids << choices.active.last(:order => 'RAND()', :select => 'id').id
end until (@the_choice_ids.size == rank)
logger.info "List populated and looks like #{@the_choice_ids.inspect}"
return @the_choice_ids.to_a
end
def picked_prompt_id
picked_prompt.id
end
def left_choice_text(prompt = nil)
picked_prompt.left_choice.item.data
end
def right_choice_text(prompt = nil)
picked_prompt.right_choice.item.data
end
def self.voted_on_by(u)
select {|z| z.voted_on_by_user?(u)}
end
def voted_on_by_user?(u)
u.questions_voted_on.include? self
end
def should_autoactivate_ideas?
it_should_autoactivate_ideas?
end
validates_presence_of :site, :on => :create, :message => "can't be blank"
validates_presence_of :creator, :on => :create, :message => "can't be blank"
def ensure_at_least_two_choices
the_ideas = (self.ideas.blank? || self.ideas.empty?) ? ['sample idea 1', 'sample idea 2'] : self.ideas
the_ideas << 'sample choice' if the_ideas.length < 2
if self.choices.empty?
the_ideas.each { |choice_text|
item = Item.create!({:data => choice_text, :creator => creator})
puts item.inspect
choice = choices.create!(:item => item, :creator => creator, :active => true, :data => choice_text)
puts choice.inspect
}
end
end
def density
# slow code, only to be run by cron job once at night
the_prompts = prompts.find(:all, :include => ['left_choice', 'right_choice'])
seed_seed_sum = 0
seed_seed_total = 0
seed_nonseed_sum= 0
seed_nonseed_total= 0
nonseed_seed_sum= 0
nonseed_seed_total= 0
nonseed_nonseed_sum= 0
nonseed_nonseed_total= 0
the_prompts.each do |p|
if p.left_choice.user_created == false && p.right_choice.user_created == false
seed_seed_sum += p.appearances.size
seed_seed_total +=1
elsif p.left_choice.user_created == false && p.right_choice.user_created == true
seed_nonseed_sum += p.appearances.size
seed_nonseed_total +=1
elsif p.left_choice.user_created == true && p.right_choice.user_created == false
nonseed_seed_sum += p.appearances.size
nonseed_seed_total +=1
elsif p.left_choice.user_created == true && p.right_choice.user_created == true
nonseed_nonseed_sum += p.appearances.size
nonseed_nonseed_total +=1
end
end
densities = {}
densities[:seed_seed] = seed_seed_sum.to_f / seed_seed_total.to_f
densities[:seed_nonseed] = seed_nonseed_sum.to_f / seed_nonseed_total.to_f
densities[:nonseed_seed] = nonseed_seed_sum.to_f / nonseed_seed_total.to_f
densities[:nonseed_nonseed] = nonseed_nonseed_sum.to_f / nonseed_nonseed_total.to_f
puts "Seed_seed sum: #{seed_seed_sum}, seed_seed total num: #{seed_seed_total}"
puts "Seed_nonseed sum: #{seed_nonseed_sum}, seed_nonseed total num: #{seed_nonseed_total}"
puts "Nonseed_seed sum: #{nonseed_seed_sum}, nonseed_seed total num: #{nonseed_seed_total}"
puts "Nonseed_nonseed sum: #{nonseed_nonseed_sum}, nonseed_nonseed total num: #{nonseed_nonseed_total}"
densities
end
def save_densities!
d_hash = density
d_hash.each do |type, average|
d = Density.new
d.question_id = self.id
d.prompt_type = type.to_s
d.value = average.nan? ? nil : average
d.save!
end
end
end