package main import ( "cmp" "slices" ) const increment = 50 // ops/sec // findOptimalSplit computes and returns a value that separates the given pass // and fail measurements optimally, such that the number of mis-classified // passes (pass values that fall above the split) and fails (fail values that // fall below the split) is minimized. // // The following gives a visual representation of the problem: // // Optimal partition (=550) -----> | // | // Passes: o o o o o o oo | // Fails: x x |x x x x x x // |---------|---------|---------|---------|---------|----|----|---------|---------|---------|---> x // 0 100 200 300 400 500 | 600 700 800 900 // // The algorithm works by computing the error (i.e. mis-classifications) at // various points along the x-axis, starting from the origin and increasing by // the given increment. func findOptimalSplit(pass, fail []int) int { // Not enough data to compute a sensible score. if len(pass) == 0 || len(fail) == 0 { return -1 } // Maintain counters for the number of incorrectly classified passes and // fails. All passes are initially incorrect, as we start at 0. Conversely, // no fails are incorrectly classified, as all scores are >= 0. pCount, fCount := len(pass), 0 p, f := make([]int, len(pass)), make([]int, len(fail)) copy(p, pass) copy(f, fail) // Sort the inputs. slices.Sort(p) slices.Sort(f) // Find the global min and max. min, max := p[0], f[len(fail)-1] // Iterate over the range in increments. var result [][]int for x := min; x <= max; x = x + increment { // Reduce the count of incorrect passes as x increases (i.e. fewer pass // values are incorrect as x increases). for len(p) > 0 && p[0] <= x { pCount-- p = p[1:] } // Increase the count of incorrect fails as x increases (i.e. more fail // values are incorrect as x increases). for len(f) > 0 && f[0] < x { fCount++ f = f[1:] } // Add a (x, score) tuple to result slice. result = append(result, []int{x, pCount + fCount}) } // Sort the (x, score) result slice by score ascending. Tie-break by x // ascending. slices.SortFunc(result, func(a, b []int) int { if v := cmp.Compare(a[1], b[1]); v != 0 { return v } return cmp.Compare(a[0], b[0]) }) // If there is more than one interval, split the difference between the min // and the max. splitMin, splitMax := result[0][0], result[0][0] for i := 1; i < len(result); i++ { if result[i][1] != result[0][1] { break } splitMax = result[i][0] } return (splitMin + splitMax) / 2 }