diff --git a/docs/src/manpage.md b/docs/src/manpage.md index 41b5b3676..c75ce93fa 100644 --- a/docs/src/manpage.md +++ b/docs/src/manpage.md @@ -2223,6 +2223,12 @@ This is simply a copy of what you should see on running `man mlr` at a command p 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`. + -w {n} Sliding-window mode: compute statistics over a trailing + window of up to n records (including the current one), rather + than over the whole record stream. Windows are kept per group + when -g is used. One output record is emitted per input + record, with the windowed statistics appended to it. Not + compatible with -s. -S No-op flag for backward compatibility with Miller 5. -F No-op flag for backward compatibility with Miller 5. -h|--help Show this message. @@ -2249,6 +2255,9 @@ This is simply a copy of what you should see on running `man mlr` at a command p Example: mlr stats1 -a min,p10,p50,p90,max -f value -g size,shape Example: mlr stats1 -a count,mode -f size Example: mlr stats1 -a count,mode -f size -g shape + Example: mlr stats1 -a mean,min,max -f quantity -g name -w 7 + This emits one output record per input record, with sliding-window + statistics over the last up-to-7 records for each name. Example: mlr stats1 -a count,mode --fr '^[a-h].*$' --gr '^k.*$' This computes count and mode statistics on all field names beginning with a through h, grouped by all field names starting with k. @@ -4110,5 +4119,5 @@ This is simply a copy of what you should see on running `man mlr` at a command p MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-07-08 4mMILLER24m(1) + 2026-07-14 4mMILLER24m(1) diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index a5e8bd3b8..daf681cfe 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -2202,6 +2202,12 @@ 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`. + -w {n} Sliding-window mode: compute statistics over a trailing + window of up to n records (including the current one), rather + than over the whole record stream. Windows are kept per group + when -g is used. One output record is emitted per input + record, with the windowed statistics appended to it. Not + compatible with -s. -S No-op flag for backward compatibility with Miller 5. -F No-op flag for backward compatibility with Miller 5. -h|--help Show this message. @@ -2228,6 +2234,9 @@ Example: mlr stats1 -a min,p10,p50,p90,max -f value -g size,shape Example: mlr stats1 -a count,mode -f size Example: mlr stats1 -a count,mode -f size -g shape + Example: mlr stats1 -a mean,min,max -f quantity -g name -w 7 + This emits one output record per input record, with sliding-window + statistics over the last up-to-7 records for each name. Example: mlr stats1 -a count,mode --fr '^[a-h].*$' --gr '^k.*$' This computes count and mode statistics on all field names beginning with a through h, grouped by all field names starting with k. @@ -4089,4 +4098,4 @@ MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-07-08 4mMILLER24m(1) + 2026-07-14 4mMILLER24m(1) diff --git a/docs/src/reference-verbs.md b/docs/src/reference-verbs.md index 547bb2938..d3b03c59f 100644 --- a/docs/src/reference-verbs.md +++ b/docs/src/reference-verbs.md @@ -3697,6 +3697,12 @@ Options: 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`. +-w {n} Sliding-window mode: compute statistics over a trailing + window of up to n records (including the current one), rather + than over the whole record stream. Windows are kept per group + when -g is used. One output record is emitted per input + record, with the windowed statistics appended to it. Not + compatible with -s. -S No-op flag for backward compatibility with Miller 5. -F No-op flag for backward compatibility with Miller 5. -h|--help Show this message. @@ -3723,6 +3729,9 @@ Names of accumulators for -a, one or more of: Example: mlr stats1 -a min,p10,p50,p90,max -f value -g size,shape Example: mlr stats1 -a count,mode -f size Example: mlr stats1 -a count,mode -f size -g shape +Example: mlr stats1 -a mean,min,max -f quantity -g name -w 7 + This emits one output record per input record, with sliding-window + statistics over the last up-to-7 records for each name. Example: mlr stats1 -a count,mode --fr '^[a-h].*$' --gr '^k.*$' This computes count and mode statistics on all field names beginning with a through h, grouped by all field names starting with k. @@ -3812,6 +3821,44 @@ square red circle red +With `-w {n}`, statistics are computed over a sliding window of the last up-to-`n` +records -- within each group, when `-g` is used -- and one output record is emitted +per input record, with the windowed statistics appended to it: + +
+mlr --icsv --opprint --from example.csv stats1 -a mean,min,max -f quantity -w 4 ++
+color shape flag k index quantity rate quantity_mean quantity_min quantity_max +yellow triangle true 1 11 43.6498 9.8870 43.6498 43.6498 43.6498 +red square true 2 15 79.2778 0.0130 61.4638 43.6498 79.2778 +red circle true 3 16 13.8103 2.9010 45.579299999999996 13.8103 79.2778 +red square false 4 48 77.5542 7.4670 53.573025 13.8103 79.2778 +purple triangle false 5 51 81.2290 8.5910 62.96782499999999 13.8103 81.229 +red square false 6 64 77.1991 9.5310 62.44815 13.8103 81.229 +purple triangle false 7 65 80.1405 5.8240 79.0307 77.1991 81.229 +yellow circle true 8 73 63.9785 4.2370 75.636775 63.9785 81.229 +yellow circle true 9 87 63.5058 8.3350 71.20597500000001 63.5058 80.1405 +purple square false 10 91 72.3735 8.2430 69.999575 63.5058 80.1405 ++ +
+mlr --icsv --opprint --from example.csv stats1 -a mean -f quantity -g shape -w 2 ++
+color shape flag k index quantity rate quantity_mean +yellow triangle true 1 11 43.6498 9.8870 43.6498 +red square true 2 15 79.2778 0.0130 79.2778 +red circle true 3 16 13.8103 2.9010 13.8103 +red square false 4 48 77.5542 7.4670 78.416 +purple triangle false 5 51 81.2290 8.5910 62.4394 +red square false 6 64 77.1991 9.5310 77.37665 +purple triangle false 7 65 80.1405 5.8240 80.68475000000001 +yellow circle true 8 73 63.9785 4.2370 38.8944 +yellow circle true 9 87 63.5058 8.3350 63.742149999999995 +purple square false 10 91 72.3735 8.2430 74.78630000000001 ++ ## stats2
diff --git a/docs/src/reference-verbs.md.in b/docs/src/reference-verbs.md.in
index 1d4b74da3..04624a259 100644
--- a/docs/src/reference-verbs.md.in
+++ b/docs/src/reference-verbs.md.in
@@ -1132,6 +1132,18 @@ GENMD-RUN-COMMAND
mlr --c2p stats1 -a mode -f color -g shape data/colored-shapes.csv
GENMD-EOF
+With `-w {n}`, statistics are computed over a sliding window of the last up-to-`n`
+records -- within each group, when `-g` is used -- and one output record is emitted
+per input record, with the windowed statistics appended to it:
+
+GENMD-RUN-COMMAND
+mlr --icsv --opprint --from example.csv stats1 -a mean,min,max -f quantity -w 4
+GENMD-EOF
+
+GENMD-RUN-COMMAND
+mlr --icsv --opprint --from example.csv stats1 -a mean -f quantity -g shape -w 2
+GENMD-EOF
+
## stats2
GENMD-RUN-COMMAND
diff --git a/man/manpage.txt b/man/manpage.txt
index a5e8bd3b8..daf681cfe 100644
--- a/man/manpage.txt
+++ b/man/manpage.txt
@@ -2202,6 +2202,12 @@
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`.
+ -w {n} Sliding-window mode: compute statistics over a trailing
+ window of up to n records (including the current one), rather
+ than over the whole record stream. Windows are kept per group
+ when -g is used. One output record is emitted per input
+ record, with the windowed statistics appended to it. Not
+ compatible with -s.
-S No-op flag for backward compatibility with Miller 5.
-F No-op flag for backward compatibility with Miller 5.
-h|--help Show this message.
@@ -2228,6 +2234,9 @@
Example: mlr stats1 -a min,p10,p50,p90,max -f value -g size,shape
Example: mlr stats1 -a count,mode -f size
Example: mlr stats1 -a count,mode -f size -g shape
+ Example: mlr stats1 -a mean,min,max -f quantity -g name -w 7
+ This emits one output record per input record, with sliding-window
+ statistics over the last up-to-7 records for each name.
Example: mlr stats1 -a count,mode --fr '^[a-h].*$' --gr '^k.*$'
This computes count and mode statistics on all field names beginning
with a through h, grouped by all field names starting with k.
@@ -4089,4 +4098,4 @@
MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
https://miller.readthedocs.io
- 2026-07-08 4mMILLER24m(1)
+ 2026-07-14 4mMILLER24m(1)
diff --git a/man/mlr.1 b/man/mlr.1
index 272395a6a..14c0fbfba 100644
--- a/man/mlr.1
+++ b/man/mlr.1
@@ -2,12 +2,12 @@
.\" Title: mlr
.\" Author: [see the "AUTHOR" section]
.\" Generator: ./mkman.rb
-.\" Date: 2026-07-08
+.\" Date: 2026-07-14
.\" Manual: \ \&
.\" Source: \ \&
.\" Language: English
.\"
-.TH "MILLER" "1" "2026-07-08" "\ \&" "\ \&"
+.TH "MILLER" "1" "2026-07-14" "\ \&" "\ \&"
.\" -----------------------------------------------------------------
.\" * Portability definitions
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2738,6 +2738,12 @@ Options:
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`.
+-w {n} Sliding-window mode: compute statistics over a trailing
+ window of up to n records (including the current one), rather
+ than over the whole record stream. Windows are kept per group
+ when -g is used. One output record is emitted per input
+ record, with the windowed statistics appended to it. Not
+ compatible with -s.
-S No-op flag for backward compatibility with Miller 5.
-F No-op flag for backward compatibility with Miller 5.
-h|--help Show this message.
@@ -2764,6 +2770,9 @@ Names of accumulators for -a, one or more of:
Example: mlr stats1 -a min,p10,p50,p90,max -f value -g size,shape
Example: mlr stats1 -a count,mode -f size
Example: mlr stats1 -a count,mode -f size -g shape
+Example: mlr stats1 -a mean,min,max -f quantity -g name -w 7
+ This emits one output record per input record, with sliding-window
+ statistics over the last up-to-7 records for each name.
Example: mlr stats1 -a count,mode --fr '^[a-h].*$' --gr '^k.*$'
This computes count and mode statistics on all field names beginning
with a through h, grouped by all field names starting with k.
diff --git a/pkg/transformers/stats1.go b/pkg/transformers/stats1.go
index 245960bb2..eb51a9018 100644
--- a/pkg/transformers/stats1.go
+++ b/pkg/transformers/stats1.go
@@ -28,6 +28,7 @@ var stats1Options = []OptionSpec{
{Flag: "--grfx", Arg: "{regex}", Type: "regex", Desc: "Shorthand for --gr {regex} --fx {that same regex}."},
{Flag: "-i", Type: "bool", Desc: "Use interpolated percentiles, like R's type=7; default like type=1. Not sensical for string-valued fields."},
{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: "-w", Arg: "{n}", Type: "int", Desc: "Sliding-window mode: compute statistics over a trailing window of up to n records (including the current one), rather than over the whole record stream. Windows are kept per group when -g is used. One output record is emitted per input record, with the windowed statistics appended to it. Not compatible with -s."},
{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."},
}
@@ -62,6 +63,11 @@ the input record stream.
"Example: mlr stats1 -a count,mode -f size")
fmt.Fprintln(o,
"Example: mlr stats1 -a count,mode -f size -g shape")
+ fmt.Fprintln(o,
+ "Example: mlr stats1 -a mean,min,max -f quantity -g name -w 7")
+ fmt.Fprintln(o,
+ ` This emits one output record per input record, with sliding-window
+ statistics over the last up-to-7 records for each name.`)
fmt.Fprintln(o,
"Example: mlr stats1 -a count,mode --fr '^[a-h].*$' --gr '^k.*$'")
fmt.Fprintln(o,
@@ -106,6 +112,7 @@ func transformerStats1ParseCLI(
doInterpolatedPercentiles := false
doIterativeStats := false
+ slidingWindowSize := int64(0)
var err error
for argi < argc /* variable increment: 1 or 2 depending on flag */ {
@@ -185,6 +192,15 @@ func transformerStats1ParseCLI(
case "-s":
doIterativeStats = true
+ case "-w":
+ slidingWindowSize, err = cli.VerbGetIntArg(verb, opt, args, &argi, argc)
+ if err != nil {
+ return nil, err
+ }
+ if slidingWindowSize < 1 {
+ return nil, cli.VerbErrorf(verbNameStats1, "-w argument must be positive; got %d", slidingWindowSize)
+ }
+
case "-S", "-F":
// No-op pass-through for backward compatibility with Miller 5
@@ -200,6 +216,9 @@ func transformerStats1ParseCLI(
if len(valueFieldNameList) == 0 {
return nil, cli.VerbErrorf(verbNameStats1, "-f option is required")
}
+ if doIterativeStats && slidingWindowSize > 0 {
+ return nil, cli.VerbErrorf(verbNameStats1, "-s and -w may not be used together")
+ }
*pargi = argi
if !doConstruct { // All transformers must do this for main command-line parsing
@@ -218,6 +237,7 @@ func transformerStats1ParseCLI(
doInterpolatedPercentiles,
doIterativeStats,
+ slidingWindowSize,
)
if err != nil {
return nil, err
@@ -250,9 +270,19 @@ type TransformerStats1 struct {
doInterpolatedPercentiles bool
doIterativeStats bool
+ // If positive, statistics are computed over a trailing window of up to
+ // this many records (per grouping key), with one output record emitted per
+ // input record.
+ slidingWindowSize int64
+
// State:
accumulatorFactory *utils.Stats1AccumulatorFactory
+ // For sliding-window mode: per grouping key, the last (up to)
+ // slidingWindowSize windowed entries. Each entry is a small record holding
+ // copies of only the relevant value fields from one input record.
+ slidingWindows map[string][]*mlrval.Mlrmap
+
// Accumulators are indexed by
// groupByFieldName -> valueFieldName -> accumulatorName -> accumulator object
// This would be
@@ -319,6 +349,7 @@ func NewTransformerStats1(
doInterpolatedPercentiles bool,
doIterativeStats bool,
+ slidingWindowSize int64,
) (*TransformerStats1, error) {
for _, name := range accumulatorNameList {
if !utils.ValidateStats1AccumulatorName(name) {
@@ -339,7 +370,9 @@ func NewTransformerStats1(
doInterpolatedPercentiles: doInterpolatedPercentiles,
doIterativeStats: doIterativeStats,
+ slidingWindowSize: slidingWindowSize,
accumulatorFactory: utils.NewStats1AccumulatorFactory(),
+ slidingWindows: make(map[string][]*mlrval.Mlrmap),
namedAccumulators: lib.NewOrderedMap[*lib.OrderedMap[*lib.OrderedMap[*utils.Stats1NamedAccumulator]]](),
groupingKeysToGroupByFieldValues: make(map[string]*lib.OrderedMap[*mlrval.Mlrval]),
}
@@ -378,6 +411,17 @@ func (tr *TransformerStats1) Transform(
func (tr *TransformerStats1) handleInputRecord(
inrecAndContext *types.RecordAndContext,
outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext
+) {
+ if tr.slidingWindowSize > 0 {
+ tr.handleInputRecordWindowed(inrecAndContext, outputRecordsAndContexts)
+ } else {
+ tr.handleInputRecordNonWindowed(inrecAndContext, outputRecordsAndContexts)
+ }
+}
+
+func (tr *TransformerStats1) handleInputRecordNonWindowed(
+ inrecAndContext *types.RecordAndContext,
+ outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext
) {
inrec := inrecAndContext.Record
@@ -430,6 +474,103 @@ func (tr *TransformerStats1) handleInputRecord(
}
}
+// handleInputRecordWindowed processes one input record in sliding-window mode
+// (-w {n}). For each grouping key we retain the relevant value fields of the
+// last up-to-n records; on every input record the accumulators for that
+// grouping key are reset and re-fed from the window, then the windowed
+// statistics are appended to the record, which is emitted immediately.
+func (tr *TransformerStats1) handleInputRecordWindowed(
+ inrecAndContext *types.RecordAndContext,
+ outputRecordsAndContexts *[]*types.RecordAndContext, // list of *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".
+ var groupingKey string
+ var groupByFieldValues *lib.OrderedMap[*mlrval.Mlrval] // OrderedMap[string]*mlrval.Mlrval
+ var ok bool
+ if tr.doRegexGroupByFieldNames {
+ groupingKey, groupByFieldValues, ok = tr.getGroupByFieldNamesWithRegexes(inrec)
+ } else {
+ groupingKey, ok = tr.getGroupingKeyWithoutRegexes(inrec)
+ }
+ if !ok {
+ return
+ }
+
+ level2 := tr.namedAccumulators.Get(groupingKey)
+ if level2 == nil {
+ level2 = lib.NewOrderedMap[*lib.OrderedMap[*utils.Stats1NamedAccumulator]]()
+ tr.namedAccumulators.Put(groupingKey, level2)
+ if !tr.doRegexGroupByFieldNames {
+ groupByFieldValues, ok = tr.buildGroupByFieldValuesWithoutRegexes(inrec)
+ if !ok {
+ return
+ }
+ }
+ tr.groupingKeysToGroupByFieldValues[groupingKey] = groupByFieldValues
+ } else if !tr.doRegexGroupByFieldNames {
+ groupByFieldValues = tr.groupingKeysToGroupByFieldValues[groupingKey]
+ }
+
+ // Update this grouping key's window with the current record's value fields.
+ window := tr.slidingWindows[groupingKey]
+ if int64(len(window)) >= tr.slidingWindowSize {
+ window = window[1:]
+ }
+ window = append(window, tr.buildWindowEntry(inrec))
+ tr.slidingWindows[groupingKey] = window
+
+ // Reset the accumulators for this grouping key, then re-ingest the values
+ // retained in the window. This is O(window size) per record but is fully
+ // generic: it works for any accumulator, including order-sensitive ones
+ // like mode/antimode and non-invertible ones like percentiles.
+ for pb := level2.Head; pb != nil; pb = pb.Next {
+ for pc := pb.Value.Head; pc != nil; pc = pc.Next {
+ pc.Value.Reset()
+ }
+ }
+ for _, windowEntry := range window {
+ if tr.doRegexValueFieldNames {
+ tr.ingestWithValueFieldRegexes(windowEntry, groupingKey, level2)
+ } else {
+ tr.ingestWithoutValueFieldRegexes(windowEntry, groupingKey, level2)
+ }
+ }
+
+ tr.emitIntoOutputRecord(
+ inrec,
+ groupByFieldValues,
+ level2,
+ inrec,
+ )
+ *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext)
+}
+
+// buildWindowEntry makes a small record holding copies of only the relevant
+// value fields from the given input record, for retention in a sliding window.
+func (tr *TransformerStats1) buildWindowEntry(
+ inrec *mlrval.Mlrmap,
+) *mlrval.Mlrmap {
+ windowEntry := mlrval.NewMlrmapAsRecord()
+ if tr.doRegexValueFieldNames {
+ for pe := inrec.Head; pe != nil; pe = pe.Next {
+ if tr.matchValueFieldName(pe.Key) {
+ windowEntry.PutCopy(pe.Key, pe.Value)
+ }
+ }
+ } else {
+ for _, valueFieldName := range tr.valueFieldNameList {
+ valueFieldValue := inrec.Get(valueFieldName)
+ if valueFieldValue != nil {
+ windowEntry.PutCopy(valueFieldName, valueFieldValue)
+ }
+ }
+ }
+ return windowEntry
+}
+
// E.g. if grouping by "a" and "b", and the current record has a=circle,
// b=blue, then groupingKey is the string "circle,blue". For grouping without
// regexed group-by field names, the group-by field names/values are the same
@@ -610,7 +751,7 @@ func (tr *TransformerStats1) handleEndOfRecordStream(
inrecAndContext *types.RecordAndContext,
outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext
) {
- if tr.doIterativeStats {
+ if tr.doIterativeStats || tr.slidingWindowSize > 0 {
*outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) // end-of-stream marker
return
}
diff --git a/pkg/transformers/stats1_test.go b/pkg/transformers/stats1_test.go
new file mode 100644
index 000000000..2d5ec5bf9
--- /dev/null
+++ b/pkg/transformers/stats1_test.go
@@ -0,0 +1,150 @@
+package transformers
+
+import (
+ "testing"
+
+ "github.com/johnkerl/miller/v6/pkg/mlrval"
+ "github.com/johnkerl/miller/v6/pkg/types"
+)
+
+// makeStats1TestRecord builds a record from alternating key/value pairs, e.g.
+// makeStats1TestRecord("x", "1", "g", "a").
+func makeStats1TestRecord(kvPairs ...string) *types.RecordAndContext {
+ record := mlrval.NewMlrmapAsRecord()
+ for i := 0; i < len(kvPairs); i += 2 {
+ record.PutReference(kvPairs[i], mlrval.FromInferredType(kvPairs[i+1]))
+ }
+ context := types.NewNilContext()
+ return types.NewRecordAndContext(record, context)
+}
+
+func runStats1TestTransformer(
+ tr RecordTransformer,
+ inputs []*types.RecordAndContext,
+) []*types.RecordAndContext {
+ inputDownstreamDoneChannel := make(chan bool, 1)
+ outputDownstreamDoneChannel := make(chan bool, 1)
+ outputs := make([]*types.RecordAndContext, 0)
+ for _, input := range inputs {
+ tr.Transform(input, &outputs, inputDownstreamDoneChannel, outputDownstreamDoneChannel)
+ }
+ // End-of-stream marker
+ eos := types.NewRecordAndContext(nil, types.NewNilContext())
+ eos.EndOfStream = true
+ tr.Transform(eos, &outputs, inputDownstreamDoneChannel, outputDownstreamDoneChannel)
+ return outputs
+}
+
+// TestStats1SlidingWindow exercises 'mlr stats1 -a sum,mean,min,max -f x -w 3'.
+func TestStats1SlidingWindow(t *testing.T) {
+ tr, err := NewTransformerStats1(
+ []string{"sum", "mean", "min", "max"},
+ []string{"x"},
+ []string{}, // groupByFieldNameList
+ false, false, // doRegexValueFieldNames, doRegexGroupByFieldNames
+ false, false, // invertRegexValueFieldNames, invertRegexGroupByFieldNames
+ false, // doInterpolatedPercentiles
+ false, // doIterativeStats
+ 3, // slidingWindowSize
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ inputs := []*types.RecordAndContext{
+ makeStats1TestRecord("x", "1"),
+ makeStats1TestRecord("x", "2"),
+ makeStats1TestRecord("x", "3"),
+ makeStats1TestRecord("x", "4"),
+ makeStats1TestRecord("x", "5"),
+ }
+ outputs := runStats1TestTransformer(tr, inputs)
+
+ // 5 data records plus end-of-stream marker
+ if len(outputs) != 6 {
+ t.Fatalf("got %d outputs, want 6", len(outputs))
+ }
+
+ expectations := []struct{ sum, mean, min, max string }{
+ {"1", "1", "1", "1"}, // window [1]
+ {"3", "1.5", "1", "2"}, // window [1,2]
+ {"6", "2", "1", "3"}, // window [1,2,3]
+ {"9", "3", "2", "4"}, // window [2,3,4]
+ {"12", "4", "3", "5"}, // window [3,4,5]
+ }
+ for i, expectation := range expectations {
+ record := outputs[i].Record
+ for name, want := range map[string]string{
+ "x_sum": expectation.sum,
+ "x_mean": expectation.mean,
+ "x_min": expectation.min,
+ "x_max": expectation.max,
+ } {
+ value := record.Get(name)
+ if value == nil {
+ t.Fatalf("record %d: field %s missing", i, name)
+ }
+ if value.String() != want {
+ t.Errorf("record %d: %s got %s, want %s", i, name, value.String(), want)
+ }
+ }
+ }
+
+ if !outputs[5].EndOfStream {
+ t.Errorf("last output should be the end-of-stream marker")
+ }
+}
+
+// TestStats1SlidingWindowGrouped exercises 'mlr stats1 -a count,sum -f x -g g -w 2':
+// windows are maintained per group.
+func TestStats1SlidingWindowGrouped(t *testing.T) {
+ tr, err := NewTransformerStats1(
+ []string{"count", "sum"},
+ []string{"x"},
+ []string{"g"}, // groupByFieldNameList
+ false, false, // doRegexValueFieldNames, doRegexGroupByFieldNames
+ false, false, // invertRegexValueFieldNames, invertRegexGroupByFieldNames
+ false, // doInterpolatedPercentiles
+ false, // doIterativeStats
+ 2, // slidingWindowSize
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ inputs := []*types.RecordAndContext{
+ makeStats1TestRecord("g", "a", "x", "1"),
+ makeStats1TestRecord("g", "b", "x", "10"),
+ makeStats1TestRecord("g", "a", "x", "2"),
+ makeStats1TestRecord("g", "b", "x", "20"),
+ makeStats1TestRecord("g", "a", "x", "3"),
+ }
+ outputs := runStats1TestTransformer(tr, inputs)
+
+ // 5 data records plus end-of-stream marker
+ if len(outputs) != 6 {
+ t.Fatalf("got %d outputs, want 6", len(outputs))
+ }
+
+ expectations := []struct{ count, sum string }{
+ {"1", "1"}, // group a, window [1]
+ {"1", "10"}, // group b, window [10]
+ {"2", "3"}, // group a, window [1,2]
+ {"2", "30"}, // group b, window [10,20]
+ {"2", "5"}, // group a, window [2,3]
+ }
+ for i, expectation := range expectations {
+ record := outputs[i].Record
+ count := record.Get("x_count")
+ sum := record.Get("x_sum")
+ if count == nil || sum == nil {
+ t.Fatalf("record %d: missing x_count or x_sum", i)
+ }
+ if count.String() != expectation.count {
+ t.Errorf("record %d: x_count got %s, want %s", i, count.String(), expectation.count)
+ }
+ if sum.String() != expectation.sum {
+ t.Errorf("record %d: x_sum got %s, want %s", i, sum.String(), expectation.sum)
+ }
+ }
+}
diff --git a/test/cases/cli-help/0001/expout b/test/cases/cli-help/0001/expout
index a6a2d3275..208b81831 100644
--- a/test/cases/cli-help/0001/expout
+++ b/test/cases/cli-help/0001/expout
@@ -1266,6 +1266,12 @@ Options:
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`.
+-w {n} Sliding-window mode: compute statistics over a trailing
+ window of up to n records (including the current one), rather
+ than over the whole record stream. Windows are kept per group
+ when -g is used. One output record is emitted per input
+ record, with the windowed statistics appended to it. Not
+ compatible with -s.
-S No-op flag for backward compatibility with Miller 5.
-F No-op flag for backward compatibility with Miller 5.
-h|--help Show this message.
@@ -1292,6 +1298,9 @@ Names of accumulators for -a, one or more of:
Example: mlr stats1 -a min,p10,p50,p90,max -f value -g size,shape
Example: mlr stats1 -a count,mode -f size
Example: mlr stats1 -a count,mode -f size -g shape
+Example: mlr stats1 -a mean,min,max -f quantity -g name -w 7
+ This emits one output record per input record, with sliding-window
+ statistics over the last up-to-7 records for each name.
Example: mlr stats1 -a count,mode --fr '^[a-h].*$' --gr '^k.*$'
This computes count and mode statistics on all field names beginning
with a through h, grouped by all field names starting with k.
diff --git a/test/cases/verb-stats1/0020/cmd b/test/cases/verb-stats1/0020/cmd
new file mode 100644
index 000000000..436f058e7
--- /dev/null
+++ b/test/cases/verb-stats1/0020/cmd
@@ -0,0 +1 @@
+mlr --opprint --from test/input/abixy stats1 -a mean,sum,min,max,count -f i,x -w 3
diff --git a/test/cases/verb-stats1/0020/experr b/test/cases/verb-stats1/0020/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-stats1/0020/expout b/test/cases/verb-stats1/0020/expout
new file mode 100644
index 000000000..1a17d9546
--- /dev/null
+++ b/test/cases/verb-stats1/0020/expout
@@ -0,0 +1,11 @@
+a b i x y i_mean i_sum i_min i_max i_count x_mean x_sum x_min x_max x_count
+pan pan 1 0.34679014 0.72680286 1 1 1 1 1 0.34679014 0.34679014 0.34679014 0.34679014 1
+eks pan 2 0.75867996 0.52215111 1.50000000 3 1 2 2 0.55273505 1.10547011 0.34679014 0.75867996 2
+wye wye 3 0.20460331 0.33831853 2 6 1 3 3 0.43669114 1.31007341 0.20460331 0.75867996 3
+eks wye 4 0.38139939 0.13418874 3 9 2 4 3 0.44822755 1.34468266 0.20460331 0.75867996 3
+wye pan 5 0.57328892 0.86362447 4 12 3 5 3 0.38643054 1.15929162 0.20460331 0.57328892 3
+zee pan 6 0.52712616 0.49322129 5 15 4 6 3 0.49393816 1.48181447 0.38139939 0.57328892 3
+eks zee 7 0.61178406 0.18788492 6 18 5 7 3 0.57073305 1.71219914 0.52712616 0.61178406 3
+zee wye 8 0.59855401 0.97618139 7 21 6 8 3 0.57915474 1.73746423 0.52712616 0.61178406 3
+hat wye 9 0.03144188 0.74955076 8 24 7 9 3 0.41392665 1.24177995 0.03144188 0.61178406 3
+pan wye 10 0.50262601 0.95261836 9 27 8 10 3 0.37754063 1.13262189 0.03144188 0.59855401 3
diff --git a/test/cases/verb-stats1/0021/cmd b/test/cases/verb-stats1/0021/cmd
new file mode 100644
index 000000000..61f261df6
--- /dev/null
+++ b/test/cases/verb-stats1/0021/cmd
@@ -0,0 +1 @@
+mlr --opprint --from test/input/abixy stats1 -a count,mean -f i -g a -w 2
diff --git a/test/cases/verb-stats1/0021/experr b/test/cases/verb-stats1/0021/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-stats1/0021/expout b/test/cases/verb-stats1/0021/expout
new file mode 100644
index 000000000..a553838c7
--- /dev/null
+++ b/test/cases/verb-stats1/0021/expout
@@ -0,0 +1,11 @@
+a b i x y i_count i_mean
+pan pan 1 0.34679014 0.72680286 1 1
+eks pan 2 0.75867996 0.52215111 1 2
+wye wye 3 0.20460331 0.33831853 1 3
+eks wye 4 0.38139939 0.13418874 2 3
+wye pan 5 0.57328892 0.86362447 2 4
+zee pan 6 0.52712616 0.49322129 1 6
+eks zee 7 0.61178406 0.18788492 2 5.50000000
+zee wye 8 0.59855401 0.97618139 2 7
+hat wye 9 0.03144188 0.74955076 1 9
+pan wye 10 0.50262601 0.95261836 2 5.50000000
diff --git a/test/cases/verb-stats1/0022/cmd b/test/cases/verb-stats1/0022/cmd
new file mode 100644
index 000000000..f1160f96f
--- /dev/null
+++ b/test/cases/verb-stats1/0022/cmd
@@ -0,0 +1 @@
+mlr --opprint --from test/input/abixy stats1 -a p25,median,p75,distinct_count,mode -f i -w 4
diff --git a/test/cases/verb-stats1/0022/experr b/test/cases/verb-stats1/0022/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-stats1/0022/expout b/test/cases/verb-stats1/0022/expout
new file mode 100644
index 000000000..98980da26
--- /dev/null
+++ b/test/cases/verb-stats1/0022/expout
@@ -0,0 +1,11 @@
+a b i x y i_p25 i_median i_p75 i_distinct_count i_mode
+pan pan 1 0.34679014 0.72680286 1 1 1 1 1
+eks pan 2 0.75867996 0.52215111 1 2 2 2 1
+wye wye 3 0.20460331 0.33831853 1 2 3 3 1
+eks wye 4 0.38139939 0.13418874 2 3 4 4 1
+wye pan 5 0.57328892 0.86362447 3 4 5 4 2
+zee pan 6 0.52712616 0.49322129 4 5 6 4 3
+eks zee 7 0.61178406 0.18788492 5 6 7 4 4
+zee wye 8 0.59855401 0.97618139 6 7 8 4 5
+hat wye 9 0.03144188 0.74955076 7 8 9 4 6
+pan wye 10 0.50262601 0.95261836 8 9 10 4 7
diff --git a/test/cases/verb-stats1/0023/cmd b/test/cases/verb-stats1/0023/cmd
new file mode 100644
index 000000000..93551ce46
--- /dev/null
+++ b/test/cases/verb-stats1/0023/cmd
@@ -0,0 +1 @@
+mlr --ojson --from test/input/abixy-het stats1 -a sum,count -f x,y -w 3
diff --git a/test/cases/verb-stats1/0023/experr b/test/cases/verb-stats1/0023/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-stats1/0023/expout b/test/cases/verb-stats1/0023/expout
new file mode 100644
index 000000000..3993a3ac2
--- /dev/null
+++ b/test/cases/verb-stats1/0023/expout
@@ -0,0 +1,112 @@
+[
+{
+ "a": "pan",
+ "b": "pan",
+ "i": 1,
+ "x": 0.34679014,
+ "y": 0.72680286,
+ "x_sum": 0.34679014,
+ "x_count": 1,
+ "y_sum": 0.72680286,
+ "y_count": 1
+},
+{
+ "a": "eks",
+ "b": "pan",
+ "i": 2,
+ "x": 0.75867996,
+ "y": 0.52215111,
+ "x_sum": 1.10547011,
+ "x_count": 2,
+ "y_sum": 1.24895397,
+ "y_count": 2
+},
+{
+ "aaa": "wye",
+ "b": "wye",
+ "i": 3,
+ "x": 0.20460331,
+ "y": 0.33831853,
+ "x_sum": 1.31007341,
+ "x_count": 3,
+ "y_sum": 1.58727250,
+ "y_count": 3
+},
+{
+ "a": "eks",
+ "bbb": "wye",
+ "i": 4,
+ "x": 0.38139939,
+ "y": 0.13418874,
+ "x_sum": 1.34468266,
+ "x_count": 3,
+ "y_sum": 0.99465838,
+ "y_count": 3
+},
+{
+ "a": "wye",
+ "b": "pan",
+ "i": 5,
+ "xxx": 0.57328892,
+ "y": 0.86362447,
+ "x_sum": 0.58600270,
+ "x_count": 2,
+ "y_sum": 1.33613174,
+ "y_count": 3
+},
+{
+ "a": "zee",
+ "b": "pan",
+ "i": 6,
+ "x": 0.52712616,
+ "y": 0.49322129,
+ "x_sum": 0.90852555,
+ "x_count": 2,
+ "y_sum": 1.49103450,
+ "y_count": 3
+},
+{
+ "a": "eks",
+ "b": "zee",
+ "iii": 7,
+ "x": 0.61178406,
+ "y": 0.18788492,
+ "x_sum": 1.13891022,
+ "x_count": 2,
+ "y_sum": 1.54473068,
+ "y_count": 3
+},
+{
+ "a": "zee",
+ "b": "wye",
+ "i": 8,
+ "x": 0.59855401,
+ "yyy": 0.97618139,
+ "x_sum": 1.73746423,
+ "x_count": 3,
+ "y_sum": 0.68110621,
+ "y_count": 2
+},
+{
+ "aaa": "hat",
+ "bbb": "wye",
+ "i": 9,
+ "x": 0.03144188,
+ "y": 0.74955076,
+ "x_sum": 1.24177995,
+ "x_count": 3,
+ "y_sum": 0.93743568,
+ "y_count": 2
+},
+{
+ "a": "pan",
+ "b": "wye",
+ "i": 10,
+ "x": 0.50262601,
+ "y": 0.95261836,
+ "x_sum": 1.13262189,
+ "x_count": 3,
+ "y_sum": 1.70216912,
+ "y_count": 2
+}
+]
diff --git a/test/cases/verb-stats1/0024/cmd b/test/cases/verb-stats1/0024/cmd
new file mode 100644
index 000000000..05b8f2087
--- /dev/null
+++ b/test/cases/verb-stats1/0024/cmd
@@ -0,0 +1 @@
+mlr --opprint --from test/input/abixy stats1 -a sum --fr '^[xy]$' -w 3
diff --git a/test/cases/verb-stats1/0024/experr b/test/cases/verb-stats1/0024/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-stats1/0024/expout b/test/cases/verb-stats1/0024/expout
new file mode 100644
index 000000000..8e44b9edb
--- /dev/null
+++ b/test/cases/verb-stats1/0024/expout
@@ -0,0 +1,11 @@
+a b i x y x_sum y_sum
+pan pan 1 0.34679014 0.72680286 0.34679014 0.72680286
+eks pan 2 0.75867996 0.52215111 1.10547011 1.24895397
+wye wye 3 0.20460331 0.33831853 1.31007341 1.58727250
+eks wye 4 0.38139939 0.13418874 1.34468266 0.99465838
+wye pan 5 0.57328892 0.86362447 1.15929162 1.33613174
+zee pan 6 0.52712616 0.49322129 1.48181447 1.49103450
+eks zee 7 0.61178406 0.18788492 1.71219914 1.54473068
+zee wye 8 0.59855401 0.97618139 1.73746423 1.65728759
+hat wye 9 0.03144188 0.74955076 1.24177995 1.91361707
+pan wye 10 0.50262601 0.95261836 1.13262189 2.67835051
diff --git a/test/cases/verb-stats1/0025/cmd b/test/cases/verb-stats1/0025/cmd
new file mode 100644
index 000000000..44e2dae96
--- /dev/null
+++ b/test/cases/verb-stats1/0025/cmd
@@ -0,0 +1 @@
+mlr --from test/input/abixy stats1 -a mean -f x -s -w 3
diff --git a/test/cases/verb-stats1/0025/experr b/test/cases/verb-stats1/0025/experr
new file mode 100644
index 000000000..339ba219d
--- /dev/null
+++ b/test/cases/verb-stats1/0025/experr
@@ -0,0 +1 @@
+mlr stats1: -s and -w may not be used together
diff --git a/test/cases/verb-stats1/0025/expout b/test/cases/verb-stats1/0025/expout
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-stats1/0025/should-fail b/test/cases/verb-stats1/0025/should-fail
new file mode 100644
index 000000000..e69de29bb