view lucene/src/luan/modules/lucene/Ab_testing.luan @ 292:e8a2153f6ce1

minor git-svn-id: https://luan-java.googlecode.com/svn/trunk@293 21e917c8-12df-6dd8-5cb6-c86387c605b9
author fschmidt@gmail.com <fschmidt@gmail.com@21e917c8-12df-6dd8-5cb6-c86387c605b9>
date Wed, 10 Dec 2014 21:44:19 +0000
parents a35d1177bbf0
children 5652cdea25f5
line wrap: on
line source

import "luan:Math"
import "luan:Table"
import "luan:Io"
import "luan:web/Http"


function of(index)

	local ab_testing = {}

	ab_testing.test_map = {}

	function ab_testing.test(test)
		test.name or error "name not defined"
		test.values or error "values not defined"

		-- list of event names
		test.events or error "events not defined"

		-- map of event name to aggregator factory
		test.aggregator_factories or error "aggregator_factories not defined"

		-- test.date_field is optional

		local field = "ab_test_" .. test.name
		index.fields[field] == nil or error("test "+test.name+" already defined")
		index.fields[field] = field .. " index"
		test.field = field

		-- returns map of event name to (map of value to result) and "start_date"
		function test.results()
			return index.Searcher( function(searcher)
				local results = {}
				for name in pairs(test.aggregator_factories) do
					results[name] = {}
				end
				local date_field = test.date_field
				local start_date = nil
				for _, value in ipairs(test.values) do
					local aggregators = {}
					for name, factory in pairs(test.aggregator_factories) do
						aggregators[name] = factory()
					end
					local query = index.Query.term{ [field] = value }
					searcher.search(query, function(doc)
						for _, aggregator in pairs(aggregators) do
							aggregator.aggregate(doc)
						end
						if date_field ~= nil then
							local date = doc[date_field]
							if date ~= nil and (start_date==nil or start_date > date) then
								start_date = date
							end
						end
					end)
					for name, aggregator in pairs(aggregators) do
						results[name][value] = aggregator.result
					end
				end
				results.start_date = start_date
				return results
			end )
		end

		function test.fancy_results()
			local events = test.events
			local results = test.results()
			local fancy = {}
			fancy.start_date = results.start_date
			local event = events[1]
			fancy[event] = {}
			for value, count in pairs(results[event]) do
				fancy[event][value] = {}
				fancy[event][value].count = count
				fancy[event][value].pct_of_total = 100
				fancy[event][value].pct_of_prev = 100
			end
			local all = results[event]
			local prev = all
			for i in range(2,#events) do
				event = events[i]
				fancy[event] = {}
				for value, count in pairs(results[event]) do
					fancy[event][value] = {}
					fancy[event][value].count = count
					fancy[event][value].pct_of_total = percent(count,all[value])
					fancy[event][value].pct_of_prev = percent(count,prev[value])
				end
				prev = results[event]
			end
			return fancy
		end

		ab_testing.test_map[test.name] = test

		return test
	end
	
	function ab_testing.value(test_name,values)
		return values[test_name] or ab_testing.test_map[test_name].values[1]
	end
	
	-- returns map from test name to value
	function ab_testing.from_doc(doc)
		local values = {}
		for _, test in pairs(ab_testing.test_map) do
			values[test.name] = doc[test.field]
		end
		return values
	end

	function ab_testing.to_doc(doc,values,tests)
		tests = tests or ab_testing.test_map
		if values == nil then
			for _, test in ipairs(tests) do
				doc[test.field] = test.values[Math.random(#test.values)]
			end
		else
			for _, test in ipairs(tests) do
				doc[test.field] = values[test.name]
			end
		end
	end

	function ab_testing.web_page(test_names)
		return { service = function()
			local results = {}
			for _, name in ipairs(test_names) do
				local test = ab_testing.test_map[name]
				test or error("test not found: "..name)
				results[name] = test.fancy_results()
			end
			Io.stdout = Http.response.text_writer()
			html(test_names,ab_testing.test_map,results)
		end }
	end

	return ab_testing
end


-- aggregator factories

-- fn(doc) should return boolean whether doc should be counted
function count(fn)
	return function()
		local aggregator = {}
		aggregator.result = 0
		function aggregator.aggregate(doc)
			if fn(doc) then
				aggregator.result = aggregator.result + 1
			end
		end
		return aggregator
	end
end

count_all = count( function(doc) return true end )

-- fn(doc) should return number to add to result, return 0 for nothing
function sum(fn)
	return function()
		local aggregator = {}
		aggregator.result = 0
		function aggregator.aggregate(doc)
			aggregator.result = aggregator.result + fn(doc)
		end
		return aggregator
	end
end



function percent(x,total)
	if total==0 then
		return 0
	else
		return 100 * x / total
	end
end


function html(test_names,tests,results) %>
<html>
<body>
<h2>A/B Tests</h2>
<%
	for _, test_name in ipairs(test_names) do
		local test = tests[test_name]
		local result = results[test_name]
		local n = #test.values
		%>
		<h3><%=test_name%></h3>
		<table>
			<tr>
				<th>Event</th>
				<th class="top" colspan="<%=n%>">Count</th>
				<th class="top" colspan="<%=n%>">% of total</th>
				<th class="top" colspan="<%=n%>">% of prev</th>
			</tr>
			<tr>
				<th></th>
				<%
				for _ in range(1,3) do
					for _, value in ipairs(test.values) do
						%>
						<th class="top"><%=value%></th>
						<%
					end
				end
				%>
			</tr>
			<%
			for _, event in ipairs(test.events) do
				local event_values = result[event]
				%>
				<tr>
					<td><%=event%></td>
					<%
					for _, value in ipairs(test.values) do
						%>
						<td><%=event_values[value].count%></th>
						<%
					end
					for _, value in ipairs(test.values) do
						%>
						<td><%=event_values[value].pct_of_total%></th>
						<%
					end
					for _, value in ipairs(test.values) do
						%>
						<td><%=event_values[value].pct_of_prev%></th>
						<%
					end
					%>
				</tr>
				<%
			end
			%>
		</table>
		<%
	end
%>
</body>
</html>
<% end