Skip to content

MGijon/TemporalSeries

Repository files navigation

TemporalSeries

A Rust library for quantitative time-series analysis.

Features

  • Percentage change (pct_change)
  • First-order difference (diff)
  • Lag / forward shift (shift)
  • Rolling window mean (rolling().mean())

Usage

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();

Error handling

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}");
    }
    _ => {}
}

Storage backends

TemporalSeries<T, B> is generic over its storage backend, so you can choose the in-memory layout that best fits your workload.

Columnar

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)

Row

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

Timestamp units (chrono feature)

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"] }

Build from DateTime<Utc> values

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]

Attach a unit to an existing i64 index

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

Available units

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)

Without the feature

TimeUnit, with_unit, and time_unit() are always available. Only from_datetimes and datetimes require --features chrono.

NaN convention

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.

Examples

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

Development

# 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.html

Criterion 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.html

Coverage

Prerequisites (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.html

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

License

MIT

Packages

 
 
 

Contributors

Languages