mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-18 00:45:47 +00:00
* Tier-2 structured verb options: OptionSpec, initial migration (#2098) PR 3 of the AI-friendly roadmap (plans/plan-2098-llm.md). Infrastructure: - Add OptionSpec{Flag,Arg,Type,Desc,Repeatable,Values} to pkg/transformers/aaa_record_transformer.go alongside TransformerSetup. Type is one of: bool, string, int, float, csv-list, regex, filename, format, enum. For type=="enum", Values lists the valid choices. - Add Options []OptionSpec to TransformerSetup (nil = not yet migrated). - Emit Options in VerbInfoForJSON (omitempty so unmigrated verbs stay backward-compatible; agents check key presence for Tier-2 availability). UsageText is always present as the Tier-1 prose fallback. - Add VerbOptionsNilCheck() in aaa_verb_options_check.go: progress report of migrated vs. unmigrated verbs, analogous to FLAG_TABLE.NilCheck(). - Wire verb-options-nil-check into mlr help (internal/docgen section). Initial migration (5/70 verbs): - nothing: empty Options (no verb-specific options, explicitly migrated) - cat: -n (bool), -N (string), -g (csv-list), --filename, --filenum (bool) - head: -g (csv-list), -n (int) - tail: -g (csv-list), -n (int) - tee: -a, -p (bool) Tests: - 5 new unit tests in aaa_transformer_json_test.go covering migrated/ unmigrated paths, field population, JSON round-trip, and key-presence. - Regression test case 0003: mlr help verb-options-nil-check golden output. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Migrate all 70 verbs to structured OptionSpec; bump catalog schema to v2 Completes the Tier-2 migration started in the previous commit. Every verb in TRANSFORMER_LOOKUP_TABLE now has a non-nil Options field. - Workflow-migrated all 65 remaining verbs. Each Setup var now carries Options: []OptionSpec{...} with Flag/Arg/Type/Desc fields. Verbs with no verb-specific options (altkv, check, group-like, nothing, etc.) use an empty slice to signal "migrated but no options." - Drop `omitempty` from VerbInfoForJSON.Options: empty slices were silently dropped, making migrated-no-option verbs indistinguishable from unmigrated ones in JSON. Without omitempty: null=unmigrated, []=migrated-no-options, [...]= migrated-with-options. Bump catalogSchemaVersion 1→2 for this shape change. - Replace the two "unmigrated-verb" unit tests (which used stats1 as an example) with TestAllVerbsFullyMigrated (asserts every verb has non-nil Options) and TestAllVerbsHaveOptionsKeyInJSON (asserts every migrated verb emits the "options" key in JSON). - Regenerate test/cases/cli-help/0003/expout: now reads "Verb options migration: 70/70 migrated." Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * remove a transitional helper * git rms * Render verb usage Options blocks from structured OptionSpec Each verb's usage message and its Tier-2 OptionSpec list previously duplicated the option text. New WriteVerbOptions (aaa_verb_usage.go) renders the "Options:" block from the specs: aligned flag column, descriptions word-wrapped at 80, uniform trailing -h|--help line. - OptionSpec gains Aliases (JSON "aliases") so long-form spellings like join's --lk|--left-keep-field-names survive in both outputs - All 70 verbs migrated; options literals hoisted to package-level vars (usage funcs can't reference their Setup var without a Go init cycle) - Hand-written per-option details the specs had condensed away are merged into Desc, enriching the JSON catalog - Non-option prose (examples, cross-references, dynamic accumulator listings) kept verbatim - Regenerated the six usage-embedding regression expectations and the two affected doc pages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix pre-existing usage-text bugs surfaced by the OptionSpec migration - gap: usage said "One of -f or -g is required" but the parser takes -n or -g - seqgen: drop description line copy-pasted from cat ("Passes input records directly to output...") which contradicted "Discards the input record stream" - utf8-to-latin1: description read inverted ("from Latin-1 to UTF-8") - sec2gmtdate: usage said "../c/mlr" instead of "mlr" - top: document the accepted-but-undocumented --max flag - stats2: add linreg-pca to the -a enum values, matching the runtime accumulator table Regression expectations and docs regenerated accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix check usage sentence order; stats1 usage blank line to usage stream - check: the description's second and third lines were swapped, reading "Consumes records without printing any output, / Useful for doing a well-formatted check on input data. / with the exception that warnings are printed to stderr." - stats1: a bare fmt.Println() in the usage func wrote its blank line to process stdout instead of the usage output stream Regression expectation and docs regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
466 lines
15 KiB
Go
466 lines
15 KiB
Go
package transformers
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/johnkerl/miller/v6/pkg/cli"
|
|
"github.com/johnkerl/miller/v6/pkg/lib"
|
|
"github.com/johnkerl/miller/v6/pkg/mlrval"
|
|
"github.com/johnkerl/miller/v6/pkg/transformers/utils"
|
|
"github.com/johnkerl/miller/v6/pkg/types"
|
|
)
|
|
|
|
const verbNameStats2 = "stats2"
|
|
|
|
// For joining "x" and "y" into "x...y" for map keys. "," is another natural choice but would break
|
|
// if we were ever asked to process field names with commas in them.
|
|
const stats2KeySeparator = "\001"
|
|
|
|
var stats2Options = []OptionSpec{
|
|
{Flag: "-a", Arg: "{linreg-ols,corr,...}", Type: "enum", Desc: "Names of accumulators: one or more of the listed values.", Values: []string{"linreg-ols", "linreg-pca", "r2", "logireg", "corr", "cov"}},
|
|
{Flag: "-f", Arg: "{a,b,c,d}", Type: "csv-list", Desc: "Value-field name-pairs on which to compute statistics. There must be an even number of names."},
|
|
{Flag: "-g", Arg: "{e,f,g}", Type: "csv-list", Desc: "Optional group-by-field names."},
|
|
{Flag: "-v", Type: "bool", Desc: "Print additional output for linreg-pca."},
|
|
{Flag: "-s", Type: "bool", Desc: "Print iterative stats. Useful in tail -f contexts, in which case please avoid pprint-format output since end of input stream will never be seen. Likewise, if input is coming from `tail -f`, be sure to use `--records-per-batch 1`."},
|
|
{Flag: "--fit", Type: "bool", Desc: "Rather than printing regression parameters, applies them to the input data to compute new fit fields. All input records are held in memory until end of input stream. Has effect only for linreg-ols, linreg-pca, and logireg."},
|
|
{Flag: "-S", Type: "bool", Desc: "No-op flag for backward compatibility with Miller 5."},
|
|
{Flag: "-F", Type: "bool", Desc: "No-op flag for backward compatibility with Miller 5."},
|
|
}
|
|
|
|
var Stats2Setup = TransformerSetup{
|
|
Verb: verbNameStats2,
|
|
UsageFunc: transformerStats2Usage,
|
|
ParseCLIFunc: transformerStats2ParseCLI,
|
|
IgnoresInput: false,
|
|
Options: stats2Options,
|
|
}
|
|
|
|
func transformerStats2Usage(
|
|
o *os.File,
|
|
) {
|
|
argv0 := "mlr"
|
|
verb := verbNameStats2
|
|
|
|
fmt.Fprintf(o, "Usage: %s %s [options]\n", argv0, verb)
|
|
fmt.Fprintf(o, "Computes bivariate statistics for one or more given field-name pairs,\n")
|
|
fmt.Fprintf(o, "accumulated across the input record stream.\n")
|
|
WriteVerbOptions(o, stats2Options)
|
|
fmt.Fprintf(o, "Names of accumulators for -a, one or more of:\n")
|
|
|
|
utils.ListStats2Accumulators(o)
|
|
|
|
fmt.Fprintf(o, "Only one of -s or --fit may be used.\n")
|
|
fmt.Fprintf(o, "Example: %s %s -a linreg-pca -f x,y\n", argv0, verb)
|
|
fmt.Fprintf(o, "Example: %s %s -a linreg-ols,r2 -f x,y -g size,shape\n", argv0, verb)
|
|
fmt.Fprintf(o, "Example: %s %s -a corr -f x,y\n", argv0, verb)
|
|
}
|
|
|
|
func transformerStats2ParseCLI(
|
|
pargi *int,
|
|
argc int,
|
|
args []string,
|
|
_ *cli.TOptions,
|
|
doConstruct bool, // false for first pass of CLI-parse, true for second pass
|
|
) (RecordTransformer, error) {
|
|
|
|
// Skip the verb name from the current spot in the mlr command line
|
|
argi := *pargi
|
|
verb := args[argi]
|
|
argi++
|
|
|
|
var accumulatorNameList []string = nil
|
|
var valueFieldNameList []string = nil
|
|
groupByFieldNameList := []string{}
|
|
doVerbose := false
|
|
doIterativeStats := false
|
|
doHoldAndFit := false
|
|
|
|
var err error
|
|
for argi < argc /* variable increment: 1 or 2 depending on flag */ {
|
|
opt := args[argi]
|
|
if !strings.HasPrefix(opt, "-") {
|
|
break // No more flag options to process
|
|
}
|
|
if args[argi] == "--" {
|
|
break // All transformers must do this so main-flags can follow verb-flags
|
|
}
|
|
argi++
|
|
|
|
switch opt {
|
|
case "-h", "--help":
|
|
transformerStats2Usage(os.Stdout)
|
|
return nil, cli.ErrHelpRequested
|
|
|
|
case "-a":
|
|
accumulatorNameList, err = cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case "-f":
|
|
valueFieldNameList, err = cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case "-g":
|
|
groupByFieldNameList, err = cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case "-v":
|
|
doVerbose = true
|
|
|
|
case "-s":
|
|
doIterativeStats = true
|
|
|
|
case "--fit":
|
|
doHoldAndFit = true
|
|
|
|
case "-S":
|
|
// No-op pass-through for backward compatibility with Miller 5
|
|
|
|
case "-F":
|
|
// The -F flag isn't used for stats2: all arithmetic here is
|
|
// floating-point. Yet it is supported for step and stats1 for all
|
|
// applicable stats1/step accumulators, so we accept here as well
|
|
// for all applicable stats2 accumulators (i.e. none of them).
|
|
|
|
default:
|
|
return nil, cli.VerbErrorf(verb, "option \"%s\" not recognized", opt)
|
|
}
|
|
}
|
|
|
|
if doIterativeStats && doHoldAndFit {
|
|
return nil, cli.VerbErrorf(verb, "cannot combine -I and -H")
|
|
}
|
|
if accumulatorNameList == nil {
|
|
return nil, cli.VerbErrorf(verb, "-a option is required")
|
|
}
|
|
if valueFieldNameList == nil {
|
|
return nil, cli.VerbErrorf(verb, "-f option is required")
|
|
}
|
|
if len(valueFieldNameList)%2 != 0 {
|
|
return nil, cli.VerbErrorf(verb, "argument to -f must have even number of fields")
|
|
}
|
|
|
|
*pargi = argi
|
|
if !doConstruct { // All transformers must do this for main command-line parsing
|
|
return nil, nil
|
|
}
|
|
|
|
transformer, err := NewTransformerStats2(
|
|
accumulatorNameList,
|
|
valueFieldNameList,
|
|
groupByFieldNameList,
|
|
doVerbose,
|
|
doIterativeStats,
|
|
doHoldAndFit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transformer, nil
|
|
}
|
|
|
|
type TransformerStats2 struct {
|
|
// Input:
|
|
accumulatorNameList []string
|
|
valueFieldNameList []string
|
|
groupByFieldNameList []string
|
|
|
|
doVerbose bool
|
|
doIterativeStats bool
|
|
doHoldAndFit bool
|
|
|
|
// State:
|
|
accumulatorFactory *utils.Stats2AccumulatorFactory
|
|
|
|
// Accumulators are indexed by
|
|
// groupByFieldName . value1FieldName+sep+value2FieldName . accumulatorName . accumulator object
|
|
// This would be
|
|
// namedAccumulators map[string]map[string]map[string]IStats2Accumulator
|
|
// except we need maps that preserve insertion order.
|
|
namedAccumulators *lib.OrderedMap[*lib.OrderedMap[*lib.OrderedMap[utils.IStats2Accumulator]]]
|
|
|
|
groupingKeysToGroupByFieldValues *lib.OrderedMap[[]*mlrval.Mlrval]
|
|
|
|
// For hold-and-fit:
|
|
// ordered map from grouping-key to list of RecordAndContext
|
|
recordGroups *lib.OrderedMap[*[]*types.RecordAndContext]
|
|
}
|
|
|
|
func NewTransformerStats2(
|
|
accumulatorNameList []string,
|
|
valueFieldNameList []string,
|
|
groupByFieldNameList []string,
|
|
doVerbose bool,
|
|
doIterativeStats bool,
|
|
doHoldAndFit bool,
|
|
) (*TransformerStats2, error) {
|
|
for _, name := range accumulatorNameList {
|
|
if !utils.ValidateStats2AccumulatorName(name) {
|
|
return nil, fmt.Errorf(`mlr stats2: accumulator "%s" not found`, name)
|
|
}
|
|
}
|
|
|
|
tr := &TransformerStats2{
|
|
accumulatorNameList: accumulatorNameList,
|
|
valueFieldNameList: valueFieldNameList,
|
|
groupByFieldNameList: groupByFieldNameList,
|
|
doVerbose: doVerbose,
|
|
doIterativeStats: doIterativeStats,
|
|
doHoldAndFit: doHoldAndFit,
|
|
accumulatorFactory: utils.NewStats2AccumulatorFactory(),
|
|
namedAccumulators: lib.NewOrderedMap[*lib.OrderedMap[*lib.OrderedMap[utils.IStats2Accumulator]]](),
|
|
groupingKeysToGroupByFieldValues: lib.NewOrderedMap[[]*mlrval.Mlrval](),
|
|
recordGroups: lib.NewOrderedMap[*[]*types.RecordAndContext](),
|
|
}
|
|
return tr, nil
|
|
}
|
|
|
|
// Given: accumulate corr,cov on values x,y group by a,b.
|
|
// Example input: Example output:
|
|
// a b x y a b x_corr x_cov y_corr y_cov
|
|
// s t 1 2 s t 2 6 2 8
|
|
// u v 3 4 u v 1 3 1 4
|
|
// s t 5 6 u w 1 7 1 9
|
|
// u w 7 9
|
|
//
|
|
// Multilevel hashmap structure:
|
|
// {
|
|
// ["s","t"] : { <--- group-by field names
|
|
// ["x","y"] : { <--- value field names
|
|
// "corr" : stats2_corr object,
|
|
// "cov" : stats2_cov object
|
|
// }
|
|
// },
|
|
// ["u","v"] : {
|
|
// ["x","y"] : {
|
|
// "corr" : stats2_corr object,
|
|
// "cov" : stats2_cov object
|
|
// }
|
|
// },
|
|
// ["u","w"] : {
|
|
// ["x","y"] : {
|
|
// "corr" : stats2_corr object,
|
|
// "cov" : stats2_cov object
|
|
// }
|
|
// },
|
|
// }
|
|
//
|
|
// In the iterative case, add to the current record its current group's stats fields.
|
|
// In the non-iterative case, produce output only at the end of the input stream.
|
|
|
|
func (tr *TransformerStats2) Transform(
|
|
inrecAndContext *types.RecordAndContext,
|
|
outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext
|
|
inputDownstreamDoneChannel <-chan bool,
|
|
outputDownstreamDoneChannel chan<- bool,
|
|
) {
|
|
HandleDefaultDownstreamDone(inputDownstreamDoneChannel, outputDownstreamDoneChannel)
|
|
if !inrecAndContext.EndOfStream {
|
|
|
|
tr.ingest(inrecAndContext)
|
|
|
|
if tr.doIterativeStats {
|
|
// The input record is modified in this case, with new fields appended
|
|
*outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext)
|
|
}
|
|
// if tr.doHoldAndFit, the input record is held by the ingestor
|
|
|
|
} else { // end of record stream
|
|
if !tr.doIterativeStats { // in the iterative case, already emitted per-record
|
|
if tr.doHoldAndFit {
|
|
tr.fit(outputRecordsAndContexts)
|
|
} else {
|
|
tr.emit(outputRecordsAndContexts, &inrecAndContext.Context)
|
|
}
|
|
}
|
|
*outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) // end-of-stream marker
|
|
}
|
|
}
|
|
|
|
func (tr *TransformerStats2) ingest(
|
|
inrecAndContext *types.RecordAndContext,
|
|
) {
|
|
inrec := inrecAndContext.Record
|
|
|
|
// E.g. if grouping by "a" and "b", and the current record has a=circle, b=blue,
|
|
// then groupingKey is the string "circle,blue".
|
|
groupingKey, groupByFieldValues, ok := inrec.GetSelectedValuesAndJoined(tr.groupByFieldNameList)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
tr.groupingKeysToGroupByFieldValues.Put(groupingKey, groupByFieldValues)
|
|
|
|
groupToValueFields := tr.namedAccumulators.Get(groupingKey)
|
|
if groupToValueFields == nil {
|
|
groupToValueFields = lib.NewOrderedMap[*lib.OrderedMap[utils.IStats2Accumulator]]()
|
|
tr.namedAccumulators.Put(groupingKey, groupToValueFields)
|
|
}
|
|
|
|
if tr.doHoldAndFit { // Retain the input record in memory, for fitting and delivery at end of stream
|
|
groupToRecords := tr.recordGroups.Get(groupingKey)
|
|
if groupToRecords == nil {
|
|
records := []*types.RecordAndContext{}
|
|
groupToRecords = &records
|
|
tr.recordGroups.Put(groupingKey, groupToRecords)
|
|
}
|
|
*groupToRecords = append(*groupToRecords, inrecAndContext)
|
|
}
|
|
|
|
// for [["x","y"]]
|
|
n := len(tr.valueFieldNameList)
|
|
for i := 0; i < n; i += 2 {
|
|
valueFieldName1 := tr.valueFieldNameList[i]
|
|
valueFieldName2 := tr.valueFieldNameList[i+1]
|
|
|
|
key := valueFieldName1 + stats2KeySeparator + valueFieldName2
|
|
|
|
valueFieldsToAccumulator := groupToValueFields.Get(key)
|
|
if valueFieldsToAccumulator == nil {
|
|
valueFieldsToAccumulator = lib.NewOrderedMap[utils.IStats2Accumulator]()
|
|
groupToValueFields.Put(key, valueFieldsToAccumulator)
|
|
}
|
|
|
|
mval1 := inrec.Get(valueFieldName1)
|
|
mval2 := inrec.Get(valueFieldName2)
|
|
if mval1 == nil || mval2 == nil { // Key absent in current record
|
|
continue
|
|
}
|
|
if mval1.IsVoid() || mval2.IsVoid() { // Key present in current record but with empty value
|
|
continue
|
|
}
|
|
|
|
// for ["corr", "cov"]
|
|
for _, accumulatorName := range tr.accumulatorNameList {
|
|
accumulator := valueFieldsToAccumulator.Get(accumulatorName)
|
|
if accumulator == nil {
|
|
accumulator = tr.accumulatorFactory.Make(
|
|
valueFieldName1,
|
|
valueFieldName2,
|
|
accumulatorName,
|
|
tr.doVerbose,
|
|
)
|
|
if accumulator == nil {
|
|
fmt.Fprintf(os.Stderr, "mlr stats2: accumulator creation failed\n")
|
|
os.Exit(1)
|
|
}
|
|
valueFieldsToAccumulator.Put(accumulatorName, accumulator)
|
|
}
|
|
accumulator.Ingest(
|
|
mval1.GetNumericToFloatValueOrDie(),
|
|
mval2.GetNumericToFloatValueOrDie(),
|
|
)
|
|
}
|
|
|
|
if tr.doIterativeStats {
|
|
tr.populateRecord(
|
|
inrecAndContext.Record,
|
|
valueFieldName1,
|
|
valueFieldName2,
|
|
valueFieldsToAccumulator,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tr *TransformerStats2) emit(
|
|
outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext
|
|
context *types.Context,
|
|
) {
|
|
for pa := tr.namedAccumulators.Head; pa != nil; pa = pa.Next {
|
|
outrec := mlrval.NewMlrmapAsRecord()
|
|
|
|
// Add in a=s,b=t fields:
|
|
groupingKey := pa.Key
|
|
groupByFieldValues := tr.groupingKeysToGroupByFieldValues.Get(groupingKey)
|
|
for i, groupByFieldName := range tr.groupByFieldNameList {
|
|
outrec.PutReference(groupByFieldName, groupByFieldValues[i].Copy())
|
|
}
|
|
|
|
// Add in fields such as x_y_corr, etc.
|
|
groupToValueFields := tr.namedAccumulators.Get(groupingKey)
|
|
|
|
// For "x","y"
|
|
for pc := groupToValueFields.Head; pc != nil; pc = pc.Next {
|
|
|
|
pairs := strings.Split(pc.Key, stats2KeySeparator)
|
|
valueFieldName1 := pairs[0]
|
|
valueFieldName2 := pairs[1]
|
|
valueFieldsToAccumulator := pc.Value
|
|
|
|
tr.populateRecord(outrec, valueFieldName1, valueFieldName2, valueFieldsToAccumulator)
|
|
|
|
// For "corr", "linreg"
|
|
for pd := valueFieldsToAccumulator.Head; pd != nil; pd = pd.Next {
|
|
accumulator := pd.Value
|
|
accumulator.Populate(valueFieldName1, valueFieldName2, outrec)
|
|
}
|
|
}
|
|
|
|
*outputRecordsAndContexts = append(*outputRecordsAndContexts, types.NewRecordAndContext(outrec, context))
|
|
}
|
|
}
|
|
|
|
func (tr *TransformerStats2) populateRecord(
|
|
outrec *mlrval.Mlrmap,
|
|
valueFieldName1 string,
|
|
valueFieldName2 string,
|
|
valueFieldsToAccumulator *lib.OrderedMap[utils.IStats2Accumulator],
|
|
) {
|
|
// For "corr", "linreg"
|
|
for pe := valueFieldsToAccumulator.Head; pe != nil; pe = pe.Next {
|
|
accumulator := pe.Value
|
|
accumulator.Populate(valueFieldName1, valueFieldName2, outrec)
|
|
}
|
|
}
|
|
|
|
func (tr *TransformerStats2) fit(
|
|
outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext
|
|
) {
|
|
for pa := tr.namedAccumulators.Head; pa != nil; pa = pa.Next {
|
|
groupingKey := pa.Key
|
|
groupToValueFields := pa.Value
|
|
recordsAndContexts := tr.recordGroups.Get(groupingKey)
|
|
if recordsAndContexts == nil {
|
|
continue
|
|
}
|
|
for _, recordAndContext := range *recordsAndContexts {
|
|
record := recordAndContext.Record
|
|
|
|
// For "x","y"
|
|
for pb := groupToValueFields.Head; pb != nil; pb = pb.Next {
|
|
pairs := strings.Split(pb.Key, stats2KeySeparator)
|
|
valueFieldName1 := pairs[0]
|
|
valueFieldName2 := pairs[1]
|
|
valueFieldsToAccumulator := pb.Value
|
|
|
|
// For "linreg-ols", "logireg"
|
|
for pc := valueFieldsToAccumulator.Head; pc != nil; pc = pc.Next {
|
|
accumulator := pc.Value
|
|
|
|
// Note R2, cov, corr, etc have no non-trivial fit-function
|
|
mval1 := record.Get(valueFieldName1)
|
|
mval2 := record.Get(valueFieldName2)
|
|
if mval1 != nil && mval2 != nil {
|
|
accumulator.Fit(
|
|
mval1.GetNumericToFloatValueOrDie(),
|
|
mval2.GetNumericToFloatValueOrDie(),
|
|
record,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
*outputRecordsAndContexts = append(*outputRecordsAndContexts, recordAndContext)
|
|
}
|
|
*recordsAndContexts = (*recordsAndContexts)[:0]
|
|
}
|
|
}
|