Data Format¶
Data returned by get_data() is organized in tabular form as a
Polars DataFrame with the following columns:
Name |
Datatype |
Description |
|---|---|---|
|
absolute timestamp, see section Timestamps below. |
|
|
zero-based trial index. |
|
|
state machine hash, see |
|
|
name of the current |
|
|
type of the current event. |
|
|
input event name; only for events of type |
|
|
name of the respective input or output channel. |
|
|
value of the channel. |
For the state machine in Listing 9 the returned
DataFrame could look like this:
>>> data
shape: (1_100, 8)
┌────────────────────────────┬───────┬──────────────────┬───────┬─────────────────┬───────┬─────────┬───────┐
│ time ┆ trial ┆ state machine ┆ state ┆ type ┆ event ┆ channel ┆ value │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ u16 ┆ cat ┆ cat ┆ enum ┆ cat ┆ cat ┆ u8 │
╞════════════════════════════╪═══════╪══════════════════╪═══════╪═════════════════╪═══════╪═════════╪═══════╡
│ 2026-04-16 20:29:12.948426 ┆ 0 ┆ d1af27e5c2b13891 ┆ null ┆ TrialStart ┆ null ┆ null ┆ null │
│ 2026-04-16 20:29:12.948426 ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ StateStart ┆ null ┆ null ┆ null │
│ 2026-04-16 20:29:12.948426 ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ OutputAction ┆ null ┆ PWM1 ┆ 235 │
│ 2026-04-16 20:29:13.022426 ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ InputEvent ┆ Tup ┆ null ┆ null │
│ 2026-04-16 20:29:13.022426 ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ StateEnd ┆ null ┆ null ┆ null │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 2026-04-16 20:29:19.121326 ┆ 99 ┆ 1719d07df94acabf ┆ s2 ┆ OutputAction ┆ null ┆ PWM1 ┆ 0 │
│ 2026-04-16 20:29:19.131326 ┆ 99 ┆ 1719d07df94acabf ┆ s2 ┆ InputEvent ┆ Tup ┆ null ┆ null │
│ 2026-04-16 20:29:19.131326 ┆ 99 ┆ 1719d07df94acabf ┆ s2 ┆ StateEnd ┆ null ┆ null ┆ null │
│ 2026-04-16 20:29:19.131426 ┆ 99 ┆ 1719d07df94acabf ┆ null ┆ TrialEnd ┆ null ┆ null ┆ null │
│ 2026-04-16 20:29:19.131328 ┆ 99 ┆ 1719d07df94acabf ┆ null ┆ TrialEndControl ┆ null ┆ null ┆ null │
└────────────────────────────┴───────┴──────────────────┴───────┴─────────────────┴───────┴─────────┴───────┘
Timestamps¶
The data is stored using absolute timestamps rather than relative ones. This avoids ambiguity when combining data across trials, state machines, or external data sources, and preserves the original temporal ordering without requiring additional context.
Absolute time is computed from the Bpod’s hardware timer (microseconds since boot),
offset by the host computer’s system time at instantiation of the
Bpod class. Because the two clocks are independent, they will
drift apart over time. You can call reset_session_clock()
outside of a state machine run to resynchronize them.
Relative timestamps can be derived on demand by subtracting the first timestamp from the
time column. The inherent unit of the time column is preserved during this
operation, and the result automatically takes on the Duration
type:
>>> data.with_columns(pl.col("time") - pl.col("time").first())
shape: (1_100, 8)
┌──────────────┬───────┬──────────────────┬───────┬─────────────────┬───────┬─────────┬───────┐
│ time ┆ trial ┆ state machine ┆ state ┆ type ┆ event ┆ channel ┆ value │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ duration[μs] ┆ u16 ┆ cat ┆ cat ┆ enum ┆ cat ┆ cat ┆ u8 │
╞══════════════╪═══════╪══════════════════╪═══════╪═════════════════╪═══════╪═════════╪═══════╡
│ 0µs ┆ 0 ┆ d1af27e5c2b13891 ┆ null ┆ TrialStart ┆ null ┆ null ┆ null │
│ 0µs ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ StateStart ┆ null ┆ null ┆ null │
│ 0µs ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ OutputAction ┆ null ┆ PWM1 ┆ 235 │
│ 74ms ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ InputEvent ┆ Tup ┆ null ┆ null │
│ 74ms ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ StateEnd ┆ null ┆ null ┆ null │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 6s 172900µs ┆ 99 ┆ 1719d07df94acabf ┆ s2 ┆ OutputAction ┆ null ┆ PWM1 ┆ 0 │
│ 6s 182900µs ┆ 99 ┆ 1719d07df94acabf ┆ s2 ┆ InputEvent ┆ Tup ┆ null ┆ null │
│ 6s 182900µs ┆ 99 ┆ 1719d07df94acabf ┆ s2 ┆ StateEnd ┆ null ┆ null ┆ null │
│ 6s 183ms ┆ 99 ┆ 1719d07df94acabf ┆ null ┆ TrialEnd ┆ null ┆ null ┆ null │
│ 6s 182902µs ┆ 99 ┆ 1719d07df94acabf ┆ null ┆ TrialEndControl ┆ null ┆ null ┆ null │
└──────────────┴───────┴──────────────────┴───────┴─────────────────┴───────┴─────────┴───────┘
Filtering¶
The different columns are designed to facilitate filtering the data. If, for instance,
you wanted to look at all events that affect the output channel PWM1, you could
filter the table like so:
>>> import polars as pl
>>> data.filter(pl.col("channel") == "PWM1")
shape: (200, 8)
┌────────────────────────────┬───────┬──────────────────┬───────┬──────────────┬───────┬─────────┬───────┐
│ time ┆ trial ┆ state machine ┆ state ┆ type ┆ event ┆ channel ┆ value │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ u16 ┆ cat ┆ cat ┆ enum ┆ cat ┆ cat ┆ u8 │
╞════════════════════════════╪═══════╪══════════════════╪═══════╪══════════════╪═══════╪═════════╪═══════╡
│ 2026-04-16 20:29:12.948426 ┆ 0 ┆ d1af27e5c2b13891 ┆ s1 ┆ OutputAction ┆ null ┆ PWM1 ┆ 235 │
│ 2026-04-16 20:29:13.022426 ┆ 0 ┆ d1af27e5c2b13891 ┆ s2 ┆ OutputAction ┆ null ┆ PWM1 ┆ 0 │
│ 2026-04-16 20:29:13.032526 ┆ 1 ┆ 007c1ac779aa4864 ┆ s1 ┆ OutputAction ┆ null ┆ PWM1 ┆ 154 │
│ 2026-04-16 20:29:13.074626 ┆ 1 ┆ 007c1ac779aa4864 ┆ s2 ┆ OutputAction ┆ null ┆ PWM1 ┆ 0 │
│ 2026-04-16 20:29:13.084726 ┆ 2 ┆ 4c848e03a2b511e4 ┆ s1 ┆ OutputAction ┆ null ┆ PWM1 ┆ 214 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 2026-04-16 20:29:18.998726 ┆ 97 ┆ 1257c55134308481 ┆ s2 ┆ OutputAction ┆ null ┆ PWM1 ┆ 0 │
│ 2026-04-16 20:29:19.008826 ┆ 98 ┆ 7d51489ce6f844f8 ┆ s1 ┆ OutputAction ┆ null ┆ PWM1 ┆ 27 │
│ 2026-04-16 20:29:19.057726 ┆ 98 ┆ 7d51489ce6f844f8 ┆ s2 ┆ OutputAction ┆ null ┆ PWM1 ┆ 0 │
│ 2026-04-16 20:29:19.067826 ┆ 99 ┆ 1719d07df94acabf ┆ s1 ┆ OutputAction ┆ null ┆ PWM1 ┆ 11 │
│ 2026-04-16 20:29:19.121326 ┆ 99 ┆ 1719d07df94acabf ┆ s2 ┆ OutputAction ┆ null ┆ PWM1 ┆ 0 │
└────────────────────────────┴───────┴──────────────────┴───────┴──────────────┴───────┴─────────┴───────┘
Plotting¶
Filtering makes it straightforward to extract data for plotting. We use data returned
by the state machines in Listing 9 to produce a stairstep graph of the
PWM1 output channel over relative time:
import matplotlib.pyplot as plt
import polars as pl
# filter data to values of the 'PWM1' channel across the first 10 trials
filtered = data.filter(
(pl.col("channel") == "PWM1") &
(pl.col("trial") <= 10)
)
# extract two columns, relative 'time' and 'value'
x = filtered['time'] - filtered['time'].first()
y = filtered['value']
# matplotlib doesn't play nice with timedelta values, so we convert them to float
x = x.dt.total_seconds(fractional=True)
# plot
plt.step(x, y, where='post')
plt.xlabel('Time (s)')
plt.ylabel('PWM Value')
plt.show()
Fig. 15 Visualising values of the PWM1 channel across 10 trials.¶
See also
See the Polars documentation for details about the support for various visualization libraries.
Storing Data¶
Because most of the columns consist of Categorical data,
data can be very efficiently written to disk as a Parquet
file using write_parquet():
data.write_parquet('data.pqt')
If you prefer Comma-Separated Values (CSV)—for example, to inspect the data in a
spreadsheet editor such as Excel—use the write_csv() method
instead:
data.write_csv('data.csv')
Note that CSV files are typically about an order of magnitude larger than their Parquet equivalents.
Pandas¶
If you prefer Pandas over
Polars, you can easily convert the data to a
pandas.DataFrame using the built-in to_pandas() method:
>>> data.to_pandas()
time trial state machine state type event channel value
0 2026-04-16 20:29:12.948426 0 d1af27e5c2b13891 NaN TrialStart NaN NaN NaN
1 2026-04-16 20:29:12.948426 0 d1af27e5c2b13891 s1 StateStart NaN NaN NaN
2 2026-04-16 20:29:12.948426 0 d1af27e5c2b13891 s1 OutputAction NaN PWM1 235.0
3 2026-04-16 20:29:13.022426 0 d1af27e5c2b13891 s1 InputEvent Tup NaN NaN
4 2026-04-16 20:29:13.022426 0 d1af27e5c2b13891 s1 StateEnd NaN NaN NaN
... ... ... ... ... ... ... ... ...
1095 2026-04-16 20:29:19.121326 99 1719d07df94acabf s2 OutputAction NaN PWM1 0.0
1096 2026-04-16 20:29:19.131326 99 1719d07df94acabf s2 InputEvent Tup NaN NaN
1097 2026-04-16 20:29:19.131326 99 1719d07df94acabf s2 StateEnd NaN NaN NaN
1098 2026-04-16 20:29:19.131426 99 1719d07df94acabf NaN TrialEnd NaN NaN NaN
1099 2026-04-16 20:29:19.131328 99 1719d07df94acabf NaN TrialEndControl NaN NaN NaN
[1100 rows x 8 columns]
More on Polars¶
The examples above only cover a small subset of what is possible with Polars. You can use the full expression system to filter, transform, aggregate, and analyze the data efficiently, even for large datasets. For a comprehensive overview of available operations and patterns, refer to the Polars documentation.