comparison src/luan/modules/lucene/Ab_testing.luan @ 775:1a68fc55a80c

simplify dir structure
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 26 Aug 2016 14:36:40 -0600
parents lucene/src/luan/modules/lucene/Ab_testing.luan@ca169567ce07
children bae2d0c2576c
comparison
equal deleted inserted replaced
774:3e30cf310e56 775:1a68fc55a80c
1 local Luan = require "luan:Luan.luan"
2 local pairs = Luan.pairs
3 local ipairs = Luan.ipairs
4 local error = Luan.error
5 local range = Luan.range
6 local Math = require "luan:Math.luan"
7 local Table = require "luan:Table.luan"
8 local String = require "luan:String.luan"
9 local gsub = String.gsub
10 local Io = require "luan:Io.luan"
11 local Http = require "luan:http/Http.luan"
12 local Logging = require "luan:logging/Logging.luan"
13 local Lucene = require "luan:lucene/Lucene.luan"
14
15 local M = {}
16
17 local logger = Logging.logger "Ab_testing"
18
19 function M.of(index)
20
21 local ab_testing = {}
22
23 ab_testing.test_map = {}
24
25 function ab_testing.test(test)
26 test.name or error "name not defined"
27 test.values or error "values not defined"
28
29 -- list of event names
30 test.events or error "events not defined"
31
32 -- map of event name to aggregator factory
33 test.aggregator_factories or error "aggregator_factories not defined"
34
35 -- test.date_field is optional
36
37 local field = "ab_test_" .. test.name
38 index.indexed_fields[field] == nil or error("test "+test.name+" already defined")
39 index.indexed_fields[field] = Lucene.type.string
40 test.field = field
41
42 -- returns map of event name to (map of value to result) and "start_date"
43 function test.results()
44 local results = {}
45 for name in pairs(test.aggregator_factories) do
46 results[name] = {}
47 end
48 local date_field = test.date_field
49 local start_date = nil
50 for _, value in ipairs(test.values) do
51 local aggregators = {}
52 for name, factory in pairs(test.aggregator_factories) do
53 aggregators[name] = factory()
54 end
55 local query = field..":"..value
56 index.advanced_search(query, function(_,doc_fn)
57 local doc = doc_fn()
58 for _, aggregator in pairs(aggregators) do
59 aggregator.aggregate(doc)
60 end
61 if date_field ~= nil then
62 local date = doc[date_field]
63 if date ~= nil and (start_date==nil or start_date > date) then
64 start_date = date
65 end
66 end
67 end)
68 for name, aggregator in pairs(aggregators) do
69 results[name][value] = aggregator.result
70 end
71 end
72 results.start_date = start_date
73 return results
74 end
75
76 function test.fancy_results()
77 local events = test.events
78 local results = test.results()
79 local fancy = {}
80 fancy.start_date = results.start_date
81 local event = events[1]
82 fancy[event] = {}
83 for value, count in pairs(results[event]) do
84 fancy[event][value] = {}
85 fancy[event][value].count = count
86 fancy[event][value].pct_of_total = 100
87 fancy[event][value].pct_of_prev = 100
88 end
89 local all = results[event]
90 local prev = all
91 for i in range(2,#events) do
92 event = events[i]
93 fancy[event] = {}
94 for value, count in pairs(results[event]) do
95 fancy[event][value] = {}
96 fancy[event][value].count = count
97 fancy[event][value].pct_of_total = M.percent(count,all[value])
98 fancy[event][value].pct_of_prev = M.percent(count,prev[value])
99 end
100 prev = results[event]
101 end
102 return fancy
103 end
104
105 ab_testing.test_map[test.name] = test
106
107 return test
108 end
109
110 function ab_testing.value(test_name,values)
111 return values[test_name] or ab_testing.test_map[test_name].values[1]
112 end
113
114 -- returns map from test name to value
115 function ab_testing.from_doc(doc)
116 local values = {}
117 for _, test in pairs(ab_testing.test_map) do
118 values[test.name] = doc[test.field]
119 end
120 return values
121 end
122
123 function ab_testing.to_doc(doc,values,tests)
124 tests = tests or ab_testing.test_map
125 if values == nil then
126 values = {}
127 for _, test in pairs(tests) do
128 values[test.name] = test.values[Math.random(#test.values)]
129 end
130 end
131 for _, test in pairs(tests) do
132 doc[test.field] = values[test.name]
133 end
134 return values
135 end
136
137 function ab_testing.web_page(test_names)
138 return function()
139 local results = {}
140 for _, name in ipairs(test_names) do
141 local test = ab_testing.test_map[name]
142 test or error("test not found: "..name)
143 results[name] = test.fancy_results()
144 end
145 Io.stdout = Http.response.text_writer()
146 M.html(test_names,ab_testing.test_map,results)
147 end
148 end
149
150 return ab_testing
151 end
152
153
154 -- aggregator factories
155
156 -- fn(doc) should return boolean whether doc should be counted
157 function M.count(fn)
158 return function()
159 local aggregator = {}
160 aggregator.result = 0
161 function aggregator.aggregate(doc)
162 if fn(doc) then
163 aggregator.result = aggregator.result + 1
164 end
165 end
166 return aggregator
167 end
168 end
169
170 M.count_all = M.count( function(doc) return true end )
171
172 -- fn(doc) should return number to add to result, return 0 for nothing
173 function M.sum(fn)
174 return function()
175 local aggregator = {}
176 aggregator.result = 0
177 function aggregator.aggregate(doc)
178 aggregator.result = aggregator.result + fn(doc)
179 end
180 return aggregator
181 end
182 end
183
184
185
186 function M.percent(x,total)
187 if total==0 then
188 return 0
189 else
190 return 100 * x / total
191 end
192 end
193
194 -- I will change this to use SimplyHTML when this is used again.
195 local function basic_style() %>
196 body {font-family:'Arial',sans-serif;font-size:16px;padding:1em 2em}
197 h1 {font-weight:bold;font-size:20px}
198 h2 {margin:2em 0 0em;font-size:18px;color:#3589B1}
199 table.results {margin-top:.5em;border-collapse:collapse;font-size:90%}
200 table.results th {background:#eee}
201 table.results th,table.results td {border-left:1px solid #bbb;padding:.4em 2em}
202 table.results tr:nth-child(odd) td {background:#f8f8f8}
203 <% end
204
205 local function format(v)
206 v = v .. ''
207 return gsub( v, [[(\d+\.\d{1})\d+]], '$1' )
208 end
209
210 function M.html(test_names,tests,results) %>
211 <!DOCTYPE html>
212 <html lang="en">
213 <head>
214 <title>A/B Test Results</title>
215 <style><% basic_style() %></style>
216 </head>
217 <body>
218 <h1>A/B Test Results</h1>
219 <%
220 for _, test_name in ipairs(test_names) do
221 local test = tests[test_name]
222 local result = results[test_name]
223 local n = #test.values
224 %>
225 <h2><%=test_name%></h2>
226 <table class="results">
227 <tr>
228 <th>Event</th>
229 <th class="top" colspan="<%=n%>">Count</th>
230 <th class="top" colspan="<%=n%>">% of total</th>
231 <th class="top" colspan="<%=n%>">% of prev</th>
232 </tr>
233 <tr>
234 <th></th>
235 <%
236 for _ in range(1,3) do
237 for _, value in ipairs(test.values) do
238 %><th class="top"><%=value%></th><%
239 end
240 end
241 %>
242 </tr>
243 <%
244 for _, event in ipairs(test.events) do
245 local event_values = result[event]
246 %>
247 <tr>
248 <td><%=event%></td>
249 <%
250 for _, value in ipairs(test.values) do
251 %><td><%=format(event_values[value].count)%></th><%
252 end
253 for _, value in ipairs(test.values) do
254 %><td><%=format(event_values[value].pct_of_total)%></th><%
255 end
256 for _, value in ipairs(test.values) do
257 %><td><%=format(event_values[value].pct_of_prev)%></th><%
258 end
259 %>
260 </tr>
261 <%
262 end
263 %>
264 </table>
265 <%
266 end
267 %>
268 </body>
269 </html>
270 <% end
271
272 return M