---
myst:
html_meta:
description: "Read decimated channel data for charts, and fetch more detail as you zoom in."
---
# Decimate data
{.lead}
Decimation reads a fixed number of time buckets, each with the minimum, maximum, mean, count and variance of its points.
It charts long, high-rate recordings quickly while keeping spikes visible.
The chart needs `pip install plotly`, and zooming needs `ipywidgets` in [Jupyter](https://jupyter.org/).
```{video} https://res.cloudinary.com/didkpxvqu/video/upload/v1732190709/decimate_zoom_lqicxl.mov
```
```{include} /guides/_snippets/connect-the-client.md
```
## Upload sample data
One hour of a 200 Hz signal is 720,000 points.
This example generates one, with a few spikes, and uploads it with {py:func}`~nominal.thirdparty.pandas.upload_dataframe`:
```python
from datetime import datetime, timedelta, timezone
import numpy as np
import pandas as pd
from nominal.thirdparty.pandas import upload_dataframe
start = datetime(2026, 9, 10, 14, 0, tzinfo=timezone.utc)
end = start + timedelta(hours=1)
n = 720_000
value = np.random.normal(0, 0.3, n).cumsum() + 50
value[np.random.choice(n, 10)] *= 2 # spikes that decimation keeps
df = pd.DataFrame({"time": start.timestamp() + np.arange(n) / 200, "value": value})
dataset = upload_dataframe(client, df, "Decimation sample", "time", timestamp_type="epoch_seconds")
channel = dataset.get_channel("value")
```
## Read decimated data
{py:func}`~nominal.thirdparty.pandas.channel_to_dataframe_decimated` returns one row per bucket:
```python
from nominal.thirdparty.pandas import channel_to_dataframe_decimated
df = channel_to_dataframe_decimated(channel, start, end, buckets=2000)
```
The columns are `min`, `max`, `mean`, `count` and `variance`.
When the range has fewer than 1,000 points, it returns them unchanged, in one `value` column.
It works for `DOUBLE` and `INT` channels.
## Chart it and zoom in
Plotting the bucket minimum and maximum as a band shows the signal's full range:
```python
import plotly.graph_objects as go
def band(df: pd.DataFrame) -> list[go.Scatter]:
if "value" in df.columns: # raw points
return [go.Scatter(x=df.index, y=df["value"], mode="lines")]
return [
go.Scatter(x=df.index, y=df["max"], mode="lines", line_width=0, line_shape="vh"),
go.Scatter(x=df.index, y=df["min"], mode="lines", line_width=0, line_shape="vh", fill="tonexty"),
]
fig = go.FigureWidget(band(df), layout={"showlegend": False})
def on_zoom(layout, x_range): # fetch 2,000 buckets for the visible range
zoomed = channel_to_dataframe_decimated(channel, f"{x_range[0]}Z", f"{x_range[1]}Z", buckets=2000)
with fig.batch_update():
fig.data = []
fig.add_traces(band(zoomed))
fig.layout.on_change(on_zoom, "xaxis.range")
fig
```
Zooming in requests new buckets for the visible range, so detail increases until the chart shows raw points.
