// Copyright 2022 wanderer // 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 }