--[[ generate and return text-based vertical histogram, with bins of given width across interval from max to min, transforming values to numbers via func, scaling bar-length by scale ]] local function histogram(list, min, max, width, func, scale) local bins = {} local oorc = 0 local prc = 0 for _, v in ipairs(list) do local n = func(v) if n == n and n >= min and n < max + width then local k = math.floor((n - min) / width) + 1 bins[k] = (bins[k] or 0) + 1 else oorc = oorc + 1 end prc = prc + 1 end local s = "" for n = min, max, width do local k = math.floor((n - min) / width) + 1 s = string.format("%s%.2f\t%s %d\n", s, n, ("\u{2588}"):rep(math.ceil(scale * (bins[k] or 0))), bins[k] or 0) end return s .. string.format("(%d out of range)\n", oorc) end local function fn(config, deck, deckfname, qs) local tcc = 0 local tsc = 0 local tfc = 0 local spc = 0 local expdeck = {} for _, card in ipairs(deck) do local rtc if not qs or card.note:find(qs) then rtc = true for _, pr in ipairs(card.pr) do tsc = tsc + pr.succ tfc = tfc + pr.fail if pr.delay < 0 then spc = spc + 1 else table.insert(expdeck, pr) end end end if rtc then tcc = tcc + 1 end end local qfn if qs then qfn = string.format(" (filtered with query %q)", qs) else qfn = "" end io.write(string.format( "%s%s has %d cards and %d total prompts\n", deckfname, qfn, tcc, #expdeck + spc )) io.write(string.format( "(average %.2f prompts per card, %d/%.1f%% prompts suspended).\n", (#expdeck + spc) / tcc, spc, 100 * spc / (#expdeck + spc) )) io.write(string.format( "The deck's prompts have been reviewed a total of %d times\n", tsc + tfc )) io.write(string.format( "(average %.2f reviews per prompt), overall retention %.2f%%.\n", (tsc + tfc) / (#expdeck + spc), 100 * tsc / (tsc + tfc) )) io.write("All further data excludes suspended prompts.\n") io.write("\nEase\n" .. histogram(expdeck, 1.0, 3.3, 0.1, function(pr) return pr.ease end, 0.03)) io.write("\nLog2-delay (seconds)\n" .. histogram(expdeck, 15, 28, 0.5, function(pr) return math.log(pr.delay, 2) end, 0.1)) io.write("\nDelay-last review ratio\n" .. histogram(expdeck, 0, 2, 0.1, function(pr) return (os.time() - pr.time) / pr.delay end, 0.1)) io.write("\nRetention (%)\n" .. histogram(expdeck, 30, 100, 10, function(pr) return 100 * pr:retention() end, 0.02)) io.write("\nReview counts\n" .. histogram(expdeck, 0, 20, 2, function(pr) return pr.succ + pr.fail end, 0.03)) io.write("\nLog2-age (seconds)\n" .. histogram(deck, 18, 28, 0.5, function(card) return math.log(os.time() - (card.created or 2 * os.time()), 2) end, 0.05)) io.write("\nPrompt-count\n" .. histogram(deck, 1, 10, 1, function(card) return #card.pr end, 0.03)) local cpcs = {} for _, card in ipairs(deck) do local cp = card.note:match("([^:]+):") or "" cpcs[cp] = (cpcs[cp] or 0) + 1 end local scl = {} for l, c in pairs(cpcs) do table.insert(scl, { l = l, c = c }) end table.sort(scl, function(a, b) return a.c > b.c end) for k, lc in ipairs(scl) do scl[k] = string.format("%s (%d)", lc.l ~= "" and lc.l or "(none)", lc.c) end io.write("\nMost common contexts: " .. table.concat(scl, ", ", 1, math.min(20, #scl)) .. "\n") table.sort(expdeck, function(a, b) return a.time + a.delay < b.time + b.delay end) io.write("\nUpcoming due prompts wrt days from now:\n") local ppi = 1 local mpi = 1 for dc = 1,100 do for uc = ppi,#expdeck do if expdeck[uc].time + expdeck[uc].delay - os.time() > dc * 86400 then mpi = uc break end end if dc <= 10 or dc % 10 == 0 then io.write(string.format("%d\t%s %d\n", dc, ("\u{2588}"):rep(math.ceil((mpi - ppi) / 10)), mpi - ppi)) end ppi = mpi end end return { desc = "(s)tatistics (with optional query)", nd = true, nq = true, fn = fn }