1. Write a forecast run for series_id=42, issued at 06:00.
May 28, 2026 · View on GitHub
TimeDB stores overlapping forecast revisions, auditable corrections, and "time-of-knowledge" history as plain rows in ClickHouse. Every value carries a three-dimensional timestamp so you can replay exactly what was known at any past instant.
Traditional time-series stores assume one immutable value per timestamp. TimeDB is built for the messy reality of forecasts that get revised, observations that get corrected, and backtests that need strict point-in-time audits.
🧊 The 3D Temporal Data Model
At the heart of TimeDB is its three-dimensional approach to time. We track not just when data is valid, but when it became known and when it was altered.
| Dimension | Description | Real-World Example |
|---|---|---|
📅 valid_time | The time the value represents a fact for. | "Wind speed forecast for Wednesday 12:00" |
⏰ knowledge_time | The time when the value was predicted/known. | "Generated on Monday 18:00" |
✏️ change_time | The time when the value was written or changed. | "Manually overridden on Tuesday 09:00" |
Audit & Metadata: Every row also carries
changed_byandannotationtext fields so corrections leave a readable trail instead of silent overwrites.
✨ Why Choose TimeDB?
- 📊 Forecast Revisions: Store overlapping forecasts side-by-side — every
knowledge_timeis preserved. - 🔄 Auditable Updates: Corrections are new rows with a fresh
change_time; reads collapse them into the latest state, with full history available on demand. - ⏪ True Backtesting: Query historical data as of any point in time ("What did our model know last Monday?"), or use
read_relative()for per-window day-ahead cutoffs. - 🗂️ Retention Tiers: Pick
short/medium/long/foreverper series; ClickHouse drops whole partitions when TTLs expire. - 🪶 Stateless & Minimal: One class, two tables, no catalog. Series identity (
series_id) is owned by the caller — no naming, units, or labels to keep in sync.
🚀 Quick Start
1. Installation
pip install timedb
Requires Python 3.12+ and a reachable ClickHouse instance. TimeDB reads its connection string from TIMEDB_CH_URL (also picked up from a .env file).
2. Usage Example
import polars as pl
from datetime import datetime, timezone
from timedb import TimeDBClient
td = TimeDBClient() # reads TIMEDB_CH_URL
td.create() # creates series_values + run_series tables
# 1. Write a forecast run for series_id=42, issued at 06:00.
kt = datetime(2025, 1, 1, 6, tzinfo=timezone.utc)
df = pl.DataFrame({
"series_id": [42] * 24,
"valid_time": [datetime(2025, 1, 1, h, tzinfo=timezone.utc) for h in range(24)],
"value": [100.0 + i * 2 for i in range(24)],
})
td.write(df, retention="medium", knowledge_time=kt)
# 2. A later forecast revision — same valid_time window, higher knowledge_time.
kt2 = datetime(2025, 1, 1, 12, tzinfo=timezone.utc)
td.write(df.with_columns(pl.col("value") + 5), retention="medium", knowledge_time=kt2)
# 3. Latest value per valid_time (the second run wins).
latest = td.read(series_ids=[42])
# 4. Full forecast history — one row per (knowledge_time, valid_time).
history = td.read(series_ids=[42], include_knowledge_time=True)
Required write columns are series_id, valid_time, value. Everything else (change_time, run_id, changed_by, annotation, valid_time_end) is optional and stamped with safe defaults per batch. All timestamp columns must be timezone-aware.
🤝 The Full Stack — TimeDB + EnergyDB
TimeDB stores rows keyed by integer series_id and nothing else. That's enough when you're managing identity in your own application — but for the energy domain, we ship the full stack.
EnergyDB adds, on top of the same ClickHouse store (plus a thin PostgreSQL catalog):
- 🌳 Typed asset trees:
Portfolio→Site→WindTurbine/PVArray/Battery, etc. — every asset class from EnergyDataModel. - 🔗 Grid edges:
Line, transformer, pipe — connect any two nodes, attach their own time series. - 🧭 Fluent path scopes:
client.get_node("my-portfolio", "Offshore-1", "T01").read(name="power", data_type="actual")resolves to one indexed SQL query. - ⚖️ Per-series canonical units: declare
MWonce;pintconverts on every read and write via aunit=kwarg. - 🧬 Run/workflow provenance:
workflow_id,model_name,run_start_time, etc. attached at write time. - 📋 Diffable structural changes:
dry_run=Truepreviews every rename, move, delete, or insert as aTreeDiffbefore you commit.
Use TimeDB for the raw storage primitive. Use EnergyDB for an asset-aware catalog of energy portfolios.
🧪 Try it in Google Colab
Want to try TimeDB without a local setup? Open our Quickstart in Colab — the first cell automatically installs ClickHouse inside the VM.
Note: Data persists only within the active Colab session. Additional notebooks are available in the
examples/directory.
📚 Documentation & Resources
🤝 Contributing
Contributions are welcome! If you're interested in improving TimeDB, please see our Development Guide for local setup instructions.
Licensed under the Apache-2.0 License.
Find a bug or have a feature request? Open an Issue.