A Rust library for quantitative time-series analysis.
- Percentage change (
pct_change) - First-order difference (
diff) - Lag / forward shift (
shift) - Rolling window mean (
rolling().mean())
use temporalseries::series::TimeSeries;
let ts = TimeSeries::new(
vec![1, 2, 3, 4, 5],
vec![100.0, 101.0, 102.0, 103.0, 104.0],
).unwrap();
// Simple returns
let returns = ts.pct_change().unwrap();
// Rolling mean with window of 3
let momentum = returns.rolling(3).mean().unwrap();All fallible operations return Result<TimeSeries, TemporalSeriesError>.
use temporalseries::{series::TimeSeries, errors::TemporalSeriesError};
match TimeSeries::new(vec![1, 2], vec![1.0]) {
Err(TemporalSeriesError::LengthMismatch { index_len, values_len }) => {
eprintln!("index length {index_len} != values length {values_len}");
}
_ => {}
}TemporalSeries<T, B> is generic over its storage backend, so you can choose
the in-memory layout that best fits your workload.
Values are stored in a single contiguous Vec<T>, separate from the index.
This is the most cache-friendly layout for operations that scan the value
column alone (aggregations, rolling windows).
use temporalseries::series::TemporalSeries;
use temporalseries::storage::ColumnarBackend;
let index: Vec<i64> = vec![1, 2, 3, 4, 5];
let values: Vec<f64> = vec![10.0, 20.0, 30.0, 25.0, 35.0];
let backend = ColumnarBackend::new(values);
let series = TemporalSeries::new(index, backend).unwrap();
println!("{:?}", series.get(2)); // Some(30.0)Each observation is stored as a RowRecord { timestamp, value }. This layout
is a natural fit when data arrives record-by-record (database cursors, message
streams) and keeps timestamps co-located with their values.
use temporalseries::series::TemporalSeries;
use temporalseries::storage::{RowBackend, RowRecord};
let rows: Vec<RowRecord<f64>> = vec![
RowRecord { timestamp: 1, value: 10.0 },
RowRecord { timestamp: 2, value: 20.0 },
RowRecord { timestamp: 3, value: 30.0 },
];
let index: Vec<i64> = rows.iter().map(|r| r.timestamp).collect();
let backend = RowBackend::new(rows);
let series = TemporalSeries::new(index, backend).unwrap();
println!("{:?}", series.get(2)); // Some(30.0)| Columnar | Row | |
|---|---|---|
| Memory layout | values packed, index separate | timestamp + value interleaved per record |
| Best for | column scans, aggregations | record-at-a-time ingestion |
| Extra allocation | separate index Vec |
none beyond the records themselves |
Timestamps are always stored as i64 integers. The optional chrono feature
adds [TimeUnit] — an enum that records the unit the integers are expressed in
— and two convenience methods on TemporalSeries that convert between i64
and [chrono::DateTime<Utc>].
Enable the feature in your Cargo.toml:
temporalseries = { version = "0.1", features = ["chrono"] }use chrono::{TimeZone, Utc};
use temporalseries::series::TemporalSeries;
use temporalseries::storage::ColumnarBackend;
use temporalseries::time::TimeUnit;
let datetimes = vec![
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
];
let series = TemporalSeries::from_datetimes(
datetimes,
ColumnarBackend::new(vec![150.0_f64, 152.5]),
TimeUnit::Seconds,
).unwrap();
println!("{:?}", series.index); // [1704067200, 1704153600]use temporalseries::series::TemporalSeries;
use temporalseries::storage::ColumnarBackend;
use temporalseries::time::TimeUnit;
let series = TemporalSeries::new(
vec![0_i64, 1_000, 2_000], // milliseconds
ColumnarBackend::new(vec![1.0_f64, 2.0, 3.0]),
)
.unwrap()
.with_unit(TimeUnit::Milliseconds);
// Convert the index back to DateTime<Utc>
let dts = series.datetimes().unwrap();
println!("{}", dts[1]); // 1970-01-01 00:00:01 UTC| Variant | Epoch reference | Typical use |
|---|---|---|
TimeUnit::Seconds |
Unix seconds | Databases, REST APIs |
TimeUnit::Milliseconds |
Unix milliseconds | JavaScript dates, most financial feeds |
TimeUnit::Microseconds |
Unix microseconds | High-frequency trading, system logs |
TimeUnit::Nanoseconds |
Unix nanoseconds | Kernel tracing (overflows past ~year 2262) |
TimeUnit, with_unit, and time_unit() are always available. Only
from_datetimes and datetimes require --features chrono.
Operations that cannot produce a value for a position (e.g. the first element
of diff or pct_change, or the first window - 1 elements of a rolling mean)
return NaN at that position rather than truncating the series. This keeps the
output length equal to the input length and preserves index alignment.
| Example | Command | Description |
|---|---|---|
basic |
cargo run --example basic |
Constructs a series and computes pct_change followed by a rolling mean |
input_output |
cargo run --example input_output |
Reads a series from examples/input.csv, writes it to examples/output.csv, and reads it back |
temporal_series_with_columnar_backend |
cargo run --example temporal_series_with_columnar_backend |
Builds a TemporalSeries with a ColumnarBackend and demonstrates access and iteration |
temporal_series_with_row_backend |
cargo run --example temporal_series_with_row_backend |
Builds a TemporalSeries with a RowBackend and demonstrates access and iteration |
panel |
cargo run --example panel |
Builds a Panel of named series on a shared index and extracts one series for analysis |
temporal_series_with_chrono |
cargo run --example temporal_series_with_chrono --features chrono |
Builds a TemporalSeries from DateTime<Utc> values and round-trips the index back to calendar dates |
# Run tests
cargo test
# Run linter
cargo clippy -- -D warnings
# Open API docs
cargo doc --open
# Run all benchmarks
cargo bench
# Run a single benchmark target
cargo bench --bench time_series
cargo bench --bench columnar_backend
cargo bench --bench row_backend
# Open the HTML report in the browser (macOS)
open target/criterion/report/index.htmlCriterion writes an HTML report to target/criterion/ after every run.
Each benchmark target has its own sub-report, and there is a combined index at
target/criterion/report/index.html that covers all targets.
To open a single target's report directly:
open target/criterion/columnar_backend/report/index.html
open target/criterion/row_backend/report/index.html
open target/criterion/time_series/report/index.htmlPrerequisites (one-time setup):
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov# Print a coverage summary to the terminal
cargo llvm-cov
# Generate an HTML report (all features)
cargo llvm-cov --html --features chrono
# Open the report in the browser (macOS)
open target/llvm-cov/html/index.htmlThe HTML report is also generated automatically in CI on every push and pull
request to main. It is uploaded as the coverage-report artifact and kept
for 15 days. Download it from the Actions tab of the repository, open
the run, and grab the artifact from the summary page.
MIT