math-optim/stats/table.go
surtur 71e67bdb58
All checks were successful
continuous-integration/drone/push Build is passing
go(stats): actually calculate median
2022-07-18 04:17:58 +02:00

127 lines
3.0 KiB
Go

// Copyright 2022 wanderer <a_mirre at utb dot cz>
// SPDX-License-Identifier: GPL-3.0-or-later
package stats
import (
"fmt"
"sort"
"git.dotya.ml/wanderer/math-optim/report"
"gonum.org/v1/gonum/floats"
"gonum.org/v1/gonum/stat"
)
// getColLayout returns a string slice of Latex table column alignment
// settings.
func getColLayout() []string {
return []string{"c", "c", "c", "c", "c"}
}
// getColNames returns names of table columns, i.e. statistical features we are
// interested in.
func getColNames() []string {
// the first column describes specific iteration settings and is therefore
// dynamically set, hence not present here nor mentioned in
// `getColLayout()`.
return []string{"min", "max", "mean", "median", "stddev"}
}
// SaveTable sifts through computed values, organises data in table-like
// structure as defined in the `report` pkg and passes it on to be fed to a
// tmpl, result of which is then saved in a `.tex` file (filename based on the
// algo name string).
func SaveTable(algo string, algoStats [][]Stats) {
table := report.NewTable()
table.Algo = algo
table.Header = getColNames()
table.ColLayout = getColLayout()
for _, singleFunc := range algoStats {
// append/merge(...) if necessary.
table.Rows = append(table.Rows, parseSingleBenchStats(singleFunc)...)
}
report.SaveTableToFile(*table)
}
// parseSingleBenchStats processes results of a particular bench and constructs
// statistics.
func parseSingleBenchStats(benchStats []Stats) []report.Row {
rows := make([]report.Row, 0)
for _, s := range benchStats {
for _, dim := range s.BenchFuncStats {
row := report.NewRow()
row.Title = "D=" + fmt.Sprint(s.Dimens) + ", f=" + dim.BenchName +
", G=" + fmt.Sprint(s.Generations) +
", I=" + fmt.Sprint(s.Iterations)
row.Title = makeRowTitle(
dim.BenchName,
s.Dimens,
s.Generations,
s.Iterations,
)
// collect the best.
var best []float64
for _, iter := range dim.Solution {
last := s.Generations - 1
best = append(best, iter.Results[last])
}
row.Values = statsFromBest(best)
rows = append(rows, *row)
}
}
return rows
}
func makeRowTitle(bench string, dimens, generations, iterations int) string {
return "D=" + fmt.Sprint(dimens) + ", f=" + bench +
", G=" + fmt.Sprint(generations) +
", I=" + fmt.Sprint(iterations)
}
// statsFromBest computes the actual statistics upon the slice of best results,
// returns a slice of float64s.
func statsFromBest(best []float64) []float64 {
s := make([]float64, len(getColNames()))
s[0] = floats.Min(best)
s[1] = floats.Max(best)
s[2] = stat.Mean(best, nil)
s[3] = median(best)
s[4] = stat.StdDev(best, nil)
return s
}
// as per https://gosamples.dev/calculate-median/.
func median(data []float64) float64 {
dataCopy := make([]float64, len(data))
copy(dataCopy, data)
sort.Float64s(dataCopy)
var median float64
//nolint: gocritic
if l := len(dataCopy); l == 0 {
return 0
} else if l%2 == 0 {
median = (dataCopy[l/2-1] + dataCopy[l/2]) / 2
} else {
median = dataCopy[l/2]
}
return median
}