aboutsummaryrefslogtreecommitdiff
path: root/cmds/train.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmds/train.go')
-rw-r--r--cmds/train.go32
1 files changed, 32 insertions, 0 deletions
diff --git a/cmds/train.go b/cmds/train.go
index 234ef06..4e9a54f 100644
--- a/cmds/train.go
+++ b/cmds/train.go
@@ -122,6 +122,10 @@ func (c *TrainCommand) Run(stdin io.Reader, stdout io.Writer) error {
fmt.Fprintf(stdout, "train: pos=%d neg=%d | val: pos=%d neg=%d | precision@15=%.3f youden_j=%.3f | vocab=%d → %s\n",
report.TrainPos, report.TrainNeg, report.ValPos, report.ValNeg,
report.PrecisionAt15, report.YoudensJ, report.VocabSize, c.ModelOut)
+ if report.ValStampShare >= 0.5 {
+ fmt.Fprintf(stdout, "note: %d val rows (%.0f%%) share captured_at %s; one batch shaped this validation — treat precision@15 as that batch's measurement, not the model's\n",
+ report.ValStampCount, report.ValStampShare*100, report.ValStamp.Format(time.RFC3339))
+ }
return nil
}
@@ -202,6 +206,11 @@ func (c *TrainCommand) trainAndEvaluate(trainRows, valRows []core.LabelRow, now
}
}
+ // Val concentration: how much of the validation window is one capture
+ // batch (a bulk action or a single flood-heavy export). High share means
+ // precision@15 measured that batch, not steady-state taste.
+ stampShare, stampCount, stampAt := valStampConcentration(valRows)
+
vecParams := map[string]any{
"ngram_min": vectorizer.NgramMin, "ngram_max": vectorizer.NgramMax,
"min_df": vectorizer.MinDF, "max_df": vectorizer.MaxDF, "vocab_cap": vectorizer.VocabCap,
@@ -242,12 +251,32 @@ func (c *TrainCommand) trainAndEvaluate(trainRows, valRows []core.LabelRow, now
TrainWindowMin: minCap.UTC(), TrainWindowMax: maxCap.UTC(),
ValFrac: c.ValFrac, VocabSize: len(vectorizer.OrderedVocab),
PrecisionAt15: precisionAt15, YoudensJ: youdenJ,
+ ValStampShare: stampShare, ValStampCount: stampCount, ValStamp: stampAt.UTC(),
PositiveScores: posStats, NegativeScores: negStats,
VectorizerParams: vecParams, ModelParams: modelParams,
}
return model, report, nil
}
+// valStampConcentration returns the largest share of validation rows that
+// share one captured_at, that stamp's row count, and the stamp itself.
+// Deterministic: ties on count pick the newer stamp.
+func valStampConcentration(valRows []core.LabelRow) (share float64, count int, stamp time.Time) {
+ if len(valRows) == 0 {
+ return 0, 0, time.Time{}
+ }
+ byStamp := make(map[time.Time]int)
+ for _, r := range valRows {
+ byStamp[r.CapturedAt]++
+ }
+ for ts, n := range byStamp {
+ if n > count || (n == count && ts.After(stamp)) {
+ share, count, stamp = float64(n)/float64(len(valRows)), n, ts
+ }
+ }
+ return share, count, stamp
+}
+
// downsampleBalance downsamples the majority class to a 1:1 ratio with the
// minority, using a seeded RNG for reproducibility. If already balanced,
// returns the inputs unchanged.
@@ -423,6 +452,9 @@ type trainReport struct {
VocabSize int `json:"vocab_size"`
PrecisionAt15 float64 `json:"precision_at_15"`
YoudensJ float64 `json:"youdens_j"`
+ ValStampShare float64 `json:"val_max_stamp_share"`
+ ValStampCount int `json:"val_max_stamp_count"`
+ ValStamp time.Time `json:"val_max_stamp"`
PositiveScores map[string]float64 `json:"positive_scores"`
NegativeScores map[string]float64 `json:"negative_scores"`
VectorizerParams map[string]any `json:"vectorizer_params"`