DEV Community

Beck_Moulton
Beck_Moulton

Posted on

Quantified Self: How I Handled 100GB of Messy Biosensor Data with Polars and Dask

We live in the era of the "Quantified Self." Between my Oura ring, Apple Watch, and continuous glucose monitor (CGM), I’m generating more data than a mid-sized SaaS startup from 2010. But here’s the reality: Raw biosensor data is a nightmare. It’s inconsistent, high-frequency, and—at 100GB+ of raw CSVs—it will make pandas.read_csv() laugh in your face before crashing your kernel.

In this tutorial, we are diving deep into Data Engineering patterns to build a high-performance pipeline. We’ll be using Polars for lightning-fast transformations, Apache Arrow for memory-efficient storage, and Dask to orchestrate the heavy lifting across multiple cores. If you've been struggling with large scale data processing and biosensor data cleaning, you’re in the right place. 🥑

The Architecture: From Raw Pulses to Clean Insights

Handling 100GB+ of data requires a "Lazy" approach. We don't want to load everything at once; we want to build a computation graph and execute it only when necessary.

graph TD
    A[Raw Biosensor CSVs/JSONs] --> B{Dask Worker Pool}
    B --> C[Polars LazyFrame Scan]
    C --> D[Data Cleaning: Handling Missing Values]
    D --> E[Resampling & Interpolation]
    E --> F[Parquet/Arrow Storage]
    F --> G[Streamlit Dashboard]

    subgraph "The Processing Engine"
    C
    D
    E
    end
Enter fullscreen mode Exit fullscreen mode

Prerequisites

Before we get our hands dirty, make sure you have the stack ready:

  • Polars: The blazing-fast DataFrame library written in Rust.
  • Apache Arrow: The backbone for memory-efficient data interchange.
  • Dask: For distributed task scheduling.
  • Streamlit: To visualize the results without writing a lick of CSS.
pip install polars dask[complete] pyarrow streamlit
Enter fullscreen mode Exit fullscreen mode

Step 1: Solving the RAM Crisis with Polars Lazy API

When dealing with 100GB of data, the "Eager" execution (loading everything into RAM) is your enemy. Polars' LazyFrame allows us to define operations without executing them immediately.

import polars as pl

# Instead of loading 100GB, we scan it. 
# This creates a pointer to the files and infers the schema.
def load_sensor_data(file_pattern: str):
    return pl.scan_csv(file_pattern) \
             .with_columns([
                 pl.col("timestamp").str.to_datetime(),
                 pl.col("heart_rate").cast(pl.Float32)
             ])

# Example usage
lazy_df = load_sensor_data("data/raw_sensors_*.csv")
print("Query Plan structure created! 🏗️")
Enter fullscreen mode Exit fullscreen mode

Step 2: The "Sampling Rate" Nightmare

Biosensors are notorious for inconsistent sampling. Your heart rate sensor might tick every 1 second, while your accelerometer ticks at 50Hz. To analyze them together, we need to align the time series.

In Polars, we can use upsample combined with linear interpolation to fill the gaps created by sensor dropouts.

def clean_and_align(lf: pl.LazyFrame):
    return (
        lf
        # Sort by time first for time-series operations
        .sort("timestamp")
        # Handle duplicate timestamps (common in dirty sensor data)
        .unique(subset=["timestamp"], keep="first")
        # Upsample to a consistent 1Hz frequency
        .upsample(time_column="timestamp", every="1s")
        # Interpolate missing heart rate values
        .with_columns(
            pl.col("heart_rate").interpolate()
        )
        .fill_null(strategy="backward")
    )

processed_lf = clean_and_align(lazy_df)
Enter fullscreen mode Exit fullscreen mode

Step 3: Scaling with Dask and Apache Arrow

While Polars is incredibly fast on a single machine, we use Dask to orchestrate the processing of hundreds of files in parallel and save the results in Apache Arrow (Parquet) format. Parquet is essential here because it supports predicate pushdown—meaning our future queries only read the columns they need.

from dask.distributed import Client

client = Client() # Starts a local cluster

def process_partition(partition_path):
    # This runs inside a Dask worker
    df = pl.scan_csv(partition_path).pipe(clean_and_align).collect()
    output_path = partition_path.replace(".csv", ".parquet")
    df.write_parquet(output_path)
    return output_path

# Imagine a list of 1000 files
files = ["data/sensor_batch_1.csv", "data/sensor_batch_2.csv", ...]
futures = client.map(process_partition, files)
results = client.gather(futures)
print(f"Processed {len(results)} files to Parquet! ✅")
Enter fullscreen mode Exit fullscreen mode

The "Official" Way to Scale 💡

While the code above works great for personal projects, enterprise-grade data engineering often requires more robust observability and schema enforcement.

If you're looking for advanced patterns on building production-ready data lakes or scaling Python pipelines to the petabyte scale, I highly recommend checking out the engineering deep-dives at wellally.tech/blog. They provide excellent resources on optimizing Arrow-based workflows that go far beyond basic scripts.


Step 4: Visualizing the Quantified Self with Streamlit

Finally, let's wrap this in a Streamlit dashboard. Because we saved our data as Parquet, the dashboard will be incredibly snappy.

import streamlit as st
import polars as pl

st.title("My Bio-Data Insights 📈")

@st.cache_data
def get_summary_data():
    # Only read the columns we need for the chart!
    return pl.read_parquet("data/*.parquet", columns=["timestamp", "heart_rate"]) \
             .filter(pl.col("heart_rate") > 0) \
             .sample(fraction=0.1) # Downsample for the UI

data = get_summary_data()

st.line_chart(data.to_pandas(), x="timestamp", y="heart_rate")
st.success("Visualizing 100GB of data in milliseconds! 🚀")
Enter fullscreen mode Exit fullscreen mode

Conclusion

Processing 100GB+ of dirty biosensor data doesn't require a massive Spark cluster if you use the right tools. By leveraging Polars' Lazy API, Apache Arrow's storage efficiency, and Dask's parallelism, you can turn a mountain of "dirty" pixels and pulses into actionable health insights on your laptop.

Key Takeaways:

  1. Stop using Eager Loading: Use scan_csv or scan_parquet.
  2. Schema is King: Cast your types early to save memory.
  3. Align your time-series: Upsampling and interpolation are your best friends in biosensor land.

What are you building with your wearable data? Drop a comment below or share your pipeline! 👇

Top comments (0)