# Decimate data

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/).

[](https://res.cloudinary.com/didkpxvqu/video/upload/v1732190709/decimate_zoom_lqicxl.mov)

## Connect the client

This guide assumes you have [installed](https://dev.nominal.io/python/guides/install.md) the client and setup your [API keys](https://dev.nominal.io/python/guides/api-keys-setup.md).

```python
from nominal.core import NominalClient

client = NominalClient.from_profile("default")
```

## 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 [`upload_dataframe()`](https://dev.nominal.io/python/ref/generated/nominal.thirdparty.pandas.upload_dataframe.md#nominal.thirdparty.pandas.upload_dataframe "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

[`channel_to_dataframe_decimated()`](https://dev.nominal.io/python/ref/generated/nominal.thirdparty.pandas.channel_to_dataframe_decimated.md#nominal.thirdparty.pandas.channel_to_dataframe_decimated "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.

![Decimated chart](https://dev.nominal.io/python/_images/decimated_lwyjmn.png)
