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 verbNameSummary = "summary" type tSummarizerType int const ( stFieldType = iota stAccumulator stPercentile ) type tSummarizerInfo struct { name string help string stype tSummarizerType } var allSummarizerInfos = []tSummarizerInfo{ {"field_type", "string, int, etc. -- if a column has mixed types, all encountered types are printed (see notes below)", stFieldType}, {"count", "+1 for every instance of the field across all records in the input record stream", stAccumulator}, {"null_count", "count of field values either empty string or JSON null", stAccumulator}, {"distinct_count", "count of distinct values for the field", stAccumulator}, {"mode", "most-frequently-occurring value for the field", stAccumulator}, {"sum", "sum of field values", stAccumulator}, {"mean", "mean of the field values", stAccumulator}, {"stddev", "standard deviation of the field values", stAccumulator}, {"var", "variance of the field values", stAccumulator}, {"skewness", "skewness of the field values", stAccumulator}, {"minlen", "length of shortest string representation for the field", stAccumulator}, {"maxlen", "length of longest string representation for the field", stAccumulator}, {"min", "minimum field value", stAccumulator}, {"p25", "first-quartile field value", stPercentile}, {"median", "median field value", stPercentile}, {"p75", "third-quartile field value", stPercentile}, {"max", "maximum field value", stAccumulator}, {"iqr", "interquartile range: p75 - p25", stPercentile}, {"lof", "lower outer fence: p25 - 3.0 * iqr", stPercentile}, {"lif", "lower inner fence: p25 - 1.5 * iqr", stPercentile}, {"uif", "upper inner fence: p75 + 1.5 * iqr", stPercentile}, {"uof", "upper outer fence: p75 + 3.0 * iqr", stPercentile}, } var summaryDefaultSummarizerNames = []string{ "field_type", "count", "mean", "min", "max", "null_count", "distinct_count", } var summaryOptions = []OptionSpec{ {Flag: "-a", Arg: "{mean,sum,etc.}", Type: "enum", Desc: "Use only the specified summarizers.", Values: []string{"field_type", "count", "null_count", "distinct_count", "mode", "sum", "mean", "stddev", "var", "skewness", "minlen", "maxlen", "min", "p25", "median", "p75", "max", "iqr", "lof", "lif", "uif", "uof"}}, {Flag: "-x", Arg: "{mean,sum,etc.}", Type: "enum", Desc: "Use all summarizers except the specified ones.", Values: []string{"field_type", "count", "null_count", "distinct_count", "mode", "sum", "mean", "stddev", "var", "skewness", "minlen", "maxlen", "min", "p25", "median", "p75", "max", "iqr", "lof", "lif", "uif", "uof"}}, {Flag: "--all", Type: "bool", Desc: "Use all available summarizers."}, {Flag: "--transpose", Type: "bool", Desc: "Show output with field names as column names."}, } var SummarySetup = TransformerSetup{ Verb: verbNameSummary, UsageFunc: transformerSummaryUsage, ParseCLIFunc: transformerSummaryParseCLI, IgnoresInput: false, Options: summaryOptions, } func transformerSummaryUsage( o *os.File, ) { fmt.Fprintf(o, "Usage: %s %s [options]\n", "mlr", verbNameSummary) fmt.Fprintf(o, "Show summary statistics about the input data.\n") fmt.Fprintf(o, "\n") fmt.Fprintf(o, "All summarizers:\n") for _, info := range allSummarizerInfos { fmt.Fprintf(o, " %-14s %s\n", info.name, info.help) } fmt.Fprintf(o, "\n") fmt.Fprintf(o, "Default summarizers:\n") fmt.Fprintf(o, " ") for _, summarizerName := range summaryDefaultSummarizerNames { fmt.Fprintf(o, " %s", summarizerName) } fmt.Fprintf(o, "\n") fmt.Fprintf(o, "\n") fmt.Fprintf(o, "Notes:\n") fmt.Fprintf(o, "* min, p25, median, p75, and max work for strings as well as numbers\n") fmt.Fprintf(o, "* Distinct-counts are computed on string representations -- so 4.1 and 4.10 are counted as distinct here.\n") fmt.Fprintf(o, "* If the mode is not unique in the input data, the first-encountered value is reported as the mode.\n") fmt.Fprintf(o, "* A field_type of \"int-string\", \"empty-string\", etc. means the column contains values of mixed types --\n") fmt.Fprintf(o, " all types encountered are printed, hyphen-joined, in the order first encountered.\n") fmt.Fprintf(o, "\n") WriteVerbOptions(o, summaryOptions) } func transformerSummaryParseCLI( 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++ summarizerNames := summaryDefaultSummarizerNames allSummarizerNamesList := make([]string, len(allSummarizerInfos)) for i, info := range allSummarizerInfos { allSummarizerNamesList[i] = info.name } allSummarizerNamesSet := make(map[string]bool) for _, summarizerName := range allSummarizerNamesList { allSummarizerNamesSet[summarizerName] = true } transposeOutput := 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": transformerSummaryUsage(os.Stdout) return nil, cli.ErrHelpRequested case "--all": summarizerNames = allSummarizerNamesList case "-a": summarizerNames, err = cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc) if err != nil { return nil, err } for _, summarizerName := range summarizerNames { if !allSummarizerNamesSet[summarizerName] { return nil, cli.VerbErrorf(verb, "summarizer \"%s\" not found", summarizerName) } } case "-x": excludeSummarizerNames, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc) if err != nil { return nil, err } for _, excludeSummarizerName := range excludeSummarizerNames { if !allSummarizerNamesSet[excludeSummarizerName] { return nil, cli.VerbErrorf(verb, "exclude summarizer \"%s\" not found", excludeSummarizerName) } } excludeSummarizerNamesSet := make(map[string]bool) for _, excludeSummarizerName := range excludeSummarizerNames { excludeSummarizerNamesSet[excludeSummarizerName] = true } summarizerNames = []string{} for _, summarizerName := range allSummarizerNamesList { if !excludeSummarizerNamesSet[summarizerName] { summarizerNames = append(summarizerNames, summarizerName) } } case "--transpose": transposeOutput = true default: return nil, cli.VerbErrorf(verb, "option \"%s\" not recognized", opt) } } *pargi = argi if !doConstruct { // All transformers must do this for main command-line parsing return nil, nil } transformer, err := NewTransformerSummary(summarizerNames, transposeOutput) if err != nil { return nil, err } return transformer, nil } type tFieldSummary struct { // Needs lib.OrderedMap, not map[string]int64, for deterministic regression-test output. // This is a map (a set really) rather than a single value in case of heterogeneous data. fieldTypesMap *lib.OrderedMap[int64] accumulators map[string]utils.IStats1Accumulator percentileKeeper *utils.PercentileKeeper } func newFieldSummary() *tFieldSummary { fieldSummary := &tFieldSummary{ fieldTypesMap: lib.NewOrderedMap[int64](), accumulators: make(map[string]utils.IStats1Accumulator), // Interpolated percentiles don't play well with string-valued input data percentileKeeper: utils.NewPercentileKeeper(false), } fieldSummary.accumulators["count"] = utils.NewStats1CountAccumulator() fieldSummary.accumulators["null_count"] = utils.NewStats1NullCountAccumulator() fieldSummary.accumulators["distinct_count"] = utils.NewStats1DistinctCountAccumulator() fieldSummary.accumulators["mode"] = utils.NewStats1ModeAccumulator() fieldSummary.accumulators["min"] = utils.NewStats1MinAccumulator() fieldSummary.accumulators["max"] = utils.NewStats1MaxAccumulator() fieldSummary.accumulators["sum"] = utils.NewStats1SumAccumulator() fieldSummary.accumulators["mean"] = utils.NewStats1MeanAccumulator() fieldSummary.accumulators["stddev"] = utils.NewStats1StddevAccumulator() fieldSummary.accumulators["var"] = utils.NewStats1VarAccumulator() fieldSummary.accumulators["skewness"] = utils.NewStats1SkewnessAccumulator() fieldSummary.accumulators["minlen"] = utils.NewStats1MinLenAccumulator() fieldSummary.accumulators["maxlen"] = utils.NewStats1MaxLenAccumulator() return fieldSummary } type TransformerSummary struct { fieldSummaries *lib.OrderedMap[*tFieldSummary] summarizerNames map[string]bool hasAnyPercentiles bool transposeOutput bool } func NewTransformerSummary( summarizerNames []string, transposeOutput bool, ) (*TransformerSummary, error) { tr := &TransformerSummary{ fieldSummaries: lib.NewOrderedMap[*tFieldSummary](), summarizerNames: make(map[string]bool), transposeOutput: transposeOutput, } for _, summarizerName := range summarizerNames { tr.summarizerNames[summarizerName] = true } // Different percentile summarizers share the same data structure. // If no percentile summarizers are requested, don't ingest percentiles. // This is to help running out of memory for large input data files. tr.hasAnyPercentiles = false for _, info := range allSummarizerInfos { if info.stype == stPercentile && tr.summarizerNames[info.name] { tr.hasAnyPercentiles = true break } } return tr, nil } func (tr *TransformerSummary) 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) } else { if tr.transposeOutput { tr.emitTransposed(inrecAndContext, outputRecordsAndContexts) } else { tr.emit(inrecAndContext, outputRecordsAndContexts) } } } func (tr *TransformerSummary) ingest( inrecAndContext *types.RecordAndContext, ) { inrec := inrecAndContext.Record for pe := inrec.Head; pe != nil; pe = pe.Next { fieldName := pe.Key fieldSummary := tr.fieldSummaries.Get(fieldName) if fieldSummary == nil { fieldSummary = newFieldSummary() tr.fieldSummaries.Put(fieldName, fieldSummary) } if tr.summarizerNames["field_type"] { typeName := pe.Value.GetTypeName() iValue, ok := fieldSummary.fieldTypesMap.GetWithCheck(typeName) if !ok { fieldSummary.fieldTypesMap.Put(typeName, int64(1)) } else { fieldSummary.fieldTypesMap.Put(typeName, iValue+1) } } for _, info := range allSummarizerInfos { if info.stype == stAccumulator && tr.summarizerNames[info.name] { fieldSummary.accumulators[info.name].Ingest(pe.Value) } } if tr.hasAnyPercentiles { fieldSummary.percentileKeeper.Ingest(pe.Value) } } } func (tr *TransformerSummary) emit( inrecAndContext *types.RecordAndContext, outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext ) { for pe := tr.fieldSummaries.Head; pe != nil; pe = pe.Next { newrec := mlrval.NewMlrmapAsRecord() fieldName := pe.Key fieldSummary := pe.Value newrec.PutCopy("field_name", mlrval.FromString(fieldName)) // Display field type(s) for this column as a list of string, hyphen-joined into a single string. if tr.summarizerNames["field_type"] { fieldTypesList := make([]string, fieldSummary.fieldTypesMap.FieldCount) i := 0 for pf := fieldSummary.fieldTypesMap.Head; pf != nil; pf = pf.Next { fieldType := pf.Key fieldTypesList[i] = fieldType i++ } newrec.PutCopy("field_type", mlrval.FromString(strings.Join(fieldTypesList, "-"))) } for _, info := range allSummarizerInfos { switch info.stype { case stAccumulator: if tr.summarizerNames[info.name] { newrec.PutCopy(info.name, fieldSummary.accumulators[info.name].Emit()) } case stPercentile: if tr.summarizerNames[info.name] { newrec.PutCopy(info.name, fieldSummary.percentileKeeper.EmitNamed(info.name)) } } } *outputRecordsAndContexts = append(*outputRecordsAndContexts, types.NewRecordAndContext(newrec, &inrecAndContext.Context)) } *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) // end-of-stream marker } func (tr *TransformerSummary) emitTransposed( inrecAndContext *types.RecordAndContext, oracs *[]*types.RecordAndContext, // list of *types.RecordAndContext ) { octx := &inrecAndContext.Context // Display field type(s) for this column as a list of string, hyphen-joined into a single string. if tr.summarizerNames["field_type"] { newrec := mlrval.NewMlrmapAsRecord() newrec.PutCopy("field_name", mlrval.FromString("field_type")) for pe := tr.fieldSummaries.Head; pe != nil; pe = pe.Next { fieldSummary := pe.Value fieldTypesList := make([]string, fieldSummary.fieldTypesMap.FieldCount) i := 0 for pf := fieldSummary.fieldTypesMap.Head; pf != nil; pf = pf.Next { fieldType := pf.Key fieldTypesList[i] = fieldType i++ } newrec.PutCopy(pe.Key, mlrval.FromString(strings.Join(fieldTypesList, "-"))) } *oracs = append(*oracs, types.NewRecordAndContext(newrec, &inrecAndContext.Context)) } for _, info := range allSummarizerInfos { switch info.stype { case stAccumulator: tr.maybeEmitAccumulatorTransposed(oracs, octx, info.name) case stPercentile: tr.maybeEmitPercentileNameTransposed(oracs, octx, info.name) } } *oracs = append(*oracs, inrecAndContext) // end-of-stream marker } // maybeEmitAccumulatorTransposed is a helper method for emitTransposed, // for "count", "sum", "mean", etc. func (tr *TransformerSummary) maybeEmitAccumulatorTransposed( oracs *[]*types.RecordAndContext, // list of *types.RecordAndContext octx *types.Context, summarizerName string, ) { if tr.summarizerNames[summarizerName] { newrec := mlrval.NewMlrmapAsRecord() newrec.PutCopy("field_name", mlrval.FromString(summarizerName)) for pe := tr.fieldSummaries.Head; pe != nil; pe = pe.Next { fieldSummary := pe.Value newrec.PutCopy(pe.Key, fieldSummary.accumulators[summarizerName].Emit()) } *oracs = append(*oracs, types.NewRecordAndContext(newrec, octx)) } } // maybeEmitPercentileNameTransposed is a helper method for emitTransposed, // for "median", "iqr", "uof", etc. func (tr *TransformerSummary) maybeEmitPercentileNameTransposed( oracs *[]*types.RecordAndContext, // list of *types.RecordAndContext octx *types.Context, summarizerName string, ) { if tr.summarizerNames[summarizerName] { newrec := mlrval.NewMlrmapAsRecord() newrec.PutCopy("field_name", mlrval.FromString(summarizerName)) for pe := tr.fieldSummaries.Head; pe != nil; pe = pe.Next { fieldSummary := pe.Value newrec.PutCopy(pe.Key, fieldSummary.percentileKeeper.EmitNamed(summarizerName)) } *oracs = append(*oracs, types.NewRecordAndContext(newrec, octx)) } }