# Metrics

Track time-series metrics that change over time - loss, accuracy, learning rate, and custom measurements.

## Basic Usage

Log train and eval metrics together with a single call:

```python

from ml_dash import Experiment

with Experiment(prefix="alice/project/my-experiment").run as exp:

    for epoch in range(10):
        train_loss, train_acc = train_one_epoch(model)
        eval_loss, eval_acc = evaluate(model)

        # Log all metrics for this epoch in one call
        exp.metrics.log(
            epoch=epoch,
            train=dict(loss=train_loss, accuracy=train_acc),
            eval=dict(loss=eval_loss, accuracy=eval_acc)
        )
```

## Alternative: Prefix-Based Logging

You can also log metrics using namespace prefixes with explicit context:

```python

with Experiment(prefix="alice/project/my-experiment").run as exp:

    for epoch in range(10):
        # Log train metrics with prefix
        exp.metrics("train").log(loss=train_loss, accuracy=train_acc, epoch=epoch)

        # Log eval metrics with prefix
        exp.metrics("eval").log(loss=eval_loss, accuracy=eval_acc, epoch=epoch)
```

Include `epoch` (or `step`) in each group's `log()` call. A bare
`exp.metrics.log(epoch=epoch)` writes to a separate unnamed series; it is not
attached to the `train` and `eval` rows. The grouped form above,
`exp.metrics.log(epoch=..., train=dict(...), eval=dict(...))`, does copy
`epoch` into every group.

## Custom Metric Groups

Define your own metric groups beyond train/eval:

```python

with Experiment(prefix="alice/project/my-experiment").run as exp:

    # Log with custom groups
    exp.metrics.log(
        step=100,
        train=dict(loss=0.5, accuracy=0.85),
        eval=dict(loss=0.6, accuracy=0.82),
        system=dict(gpu_memory=4.2, cpu_percent=45),
        lr=dict(value=0.001)
    )
```

## Reading Data

Read metric data by index range:

```python

with Experiment(prefix="alice/project/my-experiment").run as exp:
    # Log data
    for i in range(100):
        exp.metrics("train").log(loss=1.0 / (i + 1), step=i)

    # Read first 10 points
    result = exp.metrics("train").read(start_index=0, limit=10)

    for point in result['data']:
        print(f"Index {point['index']}: {point['data']}")
```

## Buffer API

For high-frequency logging (e.g., per-batch), use the buffer API to accumulate values and periodically log summary statistics:

```python

with Experiment(prefix="alice/project/my-experiment").run as exp:

    for epoch in range(10):
        for batch in dataloader:
            loss = train_step(batch)
            acc = compute_accuracy(batch)

            # Buffer per-batch values (not written to disk yet)
            exp.metrics("train").buffer(loss=loss, accuracy=acc)

        # At end of epoch, compute and log summary statistics
        exp.metrics.buffer.log_summary(epoch=epoch)  # default: mean, plus epoch
```

This produces statistics like `loss.mean`, `accuracy.mean` per epoch.

### Buffer API Methods

- **`metrics("prefix").buffer(**kwargs)`** - Accumulate values for later summarization
- **`metrics.buffer.log_summary(*aggs)`** - Compute statistics and log to all prefixes
- **`metrics.buffer.peek(prefix, *keys, limit=5)`** - Non-destructive look at buffered values

### Supported Aggregations

```python
# Default (just mean)
exp.metrics.buffer.log_summary()

# Multiple aggregations
exp.metrics.buffer.log_summary("mean", "std", "min", "max", "count")

# Percentiles
exp.metrics.buffer.log_summary("p50", "p90", "p95", "p99")

# First/last values
exp.metrics.buffer.log_summary("first", "last")
```

Available aggregations: `mean`, `std`, `min`, `max`, `count`, `median`, `sum`, `p50`, `p90`, `p95`, `p99`, `first`, `last`

### Multiple Prefixes

Buffer works across multiple prefixes simultaneously:

```python

for batch in dataloader:
    train_loss = train_step(batch)
    val_loss = validate_step(batch)

    # Buffer to different prefixes
    exp.metrics("train").buffer(loss=train_loss)
    exp.metrics("eval").buffer(loss=val_loss)

# Single call logs summaries for ALL prefixes
exp.metrics.buffer.log_summary("mean", "std")
```

## Summary Cache (Legacy API)

The summary cache API is still supported for backward compatibility:

```python

with Experiment(prefix="alice/project/my-experiment").run as exp:

    for epoch in range(10):
        for batch in dataloader:
            loss = train_step(batch)

            # Store per-batch values (not written to disk yet)
            exp.metrics("train").summary_cache.store(loss=loss)

        # At end of epoch, summarize and write to disk
        exp.metrics("train").summary_cache.set(epoch=epoch)
        exp.metrics("train").summary_cache.summarize()
```

### Summary Cache Methods

- **`store(**kwargs)`** - Accumulate values for later summarization
- **`set(**kwargs)`** - Set metadata (epoch, lr) that doesn't need aggregation
- **`summarize(clear=True)`** - Compute statistics and append to metrics
- **`peek(*keys, limit=5)`** - Non-destructive look at stored values

### Rolling vs Cumulative

```python

# Rolling window (default) - clears cache after summarize
metric.summary_cache.summarize()  # clear=True by default

# Cumulative - keeps accumulating
metric.summary_cache.summarize(clear=False)
```

## Training Loop Example

```python

with Experiment(prefix="alice/cv/mnist-training").run as exp:
    exp.params.set(learning_rate=0.001, batch_size=32)
    exp.log("Starting training")

    for epoch in range(10):
        train_loss = train_one_epoch(model, train_loader)
        val_loss, val_accuracy = validate(model, val_loader)

        # Log metrics
        exp.metrics("train").log(loss=train_loss, epoch=epoch + 1)
        exp.metrics("eval").log(loss=val_loss, accuracy=val_accuracy, epoch=epoch + 1)

        exp.log(
            f"Epoch {epoch + 1}/10 complete",
            metadata={"train_loss": train_loss, "val_loss": val_loss}
        )

    exp.log("Training complete")
```

## Multiple Metrics in One Call

Combine related metrics:

```python

with Experiment(prefix="alice/project/my-experiment").run as exp:
    for epoch in range(10):
        exp.metrics("train").log(
            epoch=epoch,
            loss=0.5 / (epoch + 1),
            accuracy=0.8 + epoch * 0.01
        )
        exp.metrics("eval").log(
            epoch=epoch,
            loss=0.6 / (epoch + 1),
            accuracy=0.75 + epoch * 0.01
        )
```

## Timestamps

Every metric data point automatically records a `_ts` field (Unix timestamp, seconds since epoch). You can also provide it explicitly or inherit it from the previous call:

```python
import time

# Auto-assigned (default)
exp.metrics("train").log(loss=0.5)
# → data: {"loss": 0.5, "_ts": 1742382000.123}

# Explicit timestamp
exp.metrics("train").log(loss=0.5, _ts=time.time())

# Inherit timestamp from previous metric or track call (same thread)
exp.metrics("train").log(loss=0.5)                  # sets _last_timestamp
exp.metrics("eval").log(loss=0.6, _ts=-1)           # inherits same _ts

# Cross-inheritance with tracks
exp.tracks("robot/pos").append(x=1.0, _ts=100.0)   # sets _last_timestamp
exp.metrics("train").log(loss=0.5, _ts=-1)          # inherits 100.0
```

`_ts=-1` inherits across **both metrics and tracks** within the same thread.

## Storage Format

**Local mode** - JSONL files:

```bash
cat .dash/alice/project/my-experiment/metrics/train/data.jsonl
```

```json
{"index": 0, "data": {"loss": 0.5, "epoch": 1, "_ts": 1742382000.1}, "createdAt": "2026-03-20T10:00:00.000000Z"}
{"index": 1, "data": {"loss": 0.45, "epoch": 2, "_ts": 1742382001.2}, "createdAt": "2026-03-20T10:00:01.000000Z"}
{"index": 2, "data": {"loss": 0.40, "epoch": 3, "_ts": 1742382002.3}, "createdAt": "2026-03-20T10:00:02.000000Z"}
```

**Remote mode** - Two-tier storage:
- **Hot tier:** Recent data in MongoDB (fast access)
- **Cold tier:** Historical data in S3 (auto-archived after 10,000 points)

---

**Next:** Learn about [Files](/guides/files.md) to upload models, plots, and artifacts.
