package learn import ( "math" "testing" ) // a group where feature 0 perfectly separates approved (high) from rejected (low). func rankFixture() []RankGroup { return []RankGroup{ {Items: []RankItem{ {Features: []float64{0.9, 0.5}, Approved: true}, {Features: []float64{0.8, 0.4}, Approved: true}, {Features: []float64{0.2, 0.6}, Approved: false}, {Features: []float64{0.1, 0.5}, Approved: false}, }}, {Items: []RankItem{ {Features: []float64{0.95, 0.3}, Approved: true}, {Features: []float64{0.15, 0.7}, Approved: false}, }}, } } func TestPairwiseDatasetIsSymmetric(t *testing.T) { groups := []RankGroup{{Items: []RankItem{ {Features: []float64{1, 2}, Approved: true}, {Features: []float64{0, 1}, Approved: false}, }}} x, y := PairwiseDataset(groups) // one approved and one rejected => one pair => two symmetric rows. if len(x) != 2 || len(y) != 2 { t.Fatalf("one pair must produce two symmetric rows, got %d", len(x)) } // row 0 is a minus b with label 1, row 1 is b minus a with label 0, exact negatives. if y[0] != 1 || y[1] != 0 { t.Fatalf("labels must be 1 then 0, got %v", y) } for j := range x[0] { if x[0][j] != -x[1][j] { t.Fatalf("the symmetric rows must be exact negatives, got %v and %v", x[0], x[1]) } } } func TestPairwiseDatasetSkipsUnanimousGroups(t *testing.T) { // a slate everyone approved carries no ordering signal, so it produces no pair. groups := []RankGroup{{Items: []RankItem{ {Features: []float64{1, 1}, Approved: true}, {Features: []float64{2, 2}, Approved: true}, }}} x, _ := PairwiseDataset(groups) if len(x) != 0 { t.Fatalf("a unanimous group must produce no pairs, got %d", len(x)) } } func TestPairwiseFitLearnsTheOrdering(t *testing.T) { groups := rankFixture() x, y := PairwiseDataset(groups) m, err := FitLogistic(x, y, 0.01) if err != nil { t.Fatalf("pairwise fit: %v", err) } // the raw space coefficients (utility weights): feature 0 separates, so its coefficient must be positive. coeffs := make([]float64, len(m.Coeffs)) for j := range m.Coeffs { coeffs[j] = m.Coeffs[j] / m.Scales[j] } if coeffs[0] <= 0 { t.Fatalf("the separating feature must get a positive utility weight, got %v", coeffs[0]) } // the learned utility must order every approved above every rejected: perfect pairwise accuracy. if acc := PairwiseAccuracy(groups, coeffs); acc < 0.99 { t.Fatalf("the learned utility must order the separable fixture perfectly, got accuracy %v", acc) } } func TestPairwiseAccuracyBounds(t *testing.T) { groups := rankFixture() // a weight vector that inverts the order (negative on the separating feature) must score below 0.5. if acc := PairwiseAccuracy(groups, []float64{-1, 0}); acc > 0.5 { t.Errorf("an inverting weight must score at or below 0.5, got %v", acc) } // the engine like positive weight orders the fixture correctly. if acc := PairwiseAccuracy(groups, []float64{1, 0}); acc < 0.99 { t.Errorf("a correct weight must score near 1.0, got %v", acc) } // equal utilities (zero weights) are all ties => 0.5. if acc := PairwiseAccuracy(groups, []float64{0, 0}); math.Abs(acc-0.5) > 1e-9 { t.Errorf("all ties must score exactly 0.5, got %v", acc) } } func TestRankUtility(t *testing.T) { if u := RankUtility([]float64{2, 3}, []float64{1, 10}); u != 32 { t.Fatalf("utility must be the dot product, got %v", u) } }