local stats = require("evalframe.eval.stats")
local M = {}
function M.action_sequences(traces, ngram_size)
ngram_size = ngram_size or 3
if ngram_size < 1 then
error("analysis.action_sequences: ngram_size must be >= 1", 2)
end
local sequences = {}
for _, trace in ipairs(traces) do
local actions = trace.actions
local succeeded = trace.success == true
local seen_in_trace = {}
for i = 1, #actions - ngram_size + 1 do
local parts = {}
for j = 0, ngram_size - 1 do
parts[#parts + 1] = actions[i + j].action
end
local key = table.concat(parts, ",")
if not seen_in_trace[key] then
seen_in_trace[key] = true
if not sequences[key] then
sequences[key] = { count = 0, success = 0 }
end
sequences[key].count = sequences[key].count + 1
if succeeded then
sequences[key].success = sequences[key].success + 1
end
end
end
end
for _, data in pairs(sequences) do
data.rate = data.count > 0 and data.success / data.count or 0
end
return sequences
end
function M.convergence(traces, target_action)
local values = {}
for _, trace in ipairs(traces) do
if target_action then
for _, a in ipairs(trace.actions) do
if a.action == target_action then
values[#values + 1] = a.tick
break
end
end
else
values[#values + 1] = trace.ticks
end
end
return stats.describe_with_ci(values, { unbounded = true })
end
function M.exploration_efficiency(trace)
local actions = trace.actions
local total = #actions
if total == 0 then
return { total = 0, unique = 0, unique_ratio = 0, duplicate_rate = 0 }
end
local seen = {}
local unique = 0
for _, a in ipairs(actions) do
if not seen[a.action] then
seen[a.action] = true
unique = unique + 1
end
end
return {
total = total,
unique = unique,
unique_ratio = unique / total,
duplicate_rate = 1 - (unique / total),
}
end
function M.worker_coordination(trace)
local actions = trace.actions
local workers = {}
local action_workers = {}
for _, a in ipairs(actions) do
if not workers[a.worker] then
workers[a.worker] = { count = 0, action_set = {} }
end
workers[a.worker].count = workers[a.worker].count + 1
workers[a.worker].action_set[a.action] = true
if not action_workers[a.action] then
action_workers[a.action] = {}
end
action_workers[a.action][a.worker] = true
end
local total_action_types = 0
local overlapping = 0
for _, worker_set in pairs(action_workers) do
total_action_types = total_action_types + 1
local count = 0
for _ in pairs(worker_set) do count = count + 1 end
if count > 1 then overlapping = overlapping + 1 end
end
local worker_count = 0
for _ in pairs(workers) do worker_count = worker_count + 1 end
return {
workers = workers,
overlap_rate = total_action_types > 0 and overlapping / total_action_types or 0,
worker_count = worker_count,
}
end
function M.action_validity(trace, is_valid_fn)
local actions = trace.actions
local total = #actions
local valid = 0
for _, a in ipairs(actions) do
if is_valid_fn(a) then valid = valid + 1 end
end
return {
total = total,
valid = valid,
rate = total > 0 and valid / total or 0,
}
end
return M