Documentation
¶
Overview ¶
Package kuma is a columnar dataframe engine.
This is the top of the library, and it is where the two types most programs use live. A Frame is a table: an ordered list of named columns, all of the same length. A Series is one of those columns read as a Go type, so a Series[float64] over a column of prices hands out a []float64 that is the memory itself rather than a copy of it.
Getting started ¶
prices, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "AAPL").Column(),
kuma.NewSeries("price", 189.5, 411.2, 190.1).Column(),
)
f, err := prices.Select("price")
s, err := f.Series[float64]("price")
for _, v := range s.Values() {
// v is a float64 read straight out of the column.
}
Filtering and expressions ¶
A query is written against column handles rather than strings. A handle is a name and a Go type, so F64 names a column of float64 values and the methods on it build a condition:
dear, err := prices.Filter(kuma.F64("price").Gt(150))
The comparisons are Eq, Ne, Lt, Le, Gt and Ge, the arithmetic is Add, Sub, Mul, Div and Mod, and each of them has an Expr version that takes another expression instead of a literal, so Gt(150) compares against a number and GtExpr(cost) compares against a column. And, Or and Not put conditions together, IsNull and IsNotNull ask about the holes, and Frame.Eval and Frame.WithExpr work an expression out as a column rather than as a filter:
f, err := prices.WithExpr("notional", kuma.F64("price").MulExpr(kuma.F64("qty")))
A literal takes the type of the column it is used with, so comparing a uint32 column against 0 leaves it a uint32 column, and a literal that cannot be used with the column is an error rather than a rounding. A row where either side is missing gives a missing answer rather than a false, so Frame.Filter drops it: a row nobody can say belongs in the result does not go in it, and a condition and its negation do not add up to the frame. Frame.FilterMask is the version that takes a mask that is already worked out.
Bind checks a frame against a Go struct and gives back the same frame with that struct as its schema, after which a handle written for the struct works on it and a handle written for anything else does not compile. Dyn is the handle for a column whose type is only known when the file is read.
Grouping ¶
Frame.GroupBy divides the rows up and hands back a GroupedFrame, which holds the division so that asking it several questions costs one grouping rather than several:
g, err := prices.GroupBy("symbol")
totals, err := g.Agg(
kuma.Sum("qty").As("total"),
kuma.Mean("price").As("avg"),
kuma.Size(),
)
A missing key is a group of its own rather than a row that disappears, and the groups come out in the order they first appear, which is deterministic without being sorted. Sort the result when the order matters.
Joining ¶
Frame.Join puts two frames together on the columns they share, in all seven of the ways SQL can, and Frame.InnerJoin and Frame.LeftJoin are the two that come up often enough to have names:
got, err := trades.InnerJoin(sectors, "symbol")
A missing key matches nothing, including another missing key, which is what SQL says and what keeps a join from gluing together every row whose field was left blank. Output order is the left frame's row order, with the matches of one left row in the right frame's order, and an outer join puts the unmatched right rows at the end. Use On when the two sides call the key different things.
Stacking ¶
Concat puts frames on top of each other and HStack puts them side by side:
week, err := kuma.Concat(monday, tuesday, wednesday)
Neither copies anything. A column is stored as a list of chunks, so stacking two frames puts the two lists together and the values stay where they are, which is why reading a directory of files and concatenating them costs about what reading them cost and nothing more. ConcatUnion is the version for frames that do not hold the same columns, and it is the only one of the three that has to build anything, being the nulls that stand in for a column a frame does not have.
Reading a file ¶
ReadCSV and ReadCSVFile read a comma separated file into a frame, working out what each column holds from the first thousand rows:
f, err := kuma.ReadCSVFile("trades.csv", nil)
What it decides and how to say otherwise is on csv.Options, which is where the delimiter, the header, the types, the values that mean nothing is there and the rest of it live. The frame is Dynamic, because a file is not a Go type and what is in it was decided by whoever wrote it.
This reads the whole file. ScanCSV, which arrives with the lazy frame, reads a chunk at a time and never holds more than one of them, which is what a file larger than memory needs.
Frame.WriteCSV and Frame.WriteCSVFile go the other way, and csv.WriteOptions is where the delimiter, the header, what a missing value looks like and how many digits a float gets live:
err := f.WriteCSVFile("out.csv", nil)
A frame written and read back is the frame that went in, except that a value that was an empty string comes back missing, since a file cannot tell an empty field from an absent one. Write a null value of your own when that difference matters.
Looking at a frame ¶
A frame prints as a table, so fmt.Println is a real way to find out what a query did:
kuma.Frame[kuma.Dynamic] 3 rows x 3 cols sym | qty | px string | int64 | float64 ---------+-------+-------- AAPL | 100 | 182.5 MSFT | null | 411.2 GOOG | 300 | 141.8
The types are in the header and a missing value shows as null, which is not the same cell as an empty string and does not look like one. Ten rows and twelve columns are shown by default and the rest are a line of dots in the middle. Frame.Render takes a PrintOptions when a different amount is wanted, and MaxRows of -1 means the whole thing.
Numbers print at the shortest text that reads back as the same number rather than rounded to a fixed number of digits, so two values that differ in the last place look different. A string that begins or ends in a space is quoted for the same reason, that space being invisible in a table where every cell is padded with spaces anyway. Series and Column print the same way, as a table of one column.
Missing values ¶
A missing value is a null, which is a bit in a bitmap beside the data rather than a value chosen out of the range of the type. That is why there is no NaN standing in for a missing float here and no integer column turning into a float column the moment a value goes missing, both of which pandas does.
Frame.IsNull and Frame.IsNotNull give a frame of boolean columns saying where the holes are, Column.NullMask and Series.ValidMask do the same for one column, and a mask goes straight back into Frame.Filter. Frame.FillNull puts a value where nothing was, Frame.DropNulls takes out the rows that are missing something, and Frame.KeepAtLeast is the same with the rule relaxed to a count. None of them looks at a value one at a time: a mask is a copy of the bitmap the column already carries, so it costs a byte per eight rows.
clean, err := prices.DropNulls("price")
filled, err := prices.FillNull("price", 0.0)
Types and schemas ¶
A frame carries its schema as a type parameter. Dynamic is the schema type meaning not known at compile time, which is what reading an arbitrary file gives you, and it is what the string based methods here are for. Bind is the way from there to a frame with a Go struct as its schema, and after it a handle written for another schema is a compile error rather than a wrong answer. The handles are NewF64Col and the rest, or the light F64 and the rest for a Dynamic frame, and kumagen writes them out of a tagged struct so that a program does not have to. Document 03 has the design.
The Go type a column is read as is not always the type it is stored as. A timestamp, a duration, a date and a time of day are all int64 values with a meaning attached, so all of them read as an int64 without copying anything, and a timestamp also reads as a time.Time. CanRead is what says which pairings are allowed and Value is the set of Go types involved.
Everything is immutable ¶
No operation changes the frame it was called on. Select, Drop, Slice and the rest return a new frame that shares the columns it did not change, so they cost a slice header rather than a copy of the data, and the same frame can be handed to several goroutines at once.
There is no index ¶
The one pandas has is the source of most of the surprising behavior in that library, where two frames silently align themselves by label in the middle of an expression. Joins here take explicit keys and nothing aligns itself behind your back.
Errors ¶
A wrong column name is an error, and the error says what the frame does hold and which of those names is one letter away from the one that was typed. The sentinels are comparable with errors.Is. An index out of range panics, the way indexing a slice does, because that is a bug in the program rather than something the data did.
Testing ¶
A test that compares two frames should print what differs rather than both frames, and that is what kuma/kumatest is for. It reports the cells that are not the same, in the same text a printed frame would show them in, with an allowance for floating point values that were computed rather than typed. It also builds a frame of random values for a benchmark or a property test.
Stability: tier 1, stable. After 1.0 this package follows the Go 1 compatibility promise. Before 1.0 it will break whenever breaking it is the right call, and that is what the leading zero in the version is for.
Index ¶
- Constants
- Variables
- func CanRead[T Value](dt dtype.DataType) bool
- func ColumnName(field string) string
- func DTypeOf[T Value]() dtype.DataType
- type Aggregation
- func Count(col string) Aggregation
- func First(col string) Aggregation
- func Last(col string) Aggregation
- func Max(col string) Aggregation
- func Mean(col string) Aggregation
- func Median(col string) Aggregation
- func Min(col string) Aggregation
- func NUnique(col string) Aggregation
- func Quantile(col string, q float64, how Interpolation) Aggregation
- func Size() Aggregation
- func Std(col string, ddof int) Aggregation
- func Sum(col string) Aggregation
- func Var(col string, ddof int) Aggregation
- type AnyCol
- func (o AnyCol) Add(v any) AnyExpr[S]
- func (o AnyCol) AddExpr(x AnyValue[S]) AnyExpr[S]
- func (c AnyCol[S]) Column(f *Frame[S]) (Column, error)
- func (o AnyCol) Div(v any) AnyExpr[S]
- func (o AnyCol) DivExpr(x AnyValue[S]) AnyExpr[S]
- func (o AnyCol) Eq(v any) BoolExpr[S]
- func (o AnyCol) EqExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyCol) Ge(v any) BoolExpr[S]
- func (o AnyCol) GeExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyCol) Gt(v any) BoolExpr[S]
- func (o AnyCol) GtExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyCol) IsNotNull() BoolExpr[S]
- func (o AnyCol) IsNull() BoolExpr[S]
- func (o AnyCol) Le(v any) BoolExpr[S]
- func (o AnyCol) LeExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyCol) Lt(v any) BoolExpr[S]
- func (o AnyCol) LtExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyCol) Mod(v any) AnyExpr[S]
- func (o AnyCol) ModExpr(x AnyValue[S]) AnyExpr[S]
- func (o AnyCol) Mul(v any) AnyExpr[S]
- func (o AnyCol) MulExpr(x AnyValue[S]) AnyExpr[S]
- func (c AnyCol[S]) Name() string
- func (o AnyCol) Ne(v any) BoolExpr[S]
- func (o AnyCol) NeExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyCol) String() string
- func (o AnyCol) Sub(v any) AnyExpr[S]
- func (o AnyCol) SubExpr(x AnyValue[S]) AnyExpr[S]
- type AnyExpr
- func (o AnyExpr) Add(v any) AnyExpr[S]
- func (o AnyExpr) AddExpr(x AnyValue[S]) AnyExpr[S]
- func (o AnyExpr) Div(v any) AnyExpr[S]
- func (o AnyExpr) DivExpr(x AnyValue[S]) AnyExpr[S]
- func (o AnyExpr) Eq(v any) BoolExpr[S]
- func (o AnyExpr) EqExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyExpr) Ge(v any) BoolExpr[S]
- func (o AnyExpr) GeExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyExpr) Gt(v any) BoolExpr[S]
- func (o AnyExpr) GtExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyExpr) IsNotNull() BoolExpr[S]
- func (o AnyExpr) IsNull() BoolExpr[S]
- func (o AnyExpr) Le(v any) BoolExpr[S]
- func (o AnyExpr) LeExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyExpr) Lt(v any) BoolExpr[S]
- func (o AnyExpr) LtExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyExpr) Mod(v any) AnyExpr[S]
- func (o AnyExpr) ModExpr(x AnyValue[S]) AnyExpr[S]
- func (o AnyExpr) Mul(v any) AnyExpr[S]
- func (o AnyExpr) MulExpr(x AnyValue[S]) AnyExpr[S]
- func (o AnyExpr) Ne(v any) BoolExpr[S]
- func (o AnyExpr) NeExpr(x AnyValue[S]) BoolExpr[S]
- func (o AnyExpr) String() string
- func (o AnyExpr) Sub(v any) AnyExpr[S]
- func (o AnyExpr) SubExpr(x AnyValue[S]) AnyExpr[S]
- type AnyValue
- type BoolCol
- func (o BoolCol) And(x BoolValue[S]) BoolExpr[S]
- func (o BoolCol) Eq(v bool) BoolExpr[S]
- func (o BoolCol) EqExpr(x BoolValue[S]) BoolExpr[S]
- func (o BoolCol) IsNotNull() BoolExpr[S]
- func (o BoolCol) IsNull() BoolExpr[S]
- func (c BoolCol[S]) Name() string
- func (o BoolCol) Ne(v bool) BoolExpr[S]
- func (o BoolCol) NeExpr(x BoolValue[S]) BoolExpr[S]
- func (o BoolCol) Not() BoolExpr[S]
- func (o BoolCol) Or(x BoolValue[S]) BoolExpr[S]
- func (c BoolCol[S]) Series(f *Frame[S]) (Series[bool], error)
- func (o BoolCol) String() string
- type BoolExpr
- func (o BoolExpr) And(x BoolValue[S]) BoolExpr[S]
- func (o BoolExpr) Eq(v bool) BoolExpr[S]
- func (o BoolExpr) EqExpr(x BoolValue[S]) BoolExpr[S]
- func (o BoolExpr) IsNotNull() BoolExpr[S]
- func (o BoolExpr) IsNull() BoolExpr[S]
- func (o BoolExpr) Ne(v bool) BoolExpr[S]
- func (o BoolExpr) NeExpr(x BoolValue[S]) BoolExpr[S]
- func (o BoolExpr) Not() BoolExpr[S]
- func (o BoolExpr) Or(x BoolValue[S]) BoolExpr[S]
- func (o BoolExpr) String() string
- type BoolValue
- type By
- type Column
- func (c Column) As[T Value]() (Series[T], error)
- func (c Column) Cast(to dtype.DataType) (Column, error)
- func (c Column) DType() dtype.DataType
- func (c Column) Data() *array.Chunked
- func (c Column) DropNulls() Column
- func (c Column) Field() dtype.Field
- func (c Column) FillNull[T Value](v T) (Column, error)
- func (c Column) HasNulls() bool
- func (c Column) IsNull(i int) bool
- func (c Column) IsValid(i int) bool
- func (c Column) Len() int
- func (c Column) MustAs[T Value]() Series[T]
- func (c Column) Name() string
- func (c Column) NullCount() int
- func (c Column) NullMask() Column
- func (c Column) Rename(name string) Column
- func (c Column) Render(o *PrintOptions) string
- func (c Column) Slice(i, j int) Column
- func (c Column) Sort(o Order) (Column, error)
- func (c Column) String() string
- func (c Column) Take(idx []int) Column
- func (c Column) Text(i int, o *PrintOptions) string
- func (c Column) TryCast(to dtype.DataType) (Column, error)
- func (c Column) ValidMask() Column
- type ColumnError
- type Dynamic
- type Expr
- type F64Col
- func (o F64Col) Add(v float64) F64Expr[S]
- func (o F64Col) AddExpr(x F64Value[S]) F64Expr[S]
- func (o F64Col) AsI64() I64Expr[S]
- func (o F64Col) Div(v float64) F64Expr[S]
- func (o F64Col) DivExpr(x F64Value[S]) F64Expr[S]
- func (o F64Col) Eq(v float64) BoolExpr[S]
- func (o F64Col) EqExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Col) Ge(v float64) BoolExpr[S]
- func (o F64Col) GeExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Col) Gt(v float64) BoolExpr[S]
- func (o F64Col) GtExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Col) IsNotNull() BoolExpr[S]
- func (o F64Col) IsNull() BoolExpr[S]
- func (o F64Col) Le(v float64) BoolExpr[S]
- func (o F64Col) LeExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Col) Lt(v float64) BoolExpr[S]
- func (o F64Col) LtExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Col) Mod(v float64) F64Expr[S]
- func (o F64Col) ModExpr(x F64Value[S]) F64Expr[S]
- func (o F64Col) Mul(v float64) F64Expr[S]
- func (o F64Col) MulExpr(x F64Value[S]) F64Expr[S]
- func (c F64Col[S]) Name() string
- func (o F64Col) Ne(v float64) BoolExpr[S]
- func (o F64Col) NeExpr(x F64Value[S]) BoolExpr[S]
- func (c F64Col[S]) Series(f *Frame[S]) (Series[float64], error)
- func (o F64Col) String() string
- func (o F64Col) Sub(v float64) F64Expr[S]
- func (o F64Col) SubExpr(x F64Value[S]) F64Expr[S]
- type F64Expr
- func (o F64Expr) Add(v float64) F64Expr[S]
- func (o F64Expr) AddExpr(x F64Value[S]) F64Expr[S]
- func (o F64Expr) AsI64() I64Expr[S]
- func (o F64Expr) Div(v float64) F64Expr[S]
- func (o F64Expr) DivExpr(x F64Value[S]) F64Expr[S]
- func (o F64Expr) Eq(v float64) BoolExpr[S]
- func (o F64Expr) EqExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Expr) Ge(v float64) BoolExpr[S]
- func (o F64Expr) GeExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Expr) Gt(v float64) BoolExpr[S]
- func (o F64Expr) GtExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Expr) IsNotNull() BoolExpr[S]
- func (o F64Expr) IsNull() BoolExpr[S]
- func (o F64Expr) Le(v float64) BoolExpr[S]
- func (o F64Expr) LeExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Expr) Lt(v float64) BoolExpr[S]
- func (o F64Expr) LtExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Expr) Mod(v float64) F64Expr[S]
- func (o F64Expr) ModExpr(x F64Value[S]) F64Expr[S]
- func (o F64Expr) Mul(v float64) F64Expr[S]
- func (o F64Expr) MulExpr(x F64Value[S]) F64Expr[S]
- func (o F64Expr) Ne(v float64) BoolExpr[S]
- func (o F64Expr) NeExpr(x F64Value[S]) BoolExpr[S]
- func (o F64Expr) String() string
- func (o F64Expr) Sub(v float64) F64Expr[S]
- func (o F64Expr) SubExpr(x F64Value[S]) F64Expr[S]
- type F64Value
- type Frame
- func Bind[S any](f *Frame[Dynamic]) (*Frame[S], error)
- func Concat[S any](frames ...*Frame[S]) (*Frame[S], error)
- func ConcatUnion(frames ...*Frame[Dynamic]) (*Frame[Dynamic], error)
- func HStack(frames ...*Frame[Dynamic]) (*Frame[Dynamic], error)
- func NewFrame(cols ...Column) (*Frame[Dynamic], error)
- func ReadCSV(r io.Reader, opts *csv.Options) (*Frame[Dynamic], error)
- func ReadCSVFile(path string, opts *csv.Options) (*Frame[Dynamic], error)
- func ReadDataset(root string, opts *dataset.Options) (*Frame[Dynamic], error)
- func ReadDatasetFiles(d *dataset.Dataset) (*Frame[Dynamic], error)
- func ReadNDJSON(r io.Reader, opts *ndjson.Options) (*Frame[Dynamic], error)
- func ReadNDJSONFile(path string, opts *ndjson.Options) (*Frame[Dynamic], error)
- func ReadParquet(r io.ReaderAt, size int64, opts *parquet.Options) (*Frame[Dynamic], error)
- func ReadParquetFile(path string, opts *parquet.Options) (*Frame[Dynamic], error)
- func (f *Frame[S]) Cast(name string, to dtype.DataType) (*Frame[Dynamic], error)
- func (f *Frame[S]) Column(name string) (Column, error)
- func (f *Frame[S]) ColumnAt(i int) Column
- func (f *Frame[S]) Columns() []Column
- func (f *Frame[S]) CrossJoin(other *Frame[Dynamic]) (*Frame[Dynamic], error)
- func (f *Frame[S]) Drop(names ...string) (*Frame[Dynamic], error)
- func (f *Frame[S]) DropNulls(names ...string) (*Frame[S], error)
- func (f *Frame[S]) Eval(e Expr[S]) (Column, error)
- func (f *Frame[S]) FillNull[T Value](name string, v T) (*Frame[Dynamic], error)
- func (f *Frame[S]) Filter(cond BoolValue[S]) (*Frame[S], error)
- func (f *Frame[S]) FilterMask(mask Series[bool]) (*Frame[S], error)
- func (f *Frame[S]) GroupBy(names ...string) (*GroupedFrame[S], error)
- func (f *Frame[S]) HasNulls() bool
- func (f *Frame[S]) Head(n int) *Frame[S]
- func (f *Frame[S]) Index(name string) int
- func (f *Frame[S]) InnerJoin(other *Frame[Dynamic], names ...string) (*Frame[Dynamic], error)
- func (f *Frame[S]) IsNotNull() *Frame[Dynamic]
- func (f *Frame[S]) IsNull() *Frame[Dynamic]
- func (f *Frame[S]) Join(other *Frame[Dynamic], on []On, how JoinType) (*Frame[Dynamic], error)
- func (f *Frame[S]) KeepAtLeast(present int, names ...string) (*Frame[S], error)
- func (f *Frame[S]) LeftJoin(other *Frame[Dynamic], names ...string) (*Frame[Dynamic], error)
- func (f *Frame[S]) Names() []string
- func (f *Frame[S]) NullCounts() []int
- func (f *Frame[S]) NumCols() int
- func (f *Frame[S]) NumRows() int
- func (f *Frame[S]) Rename(from, to string) (*Frame[Dynamic], error)
- func (f *Frame[S]) Render(o *PrintOptions) string
- func (f *Frame[S]) Schema() dtype.Schema
- func (f *Frame[S]) Select(names ...string) (*Frame[Dynamic], error)
- func (f *Frame[S]) Series[T Value](name string) (Series[T], error)
- func (f *Frame[S]) Shape() (rows, cols int)
- func (f *Frame[S]) Slice(i, j int) *Frame[S]
- func (f *Frame[S]) Sort(by ...By) (*Frame[S], error)
- func (f *Frame[S]) SortBy(names ...string) (*Frame[S], error)
- func (f *Frame[S]) SortDesc(names ...string) (*Frame[S], error)
- func (f *Frame[S]) SortIndex(by ...By) ([]int, error)
- func (f *Frame[S]) String() string
- func (f *Frame[S]) Tail(n int) *Frame[S]
- func (f *Frame[S]) Take(idx []int) *Frame[S]
- func (f *Frame[S]) TryCast(name string, to dtype.DataType) (*Frame[Dynamic], error)
- func (f *Frame[S]) WithColumn(c Column) (*Frame[Dynamic], error)
- func (f *Frame[S]) WithExpr(name string, e Expr[S]) (*Frame[Dynamic], error)
- func (f *Frame[S]) WriteCSV(w io.Writer, opts *csv.WriteOptions) error
- func (f *Frame[S]) WriteCSVFile(path string, opts *csv.WriteOptions) error
- func (f *Frame[S]) WriteNDJSON(w io.Writer, opts *ndjson.WriteOptions) error
- func (f *Frame[S]) WriteNDJSONFile(path string, opts *ndjson.WriteOptions) error
- type GroupedFrame
- func (g *GroupedFrame[S]) Agg(aggs ...Aggregation) (*Frame[Dynamic], error)
- func (g *GroupedFrame[S]) Count() (*Frame[Dynamic], error)
- func (g *GroupedFrame[S]) Frame() *Frame[S]
- func (g *GroupedFrame[S]) Groups() *kernel.Groups
- func (g *GroupedFrame[S]) Keys() []Column
- func (g *GroupedFrame[S]) Names() []string
- func (g *GroupedFrame[S]) NumGroups() int
- type I64Col
- func (o I64Col) Add(v int64) I64Expr[S]
- func (o I64Col) AddExpr(x I64Value[S]) I64Expr[S]
- func (o I64Col) AsF64() F64Expr[S]
- func (o I64Col) Div(v int64) I64Expr[S]
- func (o I64Col) DivExpr(x I64Value[S]) I64Expr[S]
- func (o I64Col) Eq(v int64) BoolExpr[S]
- func (o I64Col) EqExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Col) Ge(v int64) BoolExpr[S]
- func (o I64Col) GeExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Col) Gt(v int64) BoolExpr[S]
- func (o I64Col) GtExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Col) IsNotNull() BoolExpr[S]
- func (o I64Col) IsNull() BoolExpr[S]
- func (o I64Col) Le(v int64) BoolExpr[S]
- func (o I64Col) LeExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Col) Lt(v int64) BoolExpr[S]
- func (o I64Col) LtExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Col) Mod(v int64) I64Expr[S]
- func (o I64Col) ModExpr(x I64Value[S]) I64Expr[S]
- func (o I64Col) Mul(v int64) I64Expr[S]
- func (o I64Col) MulExpr(x I64Value[S]) I64Expr[S]
- func (c I64Col[S]) Name() string
- func (o I64Col) Ne(v int64) BoolExpr[S]
- func (o I64Col) NeExpr(x I64Value[S]) BoolExpr[S]
- func (c I64Col[S]) Series(f *Frame[S]) (Series[int64], error)
- func (o I64Col) String() string
- func (o I64Col) Sub(v int64) I64Expr[S]
- func (o I64Col) SubExpr(x I64Value[S]) I64Expr[S]
- type I64Expr
- func (o I64Expr) Add(v int64) I64Expr[S]
- func (o I64Expr) AddExpr(x I64Value[S]) I64Expr[S]
- func (o I64Expr) AsF64() F64Expr[S]
- func (o I64Expr) Div(v int64) I64Expr[S]
- func (o I64Expr) DivExpr(x I64Value[S]) I64Expr[S]
- func (o I64Expr) Eq(v int64) BoolExpr[S]
- func (o I64Expr) EqExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Expr) Ge(v int64) BoolExpr[S]
- func (o I64Expr) GeExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Expr) Gt(v int64) BoolExpr[S]
- func (o I64Expr) GtExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Expr) IsNotNull() BoolExpr[S]
- func (o I64Expr) IsNull() BoolExpr[S]
- func (o I64Expr) Le(v int64) BoolExpr[S]
- func (o I64Expr) LeExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Expr) Lt(v int64) BoolExpr[S]
- func (o I64Expr) LtExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Expr) Mod(v int64) I64Expr[S]
- func (o I64Expr) ModExpr(x I64Value[S]) I64Expr[S]
- func (o I64Expr) Mul(v int64) I64Expr[S]
- func (o I64Expr) MulExpr(x I64Value[S]) I64Expr[S]
- func (o I64Expr) Ne(v int64) BoolExpr[S]
- func (o I64Expr) NeExpr(x I64Value[S]) BoolExpr[S]
- func (o I64Expr) String() string
- func (o I64Expr) Sub(v int64) I64Expr[S]
- func (o I64Expr) SubExpr(x I64Value[S]) I64Expr[S]
- type I64Value
- type Interpolation
- type JoinType
- type On
- type Order
- type PrintOptions
- type Series
- func (s Series[T]) Cast[U Value](to dtype.DataType) (Series[U], error)
- func (s Series[T]) Column() Column
- func (s Series[T]) DType() dtype.DataType
- func (s Series[T]) Data() *array.Chunked
- func (s Series[T]) DropNulls() Series[T]
- func (s Series[T]) FillNull(v T) Series[T]
- func (s Series[T]) Filter(mask Series[bool]) (Series[T], error)
- func (s Series[T]) HasNulls() bool
- func (s Series[T]) Head(n int) Series[T]
- func (s Series[T]) IsNull(i int) bool
- func (s Series[T]) IsValid(i int) bool
- func (s Series[T]) Len() int
- func (s Series[T]) Name() string
- func (s Series[T]) NullCount() int
- func (s Series[T]) NullMask() Series[bool]
- func (s Series[T]) Rename(name string) Series[T]
- func (s Series[T]) Render(o *PrintOptions) string
- func (s Series[T]) Slice(i, j int) Series[T]
- func (s Series[T]) Sort(o Order) (Series[T], error)
- func (s Series[T]) SortIndex(o Order) ([]int, error)
- func (s Series[T]) String() string
- func (s Series[T]) Tail(n int) Series[T]
- func (s Series[T]) Take(idx []int) Series[T]
- func (s Series[T]) TryCast[U Value](to dtype.DataType) (Series[U], error)
- func (s Series[T]) ValidMask() Series[bool]
- func (s Series[T]) Validity() (*bitmap.Bitmap, bool)
- func (s Series[T]) Value(i int) T
- func (s Series[T]) Values() []T
- type StrCol
- func (o StrCol) Eq(v string) BoolExpr[S]
- func (o StrCol) EqExpr(x StrValue[S]) BoolExpr[S]
- func (o StrCol) Ge(v string) BoolExpr[S]
- func (o StrCol) GeExpr(x StrValue[S]) BoolExpr[S]
- func (o StrCol) Gt(v string) BoolExpr[S]
- func (o StrCol) GtExpr(x StrValue[S]) BoolExpr[S]
- func (o StrCol) IsNotNull() BoolExpr[S]
- func (o StrCol) IsNull() BoolExpr[S]
- func (o StrCol) Le(v string) BoolExpr[S]
- func (o StrCol) LeExpr(x StrValue[S]) BoolExpr[S]
- func (o StrCol) Lt(v string) BoolExpr[S]
- func (o StrCol) LtExpr(x StrValue[S]) BoolExpr[S]
- func (c StrCol[S]) Name() string
- func (o StrCol) Ne(v string) BoolExpr[S]
- func (o StrCol) NeExpr(x StrValue[S]) BoolExpr[S]
- func (c StrCol[S]) Series(f *Frame[S]) (Series[string], error)
- func (o StrCol) String() string
- type StrExpr
- func (o StrExpr) Eq(v string) BoolExpr[S]
- func (o StrExpr) EqExpr(x StrValue[S]) BoolExpr[S]
- func (o StrExpr) Ge(v string) BoolExpr[S]
- func (o StrExpr) GeExpr(x StrValue[S]) BoolExpr[S]
- func (o StrExpr) Gt(v string) BoolExpr[S]
- func (o StrExpr) GtExpr(x StrValue[S]) BoolExpr[S]
- func (o StrExpr) IsNotNull() BoolExpr[S]
- func (o StrExpr) IsNull() BoolExpr[S]
- func (o StrExpr) Le(v string) BoolExpr[S]
- func (o StrExpr) LeExpr(x StrValue[S]) BoolExpr[S]
- func (o StrExpr) Lt(v string) BoolExpr[S]
- func (o StrExpr) LtExpr(x StrValue[S]) BoolExpr[S]
- func (o StrExpr) Ne(v string) BoolExpr[S]
- func (o StrExpr) NeExpr(x StrValue[S]) BoolExpr[S]
- func (o StrExpr) String() string
- type StrValue
- type TimeCol
- func (o TimeCol) After(t time.Time) BoolExpr[S]
- func (o TimeCol) AfterExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeCol) AtOrAfter(t time.Time) BoolExpr[S]
- func (o TimeCol) AtOrAfterExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeCol) AtOrBefore(t time.Time) BoolExpr[S]
- func (o TimeCol) AtOrBeforeExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeCol) Before(t time.Time) BoolExpr[S]
- func (o TimeCol) BeforeExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeCol) Eq(t time.Time) BoolExpr[S]
- func (o TimeCol) EqExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeCol) IsNotNull() BoolExpr[S]
- func (o TimeCol) IsNull() BoolExpr[S]
- func (c TimeCol[S]) Name() string
- func (o TimeCol) Ne(t time.Time) BoolExpr[S]
- func (o TimeCol) NeExpr(x TimeValue[S]) BoolExpr[S]
- func (c TimeCol[S]) Series(f *Frame[S]) (Series[time.Time], error)
- func (o TimeCol) String() string
- type TimeExpr
- func (o TimeExpr) After(t time.Time) BoolExpr[S]
- func (o TimeExpr) AfterExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeExpr) AtOrAfter(t time.Time) BoolExpr[S]
- func (o TimeExpr) AtOrAfterExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeExpr) AtOrBefore(t time.Time) BoolExpr[S]
- func (o TimeExpr) AtOrBeforeExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeExpr) Before(t time.Time) BoolExpr[S]
- func (o TimeExpr) BeforeExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeExpr) Eq(t time.Time) BoolExpr[S]
- func (o TimeExpr) EqExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeExpr) IsNotNull() BoolExpr[S]
- func (o TimeExpr) IsNull() BoolExpr[S]
- func (o TimeExpr) Ne(t time.Time) BoolExpr[S]
- func (o TimeExpr) NeExpr(x TimeValue[S]) BoolExpr[S]
- func (o TimeExpr) String() string
- type TimeValue
- type Value
Examples ¶
- Aggregation.As
- CanRead
- Column.As
- Column.TryCast
- ColumnError
- ColumnName
- Concat
- ConcatUnion
- DTypeOf
- Frame.Cast
- Frame.DropNulls
- Frame.FillNull
- Frame.Filter
- Frame.FilterMask
- Frame.GroupBy
- Frame.GroupBy (Sorted)
- Frame.Head
- Frame.Join
- Frame.Join (Semi)
- Frame.KeepAtLeast
- Frame.LeftJoin
- Frame.NullCounts
- Frame.Render
- Frame.Select
- Frame.Series
- Frame.Sort
- Frame.SortBy
- Frame.Take
- Frame.WithColumn
- Frame.WriteCSV
- GroupedFrame.Agg
- GroupedFrame.Count
- HStack
- NewFrame
- NewSeries
- On
- ReadCSV
- ReadDataset
- Series.Cast
- Series.Cast (AsTime)
- Series.FillNull
- Series.Head
- Series.Sort
- Series.SortIndex
- Series.Take
- Series.ValidMask
- Series.Values
- SeriesFrom
Constants ¶
const ( // Linear splits the two neighbors by the fraction the position landed at. Linear = kernel.Linear // Lower takes the smaller neighbor and Higher the larger one, so a quantile // is always a value that was in the data. Lower = kernel.Lower Higher = kernel.Higher // Nearest takes the closer neighbor, and the even index when they are // equally close. Nearest = kernel.Nearest // Midpoint splits the two neighbors evenly whatever the fraction was. Midpoint = kernel.Midpoint )
The five ways a quantile that falls between two values can be answered. They are the five pandas and numpy offer, and they mean the same things here.
const ( // InnerJoin keeps the pairs that matched and nothing else. InnerJoin = kernel.InnerJoin // LeftJoin keeps every left row, with the right columns missing where // nothing matched, and RightJoin is the same thing the other way round. LeftJoin = kernel.LeftJoin RightJoin = kernel.RightJoin // OuterJoin keeps every row of both frames. It is what SQL calls a full // outer join. OuterJoin = kernel.OuterJoin // SemiJoin keeps the left rows that matched, once each, and takes no // columns from the right frame. It is an EXISTS. SemiJoin = kernel.SemiJoin // AntiJoin keeps the left rows that matched nothing, which is a NOT EXISTS. AntiJoin = kernel.AntiJoin // CrossJoin pairs every left row with every right row and looks at no keys. CrossJoin = kernel.CrossJoin )
The seven joins, which are the seven SQL has and mean the same things here.
const Version = "0.0.0-dev"
Version is the current version of the library.
It is a placeholder until there is something to version.
Variables ¶
var ( // ErrNoColumn is returned when a column name is not in the frame. The error // lists the names that are, and suggests one if the name looks like a typo. // It is the same error a plan gives for a name that is not in the schema, // so one check covers a query however it went wrong. ErrNoColumn = plan.ErrNoColumn // ErrDuplicateColumn is returned when two columns in the same frame have // the same name. It is the same error a plan gives for an operator that // would produce two of them, for the reason ErrNoColumn is. ErrDuplicateColumn = plan.ErrDuplicateColumn // ErrWrongType is returned when a column is read as a Go type it is not // stored as, such as reading a float64 column as an int64, and when a value // is written into a query that no column can hold. ErrWrongType = plan.ErrWrongType // ErrLength is returned when the columns of a frame are not all the same // length. ErrLength = errors.New("columns of different length") // ErrNoValues is returned when a column is built with nothing underneath // it. ErrNoValues = errors.New("no values") )
The errors this package returns. They are comparable with errors.Is, and the error values themselves carry the detail: which column, which operation, and what the frame actually holds.
Nothing here panics across an API boundary. The exceptions are the ones Go itself makes: an index out of range panics, the same way indexing a slice does, because a program that reads past the end of a column has a bug in it rather than a condition to handle.
Functions ¶
func CanRead ¶
CanRead reports whether a column of type dt can be read as a T.
It is wider than DTypeOf in the places where two column types are the same values with a different meaning. A timestamp, a duration, a time of day and a date64 are all int64 columns, so all of them read as an int64, and a kernel that adds a number of days to a date is an integer kernel that should not have to copy the column first. What is refused is a reinterpretation that changes the width or the meaning of the bits, so a float64 column does not read as an int64.
A time.Time reads a timestamp column of any unit and any zone.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
"github.com/tamnd/kuma/dtype"
)
func main() {
// A date is int32 days since the epoch, so it reads as an int32 without
// anything being copied or converted.
fmt.Println(kuma.CanRead[int32](dtype.Date32))
fmt.Println(kuma.CanRead[int64](dtype.Float64))
}
Output: true false
func ColumnName ¶
ColumnName returns the column a struct field binds to when it carries no kuma tag, which is the field name in snake case.
It is the same rule the rest of the Go world uses for JSON: OrderID becomes order_id and TS becomes ts. A run of capitals is one word, so HTTPCode is http_code rather than h_t_t_p_code, and a digit stays with the word it follows.
It is exported because kumagen has to name the same columns Bind does, and because a program that writes its own schema out has a use for the rule.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
// This is the column a field of each of these names binds to when it carries
// no kuma tag, and it is the name kumagen writes into the handle.
fmt.Println(kuma.ColumnName("Price"))
fmt.Println(kuma.ColumnName("OrderID"))
fmt.Println(kuma.ColumnName("HTTPCode"))
}
Output: price order_id http_code
func DTypeOf ¶
DTypeOf returns the column type that values of type T are stored as.
A string is a String column, meaning the Arrow view layout, not the 64 bit offset layout. A time.Time is a Timestamp in nanoseconds, which is the unit a time.Time holds and so the only one that loses nothing.
This is the type a new column gets. It is not the only type a T can be read out of: an int64 reads a timestamp or a duration column as well, since those are int64 values with a meaning attached, and CanRead is what says so.
Example ¶
package main
import (
"fmt"
"time"
"github.com/tamnd/kuma"
)
func main() {
fmt.Println(kuma.DTypeOf[float64]())
fmt.Println(kuma.DTypeOf[time.Time]())
}
Output: float64 timestamp[ns, tz=UTC]
Types ¶
type Aggregation ¶
type Aggregation struct {
// contains filtered or unexported fields
}
Aggregation is one thing to work out about each group: which column to read, what to do to it, and what to call the answer.
The zero value is not usable. Build one with Sum, Mean, Min, Max, Count, Size, First, Last, Var, Std, Median, Quantile or NUnique, and rename it with Aggregation.As.
The type is a value with unexported fields rather than an interface, because the set of aggregations is closed and known here. An interface would let a caller write one of their own, and an aggregation that the fast kernels and the query planner cannot see inside is an aggregation that neither of them can do anything with. When user defined aggregations arrive they will arrive as their own thing, with the cost written on the label.
func Count ¶
func Count(col string) Aggregation
Count returns how many values of the named column are there in each group, not counting the missing ones.
It is the count of values and Size is the count of rows. They differ by exactly the nulls, and the difference is usually the thing you wanted to know.
func First ¶
func First(col string) Aggregation
First returns the first value of the named column in each group that is there, in the order the rows were in.
Missing values are skipped, so this is the first value and not the value of the first row. Sort the frame first if the word first is supposed to mean something other than the order the rows arrived in.
func Last ¶
func Last(col string) Aggregation
Last returns the last value of the named column in each group that is there.
func Max ¶
func Max(col string) Aggregation
Max returns the largest value of the named column in each group, keeping the column's own type.
Missing values are skipped. NaN sorts after every number, so a group with a NaN in it has a NaN for its largest value, which is the honest answer to what the largest of these is when one of them is not a number.
func Mean ¶
func Mean(col string) Aggregation
Mean returns the average of the named column in each group, as a float64.
The average of an empty group is missing, since there is no number that is the average of nothing.
func Median ¶
func Median(col string) Aggregation
Median returns the middle value of the named column in each group, as a float64. It is Quantile at a half, interpolated linearly.
func Min ¶
func Min(col string) Aggregation
Min returns the smallest value of the named column in each group, keeping the column's own type.
Missing values are skipped, so the smallest of a group that is all missing is missing. NaN is a value rather than a missing one and it sorts after every number, so it is only the smallest when it is the only value.
func NUnique ¶
func NUnique(col string) Aggregation
NUnique returns how many distinct values of the named column each group has, not counting the missing ones. It is what pandas calls nunique and SQL calls COUNT DISTINCT.
Distinct means what it means to Frame.GroupBy, since it is the same encoding doing the deciding, so every NaN counts as one value and negative zero counts as the same value as zero.
func Quantile ¶
func Quantile(col string, q float64, how Interpolation) Aggregation
Quantile returns the value q of the way through the sorted values of the named column in each group, as a float64.
A q of a half is the median, 0.95 is the ninety fifth percentile, zero is the smallest value and one is the largest. How says what to do when q lands between two values, which on a small group it nearly always does.
It reports an error when the aggregation runs if q is below zero, above one or not a number, or if how is not one of the five.
func Size ¶
func Size() Aggregation
Size returns how many rows each group has, missing values included.
It reads no column, so it is the one aggregation that cannot fail on a type, and it is called "size" unless Aggregation.As says otherwise.
func Std ¶
func Std(col string, ddof int) Aggregation
Std returns the standard deviation of the named column in each group, which is the square root of Var and takes the same ddof.
func Sum ¶
func Sum(col string) Aggregation
Sum returns the total of the named column in each group.
The result widens: the sums of the signed columns are int64, of the unsigned ones uint64, of the floats float64, and of a boolean column the number of true values. A duration keeps its unit, because the total of a run of durations is a duration. An int64 total that overflows wraps, the way Go addition does.
The total of an empty group is zero rather than missing, since that is what adding nothing up gives.
func Var ¶
func Var(col string, ddof int) Aggregation
Var returns the variance of the named column in each group, as a float64.
The divisor is the number of values less ddof. A ddof of one is the sample variance, which is what pandas and Polars both give when nobody says otherwise, and a ddof of zero is the population variance, which is what numpy gives. A group with fewer values than the divisor wants is missing rather than infinite.
func (Aggregation) As ¶
func (a Aggregation) As(name string) Aggregation
As renames the result column.
Without it an aggregation is called after the column it reads, so two aggregations of one column need at least one of them named. With it the query reads like the table it produces:
f.Agg(kuma.Sum("qty").As("total"), kuma.Mean("price").As("avg"))
Example ¶
Aggregating two things about one column needs at least one of them named, because otherwise both result columns want to be called price.
package main
import (
"fmt"
"strings"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("day", int32(1), 1, 2, 2).Column(),
kuma.NewSeries("price", 9.0, 11.0, 4.0, 8.0).Column(),
)
if err != nil {
panic(err)
}
g, err := f.GroupBy("day")
if err != nil {
panic(err)
}
if _, clash := g.Agg(kuma.Min("price"), kuma.Max("price")); clash != nil {
fmt.Println(clash)
}
got, err := g.Agg(kuma.Min("price").As("low"), kuma.Max("price").As("high"))
if err != nil {
panic(err)
}
fmt.Println(strings.Join(got.Names(), " "))
}
Output: kuma: two columns are called "price": duplicate column day low high
func (Aggregation) Name ¶
func (a Aggregation) Name() string
Name returns what the result column will be called.
func (Aggregation) String ¶
func (a Aggregation) String() string
String returns the aggregation as it would be written, which is what an error message about it should say.
type AnyCol ¶
type AnyCol[S any] struct { // contains filtered or unexported fields }
AnyCol is a handle on a column whose type is only known at runtime.
func Dyn ¶
Dyn returns a handle on a column of any type, which is the way to write a query against a file nobody has seen at compile time.
f, err := f.Filter(kuma.Dyn("price").Gt(100))
The literal takes the column's own type when it can, so the 100 above works whether the file turned out to hold that column as an int64, a uint32 or a float64, and it is an error rather than a rounding when the two cannot be reconciled. That is the whole difference from the typed handles: the check that would be the compiler's happens when the frame is read.
Everything here is available on a typed frame as well, since a schema that covers most of a file may still leave a column nobody wants to name.
func NewAnyCol ¶
NewAnyCol returns a handle on the column called name in a frame with schema S, whatever type that column turns out to hold.
func (AnyCol[S]) Column ¶
Column returns the column itself, reporting an error if the frame has no such column.
func (AnyCol) Div ¶
Div returns the value divided by v, which truncates in an integer column and does not in a float one, the same as the Go operator on those two types.
func (AnyCol) GtExpr ¶
GtExpr returns whether the value is greater than the value of x in the same row.
func (AnyCol) IsNotNull ¶
func (o AnyCol) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (AnyCol) IsNull ¶
func (o AnyCol) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
func (AnyCol) LtExpr ¶
LtExpr returns whether the value is less than the value of x in the same row.
func (AnyCol) ModExpr ¶
ModExpr returns the remainder of the value divided by the value of x in the same row.
func (AnyCol) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
type AnyExpr ¶
type AnyExpr[S any] struct { // contains filtered or unexported fields }
AnyExpr is an expression over columns whose types are only known at runtime.
func Lit ¶
Lit is a value written in a query, for the times one is needed on the left of an operator rather than on the right.
f, err := f.Filter(kuma.Lit(100).Lt(kuma.Dyn("price")))
A literal on its own is not much use, since an expression that reads no column has one value where the frame has rows, which Frame.Eval reports rather than stretching it to fit.
func (AnyExpr) Div ¶
Div returns the value divided by v, which truncates in an integer column and does not in a float one, the same as the Go operator on those two types.
func (AnyExpr) GeExpr ¶
GeExpr returns whether the value is at least the value of x in the same row.
func (AnyExpr) GtExpr ¶
GtExpr returns whether the value is greater than the value of x in the same row.
func (AnyExpr) IsNotNull ¶
func (o AnyExpr) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (AnyExpr) IsNull ¶
func (o AnyExpr) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
func (AnyExpr) LtExpr ¶
LtExpr returns whether the value is less than the value of x in the same row.
func (AnyExpr) ModExpr ¶
ModExpr returns the remainder of the value divided by the value of x in the same row.
func (AnyExpr) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
type AnyValue ¶
AnyValue is a piece of an expression whose type is not known until the frame is read, which is an AnyCol or an AnyExpr.
type BoolCol ¶
type BoolCol[S any] struct { // contains filtered or unexported fields }
BoolCol is a handle on a boolean column of a frame with schema S.
func Bool ¶
Bool returns a handle on a boolean column of a frame with no schema behind it, which is the light version of NewBoolCol.
func NewBoolCol ¶
NewBoolCol returns a handle on the boolean column called name in a frame with schema S.
func (BoolCol) IsNotNull ¶
func (o BoolCol) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (BoolCol) IsNull ¶
func (o BoolCol) IsNull() BoolExpr[S]
IsNull returns whether the value is missing, which is the one question about a condition that always has a plain true or false answer.
func (BoolCol) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
func (BoolCol) Not ¶
func (o BoolCol) Not() BoolExpr[S]
Not returns the negation. A missing value stays missing, since the negation of a thing nobody knows is another thing nobody knows.
type BoolExpr ¶
type BoolExpr[S any] struct { // contains filtered or unexported fields }
BoolExpr is a condition: an expression whose value in each row is true, false or missing.
It is what a comparison gives, it is what Frame.Filter takes, and it is the type that carries the schema through a predicate, so a condition written against one table cannot be used to filter another.
func (BoolExpr) IsNotNull ¶
func (o BoolExpr) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (BoolExpr) IsNull ¶
func (o BoolExpr) IsNull() BoolExpr[S]
IsNull returns whether the value is missing, which is the one question about a condition that always has a plain true or false answer.
func (BoolExpr) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
func (BoolExpr) Not ¶
func (o BoolExpr) Not() BoolExpr[S]
Not returns the negation. A missing value stays missing, since the negation of a thing nobody knows is another thing nobody knows.
type BoolValue ¶
BoolValue is a boolean piece of an expression, which is a BoolCol or a BoolExpr. It is what Frame.Filter takes, so a frame can be filtered by a condition that was worked out or by a boolean column that was already there.
type By ¶
By names a column to sort by, and how.
The Asc and Desc functions cover the two common cases. The struct is there for the third one, which is null placement.
type Column ¶
type Column struct {
// contains filtered or unexported fields
}
Column is one named column of a frame, with no Go type attached.
A frame holds columns of different types, so what it holds cannot be a Series[T] for one particular T. A Column is the same thing with the Go type dropped: the name and the values, and the column type to say what the values are. Putting the type back on is As, which checks it once.
A Column is immutable and cheap to copy.
The zero Column is not usable. Use NewColumn, or Series.Column.
func (Column) As ¶
As returns the column as a Series[T], which is how the values are read as a Go type.
It reports an error unless the column can be read as a T, which is what CanRead answers. Nothing is copied.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
c := kuma.NewSeries("qty", int64(100), 50).Column()
if _, err := c.As[float64](); err != nil {
fmt.Println(err)
}
s, err := c.As[int64]()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(s.Values())
}
Output: kuma: column "qty" is a int64 column, which does not read as a float64: wrong type [100 50]
func (Column) Cast ¶
Cast returns the column with its values in the type to.
A value that will not fit is an error naming the row it was in, which is the right default because the alternative changes data quietly. TryCast is the same cast with that decision reversed. kernel.Cast documents what converts into what.
func (Column) DropNulls ¶
DropNulls returns the column with the missing values taken out. It is shorter than the column it came from by however many there were.
func (Column) Field ¶
Field returns the column as a schema field.
Nullable is whether the column has any nulls in it, rather than whether it is allowed to. A frame holds data rather than a declaration, so its schema describes what is there: a column that has been filtered down to the rows with a value in them is not nullable any more, and saying so is what lets a writer pick the narrower encoding.
func (Column) FillNull ¶
FillNull returns the column with every missing value replaced by v.
The type argument is what v is written as and the column keeps the type it already had, so a timestamp column takes a time.Time and stays a timestamp column of the unit it was. A T the column cannot be read as is an error, the same one Column.As gives.
A column with nothing missing is handed straight back rather than copied.
func (Column) MustAs ¶
MustAs is As where a wrong type is a bug rather than a condition, which is the case in a test, in an example, and anywhere the column was just built a few lines above. It panics if the column does not read as a T.
func (Column) NullMask ¶
NullMask returns a boolean column that is true where this one has no value.
It keeps the name of the column it came from, so a mask over a frame of them reads like the frame it describes. The result has no nulls of its own, because whether a value is missing is always known even when the value is not.
The per row question is Column.IsNull. This is the whole column at once, which is what a filter wants.
func (Column) Render ¶
func (c Column) Render(o *PrintOptions) string
Render returns the column as a table of one column, with the options applied.
func (Column) Slice ¶
Slice returns the values from i up to but not including j, as a column. It panics unless 0 <= i <= j <= Len.
func (Column) String ¶
String returns the column as a table of one column. It follows the same rules as Frame.String.
func (Column) Take ¶
Take returns the values at the given positions, in the order given, as a column.
A position below zero gives a null, which is what an outer join does with a row that matched nothing. A position at or past the length panics.
Unlike Slice this copies, because the values it wants are scattered through the column and there is no way to point at a scattering.
func (Column) Text ¶
func (c Column) Text(i int, o *PrintOptions) string
Text returns value i as the printer writes it, which is the text [Render] would put in that cell, cut short at PrintOptions.MaxWidth and quoted if it holds anything that would move the cursor rather than draw. A nil o means the defaults, and a missing value is PrintOptions.Null. It panics if i is out of range.
This is here for the sake of anything that lays a value out itself rather than through Render. A diff of two frames wants the same text a printed frame shows, and without this it would have to hold its own opinion of how every type in the library reads.
func (Column) TryCast ¶
TryCast is Cast with a value that will not fit becoming a null.
This is the one to reach for when the data is known to be dirty and the plan is to count the nulls afterwards, which is a real thing to want when the alternative is a file of a million rows failing on the one that says "N/A".
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
"github.com/tamnd/kuma/dtype"
)
func main() {
c := kuma.NewSeries("qty", "100", "n/a", "400").Column()
// The row that will not parse becomes a null rather than an error, which is
// what makes a file of a million rows survive the one that says n/a.
got, err := c.TryCast(dtype.Int64)
if err != nil {
panic(err)
}
s, err := got.As[int64]()
if err != nil {
panic(err)
}
fmt.Println(s.Values(), "with", got.NullCount(), "null")
}
Output: [100 0 400] with 1 null
func (Column) ValidMask ¶
ValidMask returns a boolean column that is true where this one has a value. It is Column.NullMask the other way round, and it is the mask a filter usually wants.
type ColumnError ¶
type ColumnError = plan.ColumnError
ColumnError says that a column was asked for and is not there.
It prints on several lines on purpose. A missing column name is the most common thing that goes wrong in day to day work, and the fastest way to fix it is to see what the frame does hold and to be told which of those names is one letter away from the one that was typed.
kuma: column "sym" not found in Select available: symbol, price, qty, side did you mean: symbol?
It is plan.ColumnError under this name. The plan has to report a name that is not in the schema before anything is read, and there is no reason for a caller to meet two errors that say the same thing.
Example ¶
ExampleColumnError is the error a wrong column name gives. It lists what the frame holds and points at the name that is one letter away, because that is what turns a five minute detour into a five second one.
package main
import (
"errors"
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL").Column(),
kuma.NewSeries("price", 189.5).Column(),
kuma.NewSeries("qty", int64(100)).Column(),
)
if err != nil {
fmt.Println(err)
return
}
_, err = f.Select("symbol", "prices")
fmt.Println(err)
fmt.Println(errors.Is(err, kuma.ErrNoColumn))
}
Output: kuma: column "prices" not found in Select available: symbol, price, qty did you mean: price? true
type Dynamic ¶
type Dynamic struct{}
Dynamic is the schema type for a frame whose columns are not known at compile time, which mostly means one read from a file nobody has seen yet.
Everything on a Frame[Dynamic] takes column names as strings and reports an error when a name is wrong. The name is a little clumsy on purpose, so that reaching for the dynamic path is a visible decision rather than the thing that happens by default.
type Expr ¶
Expr is anything that turns into a column when a frame is put behind it, which is a column handle or an expression built out of one.
The type parameter is the schema, so an expression written against Trade cannot be handed to a frame of orders. That check is the compiler's, and it is the reason the typed handles exist. What the handle carries underneath is a plan.Expr, which is the same expression with the schema forgotten.
The interface has an unexported method, so the expression types in this package are the whole of it. That is on purpose. An expression is a small tree that the frame walks, not an interface a caller implements, and keeping it closed is what lets the walk be a switch rather than a virtual call per row.
String returns the expression as it would be written, which is what an error about it names and what a column built from it is called.
type F64Col ¶
type F64Col[S any] struct { // contains filtered or unexported fields }
F64Col is a handle on a float64 column of a frame with schema S.
var TradeCols = struct {
Price kuma.F64Col[Trade]
}{
Price: kuma.NewF64Col[Trade]("price"),
}
The handle is one word and never escapes, so building the variable above once at package level and using it everywhere costs nothing per query.
The schema type is what stops a handle written for one table being used against another. A F64Col[Trade] cannot be handed to a Frame[Order], and the compiler says so rather than the data being read and the answer being wrong. F64 is the same handle without that check, for a frame whose columns are only known at runtime.
func F64 ¶
F64 returns a handle on a float64 column of a frame with no schema behind it, which is the light version of NewF64Col.
price := kuma.F64("price")
high, err := f.Filter(price.Gt(100))
The name is still written once rather than scattered through the code, and a string still cannot be compared to a number. What is given up is the check that the column belongs to the frame it is used against, which moves from compile time to the moment the frame is read.
func NewF64Col ¶
NewF64Col returns a handle on the float64 column called name in a frame with schema S. It is what kumagen writes and what a hand written schema variable calls.
func (F64Col) AsI64 ¶
func (o F64Col) AsI64() I64Expr[S]
AsI64 returns the value as an int64.
The fraction is thrown away, the way a Go conversion does it, so 3.9 becomes 3. A value too large for an int64, and NaN or an infinity, fit nowhere and are an error naming the row. kernel.Cast has the rest of the rule.
func (F64Col) Div ¶
Div returns the value divided by v. Dividing by zero gives an infinity or a NaN, which is what a float64 division does.
func (F64Col) Eq ¶
Eq returns whether the value equals v. A missing value equals nothing, not even another missing value, so it gives a missing answer.
func (F64Col) GtExpr ¶
GtExpr returns whether the value is greater than the value of x in the same row.
func (F64Col) IsNotNull ¶
func (o F64Col) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (F64Col) IsNull ¶
func (o F64Col) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
func (F64Col) LtExpr ¶
LtExpr returns whether the value is less than the value of x in the same row.
func (F64Col) ModExpr ¶
ModExpr returns the remainder of the value divided by the value of x in the same row.
func (F64Col) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
func (F64Col[S]) Series ¶
Series returns the column as a Series[float64], reporting an error if the frame has no such column or holds something else there.
type F64Expr ¶
type F64Expr[S any] struct { // contains filtered or unexported fields }
F64Expr is a float64 valued expression, which is what doing arithmetic to a float64 column gives. It has the same methods a column handle has, so a chain of them reads the same the whole way along.
func (F64Expr) AsI64 ¶
func (o F64Expr) AsI64() I64Expr[S]
AsI64 returns the value as an int64.
The fraction is thrown away, the way a Go conversion does it, so 3.9 becomes 3. A value too large for an int64, and NaN or an infinity, fit nowhere and are an error naming the row. kernel.Cast has the rest of the rule.
func (F64Expr) Div ¶
Div returns the value divided by v. Dividing by zero gives an infinity or a NaN, which is what a float64 division does.
func (F64Expr) Eq ¶
Eq returns whether the value equals v. A missing value equals nothing, not even another missing value, so it gives a missing answer.
func (F64Expr) GeExpr ¶
GeExpr returns whether the value is at least the value of x in the same row.
func (F64Expr) GtExpr ¶
GtExpr returns whether the value is greater than the value of x in the same row.
func (F64Expr) IsNotNull ¶
func (o F64Expr) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (F64Expr) IsNull ¶
func (o F64Expr) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
func (F64Expr) LtExpr ¶
LtExpr returns whether the value is less than the value of x in the same row.
func (F64Expr) ModExpr ¶
ModExpr returns the remainder of the value divided by the value of x in the same row.
func (F64Expr) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
type F64Value ¶
F64Value is a float64 valued piece of an expression, which is an F64Col or an F64Expr. It is what the methods taking another column rather than a literal accept, so that t.Price.GtExpr(t.Limit) and t.Price.GtExpr(t.Limit.Add(1)) are both written the same way.
type Frame ¶
type Frame[S any] struct { // contains filtered or unexported fields }
Frame is a table: an ordered list of named columns, all of the same length.
The type parameter is the schema. It is a marker rather than something the frame stores, and it is what lets a generated column handle refuse to be used against the wrong table. A frame read from a file with no struct behind it is a Frame[Dynamic].
There is no index. The one pandas has is the source of most of the surprising behavior in that library, where two frames silently align themselves by label in the middle of an arithmetic expression. Joins here take explicit keys and nothing aligns itself behind your back.
A Frame is immutable. Every operation returns a new frame sharing the columns it did not change, which is what makes Select and Drop cost a slice header rather than a copy of the data. That is also what lets the same frame be handed to several goroutines.
The zero Frame is not usable. Use NewFrame.
func Bind ¶
Bind checks a frame against the struct S and returns the same frame with S as its schema.
f, err := kuma.ReadCSV(r) typed, err := kuma.Bind[Trade](f)
This is the bridge from the dynamic world to the typed one, and it is where the names in the struct are checked against the names in the data. After it returns, a handle written for Trade can be used on the frame and the compiler takes over. The shape of most programs is a short dynamic prologue that reads whatever arrived, one Bind, and then a long typed body.
Every field of the struct has to have a column, of a type the field can be read out of. A column the struct does not mention is left alone and stays in the frame, since a file usually holds more than the part of it a program cares about.
The column for a field is the kuma tag when there is one, and the field name in snake case when there is not, which is the convention most Go code that reads JSON already follows. A field tagged "-" is skipped, and so is an unexported one.
Nothing is copied. The frame that comes back shares the columns of the one that went in.
func Concat ¶
Concat stacks frames on top of each other, so the result has the rows of the first, then the rows of the second, and so on.
Every frame has to hold the same columns, of the same types. The order they are in does not have to match, and the first frame's order is the one the result comes out in, because a frame read from one file and a frame read from another are the same table whether or not the writer put the columns in the same order. A column that is in one frame and not another is an error rather than a column of nulls, since that is much more often a mistake than an intention. ConcatUnion is the version that fills.
Nothing is copied. A column is stored as a list of chunks, so stacking two frames is appending one list to the other, and the values stay where they are.
The frames all have the same schema type and the result keeps it, so concatenating typed frames gives back a typed frame.
Example ¶
Concat stacks frames on top of each other. Nothing is copied: a column is a list of chunks, so stacking two frames puts the two lists together and the values stay where they are.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
monday, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT").Column(),
kuma.NewSeries("qty", int64(100), 50).Column(),
)
if err != nil {
panic(err)
}
tuesday, err := kuma.NewFrame(
kuma.NewSeries("symbol", "NVDA").Column(),
kuma.NewSeries("qty", int64(400)).Column(),
)
if err != nil {
panic(err)
}
week, err := kuma.Concat(monday, tuesday)
if err != nil {
panic(err)
}
qty, err := week.Series[int64]("qty")
if err != nil {
panic(err)
}
fmt.Println(week.NumRows(), qty.Values())
}
Output: 3 [100 50 400]
func ConcatUnion ¶
ConcatUnion is Concat over frames that do not hold the same columns.
The result has every column that any of the frames has, in the order they first appear, and a frame that does not have one contributes nulls for it. It is what pandas concat does by default and what Polars calls a diagonal concat.
The result is a dynamic frame whatever went in, since its schema is not the schema of any of the frames.
Example ¶
ConcatUnion is for frames that do not hold the same columns. The result has every column any of them has, and a frame that lacks one contributes nulls.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
old, err := kuma.NewFrame(kuma.NewSeries("qty", int64(100), 50).Column())
if err != nil {
panic(err)
}
later, err := kuma.NewFrame(
kuma.NewSeries("qty", int64(25)).Column(),
kuma.NewSeries("fee", 0.5).Column(),
)
if err != nil {
panic(err)
}
got, err := kuma.ConcatUnion(old, later)
if err != nil {
panic(err)
}
fmt.Println(got.Names())
for i := range got.NumRows() {
if got.ColumnAt(1).IsNull(i) {
fmt.Println("no fee")
continue
}
fmt.Println(got.ColumnAt(1).Data().Value[float64](i))
}
}
Output: [qty fee] no fee no fee 0.5
func HStack ¶
HStack puts frames side by side, so the result has the columns of the first, then the columns of the second, and so on.
Every frame has to have the same number of rows, since row 3 of the result is row 3 of each of them, and no two of them may have a column of the same name. Rename one first, or use a join if the rows should be matched up by a key rather than by where they happen to be.
Nothing is copied. The result holds the same columns the frames do.
Example ¶
HStack puts frames side by side, matching them up by position. Use a join when the rows should be matched by a key instead.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
symbols, err := kuma.NewFrame(kuma.NewSeries("symbol", "AAPL", "MSFT").Column())
if err != nil {
panic(err)
}
prices, err := kuma.NewFrame(kuma.NewSeries("price", 189.5, 411.2).Column())
if err != nil {
panic(err)
}
got, err := kuma.HStack(symbols, prices)
if err != nil {
panic(err)
}
fmt.Println(got.Names(), got.NumRows())
}
Output: [symbol price] 2
func NewFrame ¶
NewFrame returns a frame of the given columns, in the order given.
Every column has to be the same length, since a table with a column of three values and a column of four is not a table. Names have to be there and have to be unique, which is what rejects the two columns called "id" that a CSV happily contains. Rename one of them first.
A frame with no columns is fine and has no rows.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "NVDA").Column(),
kuma.NewSeries("price", 189.5, 411.2, 121.0).Column(),
)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f)
}
Output: kuma.Frame[kuma.Dynamic] 3 rows x 2 cols symbol | price string | float64 ---------+-------- AAPL | 189.5 MSFT | 411.2 NVDA | 121
func ReadCSV ¶
ReadCSV reads a whole CSV file into a frame.
A nil options is the useful default: comma separated, a header row, and the type of each column worked out from the first thousand rows. What it decides and how to say otherwise is on csv.Options.
f, err := kuma.ReadCSV(r, nil)
This reads everything. ScanCSV, which arrives with the lazy frame, reads a chunk at a time and never holds more than one of them, which is what a file larger than memory needs. Either way a column comes back in chunks, so nothing here asks for one allocation the size of the file.
The frame is Dynamic because a file is not a Go type. What is in it was decided by whoever wrote the file, so the columns are asked for by name and read with Frame.Series. A typed frame comes from kumagen, which writes the struct out of a sample of the file at build time.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/tamnd/kuma"
)
func main() {
in := `sym,qty,px
AAPL,100,182.5
MSFT,,411.2
GOOG,300,141.8
`
f, err := kuma.ReadCSV(strings.NewReader(in), nil)
if err != nil {
panic(err)
}
fmt.Println(f)
// The columns arrive as themselves, so this is a sum and not a parse.
qty, err := f.Series[int64]("qty")
if err != nil {
panic(err)
}
fmt.Println(qty.DropNulls().Values())
}
Output: kuma.Frame[kuma.Dynamic] 3 rows x 3 cols sym | qty | px string | int64 | float64 ---------+-------+-------- AAPL | 100 | 182.5 MSFT | null | 411.2 GOOG | 300 | 141.8 [100 300]
func ReadCSVFile ¶
ReadCSVFile reads the file at path. It is ReadCSV over an open file.
func ReadDataset ¶ added in v0.0.23
ReadDataset reads a tree of partitioned files under root into one frame.
A dataset is a directory whose subdirectories are named key=value, which is the layout Hive wrote and every engine since has read. The directory names are data, so a tree of orders under year=2024/month=03 comes back with a year column and a month column that are in no file.
f, err := kuma.ReadDataset("orders", nil)
The format is chosen by the extension: .parquet, .csv, .tsv, .ndjson and .jsonl. A tree holding anything else is an error rather than a guess, and a tree holding two formats at once is one too, since the first file decides.
This reads every file. Reading part of a tree is the reason the layout exists and it is two steps: dataset.Discover to find out what is there, which does not open anything, dataset.Dataset.Select to narrow it, and ReadDatasetFiles to read what is left.
Example ¶
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/tamnd/kuma"
)
func main() {
// A dataset is a tree of files whose directories are named key=value, which
// is the layout Hive wrote and every engine since has read. The directory
// names are data, so the year and the month below are columns of the frame
// and are in none of the files.
root, err := os.MkdirTemp("", "orders")
if err != nil {
fmt.Println(err)
return
}
defer os.RemoveAll(root)
for dir, rows := range map[string]string{
"year=2024/month=01": `{"sym":"AAPL","qty":100}` + "\n",
"year=2024/month=02": `{"sym":"MSFT","qty":50}` + "\n",
"year=2025/month=01": `{"sym":"GOOG","qty":25}` + "\n",
} {
if err = os.MkdirAll(filepath.Join(root, dir), 0o750); err != nil {
fmt.Println(err)
return
}
p := filepath.Join(root, dir, "part-0.ndjson")
if err = os.WriteFile(p, []byte(rows), 0o600); err != nil {
fmt.Println(err)
return
}
}
f, err := kuma.ReadDataset(root, nil)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Names())
if err = f.WriteCSV(os.Stdout, nil); err != nil {
fmt.Println(err)
}
}
Output: [sym qty year month] sym,qty,year,month AAPL,100,2024,01 MSFT,50,2024,02 GOOG,25,2025,01
func ReadDatasetFiles ¶ added in v0.0.23
ReadDatasetFiles reads the files of a dataset that has already been found, which is how to read part of a tree.
d, err := dataset.Discover("orders", nil)
march := d.Select(func(f dataset.File) bool {
return d.Value(f, "month").Text == "03"
})
f, err := kuma.ReadDatasetFiles(march)
Nothing outside the file list is opened, so a year narrowed to a month reads one twelfth of the tree. The format is chosen by the extension, the same as in ReadDataset.
Reading a format this does not know, or one it knows with options of your own, is dataset.Read with a dataset.ReadOptions.Open that calls whatever reader you like.
func ReadNDJSON ¶ added in v0.0.23
ReadNDJSON reads a whole newline delimited JSON file into a frame.
One JSON object to a line and one line to a row, which is the shape a log file, an export out of a document store and most of what an API streams come in.
f, err := kuma.ReadNDJSON(r, nil)
A nil options is the useful default: the columns are the members the first thousand lines had, and the type of each one is worked out from the values on those lines. JSON says what a value is, so that is reading rather than guessing, and what it decides and how to say otherwise is on ndjson.Options.
This reads everything. ScanNDJSON, which arrives with the lazy frame, reads a chunk at a time and never holds more than one of them, which is what a file larger than memory needs. Either way a column comes back in chunks, so nothing here asks for one allocation the size of the file.
The frame is Dynamic for the same reason a CSV frame is. A file is not a Go type, so the columns are asked for by name and read with Frame.Series. A typed frame comes from kumagen.
func ReadNDJSONFile ¶ added in v0.0.23
ReadNDJSONFile reads the file at path. It is ReadNDJSON over an open file.
func ReadParquet ¶ added in v0.0.15
ReadParquet reads a whole parquet file into a frame.
The size is the size of the file. A parquet file keeps its schema in a footer at the end, so the reader has to know where the end is and cannot be handed a plain io.Reader the way ReadCSV can.
f, err := kuma.ReadParquet(r, size, nil)
A nil options reads every column. Naming the columns is what makes this format worth using on a wide file: the values of a column sit together, so a frame of three columns out of two hundred reads three runs of pages and never touches the rest.
f, err := kuma.ReadParquet(r, size, &parquet.Options{Columns: []string{"id", "price"}})
parquet.Options.Filter is the same idea for rows and on a file written in any sort of order it saves more. A writer says in the footer what each row group holds, so a frame of one day of a year of orders reads one row group and leaves the other three hundred and sixty four alone.
f, err := kuma.ReadParquet(r, size, &parquet.Options{
Filter: []parquet.Predicate{parquet.Where("day", kernel.OpEq, int64(19000))},
})
A column comes back as the type the file's schema names, whatever the file did to store it. Most writers put a dictionary in front of nearly every column, and parquet.Options.Dictionary is how to keep that encoding for the columns it pays off on.
The frame is Dynamic for the same reason a CSV frame is. A file is not a Go type, so the columns are asked for by name and read with Frame.Series. A typed frame comes from kumagen.
func ReadParquetFile ¶ added in v0.0.15
ReadParquetFile reads the file at path. It is ReadParquet over an open file, which is where the size comes from.
func (*Frame[S]) Cast ¶
Cast returns a frame with the named column in the type to. The column keeps its position and everything else is left alone.
The result is a Dynamic frame because the schema changed. A typed frame is a promise about what the columns are, and a cast is exactly the operation that makes that promise out of date, so the type has to be established again.
A value that will not fit is an error naming the row it was in. TryCast is the same cast with that decision reversed, and kernel.Cast documents what converts into what.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
"github.com/tamnd/kuma/dtype"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT").Column(),
kuma.NewSeries("qty", int64(100), 50).Column(),
)
if err != nil {
panic(err)
}
got, err := f.Cast("qty", dtype.Float64)
if err != nil {
panic(err)
}
for _, c := range got.Columns() {
fmt.Println(c.Name(), c.DType())
}
}
Output: symbol string qty float64
func (*Frame[S]) ColumnAt ¶
ColumnAt returns column i. It panics if i is out of range, the way indexing a slice does, since a position out of range is a bug in the program rather than something the data did.
func (*Frame[S]) Columns ¶
Columns returns the columns in order.
The result shares the frame's own slice and the caller must not modify it.
func (*Frame[S]) CrossJoin ¶
CrossJoin returns every left row paired with every right row.
The result has as many rows as the two frames multiplied together, which is why it is a method a caller has to name rather than something a forgotten key falls into.
func (*Frame[S]) Drop ¶
Drop returns a frame with the named columns left out. The others keep their order.
Dropping a column that is not there is an error rather than a shrug. A drop list that has gone stale is a bug worth hearing about, and the caller who genuinely does not know can ask Index first.
func (*Frame[S]) DropNulls ¶
DropNulls returns the frame with the rows that have a missing value in any of the named columns taken out. With no names it looks at every column, which is the pandas dropna default.
Frame.KeepAtLeast is the same thing with the rule relaxed, for the caller who wants the rows that are mostly there rather than the rows that are entirely there.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
// withGaps returns a frame with a hole in it, built the way a hole usually
// turns up, which is data from two places that do not agree on the columns.
func withGaps() *kuma.Frame[kuma.Dynamic] {
old, err := kuma.NewFrame(kuma.NewSeries("qty", int64(100), 50).Column())
if err != nil {
panic(err)
}
later, err := kuma.NewFrame(
kuma.NewSeries("qty", int64(25)).Column(),
kuma.NewSeries("fee", 0.5).Column(),
)
if err != nil {
panic(err)
}
f, err := kuma.ConcatUnion(old, later)
if err != nil {
panic(err)
}
return f
}
func main() {
got, err := withGaps().DropNulls()
if err != nil {
panic(err)
}
qty, err := got.Series[int64]("qty")
if err != nil {
panic(err)
}
fmt.Println(qty.Values())
}
Output: [25]
func (*Frame[S]) Eval ¶
Eval works out an expression against the frame and returns the result as a column.
total, err := f.Eval(t.Price.MulExpr(t.Qty))
The column is named after the expression, so the one above is called "(price * qty)". Rename it, or use WithColumn, when the name matters.
It reports an error if a column named in the expression is not in the frame, if two columns have no type in common, or if a literal cannot be used with the column it was written against.
func (*Frame[S]) FillNull ¶
FillNull returns a frame with every missing value of the named column replaced by v. The column keeps its position and everything else is left alone.
The result is a Dynamic frame because the schema changed: a column with nothing missing is not nullable and one with something missing is, and that is part of what a typed frame promises.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
// withGaps returns a frame with a hole in it, built the way a hole usually
// turns up, which is data from two places that do not agree on the columns.
func withGaps() *kuma.Frame[kuma.Dynamic] {
old, err := kuma.NewFrame(kuma.NewSeries("qty", int64(100), 50).Column())
if err != nil {
panic(err)
}
later, err := kuma.NewFrame(
kuma.NewSeries("qty", int64(25)).Column(),
kuma.NewSeries("fee", 0.5).Column(),
)
if err != nil {
panic(err)
}
f, err := kuma.ConcatUnion(old, later)
if err != nil {
panic(err)
}
return f
}
func main() {
got, err := withGaps().FillNull("fee", 0.0)
if err != nil {
panic(err)
}
fees, err := got.Series[float64]("fee")
if err != nil {
panic(err)
}
fmt.Println(fees.Values(), got.HasNulls())
}
Output: [0 0 0.5] false
func (*Frame[S]) Filter ¶
Filter returns the rows a condition holds for, in the order they were in.
high, err := f.Filter(t.Price.Gt(100).And(t.Side.Eq("BUY")))
The condition is written against the frame's own schema, so a predicate meant for a table of orders cannot be used on a table of trades and the compiler is the one that says so. Dyn is the way to write one against a column that only has a name at runtime.
A row the condition is missing an answer for is not in the result. A row nobody can say belongs there does not go there, which is the same rule a null gets everywhere else here, and it is why filtering on a condition and then on its negation does not always give back every row.
It reports an error if a column named in the condition is not in the frame, or if the condition cannot be worked out over the types it found there. Frame.FilterMask is the same thing given the answers rather than the question.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "NVDA").Column(),
kuma.NewSeries("price", 189.5, 411.2, 121.0).Column(),
)
if err != nil {
panic(err)
}
// The handles would normally be a package level variable written once, or
// generated from the struct the rows are read into.
symbol, price := kuma.Str("symbol"), kuma.F64("price")
dear, err := f.Filter(price.Gt(150).And(symbol.Ne("MSFT")))
if err != nil {
panic(err)
}
symbols, err := symbol.Series(dear)
if err != nil {
panic(err)
}
fmt.Println(symbols.Values())
}
Output: [AAPL]
func (*Frame[S]) FilterMask ¶
FilterMask returns the rows that mask selects, in the order they were in.
It is Frame.Filter for a mask that is already worked out, which is what a caller who built one by hand, or got one back from somewhere else, has.
A null in the mask selects nothing, for the reason Filter gives.
It reports an error if the mask is not as long as the frame.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "NVDA").Column(),
kuma.NewSeries("price", 189.5, 411.2, 121.0).Column(),
)
if err != nil {
panic(err)
}
// A mask is an ordinary boolean column, which is what a caller who worked
// out the rows some other way already has.
prices, err := f.Series[float64]("price")
if err != nil {
panic(err)
}
keep := make([]bool, prices.Len())
for i, p := range prices.Values() {
keep[i] = p > 150
}
dear, err := f.FilterMask(kuma.NewSeries("keep", keep...))
if err != nil {
panic(err)
}
symbols, err := dear.Series[string]("symbol")
if err != nil {
panic(err)
}
fmt.Println(symbols.Values())
}
Output: [AAPL MSFT]
func (*Frame[S]) GroupBy ¶
func (f *Frame[S]) GroupBy(names ...string) (*GroupedFrame[S], error)
GroupBy divides the rows into groups by the values of the named columns.
Two rows are in the same group when they agree on every key. Missing counts as a value, so the rows whose key is missing form a group of their own, which is what SQL and Polars do. The pandas default is to drop those rows, and a row disappearing out of a total because a field was blank is the kind of thing that is noticed a quarter later.
The groups come out in the order they first appear in the frame. That is deterministic without being sorted, so a caller who wants them sorted can sort the result and one who does not pay for it does not. The pandas default is to sort, and turning it off is a keyword argument.
It reports an error if a name is not a column of the frame, or if a column is of a type there is no key encoding for yet, which today means the nested types.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "AAPL", "NVDA", "MSFT").Column(),
kuma.NewSeries("qty", int64(100), 50, 25, 400, 75).Column(),
kuma.NewSeries("price", 189.5, 411.2, 190.1, 121.0, 410.0).Column(),
)
if err != nil {
panic(err)
}
g, err := f.GroupBy("symbol")
if err != nil {
panic(err)
}
got, err := g.Agg(
kuma.Sum("qty").As("total"),
kuma.Mean("price").As("avg"),
kuma.Size(),
)
if err != nil {
panic(err)
}
fmt.Println(strings.Join(got.Names(), " "))
for i := range got.NumRows() {
fmt.Println(string(got.ColumnAt(0).Data().Bytes(i)),
got.ColumnAt(1).Data().Value[int64](i),
got.ColumnAt(2).Data().Value[float64](i),
got.ColumnAt(3).Data().Value[int64](i))
}
}
Output: symbol total avg size AAPL 125 189.8 2 MSFT 125 410.6 2 NVDA 400 121 1
Example (Sorted) ¶
Groups come out in the order they first appear, which is deterministic without being sorted. Sort the result when the order matters.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("region", "west", "east", "west", "north").Column(),
kuma.NewSeries("sales", 10.0, 40.0, 30.0, 20.0).Column(),
)
if err != nil {
panic(err)
}
g, err := f.GroupBy("region")
if err != nil {
panic(err)
}
totals, err := g.Agg(kuma.Sum("sales"))
if err != nil {
panic(err)
}
got, err := totals.SortDesc("sales")
if err != nil {
panic(err)
}
for i := range got.NumRows() {
fmt.Println(string(got.ColumnAt(0).Data().Bytes(i)),
got.ColumnAt(1).Data().Value[float64](i))
}
}
Output: west 40 east 40 north 20
func (*Frame[S]) Head ¶
Head returns the first n rows, or all of them if the frame is shorter than n. A negative n means all but the last n.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(kuma.NewSeries("qty", int64(1), 2, 3, 4, 5).Column())
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Head(2).NumRows(), f.Tail(1).NumRows(), f.Slice(1, 4).NumRows())
}
Output: 2 1 3
func (*Frame[S]) Index ¶
Index returns the position of the column with the given name, or -1 if the frame has no such column.
func (*Frame[S]) InnerJoin ¶
InnerJoin returns the rows of two frames put together on the named columns, keeping only the pairs that matched. It is Frame.Join for the common case.
func (*Frame[S]) IsNotNull ¶
IsNotNull returns a frame of boolean columns that are true where the value is there. It is Frame.IsNull the other way round.
func (*Frame[S]) IsNull ¶
IsNull returns a frame of boolean columns, one for each column of this frame and with the same name, true where the value is missing.
This is the pandas isna, and it is a frame rather than a mask because a frame can hold a column for each of the columns asked about. Counting what is missing in each column is what Frame.NullCounts is for and does not need this.
func (*Frame[S]) Join ¶
Join returns the rows of two frames put together where their keys match.
Rows match when they agree on every key in on. A missing key matches nothing, including another missing key, which is what SQL says and what keeps a join from gluing together every row whose field was left blank. It is not what pandas does.
The result has every column of the left frame followed by every column of the right frame, except that the right key columns are dropped when they are called the same thing as the left ones, since they hold the same values and nobody wants both. A semi or an anti join takes no columns from the right frame at all. Any other name that appears on both sides is an error, because silently renaming one of them is how pandas ends up with columns called price_x and price_y.
The rows come out in the left frame's order, with the matches of one left row in the right frame's order. A right join is ordered by the right frame, and the rows an outer join adds for unmatched right rows come at the end.
It reports an error if a key names a column that is not there, if the two sides have different numbers of keys, if a key column is of a type there is no encoding for, or if the result would have two columns of one name.
Example ¶
A join puts two frames together on the columns they share. This is an inner join, so a trade in a symbol the reference data has never heard of is not in the answer.
package main
import (
"fmt"
"strings"
"github.com/tamnd/kuma"
)
func main() {
trades, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "AAPL", "NVDA").Column(),
kuma.NewSeries("qty", int64(100), 50, 25, 400).Column(),
)
if err != nil {
panic(err)
}
sectors, err := kuma.NewFrame(
kuma.NewSeries("symbol", "MSFT", "AAPL").Column(),
kuma.NewSeries("sector", "software", "hardware").Column(),
)
if err != nil {
panic(err)
}
got, err := trades.InnerJoin(sectors, "symbol")
if err != nil {
panic(err)
}
fmt.Println(strings.Join(got.Names(), " "))
for i := range got.NumRows() {
fmt.Println(string(got.ColumnAt(0).Data().Bytes(i)),
got.ColumnAt(1).Data().Value[int64](i),
string(got.ColumnAt(2).Data().Bytes(i)))
}
}
Output: symbol qty sector AAPL 100 hardware MSFT 50 software AAPL 25 hardware
Example (Semi) ¶
A semi join answers which of these have one, and takes nothing from the right side. An anti join is the other half of the same question.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
orders, err := kuma.NewFrame(
kuma.NewSeries("id", int64(1), 2, 3).Column(),
kuma.NewSeries("customer", "ann", "bob", "cat").Column(),
)
if err != nil {
panic(err)
}
shipped, err := kuma.NewFrame(kuma.NewSeries("id", int64(3), 1).Column())
if err != nil {
panic(err)
}
for _, how := range []kuma.JoinType{kuma.SemiJoin, kuma.AntiJoin} {
got, err := orders.Join(shipped, kuma.Using("id"), how)
if err != nil {
panic(err)
}
fmt.Print(how, ":")
for i := range got.NumRows() {
fmt.Print(" ", string(got.ColumnAt(1).Data().Bytes(i)))
}
fmt.Println()
}
}
Output: semi: ann cat anti: bob
func (*Frame[S]) KeepAtLeast ¶
KeepAtLeast returns the frame with only the rows that have at least present values among the named columns. With no names it looks at every column.
This is the pandas thresh, and the pandas how falls out of it: how="any" is Frame.DropNulls, which is this with present set to all of the columns, and how="all" is this with present set to one.
A present of zero or below keeps every row, since every row has at least nothing.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
// withGaps returns a frame with a hole in it, built the way a hole usually
// turns up, which is data from two places that do not agree on the columns.
func withGaps() *kuma.Frame[kuma.Dynamic] {
old, err := kuma.NewFrame(kuma.NewSeries("qty", int64(100), 50).Column())
if err != nil {
panic(err)
}
later, err := kuma.NewFrame(
kuma.NewSeries("qty", int64(25)).Column(),
kuma.NewSeries("fee", 0.5).Column(),
)
if err != nil {
panic(err)
}
f, err := kuma.ConcatUnion(old, later)
if err != nil {
panic(err)
}
return f
}
func main() {
// One value present out of the two columns is enough, which is the pandas
// how="all" and keeps every row that is not entirely empty.
got, err := withGaps().KeepAtLeast(1)
if err != nil {
panic(err)
}
fmt.Println(got.NumRows())
}
Output: 3
func (*Frame[S]) LeftJoin ¶
LeftJoin returns the rows of two frames put together on the named columns, keeping every left row and filling the right columns with nulls where nothing matched.
Example ¶
A left join keeps every left row whether it matched or not, and the columns that came from the right side are missing where nothing did.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
trades, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "NVDA").Column(),
kuma.NewSeries("qty", int64(100), 400).Column(),
)
if err != nil {
panic(err)
}
sectors, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL").Column(),
kuma.NewSeries("sector", "hardware").Column(),
)
if err != nil {
panic(err)
}
got, err := trades.LeftJoin(sectors, "symbol")
if err != nil {
panic(err)
}
for i := range got.NumRows() {
sector := "unknown"
if !got.ColumnAt(2).IsNull(i) {
sector = string(got.ColumnAt(2).Data().Bytes(i))
}
fmt.Println(string(got.ColumnAt(0).Data().Bytes(i)), sector)
}
}
Output: AAPL hardware NVDA unknown
func (*Frame[S]) NullCounts ¶
NullCounts returns how many values are missing in each column, in column order.
It is a slice rather than a map because the columns of a frame are ordered and a map would throw that away. Names() lines up with it.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
// withGaps returns a frame with a hole in it, built the way a hole usually
// turns up, which is data from two places that do not agree on the columns.
func withGaps() *kuma.Frame[kuma.Dynamic] {
old, err := kuma.NewFrame(kuma.NewSeries("qty", int64(100), 50).Column())
if err != nil {
panic(err)
}
later, err := kuma.NewFrame(
kuma.NewSeries("qty", int64(25)).Column(),
kuma.NewSeries("fee", 0.5).Column(),
)
if err != nil {
panic(err)
}
f, err := kuma.ConcatUnion(old, later)
if err != nil {
panic(err)
}
return f
}
func main() {
f := withGaps()
fmt.Println(f.Names(), f.NullCounts())
}
Output: [qty fee] [0 2]
func (*Frame[S]) Rename ¶
Rename returns a frame with the column called from called to instead. The column keeps its position.
func (*Frame[S]) Render ¶
func (f *Frame[S]) Render(o *PrintOptions) string
Render returns the frame as a table, with the options applied. String is this with the defaults.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "NVDA").Column(),
kuma.NewSeries("price", 189.5, 411.2, 121.0).Column(),
)
if err != nil {
panic(err)
}
fmt.Println(f.Render(&kuma.PrintOptions{MaxRows: 2}))
}
Output: kuma.Frame[kuma.Dynamic] 3 rows x 2 cols symbol | price string | float64 ---------+-------- AAPL | 189.5 ... | ... NVDA | 121
func (*Frame[S]) Schema ¶
Schema returns the fields of the frame, in column order.
It describes the data rather than a declaration about it, so a field is nullable when the column has nulls in it. The result is a copy and the caller may keep it.
func (*Frame[S]) Select ¶
Select returns a frame holding the named columns, in the order given.
Naming the same column twice is a duplicate column and is rejected, since the result would be a frame with two columns of one name. Selecting no columns gives a frame with no columns.
The result is dynamic whatever the frame it came from, because a subset of the columns of a Trade is not a Trade. Bind is the way back to a typed frame.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT").Column(),
kuma.NewSeries("price", 189.5, 411.2).Column(),
kuma.NewSeries("qty", int64(100), 50).Column(),
)
if err != nil {
fmt.Println(err)
return
}
out, err := f.Select("qty", "symbol")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(out.Names())
}
Output: [qty symbol]
func (*Frame[S]) Series ¶
Series returns the named column read as a Go type.
prices, err := f.Series[float64]("price")
It reports an error if there is no such column, or if the column is not stored as something a T can be read out of. Nothing is copied.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "NVDA").Column(),
kuma.NewSeries("price", 189.5, 411.2, 121.0).Column(),
)
if err != nil {
fmt.Println(err)
return
}
prices, err := f.Series[float64]("price")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(prices.Values())
}
Output: [189.5 411.2 121]
func (*Frame[S]) Shape ¶
Shape returns the number of rows and the number of columns, which is the pair people print first when they want to know whether a query did what they meant.
func (*Frame[S]) Slice ¶
Slice returns the rows from i up to but not including j. It panics unless 0 <= i <= j <= NumRows.
Every column is sliced, which is constant time each, so this costs one slice per column whatever the number of rows. The result shares the memory it came from.
func (*Frame[S]) Sort ¶
Sort returns the rows ordered by the given columns.
The first key decides and each later one breaks the ties of the one before, so sorting by symbol and then by time gives the trades of each symbol in order. The sort is stable, so rows that every key calls equal come out in the order they went in.
Stability is not optional here. Both pandas and Polars let a caller give the guarantee up for speed, with a kind argument in one and a maintain_order argument in the other. The guarantee is worth more than the speed: a stable sort makes the output of a query reproducible, which is what a test needs and what a diff of two runs needs, and it lets a caller build a sort out of several passes. The cost is a scratch buffer, since the comparison is a closure over columns and dwarfs everything around it.
NaN is a value and not a missing one, so it sorts after every number and descending order puts it first.
It reports an error if a name is not a column of the frame, or if a column is of a type there is no order for yet, which today means the decimals and the nested types.
Example ¶
The first key decides and the later ones break its ties, and each key runs the way it says. This is the trades of each symbol, biggest first.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "b", "a", "b", "a").Column(),
kuma.NewSeries("qty", int64(2), 9, 7, 1).Column(),
)
if err != nil {
panic(err)
}
got, err := f.Sort(kuma.Asc("symbol"), kuma.Desc("qty"))
if err != nil {
panic(err)
}
symbols, err := got.Series[string]("symbol")
if err != nil {
panic(err)
}
qty, err := got.Series[int64]("qty")
if err != nil {
panic(err)
}
for i := range got.NumRows() {
fmt.Println(symbols.Value(i), qty.Value(i))
}
}
Output: a 9 a 1 b 7 b 2
func (*Frame[S]) SortBy ¶
SortBy returns the rows in ascending order of the named columns, with the nulls at the end. It is Frame.Sort for the common case.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "NVDA", "AAPL", "MSFT").Column(),
kuma.NewSeries("qty", int64(400), 100, 50).Column(),
)
if err != nil {
panic(err)
}
got, err := f.SortBy("symbol")
if err != nil {
panic(err)
}
symbols, err := got.Series[string]("symbol")
if err != nil {
panic(err)
}
fmt.Println(symbols.Values())
}
Output: [AAPL MSFT NVDA]
func (*Frame[S]) SortDesc ¶
SortDesc returns the rows in descending order of the named columns, with the nulls at the end.
func (*Frame[S]) SortIndex ¶
SortIndex returns the positions that Frame.Sort would put the rows in, without moving anything.
This is the operation to reach for when the order matters more than the sorted frame does: applying one frame's order to another, checking whether a frame is already sorted, or taking the first ten of a million rows without paying to move the other 999990.
func (*Frame[S]) String ¶
String returns the frame as a table: the shape, then the column names, then their types, then the rows.
It shows the first and last few rows rather than all of them, since a frame is usually longer than a screen and the whole point of printing one is to look at it. Render takes the options if a different amount is wanted.
Numbers are printed at the shortest text that reads back as the same number rather than rounded to a fixed number of digits. A printer that rounds is a printer that will one day show two values as the same when they are not, during the debugging session where that matters most.
func (*Frame[S]) Tail ¶
Tail returns the last n rows, or all of them if the frame is shorter than n. A negative n means all but the first n.
func (*Frame[S]) Take ¶
Take returns the rows at the given positions, in the order given.
This is how every reordering of a frame is done. Sorting works out the order and takes it, a join works out which row of the left goes with which row of the right and takes both, and the values move here.
A position below zero gives a row of nulls, which is what an outer join does with a row that matched nothing. A position at or past the end panics, the same way indexing a slice does.
Unlike Slice this copies every column, since the rows it wants are scattered through the frame.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL", "MSFT", "NVDA").Column(),
kuma.NewSeries("qty", int64(100), 50, 400).Column(),
)
if err != nil {
panic(err)
}
// The order some other operation worked out, and a position below zero for
// a row that matched nothing.
got := f.Take([]int{2, -1, 0})
symbols, err := got.Series[string]("symbol")
if err != nil {
panic(err)
}
for i := range symbols.Len() {
if symbols.IsNull(i) {
fmt.Println("null")
continue
}
fmt.Println(symbols.Value(i))
}
}
Output: NVDA null AAPL
func (*Frame[S]) WithColumn ¶
WithColumn returns a frame with the given column added at the end, or in place of the column of the same name if there is one.
This is the assignment that pandas spells df["x"] = value, and it is a method returning a new frame rather than a statement mutating an old one, which is what stops a column appearing in a frame that some other goroutine is reading.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(kuma.NewSeries("price", 10.0, 20.0).Column())
if err != nil {
fmt.Println(err)
return
}
// The values of a new column are worked out in Go and handed back as a
// column, which is what an expression will do for you once M3 lands.
prices, err := f.Series[float64]("price")
if err != nil {
fmt.Println(err)
return
}
taxed := make([]float64, prices.Len())
for i, p := range prices.Values() {
taxed[i] = p * 1.1
}
out, err := f.WithColumn(kuma.NewSeries("taxed", taxed...).Column())
if err != nil {
fmt.Println(err)
return
}
taxes, err := out.Series[float64]("taxed")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(out.Names(), taxes.Values())
}
Output: [price taxed] [11 22]
func (*Frame[S]) WithExpr ¶
WithExpr returns a frame with the result of an expression added as a column called name, or replacing the column of that name when there is one.
f, err := f.WithExpr("notional", t.Price.MulExpr(t.Qty))
It is Frame.Eval and Frame.WithColumn in one step, and it is the step a query spends most of its time in, so it is worth the one method. The result is a Dynamic frame for the reason WithColumn gives.
func (*Frame[S]) WriteCSV ¶
WriteCSV writes the frame as a comma separated file.
A nil options is the useful default: comma separated, a header line of the column names, an empty field where a value is missing, and floats written with the fewest digits that read back as the same value. What else it can do is on csv.WriteOptions.
err := f.WriteCSV(w, nil)
Reading the result back gives the same frame, with one thing to watch: a value that is an empty string comes back as a missing value, because a file cannot tell the two apart. Set csv.WriteOptions.NullValue to something else when the difference matters.
Example ¶
package main
import (
"os"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("sym", "AAPL", "MSFT", "GOOG").Column(),
kuma.NewSeries[int64]("qty", 100, 200, 300).Column(),
)
if err != nil {
panic(err)
}
if err := f.WriteCSV(os.Stdout, nil); err != nil {
panic(err)
}
}
Output: sym,qty AAPL,100 MSFT,200 GOOG,300
func (*Frame[S]) WriteCSVFile ¶
func (f *Frame[S]) WriteCSVFile(path string, opts *csv.WriteOptions) error
WriteCSVFile writes the frame to the file at path, creating it if it is not there and truncating it if it is. It is Frame.WriteCSV over that file.
func (*Frame[S]) WriteNDJSON ¶ added in v0.0.23
WriteNDJSON writes the frame as newline delimited JSON, one object to a line.
A nil options is the useful default: the members named and ordered the way the schema is, null where a value is missing, and floats written with the fewest digits that read back as the same value. What else it can do is on ndjson.WriteOptions.
err := f.WriteNDJSON(w, nil)
Reading the result back gives the same frame. There is no empty field problem here the way there is in a CSV file, since JSON writes null for a missing value and a pair of quotes for a string of no bytes, and those are different things on the page.
func (*Frame[S]) WriteNDJSONFile ¶ added in v0.0.23
func (f *Frame[S]) WriteNDJSONFile(path string, opts *ndjson.WriteOptions) error
WriteNDJSONFile writes the frame to the file at path, creating it if it is not there and truncating it if it is. It is Frame.WriteNDJSON over that file.
type GroupedFrame ¶
type GroupedFrame[S any] struct { // contains filtered or unexported fields }
GroupedFrame is a frame with its rows divided into groups, waiting for somebody to say what to work out about each one.
The division is done once, when Frame.GroupBy is called, and every aggregation after that reads it. That is why this is a value the caller holds rather than an argument to a method that does everything at once: asking for the total, the average and the count of a grouping costs one pass to work out the groups and three cheap passes over the values, not three groupings.
A GroupedFrame is immutable, like the frame it came from.
func (*GroupedFrame[S]) Agg ¶
func (g *GroupedFrame[S]) Agg(aggs ...Aggregation) (*Frame[Dynamic], error)
Agg works out the given aggregations for every group and returns them as a frame.
The result has the key columns first, one row per group, followed by one column per aggregation in the order they were given. So a group by symbol with a sum of qty and an average of price comes back as three columns and as many rows as there are symbols, which is the table you would have written by hand.
An aggregation is named after the column it reads unless Aggregation.As says otherwise, which is what Polars does. That means asking for two aggregations of the same column without naming them is an error about duplicate column names, and the fix is to say what you want them called.
It reports an error if an aggregation names a column that is not there, or if a column is of a type that aggregation has no answer for, such as the sum of a string.
Example ¶
A grouping is worked out once and answers as many questions as it is asked, which is why GroupBy hands one back instead of doing everything at once.
package main
import (
"fmt"
"strings"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("host", "a", "b", "a", "b", "a").Column(),
kuma.NewSeries("ms", 12.0, 240.0, 15.0, 11.0, 19.0).Column(),
)
if err != nil {
panic(err)
}
g, err := f.GroupBy("host")
if err != nil {
panic(err)
}
got, err := g.Agg(
kuma.Median("ms").As("p50"),
kuma.Quantile("ms", 0.9, kuma.Linear).As("p90"),
kuma.Max("ms").As("worst"),
kuma.Count("ms").As("n"),
)
if err != nil {
panic(err)
}
fmt.Println(strings.Join(got.Names(), " "))
for i := range got.NumRows() {
fmt.Println(string(got.ColumnAt(0).Data().Bytes(i)),
got.ColumnAt(1).Data().Value[float64](i),
got.ColumnAt(2).Data().Value[float64](i),
got.ColumnAt(3).Data().Value[float64](i),
got.ColumnAt(4).Data().Value[int64](i))
}
}
Output: host p50 p90 worst n a 15 18.2 19 3 b 125.5 217.1 240 2
func (*GroupedFrame[S]) Count ¶
func (g *GroupedFrame[S]) Count() (*Frame[Dynamic], error)
Count returns a frame of the keys and how many rows each group has, which is the group by anybody writes first.
It counts rows and not values, so it is Size rather than Count, and it cannot fail because there is no column to be the wrong type.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
f, err := kuma.NewFrame(
kuma.NewSeries("status", "ok", "ok", "error", "ok", "error").Column(),
)
if err != nil {
panic(err)
}
g, err := f.GroupBy("status")
if err != nil {
panic(err)
}
got, err := g.Count()
if err != nil {
panic(err)
}
for i := range got.NumRows() {
fmt.Println(string(got.ColumnAt(0).Data().Bytes(i)),
got.ColumnAt(1).Data().Value[int64](i))
}
}
Output: ok 3 error 2
func (*GroupedFrame[S]) Frame ¶
func (g *GroupedFrame[S]) Frame() *Frame[S]
Frame returns the frame the groups were worked out over.
func (*GroupedFrame[S]) Groups ¶
func (g *GroupedFrame[S]) Groups() *kernel.Groups
Groups returns the grouping underneath, for a caller who wants to run a kernel over it that there is no method for here.
func (*GroupedFrame[S]) Keys ¶
func (g *GroupedFrame[S]) Keys() []Column
Keys returns the key columns, one row per group, in the order the groups came out in.
func (*GroupedFrame[S]) Names ¶
func (g *GroupedFrame[S]) Names() []string
Names returns the names of the columns the rows were grouped by.
func (*GroupedFrame[S]) NumGroups ¶
func (g *GroupedFrame[S]) NumGroups() int
NumGroups returns how many groups there are.
type I64Col ¶
type I64Col[S any] struct { // contains filtered or unexported fields }
I64Col is a handle on an int64 column of a frame with schema S. It is the int64 half of what F64Col describes.
func NewI64Col ¶
NewI64Col returns a handle on the int64 column called name in a frame with schema S.
func (I64Col) AsF64 ¶
func (o I64Col) AsF64() F64Expr[S]
AsF64 returns the value as a float64, which is how an int64 column is used with a float64 one. Every int64 value has a float64 nearest to it and the two are the same number up to 2^53, above which the conversion rounds.
func (I64Col) Div ¶
Div returns the value divided by v, truncated toward zero. Dividing by zero is an error naming the row.
func (I64Col) GtExpr ¶
GtExpr returns whether the value is greater than the value of x in the same row.
func (I64Col) IsNotNull ¶
func (o I64Col) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (I64Col) IsNull ¶
func (o I64Col) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
func (I64Col) LtExpr ¶
LtExpr returns whether the value is less than the value of x in the same row.
func (I64Col) ModExpr ¶
ModExpr returns the remainder of the value divided by the value of x in the same row.
func (I64Col) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
func (I64Col[S]) Series ¶
Series returns the column as a Series[int64], reporting an error if the frame has no such column or holds something else there.
type I64Expr ¶
type I64Expr[S any] struct { // contains filtered or unexported fields }
I64Expr is an int64 valued expression, which is what doing arithmetic to an int64 column gives.
func (I64Expr) AsF64 ¶
func (o I64Expr) AsF64() F64Expr[S]
AsF64 returns the value as a float64, which is how an int64 column is used with a float64 one. Every int64 value has a float64 nearest to it and the two are the same number up to 2^53, above which the conversion rounds.
func (I64Expr) Div ¶
Div returns the value divided by v, truncated toward zero. Dividing by zero is an error naming the row.
func (I64Expr) GeExpr ¶
GeExpr returns whether the value is at least the value of x in the same row.
func (I64Expr) GtExpr ¶
GtExpr returns whether the value is greater than the value of x in the same row.
func (I64Expr) IsNotNull ¶
func (o I64Expr) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (I64Expr) IsNull ¶
func (o I64Expr) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
func (I64Expr) LtExpr ¶
LtExpr returns whether the value is less than the value of x in the same row.
func (I64Expr) ModExpr ¶
ModExpr returns the remainder of the value divided by the value of x in the same row.
func (I64Expr) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
type I64Value ¶
I64Value is an int64 valued piece of an expression, which is an I64Col or an I64Expr. It is I64's half of what F64Value describes.
type Interpolation ¶
type Interpolation = kernel.Interpolation
Interpolation is how Quantile fills the gap between two values.
It is kernel.Interpolation under another name, so the constants below are the same constants and either name may be used.
type JoinType ¶
JoinType is which rows of the two frames a join keeps.
It is kernel.JoinType under another name, so the constants below are the same constants and either name may be used.
type On ¶
On names the columns two frames are joined on.
The common case is one name on both sides, which is what Using is for. This is for the case where the same thing is called two different things, which is most real data.
Example ¶
When the two sides call the key different things, name both. Both columns are kept, since a caller who wrote two names probably wants to see both.
package main
import (
"fmt"
"strings"
"github.com/tamnd/kuma"
)
func main() {
trades, err := kuma.NewFrame(
kuma.NewSeries("symbol", "AAPL").Column(),
kuma.NewSeries("qty", int64(100)).Column(),
)
if err != nil {
panic(err)
}
sectors, err := kuma.NewFrame(
kuma.NewSeries("ticker", "AAPL").Column(),
kuma.NewSeries("sector", "hardware").Column(),
)
if err != nil {
panic(err)
}
got, err := trades.Join(sectors,
[]kuma.On{{Left: "symbol", Right: "ticker"}}, kuma.InnerJoin)
if err != nil {
panic(err)
}
fmt.Println(strings.Join(got.Names(), " "))
}
Output: symbol qty ticker sector
type Order ¶
Order says which way a sort runs and where the missing values go.
The zero value is ascending with the nulls at the end, which is what pandas does when nobody says otherwise and what most databases do.
Null placement is not part of the direction. Asking for descending order does not move the nulls, which is what a database does with an explicit NULLS LAST and what makes a query that says both mean what it says.
type PrintOptions ¶
type PrintOptions struct {
// MaxRows is how many rows to show. Zero means ten and a negative number
// means all of them. A frame with more rows than this shows the first and
// the last few with a line of dots in between.
MaxRows int
// MaxCols is how many columns to show, under the same rules. The columns
// left out are the ones in the middle.
MaxCols int
// MaxWidth is how wide one cell may be before it is cut short. Zero means
// thirty two and a negative number means no limit. A cell that is cut
// short ends in three dots, so that one column of long strings does not
// push everything after it off the side of the screen.
MaxWidth int
// Null is the text for a missing value. Empty means "null".
//
// It is not blank by default because a blank cell reads as a value that
// happens to be empty, and the difference between a value that is not
// there and a value that is there and empty is the difference this library
// exists to keep.
Null string
}
PrintOptions says how much of a frame to show and what a missing value looks like.
The zero value is what String uses. That is ten rows, twelve columns and cells of at most thirty two characters, which is a size that fits both in a terminal and in a test failure, the two places a frame ever gets printed.
type Series ¶
type Series[T Value] struct { // contains filtered or unexported fields }
Series is one named column, read as a Go type.
The values live in a chunked array underneath, which is where the memory and the nulls are. A Series is the typed view of that: it knows the column is stored as an int64 and that you want to read it as an int64, and it checked that once when it was made rather than on every value.
A Series is immutable and cheap to copy. Slice, Head and Tail return a new one over the same memory.
The zero Series is not usable. Use NewSeries or SeriesFrom, or take one out of a Frame.
func NewSeries ¶
NewSeries returns a series of the given values, with no nulls, of the column type that matches T.
It is for tests, examples and small literal columns. Loading data goes through a reader, which builds the arrays and hands them over.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
s := kuma.NewSeries("price", 189.5, 411.2, 190.1)
fmt.Println(s.Name(), s.Len(), s.DType())
fmt.Println(s.Value(1))
}
Output: price 3 float64 411.2
func SeriesFrom ¶
SeriesFrom returns a series over data, read as a T.
It reports an error unless the column can be read as a T, which is what CanRead answers. Nothing is copied.
Example ¶
ExampleSeriesFrom shows a column stored as one type being read as another. A timestamp is int64 values with a meaning attached, so it reads as a time.Time and as the int64 underneath, and neither of those copies anything.
package main
import (
"fmt"
"time"
"github.com/tamnd/kuma"
)
func main() {
ts := kuma.NewSeries("ts",
time.Date(2026, 8, 25, 9, 30, 0, 0, time.UTC),
time.Date(2026, 8, 25, 9, 31, 0, 0, time.UTC),
)
nanos, err := kuma.SeriesFrom[int64]("ts", ts.Data())
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ts.Value(0).Format(time.RFC3339))
fmt.Println(nanos.Value(0))
}
Output: 2026-08-25T09:30:00Z 1787650200000000000
func (Series[T]) Cast ¶
Cast returns the column in the type to, read as a U.
The two type arguments are doing different jobs. The argument to is what the values are stored as, so it is the one that says microseconds or twelve bytes, and U is how they are read back, so a cast to a timestamp comes back as a Series[int64] or a Series[time.Time] depending on what the caller means to do next.
A value that will not fit is an error naming the row it was in. TryCast is the same cast with that decision reversed, and kernel.Cast documents what converts into what.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
"github.com/tamnd/kuma/dtype"
)
func main() {
s := kuma.NewSeries("qty", int64(100), 50, 400)
// The type argument is what the values come back as, and the argument is
// what they are stored as.
small, err := s.Cast[int32](dtype.Int32)
if err != nil {
panic(err)
}
fmt.Println(small.DType(), small.Values())
// An int8 holds up to 127, and one of the three rows does not.
if _, err := s.Cast[int8](dtype.Int8); err != nil {
fmt.Println(err)
}
}
Output: int32 [100 50 400] kernel: cannot cast int64 to int8: row 2 is 400: value out of range
Example (AsTime) ¶
package main
import (
"fmt"
"time"
"github.com/tamnd/kuma"
"github.com/tamnd/kuma/dtype"
)
func main() {
// Seconds since the epoch, which is how a file that came out of a database
// export usually holds them.
s := kuma.NewSeries("seen", int64(1767225600), 1767225660)
seen, err := s.Cast[time.Time](dtype.Timestamp{Unit: dtype.Second, Zone: "UTC"})
if err != nil {
panic(err)
}
for i := range seen.Len() {
fmt.Println(seen.Value(i).Format(time.RFC3339))
}
}
Output: 2026-01-01T00:00:00Z 2026-01-01T00:01:00Z
func (Series[T]) Column ¶
Column returns the series as an untyped column, which is what a Frame holds.
func (Series[T]) DType ¶
DType returns the type the values are stored as, which is not always the type they are read as. A Series[int64] over a timestamp column reads int64 values out of a column whose DType is a timestamp.
func (Series[T]) Data ¶
Data returns the values underneath, which is the door out to array and to hand written kernels. It is a supported door rather than an accident.
func (Series[T]) FillNull ¶
FillNull returns the series with every missing value replaced by v.
There is no error to return here, which is the difference between this and Column.FillNull. A series is a column that has already been read as a T, so a T is by construction a type the column takes and the only thing the column version can complain about cannot happen.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
// withGaps returns a frame with a hole in it, built the way a hole usually
// turns up, which is data from two places that do not agree on the columns.
func withGaps() *kuma.Frame[kuma.Dynamic] {
old, err := kuma.NewFrame(kuma.NewSeries("qty", int64(100), 50).Column())
if err != nil {
panic(err)
}
later, err := kuma.NewFrame(
kuma.NewSeries("qty", int64(25)).Column(),
kuma.NewSeries("fee", 0.5).Column(),
)
if err != nil {
panic(err)
}
f, err := kuma.ConcatUnion(old, later)
if err != nil {
panic(err)
}
return f
}
func main() {
fees, err := withGaps().Series[float64]("fee")
if err != nil {
panic(err)
}
fmt.Println(fees.FillNull(0.25).Values())
}
Output: [0.25 0.25 0.5]
func (Series[T]) Filter ¶
Filter returns the values that mask selects, in the order they were in.
A null in the mask selects nothing, since a row that nobody can say belongs in the result does not go in the result. It reports an error if the mask is not the same length as the column.
func (Series[T]) Head ¶
Head returns the first n values, or all of them if there are fewer than n. A negative n means all but the last n.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
s := kuma.NewSeries("qty", int64(1), 2, 3, 4, 5)
fmt.Println(s.Head(2).Values())
fmt.Println(s.Tail(2).Values())
fmt.Println(s.Head(-1).Values())
}
Output: [1 2] [4 5] [1 2 3 4]
func (Series[T]) IsNull ¶
IsNull reports whether value i is missing. It panics if i is out of range.
func (Series[T]) IsValid ¶
IsValid reports whether value i is present. It panics if i is out of range.
func (Series[T]) NullMask ¶
NullMask returns a boolean series that is true where this one has no value.
func (Series[T]) Render ¶
func (s Series[T]) Render(o *PrintOptions) string
Render returns the series as a table of one column, with the options applied.
func (Series[T]) Slice ¶
Slice returns the values from i up to but not including j, as a series. It panics unless 0 <= i <= j <= Len.
It shares the memory it came from and it is constant time, give or take the null count over the range.
func (Series[T]) Sort ¶
Sort returns the values in the order o describes.
A series is one column, so there are no ties to break and the stability of the sort is not something anyone can observe. Everything else Frame.Sort says applies: the nulls go where o puts them whichever way the values run, and NaN sorts after every number.
It reports an error if the column is of a type there is no order for yet.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
s := kuma.NewSeries("qty", int64(400), 100, 50)
got, err := s.Sort(kuma.Order{Descending: true})
if err != nil {
panic(err)
}
fmt.Println(got.Values())
}
Output: [400 100 50]
func (Series[T]) SortIndex ¶
SortIndex returns the positions that Series.Sort would put the values in, without moving them. It is what pandas calls argsort.
Example ¶
SortIndex works out the order without moving anything, which is what to reach for when the order is what you are after.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
s := kuma.NewSeries("qty", int64(400), 100, 50)
idx, err := s.SortIndex(kuma.Order{})
if err != nil {
panic(err)
}
fmt.Println(idx)
}
Output: [2 1 0]
func (Series[T]) String ¶
String returns the series as a table of one column. It follows the same rules as Frame.String.
func (Series[T]) Tail ¶
Tail returns the last n values, or all of them if there are fewer than n. A negative n means all but the first n.
func (Series[T]) Take ¶
Take returns the values at the given positions, in the order given.
This is the operation everything that reorders rows is made of. A sort produces the positions and takes them, a join produces the positions and takes them, and this is where the values actually move.
A position below zero gives a null, which is what an outer join does with a row that matched nothing. A position at or past the length panics, the same way indexing a slice does.
Unlike Slice this copies, since the values it wants are scattered through the column and a scattering is not something a slice header can describe.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
s := kuma.NewSeries("qty", int64(100), 50, 400)
fmt.Println(s.Take([]int{2, 0, 2}).Values())
}
Output: [400 100 400]
func (Series[T]) ValidMask ¶
ValidMask returns a boolean series that is true where this one has a value.
Example ¶
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
// withGaps returns a frame with a hole in it, built the way a hole usually
// turns up, which is data from two places that do not agree on the columns.
func withGaps() *kuma.Frame[kuma.Dynamic] {
old, err := kuma.NewFrame(kuma.NewSeries("qty", int64(100), 50).Column())
if err != nil {
panic(err)
}
later, err := kuma.NewFrame(
kuma.NewSeries("qty", int64(25)).Column(),
kuma.NewSeries("fee", 0.5).Column(),
)
if err != nil {
panic(err)
}
f, err := kuma.ConcatUnion(old, later)
if err != nil {
panic(err)
}
return f
}
func main() {
f := withGaps()
fees, err := f.Series[float64]("fee")
if err != nil {
panic(err)
}
// The mask goes straight back into Filter, which is the whole point of it
// being a series rather than a slice of bool.
got, err := f.FilterMask(fees.ValidMask())
if err != nil {
panic(err)
}
fmt.Println(got.NumRows())
}
Output: 1
func (Series[T]) Validity ¶
Validity returns the bitmap saying which values are present, or nil when none are missing.
It is only meaningful for a column held in one chunk, and it reports whether that is the case. A column in several chunks has one bitmap per chunk, and the way to reach those is through Data.
func (Series[T]) Value ¶
Value returns value i. It panics if i is out of range.
A missing value reads as the zero value of T, which is why IsNull exists. The alternative is an ok return on every read, and a kernel that has already checked the null count does not want to pay for one.
A string is the column's own bytes rather than a copy of them, which is what makes reading a string column allocation free. It is safe because a column is immutable, and it means a string kept from a large column keeps that column's memory alive, so copy it with strings.Clone if you are keeping a handful of values out of a large file.
func (Series[T]) Values ¶
func (s Series[T]) Values() []T
Values returns every value as one Go slice.
For a column of numbers held in one chunk, which is what a column that has been through a filter or a select is, this is the memory itself: no copy, no allocation, and 64 byte aligned. A column in several chunks is copied into one slice, and so is a column of conditions, of strings or of times, since none of those is stored as a Go value.
The result of the no copy case must not be modified. A Series is immutable and this is the one place that promise is left to the caller.
Example ¶
ExampleSeries_Values shows the door out to a hand written kernel. The slice is the column's own memory rather than a copy of it.
package main
import (
"fmt"
"github.com/tamnd/kuma"
)
func main() {
s := kuma.NewSeries("qty", int64(100), 50, 25, 400)
var total int64
for _, v := range s.Values() {
total += v
}
fmt.Println(total)
}
Output: 575
type StrCol ¶
type StrCol[S any] struct { // contains filtered or unexported fields }
StrCol is a handle on a string column of a frame with schema S.
func NewStrCol ¶
NewStrCol returns a handle on the string column called name in a frame with schema S.
func Str ¶
Str returns a handle on a string column of a frame with no schema behind it, which is the light version of NewStrCol.
func (StrCol) GeExpr ¶
GeExpr returns whether the value does not sort before the value of x in the same row.
func (StrCol) IsNotNull ¶
func (o StrCol) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (StrCol) IsNull ¶
func (o StrCol) IsNull() BoolExpr[S]
IsNull returns whether the value is missing, which is not the same as being the empty string.
func (StrCol) LeExpr ¶
LeExpr returns whether the value does not sort after the value of x in the same row.
func (StrCol) LtExpr ¶
LtExpr returns whether the value sorts before the value of x in the same row.
func (StrCol) NeExpr ¶
NeExpr returns whether the value differs from the value of x in the same row.
type StrExpr ¶
type StrExpr[S any] struct { // contains filtered or unexported fields }
StrExpr is a string valued expression.
func (StrExpr) GeExpr ¶
GeExpr returns whether the value does not sort before the value of x in the same row.
func (StrExpr) GtExpr ¶
GtExpr returns whether the value sorts after the value of x in the same row.
func (StrExpr) IsNotNull ¶
func (o StrExpr) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (StrExpr) IsNull ¶
func (o StrExpr) IsNull() BoolExpr[S]
IsNull returns whether the value is missing, which is not the same as being the empty string.
func (StrExpr) LeExpr ¶
LeExpr returns whether the value does not sort after the value of x in the same row.
func (StrExpr) LtExpr ¶
LtExpr returns whether the value sorts before the value of x in the same row.
type TimeCol ¶
type TimeCol[S any] struct { // contains filtered or unexported fields }
TimeCol is a handle on a timestamp column of a frame with schema S.
func NewTimeCol ¶
NewTimeCol returns a handle on the timestamp column called name in a frame with schema S.
func Time ¶
Time returns a handle on a timestamp column of a frame with no schema behind it, which is the light version of NewTimeCol.
func (TimeCol) AfterExpr ¶
AfterExpr returns whether the value is later than the value of x in the same row.
func (TimeCol) AtOrAfterExpr ¶
AtOrAfterExpr returns whether the value is no earlier than the value of x in the same row.
func (TimeCol) AtOrBefore ¶
AtOrBefore returns whether the value is t or earlier.
func (TimeCol) AtOrBeforeExpr ¶
AtOrBeforeExpr returns whether the value is no later than the value of x in the same row.
func (TimeCol) BeforeExpr ¶
BeforeExpr returns whether the value is earlier than the value of x in the same row.
func (TimeCol) EqExpr ¶
EqExpr returns whether the value is the same instant as the value of x in the same row.
func (TimeCol) IsNotNull ¶
func (o TimeCol) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (TimeCol) IsNull ¶
func (o TimeCol) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
func (TimeCol) NeExpr ¶
NeExpr returns whether the value is a different instant from the value of x in the same row.
type TimeExpr ¶
type TimeExpr[S any] struct { // contains filtered or unexported fields }
TimeExpr is a timestamp valued expression.
func (TimeExpr) AfterExpr ¶
AfterExpr returns whether the value is later than the value of x in the same row.
func (TimeExpr) AtOrAfterExpr ¶
AtOrAfterExpr returns whether the value is no earlier than the value of x in the same row.
func (TimeExpr) AtOrBefore ¶
AtOrBefore returns whether the value is t or earlier.
func (TimeExpr) AtOrBeforeExpr ¶
AtOrBeforeExpr returns whether the value is no later than the value of x in the same row.
func (TimeExpr) BeforeExpr ¶
BeforeExpr returns whether the value is earlier than the value of x in the same row.
func (TimeExpr) EqExpr ¶
EqExpr returns whether the value is the same instant as the value of x in the same row.
func (TimeExpr) IsNotNull ¶
func (o TimeExpr) IsNotNull() BoolExpr[S]
IsNotNull returns whether the value is there.
func (TimeExpr) IsNull ¶
func (o TimeExpr) IsNull() BoolExpr[S]
IsNull returns whether the value is missing.
type TimeValue ¶
TimeValue is a timestamp valued piece of an expression, which is a TimeCol or a TimeExpr.
type Value ¶
type Value interface {
bool |
int8 | int16 | int32 | int64 |
uint8 | uint16 | uint32 | uint64 |
float32 | float64 |
string | time.Time
}
Value is the set of Go types a Series can be read as.
The types are exact rather than approximate, so a named type such as type Price float64 is not a Value. That is deliberate: the mapping from a Go type to a column type is a switch on the type itself, and a named type would fall off the end of it. A column of prices is a Series[float64] whose field in your struct is a Price, and the conversion happens where the struct is read rather than inside the column.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package array holds the values of one column.
|
Package array holds the values of one column. |
|
Package bitmap implements the Arrow validity bitmap.
|
Package bitmap implements the Arrow validity bitmap. |
|
Package buffer provides the byte buffer that columns store their values in.
|
Package buffer provides the byte buffer that columns store their values in. |
|
compress
|
|
|
snappy
Package snappy decompresses the snappy block format.
|
Package snappy decompresses the snappy block format. |
|
Package csv reads and writes comma separated values.
|
Package csv reads and writes comma separated values. |
|
Package dataset reads a tree of files as one table.
|
Package dataset reads a tree of files as one table. |
|
Package dtype describes the types a column can hold.
|
Package dtype describes the types a column can hold. |
|
Package ipc moves data between kuma and other Arrow implementations.
|
Package ipc moves data between kuma and other Arrow implementations. |
|
ipctest
Package ipctest is an Arrow C data interface producer that is not kuma.
|
Package ipctest is an Arrow C data interface producer that is not kuma. |
|
Package kernel is the compute layer, where a column turns into another column.
|
Package kernel is the compute layer, where a column turns into another column. |
|
Kumagen writes the column handles for a Go struct, so that a query names a column with a field selector the compiler checks rather than with a string.
|
Kumagen writes the column handles for a Go struct, so that a query names a column with a field selector the compiler checks rather than with a string. |
|
Package kumatest compares frames in a test and prints what differs.
|
Package kumatest compares frames in a test and prints what differs. |
|
Package ndjson reads and writes newline delimited JSON.
|
Package ndjson reads and writes newline delimited JSON. |
|
Package parquet reads and writes Apache Parquet files.
|
Package parquet reads and writes Apache Parquet files. |
|
Package plan is the logical plan a query is compiled into, and the expressions the plan is written in.
|
Package plan is the logical plan a query is compiled into, and the expressions the plan is written in. |
|
Package strview implements the Arrow variable size binary view layout, which is how kuma stores String and Binary columns.
|
Package strview implements the Arrow variable size binary view layout, which is how kuma stores String and Binary columns. |