kuma

package module
v0.0.27 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

README

kuma

A columnar dataframe engine for Go.

CI Go Reference Go Report Card

kuma is what you reach for when you have a few hundred million rows, you would rather write Go than Python, and you do not want to find out about a typo in a column name ten seconds into a job.

Status: early. Nothing here is usable yet. The design is finished and written down in docs, the work is broken into milestones in the issue tracker, and M0 is in progress. Star the repo if you want to know when it does something.

The idea

t := TradeCols

bars, err := kuma.ScanParquet[Trade]("trades/*.parquet").
    Filter(t.Price.Gt(100).And(t.Side.Eq("BUY"))).
    GroupBy(t.Symbol, t.TS.Trunc(time.Minute)).
    Agg[Bar](
        t.Price.Mul(t.Qty).Sum().To(BarCols.Volume),
        t.Price.Quantile(0.99).To(BarCols.P99),
        kuma.Count().To(BarCols.N),
    ).
    SortDesc(BarCols.Volume).
    Head(20).
    Rows(ctx)

bars is a []Bar. Not a bag of any, not a map, not something you have to type assert your way out of.

Everything in that query is checked when you compile it:

t.Prive.Gt(100)                     // does not compile, no field called Prive
t.Side.Gt(100)                      // does not compile, StrCol has no Gt(float64)
t.Price.Contains("x")               // does not compile, F64Col has no Contains
t.Price.Sum().To(BarCols.Symbol)    // does not compile, float64 into a string column

TradeCols is fifteen lines generated by kumagen from the struct. The struct itself is generated too when the data came first: kumagen -from trades.parquet -type Trade reads the schema of a Parquet file, an Arrow file or a sample of a CSV, TSV or NDJSON one and writes the Go struct for it. You can also write both by hand. Details are in docs/03-typed-api.md.

Why

Every dataframe library identifies columns by string. df["price"], pl.col("price"). It is the obvious design and everyone copies it, and it is also the single biggest source of avoidable pain in day to day data work. A typo is a runtime error. A rename means grepping for a string literal and hoping. There is no autocomplete, no jump to definition, no find all references.

Python has an excuse, because there is no compile step to check anything at. Go does not have that excuse. A stringly typed dataframe API in Go is a slower pandas with worse ergonomics, and there would be no reason for anyone to use it.

So typed columns are the default here and strings are the escape hatch, rather than the other way around.

The other reasons to want this: a single static binary with no Python runtime to install, context.Context cancellation so a query dies when its HTTP request does, and an optimizer you can inspect with Explain instead of guessing.

What is planned

The full design is eleven documents in docs. The short version:

Arrow memory layout, our own kernels. The layout buys zero copy interop with pyarrow, Polars and DuckDB. The kernels are the thing this project exists to provide.

StringView by default. A 16 byte view with an inline prefix, so most string comparisons never touch the heap.

Lazy by default, eager when you want it. Same verbs on both, with projection pushdown, predicate pushdown, join reordering and partition pruning in between. Explain and Profile are shipping features, not debug tooling.

Parallel execution. Morsel driven, work stealing, partitioned hash aggregation, sized to GOMAXPROCS.

SIMD, as an optimization and never a dependency. The library builds, tests and works correctly on stock Go with no experiment flag. Every vectorized kernel has a scalar twin, and the twin is the specification.

Full pandas coverage. docs/06-pandas-parity.md is a checklist of the entire pandas API with a Go idiomatic equivalent for each entry. There are five genuine omissions and each one says what to use instead.

Bindings. A C ABI, a Python package and a TypeScript package for Node, Bun and Deno. Data crosses the boundary as Arrow and queries cross as a serialized plan, so a query over a hundred million rows makes about four foreign calls.

Interop

kuma/ipc reads and writes the Arrow IPC file and stream formats and implements the Arrow C Data Interface in both directions, which is how a column reaches pyarrow, Polars or DuckDB without being copied. It is part of this module and it needs nothing outside the standard library.

If you are already using apache/arrow-go, kuma/arrowgo converts between its types and kuma's directly. It is a module of its own, so go get github.com/tamnd/kuma/arrowgo brings arrow-go in and go get github.com/tamnd/kuma does not.

table, err := arrowgo.ImportRecordBatch(rec)   // shares the buffers, copies nothing
rec, err := arrowgo.ExportRecordBatch(table)   // and back again

Requirements

Go 1.27 or newer. This is not negotiable and it is not conservatism about new releases. The typed API is built on generic methods, which arrived in 1.27, and it cannot be expressed at all in 1.26.

GOEXPERIMENT=simd enables the vectorized kernels. Without it you get the scalar path, which is correct and tested and simply slower. Both are supported configurations and both run in CI.

Nothing else. The module depends on the standard library and nothing more, and the one package that needs a dependency is the arrow-go bridge above, which is a module of its own so that it stays out of your build unless you ask for it.

Documentation

00-README index, the decisions already made, prerequisites
01-research-2026 the state of Go, pandas, Polars and Arrow, and what each fact forces
02-architecture layers, memory, types, plan, optimizer, execution
03-typed-api typed columns and kumagen, read this one first
04-high-level-api what using it feels like, errors, Explain, display
05-simd-kernels the kernel layer and what to vectorize in what order
06-pandas-parity the conformance checklist
07-bindings C, Python, TypeScript
08-milestones M0 through M10 and the bindings track
09-quality-bar what standard library quality means here
10-benchmarks how the numbers get made
11-package-layout the tree and the stability tiers

Benchmarks

They live in tamnd/kuma-bench, against pandas and Polars, on db-benchmark and TPC-H, running from the first milestone rather than assembled at the end to support an announcement.

Two rules there. Every timed query forces materialization, because benchmarking a lazy engine without materializing the result measures nothing. And we publish the ones we lose, because a suite that only reports wins is an advertisement and readers correctly discount everything in it.

Contributing

Read CONTRIBUTING.md. The short version is that the quality bar is docs/09-quality-bar.md, the work is in the issue tracker grouped by milestone, and anything labelled good first issue is genuinely one.

The parts that parallelize well across contributors are the pandas surface area in M7 and M8. The engine work in M3 and M4 does not parallelize and is best left to fewer hands.

Name

Bear, in Japanese. It follows the pandas and Polars lineage without being a pun about it.

There is a name collision worth knowing about. kumahq/kuma is the Kong service mesh and it is well known in its own corner of the world. It is not a dataframe library and nobody is going to confuse the two in context.

License

Apache 2.0. See LICENSE.

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

Examples

Constants

View Source
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.

View Source
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.

View Source
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

View Source
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

func CanRead[T Value](dt dtype.DataType) bool

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

func ColumnName(field string) string

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

func DTypeOf[T Value]() dtype.DataType

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

func Dyn(name string) AnyCol[Dynamic]

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

func NewAnyCol[S any](name string) AnyCol[S]

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) Add

func (o AnyCol) Add(v any) AnyExpr[S]

Add returns the value plus v.

func (AnyCol) AddExpr

func (o AnyCol) AddExpr(x AnyValue[S]) AnyExpr[S]

AddExpr returns the value plus the value of x in the same row.

func (AnyCol[S]) Column

func (c AnyCol[S]) Column(f *Frame[S]) (Column, error)

Column returns the column itself, reporting an error if the frame has no such column.

func (AnyCol) Div

func (o AnyCol) Div(v any) AnyExpr[S]

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) DivExpr

func (o AnyCol) DivExpr(x AnyValue[S]) AnyExpr[S]

DivExpr returns the value divided by the value of x in the same row.

func (AnyCol) Eq

func (o AnyCol) Eq(v any) BoolExpr[S]

Eq returns whether the value equals v.

func (AnyCol) EqExpr

func (o AnyCol) EqExpr(x AnyValue[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (AnyCol) Ge

func (o AnyCol) Ge(v any) BoolExpr[S]

Ge returns whether the value is greater than or equal to v.

func (AnyCol) GeExpr

func (o AnyCol) GeExpr(x AnyValue[S]) BoolExpr[S]

GeExpr returns whether the value is at least the value of x in the same row.

func (AnyCol) Gt

func (o AnyCol) Gt(v any) BoolExpr[S]

Gt returns whether the value is greater than v.

func (AnyCol) GtExpr

func (o AnyCol) GtExpr(x AnyValue[S]) BoolExpr[S]

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) Le

func (o AnyCol) Le(v any) BoolExpr[S]

Le returns whether the value is less than or equal to v.

func (AnyCol) LeExpr

func (o AnyCol) LeExpr(x AnyValue[S]) BoolExpr[S]

LeExpr returns whether the value is at most the value of x in the same row.

func (AnyCol) Lt

func (o AnyCol) Lt(v any) BoolExpr[S]

Lt returns whether the value is less than v.

func (AnyCol) LtExpr

func (o AnyCol) LtExpr(x AnyValue[S]) BoolExpr[S]

LtExpr returns whether the value is less than the value of x in the same row.

func (AnyCol) Mod

func (o AnyCol) Mod(v any) AnyExpr[S]

Mod returns the remainder of the value divided by v.

func (AnyCol) ModExpr

func (o AnyCol) ModExpr(x AnyValue[S]) AnyExpr[S]

ModExpr returns the remainder of the value divided by the value of x in the same row.

func (AnyCol) Mul

func (o AnyCol) Mul(v any) AnyExpr[S]

Mul returns the value times v.

func (AnyCol) MulExpr

func (o AnyCol) MulExpr(x AnyValue[S]) AnyExpr[S]

MulExpr returns the value times the value of x in the same row.

func (AnyCol[S]) Name

func (c AnyCol[S]) Name() string

Name returns the column the handle names.

func (AnyCol) Ne

func (o AnyCol) Ne(v any) BoolExpr[S]

Ne returns whether the value differs from v.

func (AnyCol) NeExpr

func (o AnyCol) NeExpr(x AnyValue[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (AnyCol) String

func (o AnyCol) String() string

String returns the expression as it would be written.

func (AnyCol) Sub

func (o AnyCol) Sub(v any) AnyExpr[S]

Sub returns the value minus v.

func (AnyCol) SubExpr

func (o AnyCol) SubExpr(x AnyValue[S]) AnyExpr[S]

SubExpr returns the value minus 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

func Lit(v any) AnyExpr[Dynamic]

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 NewLit

func NewLit[S any](v any) AnyExpr[S]

NewLit is Lit for a frame with schema S.

func (AnyExpr) Add

func (o AnyExpr) Add(v any) AnyExpr[S]

Add returns the value plus v.

func (AnyExpr) AddExpr

func (o AnyExpr) AddExpr(x AnyValue[S]) AnyExpr[S]

AddExpr returns the value plus the value of x in the same row.

func (AnyExpr) Div

func (o AnyExpr) Div(v any) AnyExpr[S]

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) DivExpr

func (o AnyExpr) DivExpr(x AnyValue[S]) AnyExpr[S]

DivExpr returns the value divided by the value of x in the same row.

func (AnyExpr) Eq

func (o AnyExpr) Eq(v any) BoolExpr[S]

Eq returns whether the value equals v.

func (AnyExpr) EqExpr

func (o AnyExpr) EqExpr(x AnyValue[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (AnyExpr) Ge

func (o AnyExpr) Ge(v any) BoolExpr[S]

Ge returns whether the value is greater than or equal to v.

func (AnyExpr) GeExpr

func (o AnyExpr) GeExpr(x AnyValue[S]) BoolExpr[S]

GeExpr returns whether the value is at least the value of x in the same row.

func (AnyExpr) Gt

func (o AnyExpr) Gt(v any) BoolExpr[S]

Gt returns whether the value is greater than v.

func (AnyExpr) GtExpr

func (o AnyExpr) GtExpr(x AnyValue[S]) BoolExpr[S]

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) Le

func (o AnyExpr) Le(v any) BoolExpr[S]

Le returns whether the value is less than or equal to v.

func (AnyExpr) LeExpr

func (o AnyExpr) LeExpr(x AnyValue[S]) BoolExpr[S]

LeExpr returns whether the value is at most the value of x in the same row.

func (AnyExpr) Lt

func (o AnyExpr) Lt(v any) BoolExpr[S]

Lt returns whether the value is less than v.

func (AnyExpr) LtExpr

func (o AnyExpr) LtExpr(x AnyValue[S]) BoolExpr[S]

LtExpr returns whether the value is less than the value of x in the same row.

func (AnyExpr) Mod

func (o AnyExpr) Mod(v any) AnyExpr[S]

Mod returns the remainder of the value divided by v.

func (AnyExpr) ModExpr

func (o AnyExpr) ModExpr(x AnyValue[S]) AnyExpr[S]

ModExpr returns the remainder of the value divided by the value of x in the same row.

func (AnyExpr) Mul

func (o AnyExpr) Mul(v any) AnyExpr[S]

Mul returns the value times v.

func (AnyExpr) MulExpr

func (o AnyExpr) MulExpr(x AnyValue[S]) AnyExpr[S]

MulExpr returns the value times the value of x in the same row.

func (AnyExpr) Ne

func (o AnyExpr) Ne(v any) BoolExpr[S]

Ne returns whether the value differs from v.

func (AnyExpr) NeExpr

func (o AnyExpr) NeExpr(x AnyValue[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (AnyExpr) String

func (o AnyExpr) String() string

String returns the expression as it would be written.

func (AnyExpr) Sub

func (o AnyExpr) Sub(v any) AnyExpr[S]

Sub returns the value minus v.

func (AnyExpr) SubExpr

func (o AnyExpr) SubExpr(x AnyValue[S]) AnyExpr[S]

SubExpr returns the value minus the value of x in the same row.

type AnyValue

type AnyValue[S any] interface {
	Expr[S]
	// contains filtered or unexported methods
}

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

func Bool(name string) BoolCol[Dynamic]

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

func NewBoolCol[S any](name string) BoolCol[S]

NewBoolCol returns a handle on the boolean column called name in a frame with schema S.

func (BoolCol) And

func (o BoolCol) And(x BoolValue[S]) BoolExpr[S]

And returns whether both this and x hold in the same row.

func (BoolCol) Eq

func (o BoolCol) Eq(v bool) BoolExpr[S]

Eq returns whether the value equals v.

func (BoolCol) EqExpr

func (o BoolCol) EqExpr(x BoolValue[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

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[S]) Name

func (c BoolCol[S]) Name() string

Name returns the column the handle names.

func (BoolCol) Ne

func (o BoolCol) Ne(v bool) BoolExpr[S]

Ne returns whether the value differs from v.

func (BoolCol) NeExpr

func (o BoolCol) NeExpr(x BoolValue[S]) BoolExpr[S]

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.

func (BoolCol) Or

func (o BoolCol) Or(x BoolValue[S]) BoolExpr[S]

Or returns whether this or x holds in the same row.

func (BoolCol[S]) Series

func (c BoolCol[S]) Series(f *Frame[S]) (Series[bool], error)

Series returns the column as a Series[bool], reporting an error if the frame has no such column or holds something else there.

func (BoolCol) String

func (o BoolCol) String() string

String returns the condition as it would be written.

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) And

func (o BoolExpr) And(x BoolValue[S]) BoolExpr[S]

And returns whether both this and x hold in the same row.

func (BoolExpr) Eq

func (o BoolExpr) Eq(v bool) BoolExpr[S]

Eq returns whether the value equals v.

func (BoolExpr) EqExpr

func (o BoolExpr) EqExpr(x BoolValue[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

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) Ne

func (o BoolExpr) Ne(v bool) BoolExpr[S]

Ne returns whether the value differs from v.

func (BoolExpr) NeExpr

func (o BoolExpr) NeExpr(x BoolValue[S]) BoolExpr[S]

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.

func (BoolExpr) Or

func (o BoolExpr) Or(x BoolValue[S]) BoolExpr[S]

Or returns whether this or x holds in the same row.

func (BoolExpr) String

func (o BoolExpr) String() string

String returns the condition as it would be written.

type BoolValue

type BoolValue[S any] interface {
	Expr[S]
	// contains filtered or unexported methods
}

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

type By struct {
	Name string
	Order
}

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.

func Asc

func Asc(name string) By

Asc returns a By that sorts the named column in ascending order, with the nulls at the end.

func Desc

func Desc(name string) By

Desc returns a By that sorts the named column in descending order, with the nulls at the end.

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 NewColumn

func NewColumn(name string, data *array.Chunked) (Column, error)

NewColumn returns a column of the given name over the given values.

func (Column) As

func (c Column) As[T Value]() (Series[T], error)

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

func (c Column) Cast(to dtype.DataType) (Column, error)

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) DType

func (c Column) DType() dtype.DataType

DType returns the type of the values.

func (Column) Data

func (c Column) Data() *array.Chunked

Data returns the values underneath.

func (Column) DropNulls

func (c Column) DropNulls() Column

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

func (c Column) Field() dtype.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

func (c Column) FillNull[T Value](v T) (Column, error)

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) HasNulls

func (c Column) HasNulls() bool

HasNulls reports whether anything in the column is missing.

func (Column) IsNull

func (c Column) IsNull(i int) bool

IsNull reports whether value i is missing. It panics if i is out of range.

func (Column) IsValid

func (c Column) IsValid(i int) bool

IsValid reports whether value i is present. It panics if i is out of range.

func (Column) Len

func (c Column) Len() int

Len returns how many values the column holds.

func (Column) MustAs

func (c Column) MustAs[T Value]() Series[T]

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) Name

func (c Column) Name() string

Name returns the name of the column.

func (Column) NullCount

func (c Column) NullCount() int

NullCount returns how many of them are missing.

func (Column) NullMask

func (c Column) NullMask() Column

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) Rename

func (c Column) Rename(name string) Column

Rename returns the same column under a different name.

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

func (c Column) Slice(i, j int) Column

Slice returns the values from i up to but not including j, as a column. It panics unless 0 <= i <= j <= Len.

func (Column) Sort

func (c Column) Sort(o Order) (Column, error)

Sort returns the values of the column in the order o describes.

func (Column) String

func (c Column) String() string

String returns the column as a table of one column. It follows the same rules as Frame.String.

func (Column) Take

func (c Column) Take(idx []int) Column

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

func (c Column) TryCast(to dtype.DataType) (Column, error)

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

func (c Column) ValidMask() Column

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

type Expr[S any] interface {
	fmt.Stringer
	// contains filtered or unexported methods
}

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

func F64(name string) F64Col[Dynamic]

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

func NewF64Col[S any](name string) F64Col[S]

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) Add

func (o F64Col) Add(v float64) F64Expr[S]

Add returns the value plus v.

func (F64Col) AddExpr

func (o F64Col) AddExpr(x F64Value[S]) F64Expr[S]

AddExpr returns the value plus the value of x in the same row.

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

func (o F64Col) Div(v float64) F64Expr[S]

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) DivExpr

func (o F64Col) DivExpr(x F64Value[S]) F64Expr[S]

DivExpr returns the value divided by the value of x in the same row.

func (F64Col) Eq

func (o F64Col) Eq(v float64) BoolExpr[S]

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) EqExpr

func (o F64Col) EqExpr(x F64Value[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (F64Col) Ge

func (o F64Col) Ge(v float64) BoolExpr[S]

Ge returns whether the value is greater than or equal to v.

func (F64Col) GeExpr

func (o F64Col) GeExpr(x F64Value[S]) BoolExpr[S]

GeExpr returns whether the value is at least the value of x in the same row.

func (F64Col) Gt

func (o F64Col) Gt(v float64) BoolExpr[S]

Gt returns whether the value is greater than v.

func (F64Col) GtExpr

func (o F64Col) GtExpr(x F64Value[S]) BoolExpr[S]

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) Le

func (o F64Col) Le(v float64) BoolExpr[S]

Le returns whether the value is less than or equal to v.

func (F64Col) LeExpr

func (o F64Col) LeExpr(x F64Value[S]) BoolExpr[S]

LeExpr returns whether the value is at most the value of x in the same row.

func (F64Col) Lt

func (o F64Col) Lt(v float64) BoolExpr[S]

Lt returns whether the value is less than v.

func (F64Col) LtExpr

func (o F64Col) LtExpr(x F64Value[S]) BoolExpr[S]

LtExpr returns whether the value is less than the value of x in the same row.

func (F64Col) Mod

func (o F64Col) Mod(v float64) F64Expr[S]

Mod returns the remainder of the value divided by v, which is math.Mod.

func (F64Col) ModExpr

func (o F64Col) ModExpr(x F64Value[S]) F64Expr[S]

ModExpr returns the remainder of the value divided by the value of x in the same row.

func (F64Col) Mul

func (o F64Col) Mul(v float64) F64Expr[S]

Mul returns the value times v.

func (F64Col) MulExpr

func (o F64Col) MulExpr(x F64Value[S]) F64Expr[S]

MulExpr returns the value times the value of x in the same row.

func (F64Col[S]) Name

func (c F64Col[S]) Name() string

Name returns the column the handle names.

func (F64Col) Ne

func (o F64Col) Ne(v float64) BoolExpr[S]

Ne returns whether the value differs from v.

func (F64Col) NeExpr

func (o F64Col) NeExpr(x F64Value[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (F64Col[S]) Series

func (c F64Col[S]) Series(f *Frame[S]) (Series[float64], error)

Series returns the column as a Series[float64], reporting an error if the frame has no such column or holds something else there.

func (F64Col) String

func (o F64Col) String() string

String returns the expression as it would be written.

func (F64Col) Sub

func (o F64Col) Sub(v float64) F64Expr[S]

Sub returns the value minus v.

func (F64Col) SubExpr

func (o F64Col) SubExpr(x F64Value[S]) F64Expr[S]

SubExpr returns the value minus the value of x in the same row.

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) Add

func (o F64Expr) Add(v float64) F64Expr[S]

Add returns the value plus v.

func (F64Expr) AddExpr

func (o F64Expr) AddExpr(x F64Value[S]) F64Expr[S]

AddExpr returns the value plus the value of x in the same row.

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

func (o F64Expr) Div(v float64) F64Expr[S]

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) DivExpr

func (o F64Expr) DivExpr(x F64Value[S]) F64Expr[S]

DivExpr returns the value divided by the value of x in the same row.

func (F64Expr) Eq

func (o F64Expr) Eq(v float64) BoolExpr[S]

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) EqExpr

func (o F64Expr) EqExpr(x F64Value[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (F64Expr) Ge

func (o F64Expr) Ge(v float64) BoolExpr[S]

Ge returns whether the value is greater than or equal to v.

func (F64Expr) GeExpr

func (o F64Expr) GeExpr(x F64Value[S]) BoolExpr[S]

GeExpr returns whether the value is at least the value of x in the same row.

func (F64Expr) Gt

func (o F64Expr) Gt(v float64) BoolExpr[S]

Gt returns whether the value is greater than v.

func (F64Expr) GtExpr

func (o F64Expr) GtExpr(x F64Value[S]) BoolExpr[S]

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) Le

func (o F64Expr) Le(v float64) BoolExpr[S]

Le returns whether the value is less than or equal to v.

func (F64Expr) LeExpr

func (o F64Expr) LeExpr(x F64Value[S]) BoolExpr[S]

LeExpr returns whether the value is at most the value of x in the same row.

func (F64Expr) Lt

func (o F64Expr) Lt(v float64) BoolExpr[S]

Lt returns whether the value is less than v.

func (F64Expr) LtExpr

func (o F64Expr) LtExpr(x F64Value[S]) BoolExpr[S]

LtExpr returns whether the value is less than the value of x in the same row.

func (F64Expr) Mod

func (o F64Expr) Mod(v float64) F64Expr[S]

Mod returns the remainder of the value divided by v, which is math.Mod.

func (F64Expr) ModExpr

func (o F64Expr) ModExpr(x F64Value[S]) F64Expr[S]

ModExpr returns the remainder of the value divided by the value of x in the same row.

func (F64Expr) Mul

func (o F64Expr) Mul(v float64) F64Expr[S]

Mul returns the value times v.

func (F64Expr) MulExpr

func (o F64Expr) MulExpr(x F64Value[S]) F64Expr[S]

MulExpr returns the value times the value of x in the same row.

func (F64Expr) Ne

func (o F64Expr) Ne(v float64) BoolExpr[S]

Ne returns whether the value differs from v.

func (F64Expr) NeExpr

func (o F64Expr) NeExpr(x F64Value[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (F64Expr) String

func (o F64Expr) String() string

String returns the expression as it would be written.

func (F64Expr) Sub

func (o F64Expr) Sub(v float64) F64Expr[S]

Sub returns the value minus v.

func (F64Expr) SubExpr

func (o F64Expr) SubExpr(x F64Value[S]) F64Expr[S]

SubExpr returns the value minus the value of x in the same row.

type F64Value

type F64Value[S any] interface {
	Expr[S]
	// contains filtered or unexported methods
}

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

func Bind[S any](f *Frame[Dynamic]) (*Frame[S], error)

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

func Concat[S any](frames ...*Frame[S]) (*Frame[S], error)

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

func ConcatUnion(frames ...*Frame[Dynamic]) (*Frame[Dynamic], error)

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

func HStack(frames ...*Frame[Dynamic]) (*Frame[Dynamic], error)

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

func NewFrame(cols ...Column) (*Frame[Dynamic], error)

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

func ReadCSV(r io.Reader, opts *csv.Options) (*Frame[Dynamic], error)

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

func ReadCSVFile(path string, opts *csv.Options) (*Frame[Dynamic], error)

ReadCSVFile reads the file at path. It is ReadCSV over an open file.

func ReadDataset added in v0.0.23

func ReadDataset(root string, opts *dataset.Options) (*Frame[Dynamic], error)

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

func ReadDatasetFiles(d *dataset.Dataset) (*Frame[Dynamic], error)

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

func ReadNDJSON(r io.Reader, opts *ndjson.Options) (*Frame[Dynamic], error)

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

func ReadNDJSONFile(path string, opts *ndjson.Options) (*Frame[Dynamic], error)

ReadNDJSONFile reads the file at path. It is ReadNDJSON over an open file.

func ReadParquet added in v0.0.15

func ReadParquet(r io.ReaderAt, size int64, opts *parquet.Options) (*Frame[Dynamic], error)

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

func ReadParquetFile(path string, opts *parquet.Options) (*Frame[Dynamic], error)

ReadParquetFile reads the file at path. It is ReadParquet over an open file, which is where the size comes from.

func (*Frame[S]) Cast

func (f *Frame[S]) Cast(name string, to dtype.DataType) (*Frame[Dynamic], error)

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]) Column

func (f *Frame[S]) Column(name string) (Column, error)

Column returns the column with the given name.

func (*Frame[S]) ColumnAt

func (f *Frame[S]) ColumnAt(i int) Column

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

func (f *Frame[S]) Columns() []Column

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

func (f *Frame[S]) CrossJoin(other *Frame[Dynamic]) (*Frame[Dynamic], error)

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

func (f *Frame[S]) Drop(names ...string) (*Frame[Dynamic], error)

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

func (f *Frame[S]) DropNulls(names ...string) (*Frame[S], error)

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

func (f *Frame[S]) Eval(e Expr[S]) (Column, error)

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

func (f *Frame[S]) FillNull[T Value](name string, v T) (*Frame[Dynamic], error)

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

func (f *Frame[S]) Filter(cond BoolValue[S]) (*Frame[S], error)

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

func (f *Frame[S]) FilterMask(mask Series[bool]) (*Frame[S], error)

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]) HasNulls

func (f *Frame[S]) HasNulls() bool

HasNulls reports whether anything in the frame is missing.

func (*Frame[S]) Head

func (f *Frame[S]) Head(n int) *Frame[S]

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

func (f *Frame[S]) Index(name string) int

Index returns the position of the column with the given name, or -1 if the frame has no such column.

func (*Frame[S]) InnerJoin

func (f *Frame[S]) InnerJoin(other *Frame[Dynamic], names ...string) (*Frame[Dynamic], error)

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

func (f *Frame[S]) IsNotNull() *Frame[Dynamic]

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

func (f *Frame[S]) IsNull() *Frame[Dynamic]

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

func (f *Frame[S]) Join(other *Frame[Dynamic], on []On, how JoinType) (*Frame[Dynamic], error)

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

func (f *Frame[S]) KeepAtLeast(present int, names ...string) (*Frame[S], error)

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

func (f *Frame[S]) LeftJoin(other *Frame[Dynamic], names ...string) (*Frame[Dynamic], error)

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]) Names

func (f *Frame[S]) Names() []string

Names returns the column names in order.

func (*Frame[S]) NullCounts

func (f *Frame[S]) NullCounts() []int

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]) NumCols

func (f *Frame[S]) NumCols() int

NumCols returns how many columns it has.

func (*Frame[S]) NumRows

func (f *Frame[S]) NumRows() int

NumRows returns how many rows the frame has.

func (*Frame[S]) Rename

func (f *Frame[S]) Rename(from, to string) (*Frame[Dynamic], error)

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

func (f *Frame[S]) Schema() dtype.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

func (f *Frame[S]) Select(names ...string) (*Frame[Dynamic], error)

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

func (f *Frame[S]) Series[T Value](name string) (Series[T], error)

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

func (f *Frame[S]) Shape() (rows, cols int)

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

func (f *Frame[S]) Slice(i, j int) *Frame[S]

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

func (f *Frame[S]) Sort(by ...By) (*Frame[S], error)

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

func (f *Frame[S]) SortBy(names ...string) (*Frame[S], error)

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

func (f *Frame[S]) SortDesc(names ...string) (*Frame[S], error)

SortDesc returns the rows in descending order of the named columns, with the nulls at the end.

func (*Frame[S]) SortIndex

func (f *Frame[S]) SortIndex(by ...By) ([]int, error)

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

func (f *Frame[S]) String() 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

func (f *Frame[S]) Tail(n int) *Frame[S]

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

func (f *Frame[S]) Take(idx []int) *Frame[S]

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]) TryCast

func (f *Frame[S]) TryCast(name string, to dtype.DataType) (*Frame[Dynamic], error)

TryCast is Cast with a value that will not fit becoming a null.

func (*Frame[S]) WithColumn

func (f *Frame[S]) WithColumn(c Column) (*Frame[Dynamic], error)

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

func (f *Frame[S]) WithExpr(name string, e Expr[S]) (*Frame[Dynamic], error)

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

func (f *Frame[S]) WriteCSV(w io.Writer, opts *csv.WriteOptions) error

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

func (f *Frame[S]) WriteNDJSON(w io.Writer, opts *ndjson.WriteOptions) error

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 I64

func I64(name string) I64Col[Dynamic]

I64 returns a handle on an int64 column of a frame with no schema behind it.

func NewI64Col

func NewI64Col[S any](name string) I64Col[S]

NewI64Col returns a handle on the int64 column called name in a frame with schema S.

func (I64Col) Add

func (o I64Col) Add(v int64) I64Expr[S]

Add returns the value plus v, wrapping the way Go wraps.

func (I64Col) AddExpr

func (o I64Col) AddExpr(x I64Value[S]) I64Expr[S]

AddExpr returns the value plus the value of x in the same row.

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

func (o I64Col) Div(v int64) I64Expr[S]

Div returns the value divided by v, truncated toward zero. Dividing by zero is an error naming the row.

func (I64Col) DivExpr

func (o I64Col) DivExpr(x I64Value[S]) I64Expr[S]

DivExpr returns the value divided by the value of x in the same row.

func (I64Col) Eq

func (o I64Col) Eq(v int64) BoolExpr[S]

Eq returns whether the value equals v.

func (I64Col) EqExpr

func (o I64Col) EqExpr(x I64Value[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (I64Col) Ge

func (o I64Col) Ge(v int64) BoolExpr[S]

Ge returns whether the value is greater than or equal to v.

func (I64Col) GeExpr

func (o I64Col) GeExpr(x I64Value[S]) BoolExpr[S]

GeExpr returns whether the value is at least the value of x in the same row.

func (I64Col) Gt

func (o I64Col) Gt(v int64) BoolExpr[S]

Gt returns whether the value is greater than v.

func (I64Col) GtExpr

func (o I64Col) GtExpr(x I64Value[S]) BoolExpr[S]

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) Le

func (o I64Col) Le(v int64) BoolExpr[S]

Le returns whether the value is less than or equal to v.

func (I64Col) LeExpr

func (o I64Col) LeExpr(x I64Value[S]) BoolExpr[S]

LeExpr returns whether the value is at most the value of x in the same row.

func (I64Col) Lt

func (o I64Col) Lt(v int64) BoolExpr[S]

Lt returns whether the value is less than v.

func (I64Col) LtExpr

func (o I64Col) LtExpr(x I64Value[S]) BoolExpr[S]

LtExpr returns whether the value is less than the value of x in the same row.

func (I64Col) Mod

func (o I64Col) Mod(v int64) I64Expr[S]

Mod returns the remainder of the value divided by v.

func (I64Col) ModExpr

func (o I64Col) ModExpr(x I64Value[S]) I64Expr[S]

ModExpr returns the remainder of the value divided by the value of x in the same row.

func (I64Col) Mul

func (o I64Col) Mul(v int64) I64Expr[S]

Mul returns the value times v.

func (I64Col) MulExpr

func (o I64Col) MulExpr(x I64Value[S]) I64Expr[S]

MulExpr returns the value times the value of x in the same row.

func (I64Col[S]) Name

func (c I64Col[S]) Name() string

Name returns the column the handle names.

func (I64Col) Ne

func (o I64Col) Ne(v int64) BoolExpr[S]

Ne returns whether the value differs from v.

func (I64Col) NeExpr

func (o I64Col) NeExpr(x I64Value[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (I64Col[S]) Series

func (c I64Col[S]) Series(f *Frame[S]) (Series[int64], error)

Series returns the column as a Series[int64], reporting an error if the frame has no such column or holds something else there.

func (I64Col) String

func (o I64Col) String() string

String returns the expression as it would be written.

func (I64Col) Sub

func (o I64Col) Sub(v int64) I64Expr[S]

Sub returns the value minus v.

func (I64Col) SubExpr

func (o I64Col) SubExpr(x I64Value[S]) I64Expr[S]

SubExpr returns the value minus the value of x in the same row.

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) Add

func (o I64Expr) Add(v int64) I64Expr[S]

Add returns the value plus v, wrapping the way Go wraps.

func (I64Expr) AddExpr

func (o I64Expr) AddExpr(x I64Value[S]) I64Expr[S]

AddExpr returns the value plus the value of x in the same row.

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

func (o I64Expr) Div(v int64) I64Expr[S]

Div returns the value divided by v, truncated toward zero. Dividing by zero is an error naming the row.

func (I64Expr) DivExpr

func (o I64Expr) DivExpr(x I64Value[S]) I64Expr[S]

DivExpr returns the value divided by the value of x in the same row.

func (I64Expr) Eq

func (o I64Expr) Eq(v int64) BoolExpr[S]

Eq returns whether the value equals v.

func (I64Expr) EqExpr

func (o I64Expr) EqExpr(x I64Value[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (I64Expr) Ge

func (o I64Expr) Ge(v int64) BoolExpr[S]

Ge returns whether the value is greater than or equal to v.

func (I64Expr) GeExpr

func (o I64Expr) GeExpr(x I64Value[S]) BoolExpr[S]

GeExpr returns whether the value is at least the value of x in the same row.

func (I64Expr) Gt

func (o I64Expr) Gt(v int64) BoolExpr[S]

Gt returns whether the value is greater than v.

func (I64Expr) GtExpr

func (o I64Expr) GtExpr(x I64Value[S]) BoolExpr[S]

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) Le

func (o I64Expr) Le(v int64) BoolExpr[S]

Le returns whether the value is less than or equal to v.

func (I64Expr) LeExpr

func (o I64Expr) LeExpr(x I64Value[S]) BoolExpr[S]

LeExpr returns whether the value is at most the value of x in the same row.

func (I64Expr) Lt

func (o I64Expr) Lt(v int64) BoolExpr[S]

Lt returns whether the value is less than v.

func (I64Expr) LtExpr

func (o I64Expr) LtExpr(x I64Value[S]) BoolExpr[S]

LtExpr returns whether the value is less than the value of x in the same row.

func (I64Expr) Mod

func (o I64Expr) Mod(v int64) I64Expr[S]

Mod returns the remainder of the value divided by v.

func (I64Expr) ModExpr

func (o I64Expr) ModExpr(x I64Value[S]) I64Expr[S]

ModExpr returns the remainder of the value divided by the value of x in the same row.

func (I64Expr) Mul

func (o I64Expr) Mul(v int64) I64Expr[S]

Mul returns the value times v.

func (I64Expr) MulExpr

func (o I64Expr) MulExpr(x I64Value[S]) I64Expr[S]

MulExpr returns the value times the value of x in the same row.

func (I64Expr) Ne

func (o I64Expr) Ne(v int64) BoolExpr[S]

Ne returns whether the value differs from v.

func (I64Expr) NeExpr

func (o I64Expr) NeExpr(x I64Value[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (I64Expr) String

func (o I64Expr) String() string

String returns the expression as it would be written.

func (I64Expr) Sub

func (o I64Expr) Sub(v int64) I64Expr[S]

Sub returns the value minus v.

func (I64Expr) SubExpr

func (o I64Expr) SubExpr(x I64Value[S]) I64Expr[S]

SubExpr returns the value minus the value of x in the same row.

type I64Value

type I64Value[S any] interface {
	Expr[S]
	// contains filtered or unexported methods
}

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

type JoinType = kernel.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

type On struct {
	Left  string
	Right string
}

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

func Using

func Using(names ...string) []On

Using returns the join keys for columns that are called the same thing on both sides.

type Order

type Order struct {
	Descending bool
	NullsFirst bool
}

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

func NewSeries[T Value](name string, values ...T) Series[T]

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

func SeriesFrom[T Value](name string, data *array.Chunked) (Series[T], error)

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

func (s Series[T]) Cast[U Value](to dtype.DataType) (Series[U], error)

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

func (s Series[T]) Column() Column

Column returns the series as an untyped column, which is what a Frame holds.

func (Series[T]) DType

func (s Series[T]) DType() dtype.DataType

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

func (s Series[T]) Data() *array.Chunked

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]) DropNulls

func (s Series[T]) DropNulls() Series[T]

DropNulls returns the series with the missing values taken out.

func (Series[T]) FillNull

func (s Series[T]) FillNull(v T) Series[T]

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

func (s Series[T]) Filter(mask Series[bool]) (Series[T], error)

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]) HasNulls

func (s Series[T]) HasNulls() bool

HasNulls reports whether anything in the series is missing.

func (Series[T]) Head

func (s Series[T]) Head(n int) Series[T]

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

func (s Series[T]) IsNull(i int) bool

IsNull reports whether value i is missing. It panics if i is out of range.

func (Series[T]) IsValid

func (s Series[T]) IsValid(i int) bool

IsValid reports whether value i is present. It panics if i is out of range.

func (Series[T]) Len

func (s Series[T]) Len() int

Len returns how many values the column holds.

func (Series[T]) Name

func (s Series[T]) Name() string

Name returns the name of the column.

func (Series[T]) NullCount

func (s Series[T]) NullCount() int

NullCount returns how many of them are missing.

func (Series[T]) NullMask

func (s Series[T]) NullMask() Series[bool]

NullMask returns a boolean series that is true where this one has no value.

func (Series[T]) Rename

func (s Series[T]) Rename(name string) Series[T]

Rename returns the same column under a different name.

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

func (s Series[T]) Slice(i, j int) Series[T]

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

func (s Series[T]) Sort(o Order) (Series[T], error)

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

func (s Series[T]) SortIndex(o Order) ([]int, error)

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

func (s Series[T]) String() string

String returns the series as a table of one column. It follows the same rules as Frame.String.

func (Series[T]) Tail

func (s Series[T]) Tail(n int) Series[T]

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

func (s Series[T]) Take(idx []int) Series[T]

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]) TryCast

func (s Series[T]) TryCast[U Value](to dtype.DataType) (Series[U], error)

TryCast is Cast with a value that will not fit becoming a null.

func (Series[T]) ValidMask

func (s Series[T]) ValidMask() Series[bool]

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

func (s Series[T]) Validity() (*bitmap.Bitmap, bool)

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

func (s Series[T]) Value(i int) T

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

func NewStrCol[S any](name string) StrCol[S]

NewStrCol returns a handle on the string column called name in a frame with schema S.

func Str

func Str(name string) StrCol[Dynamic]

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) Eq

func (o StrCol) Eq(v string) BoolExpr[S]

Eq returns whether the value equals v.

func (StrCol) EqExpr

func (o StrCol) EqExpr(x StrValue[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (StrCol) Ge

func (o StrCol) Ge(v string) BoolExpr[S]

Ge returns whether the value sorts after v or equals it.

func (StrCol) GeExpr

func (o StrCol) GeExpr(x StrValue[S]) BoolExpr[S]

GeExpr returns whether the value does not sort before the value of x in the same row.

func (StrCol) Gt

func (o StrCol) Gt(v string) BoolExpr[S]

Gt returns whether the value sorts after v.

func (StrCol) GtExpr

func (o StrCol) GtExpr(x StrValue[S]) BoolExpr[S]

GtExpr returns whether the value sorts after 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) Le

func (o StrCol) Le(v string) BoolExpr[S]

Le returns whether the value sorts before v or equals it.

func (StrCol) LeExpr

func (o StrCol) LeExpr(x StrValue[S]) BoolExpr[S]

LeExpr returns whether the value does not sort after the value of x in the same row.

func (StrCol) Lt

func (o StrCol) Lt(v string) BoolExpr[S]

Lt returns whether the value sorts before v.

func (StrCol) LtExpr

func (o StrCol) LtExpr(x StrValue[S]) BoolExpr[S]

LtExpr returns whether the value sorts before the value of x in the same row.

func (StrCol[S]) Name

func (c StrCol[S]) Name() string

Name returns the column the handle names.

func (StrCol) Ne

func (o StrCol) Ne(v string) BoolExpr[S]

Ne returns whether the value differs from v.

func (StrCol) NeExpr

func (o StrCol) NeExpr(x StrValue[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (StrCol[S]) Series

func (c StrCol[S]) Series(f *Frame[S]) (Series[string], error)

Series returns the column as a Series[string], reporting an error if the frame has no such column or holds something else there.

func (StrCol) String

func (o StrCol) String() string

String returns the expression as it would be written.

type StrExpr

type StrExpr[S any] struct {
	// contains filtered or unexported fields
}

StrExpr is a string valued expression.

func (StrExpr) Eq

func (o StrExpr) Eq(v string) BoolExpr[S]

Eq returns whether the value equals v.

func (StrExpr) EqExpr

func (o StrExpr) EqExpr(x StrValue[S]) BoolExpr[S]

EqExpr returns whether the value equals the value of x in the same row.

func (StrExpr) Ge

func (o StrExpr) Ge(v string) BoolExpr[S]

Ge returns whether the value sorts after v or equals it.

func (StrExpr) GeExpr

func (o StrExpr) GeExpr(x StrValue[S]) BoolExpr[S]

GeExpr returns whether the value does not sort before the value of x in the same row.

func (StrExpr) Gt

func (o StrExpr) Gt(v string) BoolExpr[S]

Gt returns whether the value sorts after v.

func (StrExpr) GtExpr

func (o StrExpr) GtExpr(x StrValue[S]) BoolExpr[S]

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) Le

func (o StrExpr) Le(v string) BoolExpr[S]

Le returns whether the value sorts before v or equals it.

func (StrExpr) LeExpr

func (o StrExpr) LeExpr(x StrValue[S]) BoolExpr[S]

LeExpr returns whether the value does not sort after the value of x in the same row.

func (StrExpr) Lt

func (o StrExpr) Lt(v string) BoolExpr[S]

Lt returns whether the value sorts before v.

func (StrExpr) LtExpr

func (o StrExpr) LtExpr(x StrValue[S]) BoolExpr[S]

LtExpr returns whether the value sorts before the value of x in the same row.

func (StrExpr) Ne

func (o StrExpr) Ne(v string) BoolExpr[S]

Ne returns whether the value differs from v.

func (StrExpr) NeExpr

func (o StrExpr) NeExpr(x StrValue[S]) BoolExpr[S]

NeExpr returns whether the value differs from the value of x in the same row.

func (StrExpr) String

func (o StrExpr) String() string

String returns the expression as it would be written.

type StrValue

type StrValue[S any] interface {
	Expr[S]
	// contains filtered or unexported methods
}

StrValue is a string valued piece of an expression, which is a StrCol or a StrExpr.

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

func NewTimeCol[S any](name string) TimeCol[S]

NewTimeCol returns a handle on the timestamp column called name in a frame with schema S.

func Time

func Time(name string) TimeCol[Dynamic]

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) After

func (o TimeCol) After(t time.Time) BoolExpr[S]

After returns whether the value is later than t.

func (TimeCol) AfterExpr

func (o TimeCol) AfterExpr(x TimeValue[S]) BoolExpr[S]

AfterExpr returns whether the value is later than the value of x in the same row.

func (TimeCol) AtOrAfter

func (o TimeCol) AtOrAfter(t time.Time) BoolExpr[S]

AtOrAfter returns whether the value is t or later.

func (TimeCol) AtOrAfterExpr

func (o TimeCol) AtOrAfterExpr(x TimeValue[S]) BoolExpr[S]

AtOrAfterExpr returns whether the value is no earlier than the value of x in the same row.

func (TimeCol) AtOrBefore

func (o TimeCol) AtOrBefore(t time.Time) BoolExpr[S]

AtOrBefore returns whether the value is t or earlier.

func (TimeCol) AtOrBeforeExpr

func (o TimeCol) AtOrBeforeExpr(x TimeValue[S]) BoolExpr[S]

AtOrBeforeExpr returns whether the value is no later than the value of x in the same row.

func (TimeCol) Before

func (o TimeCol) Before(t time.Time) BoolExpr[S]

Before returns whether the value is earlier than t.

func (TimeCol) BeforeExpr

func (o TimeCol) BeforeExpr(x TimeValue[S]) BoolExpr[S]

BeforeExpr returns whether the value is earlier than the value of x in the same row.

func (TimeCol) Eq

func (o TimeCol) Eq(t time.Time) BoolExpr[S]

Eq returns whether the value is the instant t.

func (TimeCol) EqExpr

func (o TimeCol) EqExpr(x TimeValue[S]) BoolExpr[S]

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[S]) Name

func (c TimeCol[S]) Name() string

Name returns the column the handle names.

func (TimeCol) Ne

func (o TimeCol) Ne(t time.Time) BoolExpr[S]

Ne returns whether the value is any instant other than t.

func (TimeCol) NeExpr

func (o TimeCol) NeExpr(x TimeValue[S]) BoolExpr[S]

NeExpr returns whether the value is a different instant from the value of x in the same row.

func (TimeCol[S]) Series

func (c TimeCol[S]) Series(f *Frame[S]) (Series[time.Time], error)

Series returns the column as a Series[time.Time], reporting an error if the frame has no such column or holds something else there.

func (TimeCol) String

func (o TimeCol) String() string

String returns the expression as it would be written.

type TimeExpr

type TimeExpr[S any] struct {
	// contains filtered or unexported fields
}

TimeExpr is a timestamp valued expression.

func (TimeExpr) After

func (o TimeExpr) After(t time.Time) BoolExpr[S]

After returns whether the value is later than t.

func (TimeExpr) AfterExpr

func (o TimeExpr) AfterExpr(x TimeValue[S]) BoolExpr[S]

AfterExpr returns whether the value is later than the value of x in the same row.

func (TimeExpr) AtOrAfter

func (o TimeExpr) AtOrAfter(t time.Time) BoolExpr[S]

AtOrAfter returns whether the value is t or later.

func (TimeExpr) AtOrAfterExpr

func (o TimeExpr) AtOrAfterExpr(x TimeValue[S]) BoolExpr[S]

AtOrAfterExpr returns whether the value is no earlier than the value of x in the same row.

func (TimeExpr) AtOrBefore

func (o TimeExpr) AtOrBefore(t time.Time) BoolExpr[S]

AtOrBefore returns whether the value is t or earlier.

func (TimeExpr) AtOrBeforeExpr

func (o TimeExpr) AtOrBeforeExpr(x TimeValue[S]) BoolExpr[S]

AtOrBeforeExpr returns whether the value is no later than the value of x in the same row.

func (TimeExpr) Before

func (o TimeExpr) Before(t time.Time) BoolExpr[S]

Before returns whether the value is earlier than t.

func (TimeExpr) BeforeExpr

func (o TimeExpr) BeforeExpr(x TimeValue[S]) BoolExpr[S]

BeforeExpr returns whether the value is earlier than the value of x in the same row.

func (TimeExpr) Eq

func (o TimeExpr) Eq(t time.Time) BoolExpr[S]

Eq returns whether the value is the instant t.

func (TimeExpr) EqExpr

func (o TimeExpr) EqExpr(x TimeValue[S]) BoolExpr[S]

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.

func (TimeExpr) Ne

func (o TimeExpr) Ne(t time.Time) BoolExpr[S]

Ne returns whether the value is any instant other than t.

func (TimeExpr) NeExpr

func (o TimeExpr) NeExpr(x TimeValue[S]) BoolExpr[S]

NeExpr returns whether the value is a different instant from the value of x in the same row.

func (TimeExpr) String

func (o TimeExpr) String() string

String returns the expression as it would be written.

type TimeValue

type TimeValue[S any] interface {
	Expr[S]
	// contains filtered or unexported methods
}

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.

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.
ipc
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL