Ubuntu TechHive
rust-data-pipelines-from-files-to-clean-databases-and-web-dashboards.md
Rust Data Pipelines: From Files to Clean Databases and Web Dashboards
article.detail

Rust Data Pipelines: From Files to Clean Databases and Web Dashboards

reading.progress 41 min read

We are building a small environmental data pipeline. Raw water-quality monitoring files arrive as CSV.

Rust Data Pipelines: From Files to Clean Databases and Web Dashboards

Introduction

We are building a small environmental data pipeline. Raw water-quality monitoring files arrive as CSV. Our Rust tool validates them, cleans bad records, fills safe gaps, stores trusted measurements, and powers a dashboard.

Data Pipeline

About the dataset used

The dataset1 contains raw water-quality monitoring data from Cork Harbour, Moy Killala, and 15 other coastal locations in Ireland. The raw extracted dataset has over 1.27 million entries, and the repository also includes a transformed/pivoted version with 29,159 rows across 11 water-quality parameters. The files are CSV, so they are easy to use for the โ€œfiles โ†’ clean database โ†’ dashboardโ€ flow.

Tools and libraries

We use Rust2 to implement our Data Pipeline by leveraging Polars3.

DataFrame

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql"] }
  //! ```

  use chrono::NaiveDate;
  use polars::{
      df,
      error::PolarsError,
      frame::DataFrame,
      prelude::{IntoLazy, col},
  };


  fn main() -> Result<(), PolarsError> {
      let mut df: DataFrame = df!(
            "name" => ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
            "birthdate" => [
                NaiveDate::from_ymd_opt(1997, 1, 10).unwrap(),
                NaiveDate::from_ymd_opt(1985, 2, 15).unwrap(),
                NaiveDate::from_ymd_opt(1997, 3, 22).unwrap(),
                NaiveDate::from_ymd_opt(1997, 4, 30).unwrap(),
            ],
            "weight" => [57.9, 72.5, 54.6, 83.1], // (kg)
            "height" => [1.56, 1.77, 1.65, 1.75], // (m)
        )
        .unwrap();
        println!("Data:");
        print!("{df}\n");

        let head = df.head(Some(2));
        println!("Head:");
        print!("{head}\n");

      Ok(())
  }
Data:
shape: (4, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name           โ”† birthdate  โ”† weight โ”† height โ”‚
โ”‚ ---            โ”† ---        โ”† ---    โ”† ---    โ”‚
โ”‚ str            โ”† date       โ”† f64    โ”† f64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Alice Archer   โ”† 1997-01-10 โ”† 57.9   โ”† 1.56   โ”‚
โ”‚ Ben Brown      โ”† 1985-02-15 โ”† 72.5   โ”† 1.77   โ”‚
โ”‚ Chloe Cooper   โ”† 1997-03-22 โ”† 54.6   โ”† 1.65   โ”‚
โ”‚ Daniel Donovan โ”† 1997-04-30 โ”† 83.1   โ”† 1.75   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Head:
shape: (2, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name         โ”† birthdate  โ”† weight โ”† height โ”‚
โ”‚ ---          โ”† ---        โ”† ---    โ”† ---    โ”‚
โ”‚ str          โ”† date       โ”† f64    โ”† f64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Alice Archer โ”† 1997-01-10 โ”† 57.9   โ”† 1.56   โ”‚
โ”‚ Ben Brown    โ”† 1985-02-15 โ”† 72.5   โ”† 1.77   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Selecting columns

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql"] }
  //! ```

  use chrono::NaiveDate;
  use polars::{
      df,
      error::PolarsError,
      frame::DataFrame,
      prelude::{IntoLazy, col},
  };


  fn main() -> Result<(), PolarsError> {
      let mut df: DataFrame = df!(
            "name" => ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
            "birthdate" => [
                NaiveDate::from_ymd_opt(1997, 1, 10).unwrap(),
                NaiveDate::from_ymd_opt(1985, 2, 15).unwrap(),
                NaiveDate::from_ymd_opt(1997, 3, 22).unwrap(),
                NaiveDate::from_ymd_opt(1997, 4, 30).unwrap(),
            ],
            "weight" => [57.9, 72.5, 54.6, 83.1], // (kg)
            "height" => [1.56, 1.77, 1.65, 1.75], // (m)
        )
        .unwrap();

        let result = df
            .clone()
            .lazy()
            .select([
                col("name"),
                col("birthdate").dt().year().alias("birth_year"),
                (col("weight") / col("height").pow(2)).alias("bmi"),
            ])
            .collect()?;
        println!("Column selection:");
        print!("{result}\n");

      Ok(())
  }
Column selection:
shape: (4, 3)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name           โ”† birth_year โ”† bmi       โ”‚
โ”‚ ---            โ”† ---        โ”† ---       โ”‚
โ”‚ str            โ”† i32        โ”† f64       โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Alice Archer   โ”† 1997       โ”† 23.791913 โ”‚
โ”‚ Ben Brown      โ”† 1985       โ”† 23.141498 โ”‚
โ”‚ Chloe Cooper   โ”† 1997       โ”† 20.055096 โ”‚
โ”‚ Daniel Donovan โ”† 1997       โ”† 27.134694 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Adding columns

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql"] }
  //! ```

  use chrono::NaiveDate;
  use polars::{
      df,
      error::PolarsError,
      frame::{DataFrame},
      prelude::{LazyFrame, IntoLazy, col},
  };


  fn main() -> Result<(), PolarsError> {
      let mut df: DataFrame = df!(
            "name" => ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
            "birthdate" => [
                NaiveDate::from_ymd_opt(1997, 1, 10).unwrap(),
                NaiveDate::from_ymd_opt(1985, 2, 15).unwrap(),
                NaiveDate::from_ymd_opt(1997, 3, 22).unwrap(),
                NaiveDate::from_ymd_opt(1997, 4, 30).unwrap(),
            ],
            "weight" => [57.9, 72.5, 54.6, 83.1], // (kg)
            "height" => [1.56, 1.77, 1.65, 1.75], // (m)
        )
        .unwrap();

        let result = df
            .clone()
            .lazy()
            .with_columns([
                col("birthdate").dt().year().alias("birth_year"),
                (col("weight") / col("height").pow(2)).alias("bmi"),
            ])
            .collect()?;
        println!("With added colums:");
        print!("{result}\n");

      Ok(())
  }
With added colums:
shape: (4, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name           โ”† birthdate  โ”† weight โ”† height โ”† birth_year โ”† bmi       โ”‚
โ”‚ ---            โ”† ---        โ”† ---    โ”† ---    โ”† ---        โ”† ---       โ”‚
โ”‚ str            โ”† date       โ”† f64    โ”† f64    โ”† i32        โ”† f64       โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Alice Archer   โ”† 1997-01-10 โ”† 57.9   โ”† 1.56   โ”† 1997       โ”† 23.791913 โ”‚
โ”‚ Ben Brown      โ”† 1985-02-15 โ”† 72.5   โ”† 1.77   โ”† 1985       โ”† 23.141498 โ”‚
โ”‚ Chloe Cooper   โ”† 1997-03-22 โ”† 54.6   โ”† 1.65   โ”† 1997       โ”† 20.055096 โ”‚
โ”‚ Daniel Donovan โ”† 1997-04-30 โ”† 83.1   โ”† 1.75   โ”† 1997       โ”† 27.134694 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Expression expansion

lit means literal and it is part of the lazy expression API of Polars3's lazy feature.

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql"] }
  //! ```

  use chrono::NaiveDate;
  use polars::{
      df,
      error::PolarsError,
      frame::DataFrame,
      prelude::{IntoLazy, col, cols, lit, RoundMode},
  };


  fn main() -> Result<(), PolarsError> {
      let mut df: DataFrame = df!(
            "name" => ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
            "birthdate" => [
                NaiveDate::from_ymd_opt(1997, 1, 10).unwrap(),
                NaiveDate::from_ymd_opt(1985, 2, 15).unwrap(),
                NaiveDate::from_ymd_opt(1997, 3, 22).unwrap(),
                NaiveDate::from_ymd_opt(1997, 4, 30).unwrap(),
            ],
            "weight" => [57.9, 72.5, 54.6, 83.1], // (kg)
            "height" => [1.56, 1.77, 1.65, 1.75], // (m)
        )
        .unwrap();

        let result = df
            .clone()
            .lazy()
            .select([
                col("name"),
                (cols(["weight", "height"]).as_expr() * lit(0.95))
                    .round(2, RoundMode::default())
                    .name()
                    .suffix("-5%"),
            ])
            .collect()?;
        println!("Transform:");
        print!("{result}\n");

      Ok(())
  }
Transform:
shape: (4, 3)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name           โ”† weight-5% โ”† height-5% โ”‚
โ”‚ ---            โ”† ---       โ”† ---       โ”‚
โ”‚ str            โ”† f64       โ”† f64       โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Alice Archer   โ”† 55.0      โ”† 1.48      โ”‚
โ”‚ Ben Brown      โ”† 68.88     โ”† 1.68      โ”‚
โ”‚ Chloe Cooper   โ”† 51.87     โ”† 1.57      โ”‚
โ”‚ Daniel Donovan โ”† 78.94     โ”† 1.66      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Filtering rows

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "is_between", "sql"] }
  //! ```

  use chrono::NaiveDate;
  use polars::{
      df,
      error::PolarsError,
      frame::{DataFrame},
      prelude::{IntoLazy, col, lit, ClosedInterval},
  };


  fn main() -> Result<(), PolarsError> {
      let mut df: DataFrame = df!(
            "name" => ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
            "birthdate" => [
                NaiveDate::from_ymd_opt(1997, 1, 10).unwrap(),
                NaiveDate::from_ymd_opt(1985, 2, 15).unwrap(),
                NaiveDate::from_ymd_opt(1997, 3, 22).unwrap(),
                NaiveDate::from_ymd_opt(1997, 4, 30).unwrap(),
            ],
            "weight" => [57.9, 72.5, 54.6, 83.1], // (kg)
            "height" => [1.56, 1.77, 1.65, 1.75], // (m)
        )
        .unwrap();

        let result = df
            .clone()
            .lazy()
            .filter(col("birthdate").dt().year().lt(lit(1990)))
            .collect()?;
        println!("With row filtering:");
        print!("{result}\n");

        let result = df
              .clone()
              .lazy()
              .filter(
                  col("birthdate")
                      .is_between(
                          lit(NaiveDate::from_ymd_opt(1982, 12, 31).unwrap()),
                          lit(NaiveDate::from_ymd_opt(1996, 1, 1).unwrap()),
                          ClosedInterval::Both,
                      )
                      .and(col("height").gt(lit(1.7))),
              )
              .collect()?;
        println!("With complex row filtering:");
        print!("{result}\n");

      Ok(())
  }
With row filtering:
shape: (1, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name      โ”† birthdate  โ”† weight โ”† height โ”‚
โ”‚ ---       โ”† ---        โ”† ---    โ”† ---    โ”‚
โ”‚ str       โ”† date       โ”† f64    โ”† f64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Ben Brown โ”† 1985-02-15 โ”† 72.5   โ”† 1.77   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
With complex row filtering:
shape: (1, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name      โ”† birthdate  โ”† weight โ”† height โ”‚
โ”‚ ---       โ”† ---        โ”† ---    โ”† ---    โ”‚
โ”‚ str       โ”† date       โ”† f64    โ”† f64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Ben Brown โ”† 1985-02-15 โ”† 72.5   โ”† 1.77   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Grouping by

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql"] }
  //! ```

  use chrono::NaiveDate;
  use polars::{
      df,
      error::PolarsError,
      frame::DataFrame,
      prelude::{IntoLazy, col, lit, len, RoundMode},
  };


  fn main() -> Result<(), PolarsError> {
      let mut df: DataFrame = df!(
            "name" => ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
            "birthdate" => [
                NaiveDate::from_ymd_opt(1997, 1, 10).unwrap(),
                NaiveDate::from_ymd_opt(1985, 2, 15).unwrap(),
                NaiveDate::from_ymd_opt(1997, 3, 22).unwrap(),
                NaiveDate::from_ymd_opt(1997, 4, 30).unwrap(),
            ],
            "weight" => [57.9, 72.5, 54.6, 83.1], // (kg)
            "height" => [1.56, 1.77, 1.65, 1.75], // (m)
        )
        .unwrap();

        let result = df
            .clone()
            .lazy()
            .group_by([(col("birthdate").dt().year() / lit(10) * lit(10)).alias("decade")])
            .agg([len()])
            .collect()?;
        println!("Grouping by birth decade:");
        print!("{result}\n");

        let result = df
            .clone()
            .lazy()
            .group_by([(col("birthdate").dt().year() / lit(10) * lit(10)).alias("decade")])
            .agg([
                len().alias("sample_size"),
                col("weight")
                    .mean()
                    .round(2, RoundMode::default())
                    .alias("avg_weight"),
                col("height").max().alias("tallest"),
            ])
            .collect()?;
        println!("Grouping by derived features:");
        println!("{result}");

      Ok(())
  }
Grouping by birth decade:
shape: (2, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”
โ”‚ decade โ”† len โ”‚
โ”‚ ---    โ”† --- โ”‚
โ”‚ i32    โ”† u32 โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ก
โ”‚ 1990   โ”† 3   โ”‚
โ”‚ 1980   โ”† 1   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜
Grouping by derived features:
shape: (2, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ decade โ”† sample_size โ”† avg_weight โ”† tallest โ”‚
โ”‚ ---    โ”† ---         โ”† ---        โ”† ---     โ”‚
โ”‚ i32    โ”† u32         โ”† f64        โ”† f64     โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 1990   โ”† 3           โ”† 65.2       โ”† 1.75    โ”‚
โ”‚ 1980   โ”† 1           โ”† 72.5       โ”† 1.77    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Data Analysis

When we receive a new dataset, the goal is not to immediately build charts or run models. The first goal is to understand whether the data can be trusted. The complete analysis is on github.

1. Inspect the raw data:

Download the data, load it with Polars3 and then print the head

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql", "csv"] }
  //! ```

  use polars::{
      error::PolarsError,
      prelude::{CsvParseOptions, CsvReadOptions, SerReader},
  };

  fn main() -> Result<(), PolarsError> {
      let df_csv = CsvReadOptions::default()
          .with_has_header(true)
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;
      println!("{df_csv}");
      Ok(())
  }
rust-script failed with exit code 1

[stderr]
Error: ComputeError(ErrString("could not parse `50.5` as dtype `i64` at column 'Alkalinity-total (as CaCO3)' (column number 4)\n\nThe current offset in the file is 7606 bytes.\n\nYou might want to try:\n- increasing `infer_schema_length` (e.g. `infer_schema_length=10000`),\n- specifying correct dtype with the `schema_overrides` argument\n- setting `ignore_errors` to `True`,\n- adding `50.5` to the `null_values` list.\n\nOriginal error: ```invalid primitive value found during CSV parsing```"))

Polars3 is not guessing the type of some of the columns correctly. Let's allow it to guess from 100 rows by default.

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql", "csv"] }
  //! ```

  use polars::{
      error::PolarsError,
      prelude::{CsvParseOptions, CsvReadOptions, SerReader},
  };

  fn main() -> Result<(), PolarsError> {
      let df_csv = CsvReadOptions::default()
          .with_has_header(true)
          .with_infer_schema_length(None)
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;
      println!("{df_csv}");
      Ok(())
  }
shape: (29_159, 14)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ WaterbodyNam โ”† Years โ”† SampleDate โ”† Alkalinity-t โ”† โ€ฆ โ”† pH   โ”† Temperature โ”† Total       โ”† True   โ”‚
โ”‚ e            โ”† ---   โ”† ---        โ”† otal (as     โ”†   โ”† ---  โ”† ---         โ”† Hardness    โ”† Colour โ”‚
โ”‚ ---          โ”† i64   โ”† str        โ”† CaCO3)       โ”†   โ”† f64  โ”† f64         โ”† (as CaCO3)  โ”† ---    โ”‚
โ”‚ str          โ”†       โ”†            โ”† ---          โ”†   โ”†      โ”†             โ”† ---         โ”† f64    โ”‚
โ”‚              โ”†       โ”†            โ”† f64          โ”†   โ”†      โ”†             โ”† f64         โ”†        โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ABBEYTOWN_01 โ”† 2023  โ”† Feb        โ”† 314.0        โ”† โ€ฆ โ”† 7.8  โ”† 10.4        โ”† 370.0       โ”† 24.0   โ”‚
โ”‚ 0            โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ Allua        โ”† 2007  โ”† Aug        โ”† 14.0         โ”† โ€ฆ โ”† 7.42 โ”† 17.8        โ”† 13.4        โ”† 35.0   โ”‚
โ”‚ Allua        โ”† 2007  โ”† Aug        โ”† 17.0         โ”† โ€ฆ โ”† 7.67 โ”† 18.1        โ”† 15.8        โ”† 29.0   โ”‚
โ”‚ Allua        โ”† 2007  โ”† Aug        โ”† 18.0         โ”† โ€ฆ โ”† 7.63 โ”† 17.8        โ”† 15.9        โ”† 31.0   โ”‚
โ”‚ Allua        โ”† 2007  โ”† Sep        โ”† 19.0         โ”† โ€ฆ โ”† 7.33 โ”† 20.1        โ”† 15.4        โ”† 23.0   โ”‚
โ”‚ โ€ฆ            โ”† โ€ฆ     โ”† โ€ฆ          โ”† โ€ฆ            โ”† โ€ฆ โ”† โ€ฆ    โ”† โ€ฆ           โ”† โ€ฆ           โ”† โ€ฆ      โ”‚
โ”‚ SULLANE_060  โ”† 2022  โ”† Sep        โ”† 31.0         โ”† โ€ฆ โ”† 7.1  โ”† 14.9        โ”† 45.0        โ”† 27.0   โ”‚
โ”‚ SULLANE_060  โ”† 2022  โ”† Nov        โ”† 22.0         โ”† โ€ฆ โ”† 6.9  โ”† 12.3        โ”† 34.0        โ”† 58.0   โ”‚
โ”‚ SULLANE_060  โ”† 2023  โ”† Mar        โ”† 36.0         โ”† โ€ฆ โ”† 7.2  โ”† 7.1         โ”† 44.0        โ”† 20.0   โ”‚
โ”‚ TWO POT      โ”† 2023  โ”† Feb        โ”† 81.0         โ”† โ€ฆ โ”† 7.4  โ”† 8.6         โ”† 120.0       โ”† 9.0    โ”‚
โ”‚ (Cork        โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ City)_010    โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ TWO POT      โ”† 2023  โ”† Feb        โ”† 82.0         โ”† โ€ฆ โ”† 7.8  โ”† 8.1         โ”† 121.0       โ”† 5.0    โ”‚
โ”‚ (Cork        โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ City)_010    โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Let us have Polars4 infer the proper types of the columns now from 10000 rows

  //! ```cargo
  //! [dependencies]
  //! chrono = "0.4.45"
  //! polars = { version = "0.54.4", features = ["lazy", "temporal", "sql", "csv"] }
  //! ```

  use polars::{
      error::PolarsError,
      prelude::{CsvParseOptions, CsvReadOptions, SerReader},
  };

  fn main() -> Result<(), PolarsError> {
      let df_csv = CsvReadOptions::default()
          .with_has_header(true)
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;
      println!("{df_csv}");
      Ok(())
  }
shape: (29_159, 14)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ WaterbodyNam โ”† Years โ”† SampleDate โ”† Alkalinity-t โ”† โ€ฆ โ”† pH   โ”† Temperature โ”† Total       โ”† True   โ”‚
โ”‚ e            โ”† ---   โ”† ---        โ”† otal (as     โ”†   โ”† ---  โ”† ---         โ”† Hardness    โ”† Colour โ”‚
โ”‚ ---          โ”† i64   โ”† str        โ”† CaCO3)       โ”†   โ”† f64  โ”† f64         โ”† (as CaCO3)  โ”† ---    โ”‚
โ”‚ str          โ”†       โ”†            โ”† ---          โ”†   โ”†      โ”†             โ”† ---         โ”† f64    โ”‚
โ”‚              โ”†       โ”†            โ”† f64          โ”†   โ”†      โ”†             โ”† f64         โ”†        โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ABBEYTOWN_01 โ”† 2023  โ”† Feb        โ”† 314.0        โ”† โ€ฆ โ”† 7.8  โ”† 10.4        โ”† 370.0       โ”† 24.0   โ”‚
โ”‚ 0            โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ Allua        โ”† 2007  โ”† Aug        โ”† 14.0         โ”† โ€ฆ โ”† 7.42 โ”† 17.8        โ”† 13.4        โ”† 35.0   โ”‚
โ”‚ Allua        โ”† 2007  โ”† Aug        โ”† 17.0         โ”† โ€ฆ โ”† 7.67 โ”† 18.1        โ”† 15.8        โ”† 29.0   โ”‚
โ”‚ Allua        โ”† 2007  โ”† Aug        โ”† 18.0         โ”† โ€ฆ โ”† 7.63 โ”† 17.8        โ”† 15.9        โ”† 31.0   โ”‚
โ”‚ Allua        โ”† 2007  โ”† Sep        โ”† 19.0         โ”† โ€ฆ โ”† 7.33 โ”† 20.1        โ”† 15.4        โ”† 23.0   โ”‚
โ”‚ โ€ฆ            โ”† โ€ฆ     โ”† โ€ฆ          โ”† โ€ฆ            โ”† โ€ฆ โ”† โ€ฆ    โ”† โ€ฆ           โ”† โ€ฆ           โ”† โ€ฆ      โ”‚
โ”‚ SULLANE_060  โ”† 2022  โ”† Sep        โ”† 31.0         โ”† โ€ฆ โ”† 7.1  โ”† 14.9        โ”† 45.0        โ”† 27.0   โ”‚
โ”‚ SULLANE_060  โ”† 2022  โ”† Nov        โ”† 22.0         โ”† โ€ฆ โ”† 6.9  โ”† 12.3        โ”† 34.0        โ”† 58.0   โ”‚
โ”‚ SULLANE_060  โ”† 2023  โ”† Mar        โ”† 36.0         โ”† โ€ฆ โ”† 7.2  โ”† 7.1         โ”† 44.0        โ”† 20.0   โ”‚
โ”‚ TWO POT      โ”† 2023  โ”† Feb        โ”† 81.0         โ”† โ€ฆ โ”† 7.4  โ”† 8.6         โ”† 120.0       โ”† 9.0    โ”‚
โ”‚ (Cork        โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ City)_010    โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ TWO POT      โ”† 2023  โ”† Feb        โ”† 82.0         โ”† โ€ฆ โ”† 7.8  โ”† 8.1         โ”† 121.0       โ”† 5.0    โ”‚
โ”‚ (Cork        โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ”‚ City)_010    โ”†       โ”†            โ”†              โ”†   โ”†      โ”†             โ”†             โ”†        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  use data_pipeline::quality_flow::inspect_raw_data;

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 1. Inspect the raw data
      inspect_raw_data(df)?;

      Ok(())
  }
  cargo run --bin inspect_raw_data
============================================================
1. Inspect the raw data
============================================================

raw dataset size:
This confirms how many rows and columns were loaded from the CSV.
shape: (2, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ metric  โ”† value โ”‚
โ”‚ ---     โ”† ---   โ”‚
โ”‚ str     โ”† i64   โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ rows    โ”† 29159 โ”‚
โ”‚ columns โ”† 14    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

inferred schema:
This shows each column name and the type Polars inferred from the file.
shape: (14, 3)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ column                          โ”† inferred_type โ”† storage_kind  โ”‚
โ”‚ ---                             โ”† ---           โ”† ---           โ”‚
โ”‚ str                             โ”† str           โ”† str           โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ WaterbodyName                   โ”† String        โ”† text or mixed โ”‚
โ”‚ Years                           โ”† Int64         โ”† number        โ”‚
โ”‚ SampleDate                      โ”† String        โ”† text or mixed โ”‚
โ”‚ Alkalinity-total (as CaCO3)     โ”† Float64       โ”† number        โ”‚
โ”‚ Ammonia-Total (as N)            โ”† Float64       โ”† number        โ”‚
โ”‚ โ€ฆ                               โ”† โ€ฆ             โ”† โ€ฆ             โ”‚
โ”‚ ortho-Phosphate (as P) - unspeโ€ฆ โ”† Float64       โ”† number        โ”‚
โ”‚ pH                              โ”† Float64       โ”† number        โ”‚
โ”‚ Temperature                     โ”† Float64       โ”† number        โ”‚
โ”‚ Total Hardness (as CaCO3)       โ”† Float64       โ”† number        โ”‚
โ”‚ True Colour                     โ”† Float64       โ”† number        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

raw row sample:
This shows one original wide record before any reshaping.
shape: (1, 14)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ WaterbodyNam โ”† Years โ”† SampleDate โ”† Alkalinity-t โ”† โ€ฆ โ”† pH  โ”† Temperature  โ”† Total       โ”† True   โ”‚
โ”‚ e            โ”† ---   โ”† ---        โ”† otal (as     โ”†   โ”† --- โ”† ---          โ”† Hardness    โ”† Colour โ”‚
โ”‚ ---          โ”† i64   โ”† str        โ”† CaCO3)       โ”†   โ”† f64 โ”† f64          โ”† (as CaCO3)  โ”† ---    โ”‚
โ”‚ str          โ”†       โ”†            โ”† ---          โ”†   โ”†     โ”†              โ”† ---         โ”† f64    โ”‚
โ”‚              โ”†       โ”†            โ”† f64          โ”†   โ”†     โ”†              โ”† f64         โ”†        โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ABBEYTOWN_01 โ”† 2023  โ”† Feb        โ”† 314.0        โ”† โ€ฆ โ”† 7.8 โ”† 10.4         โ”† 370.0       โ”† 24.0   โ”‚
โ”‚ 0            โ”†       โ”†            โ”†              โ”†   โ”†     โ”†              โ”†             โ”†        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

first-pass column roles:
This separates location/date columns from measurement columns.
shape: (14, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ column                          โ”† role        โ”‚
โ”‚ ---                             โ”† ---         โ”‚
โ”‚ str                             โ”† str         โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ WaterbodyName                   โ”† location    โ”‚
โ”‚ Years                           โ”† date        โ”‚
โ”‚ SampleDate                      โ”† date        โ”‚
โ”‚ Alkalinity-total (as CaCO3)     โ”† measurement โ”‚
โ”‚ Ammonia-Total (as N)            โ”† measurement โ”‚
โ”‚ โ€ฆ                               โ”† โ€ฆ           โ”‚
โ”‚ ortho-Phosphate (as P) - unspeโ€ฆ โ”† measurement โ”‚
โ”‚ pH                              โ”† measurement โ”‚
โ”‚ Temperature                     โ”† measurement โ”‚
โ”‚ Total Hardness (as CaCO3)       โ”† measurement โ”‚
โ”‚ True Colour                     โ”† measurement โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

long-form sample:
This previews the wide measurements as parameter/value rows.
shape: (5, 7)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ WaterbodyName โ”† Years โ”† SampleDate โ”† source_column  โ”† measurement_va โ”† parameter      โ”† unit     โ”‚
โ”‚ ---           โ”† ---   โ”† ---        โ”† ---            โ”† lue            โ”† ---            โ”† ---      โ”‚
โ”‚ str           โ”† i64   โ”† str        โ”† str            โ”† ---            โ”† str            โ”† str      โ”‚
โ”‚               โ”†       โ”†            โ”†                โ”† f64            โ”†                โ”†          โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ABBEYTOWN_010 โ”† 2023  โ”† Feb        โ”† Alkalinity-tot โ”† 314.0          โ”† Alkalinity-tot โ”† as CaCO3 โ”‚
โ”‚               โ”†       โ”†            โ”† al (as CaCO3)  โ”†                โ”† al             โ”†          โ”‚
โ”‚ Allua         โ”† 2007  โ”† Aug        โ”† Alkalinity-tot โ”† 14.0           โ”† Alkalinity-tot โ”† as CaCO3 โ”‚
โ”‚               โ”†       โ”†            โ”† al (as CaCO3)  โ”†                โ”† al             โ”†          โ”‚
โ”‚ Allua         โ”† 2007  โ”† Aug        โ”† Alkalinity-tot โ”† 17.0           โ”† Alkalinity-tot โ”† as CaCO3 โ”‚
โ”‚               โ”†       โ”†            โ”† al (as CaCO3)  โ”†                โ”† al             โ”†          โ”‚
โ”‚ Allua         โ”† 2007  โ”† Aug        โ”† Alkalinity-tot โ”† 18.0           โ”† Alkalinity-tot โ”† as CaCO3 โ”‚
โ”‚               โ”†       โ”†            โ”† al (as CaCO3)  โ”†                โ”† al             โ”†          โ”‚
โ”‚ Allua         โ”† 2007  โ”† Sep        โ”† Alkalinity-tot โ”† 19.0           โ”† Alkalinity-tot โ”† as CaCO3 โ”‚
โ”‚               โ”†       โ”†            โ”† al (as CaCO3)  โ”†                โ”† al             โ”†          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2. Profile the data

This gives us a first picture of the dataset before making decisions.

  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  use data_pipeline::quality_flow::profile_the_data;

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 2. Profile the data
      profile_the_data(df)?;

      Ok(())
  }
  cargo run --bin profile_the_data
============================================================
2. Profile the data
============================================================

profile scope:
This repeats the dataset size before summarizing each important column.
shape: (2, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ metric  โ”† value โ”‚
โ”‚ ---     โ”† ---   โ”‚
โ”‚ str     โ”† i64   โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ rows    โ”† 29159 โ”‚
โ”‚ columns โ”† 14    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

date coverage:
This combines Years and SampleDate into a usable month-level date range.
shape: (5, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ metric                    โ”† value      โ”‚
โ”‚ ---                       โ”† ---        โ”‚
โ”‚ str                       โ”† str        โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ earliest_date             โ”† 2007-01-01 โ”‚
โ”‚ latest_date               โ”† 2023-04-01 โ”‚
โ”‚ invalid_dates             โ”† 0          โ”‚
โ”‚ missing_dates             โ”† 0          โ”‚
โ”‚ gaps_over_time_gt_31_days โ”† 0          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

column profile:
This gives missing counts, distinct counts, numeric ranges, averages, and notes.
shape: (14, 9)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ column       โ”† role     โ”† type    โ”† missing โ”† โ€ฆ โ”† minimum โ”† maximum โ”† average      โ”† notes       โ”‚
โ”‚ ---          โ”† ---      โ”† ---     โ”† ---     โ”†   โ”† ---     โ”† ---     โ”† ---          โ”† ---         โ”‚
โ”‚ str          โ”† str      โ”† str     โ”† i64     โ”†   โ”† str     โ”† str     โ”† str          โ”† str         โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ WaterbodyNam โ”† location โ”† String  โ”† 0       โ”† โ€ฆ โ”†         โ”†         โ”†              โ”† unique      โ”‚
โ”‚ e            โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”†              โ”† locations:  โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”†              โ”† 160         โ”‚
โ”‚ Years        โ”† date     โ”† Int64   โ”† 0       โ”† โ€ฆ โ”† 2007    โ”† 2023    โ”† Float64(2014 โ”† included in โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† .78253712404 โ”† combined    โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 4)           โ”† date coveโ€ฆ  โ”‚
โ”‚ SampleDate   โ”† date     โ”† String  โ”† 0       โ”† โ€ฆ โ”†         โ”†         โ”†              โ”† review full โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”†              โ”† category    โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”†              โ”† list; incโ€ฆ  โ”‚
โ”‚ Alkalinity-t โ”† numeric  โ”† Float64 โ”† 0       โ”† โ€ฆ โ”† 0       โ”† 442     โ”† Float64(139. โ”†             โ”‚
โ”‚ otal (as     โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 858347851435 โ”†             โ”‚
โ”‚ CaCO3)       โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 2)           โ”†             โ”‚
โ”‚ Ammonia-Tota โ”† numeric  โ”† Float64 โ”† 0       โ”† โ€ฆ โ”† 0       โ”† 40      โ”† Float64(0.06 โ”†             โ”‚
โ”‚ l (as N)     โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 357266127096 โ”†             โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 262)         โ”†             โ”‚
โ”‚ โ€ฆ            โ”† โ€ฆ        โ”† โ€ฆ       โ”† โ€ฆ       โ”† โ€ฆ โ”† โ€ฆ       โ”† โ€ฆ       โ”† โ€ฆ            โ”† โ€ฆ           โ”‚
โ”‚ ortho-Phosph โ”† numeric  โ”† Float64 โ”† 0       โ”† โ€ฆ โ”† -0.004  โ”† 70      โ”† Float64(0.06 โ”† negative    โ”‚
โ”‚ ate (as P) - โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 878934462773 โ”† value found โ”‚
โ”‚ unspeโ€ฆ       โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 074)         โ”† (-0.004)    โ”‚
โ”‚ pH           โ”† numeric  โ”† Float64 โ”† 0       โ”† โ€ฆ โ”† 4.7     โ”† 9.8     โ”† Float64(7.55 โ”†             โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 205686066051 โ”†             โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 8)           โ”†             โ”‚
โ”‚ Temperature  โ”† numeric  โ”† Float64 โ”† 0       โ”† โ€ฆ โ”† 0.6     โ”† 637     โ”† Float64(10.8 โ”†             โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 505031036729 โ”†             โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 74)          โ”†             โ”‚
โ”‚ Total        โ”† numeric  โ”† Float64 โ”† 0       โ”† โ€ฆ โ”† 0       โ”† 642     โ”† Float64(159. โ”†             โ”‚
โ”‚ Hardness (as โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 092110326142 โ”†             โ”‚
โ”‚ CaCO3)       โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 9)           โ”†             โ”‚
โ”‚ True Colour  โ”† numeric  โ”† Float64 โ”† 0       โ”† โ€ฆ โ”† 0       โ”† 953     โ”† Float64(58.1 โ”†             โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 374635618505 โ”†             โ”‚
โ”‚              โ”†          โ”†         โ”†         โ”†   โ”†         โ”†         โ”† 45)          โ”†             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

text/category profile:
This summarizes unique text values and possible spelling variations.
shape: (2, 5)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ column        โ”† empty_values โ”† unique_values โ”† sample_unique_values โ”† possible_spelling_variatio โ”‚
โ”‚ ---           โ”† ---          โ”† ---           โ”† ---                  โ”† ns                         โ”‚
โ”‚ str           โ”† i64          โ”† i64           โ”† str                  โ”† ---                        โ”‚
โ”‚               โ”†              โ”†               โ”†                      โ”† str                        โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ WaterbodyName โ”† 0            โ”† 160           โ”† ABBEYTOWN_010,       โ”†                            โ”‚
โ”‚               โ”†              โ”†               โ”† ASKANAGAP STREAโ€ฆ     โ”†                            โ”‚
โ”‚ SampleDate    โ”† 0            โ”† 12            โ”† Apr, Aug, Dec, Feb,  โ”†                            โ”‚
โ”‚               โ”†              โ”†               โ”† Jan, Jul, โ€ฆ          โ”†                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

3. Identify data quality problems

  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  use data_pipeline::quality_flow::identify_data_quality_problems;

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 3. Identify data quality problems
      identify_data_quality_problems(df)?;

      Ok(())
  }
  cargo run --bin identify_data_quality_problems
============================================================
3. Identify data quality problems
============================================================

data quality summary:
This is the high-level checklist of problems that could make analysis unreliable.
shape: (13, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ check                           โ”† count  โ”† status โ”† note                            โ”‚
โ”‚ ---                             โ”† ---    โ”† ---    โ”† ---                             โ”‚
โ”‚ str                             โ”† i64    โ”† str    โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ measurement columns checked     โ”† 11     โ”† info   โ”† wide measurement columns becomโ€ฆ โ”‚
โ”‚ missing values                  โ”† 0      โ”† ok     โ”† null values across raw columns  โ”‚
โ”‚ duplicate rows                  โ”† 14478  โ”† review โ”† exact raw-row duplicates        โ”‚
โ”‚ numeric values stored as text   โ”† 0      โ”† ok     โ”† string columns whose values arโ€ฆ โ”‚
โ”‚ invalid date values             โ”† 0      โ”† ok     โ”† date-like values that failed pโ€ฆ โ”‚
โ”‚ โ€ฆ                               โ”† โ€ฆ      โ”† โ€ฆ      โ”† โ€ฆ                               โ”‚
โ”‚ pH outside 0-14                 โ”† 0      โ”† ok     โ”† domain rule for pH              โ”‚
โ”‚ negative concentration-like meโ€ฆ โ”† 2      โ”† review โ”† negative values outside pH andโ€ฆ โ”‚
โ”‚ outlier values                  โ”† 12652  โ”† review โ”† IQR outliers across 8 columns   โ”‚
โ”‚ duplicate location/date/parameโ€ฆ โ”† 204237 โ”† review โ”† same location, date, and paramโ€ฆ โ”‚
โ”‚ large gaps in time series       โ”† 5395   โ”† review โ”† location time periods with misโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

data quality details:
This gives the columns and counts behind the summary checks.
shape: (9, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ problem                      โ”† column                      โ”† count โ”† note                        โ”‚
โ”‚ ---                          โ”† ---                         โ”† ---   โ”† ---                         โ”‚
โ”‚ str                          โ”† str                         โ”† i64   โ”† str                         โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ outliers                     โ”† Chloride                    โ”† 1861  โ”† outside [5.999999999999998, โ”‚
โ”‚                              โ”†                             โ”†       โ”† 31โ€ฆ                         โ”‚
โ”‚ outliers                     โ”† Conductivity @25ยฐC          โ”† 20    โ”† outside [-179, 909] by IQR  โ”‚
โ”‚                              โ”†                             โ”†       โ”† rulโ€ฆ                        โ”‚
โ”‚ outliers                     โ”† Dissolved Oxygen            โ”† 2314  โ”† outside                     โ”‚
โ”‚                              โ”†                             โ”†       โ”† [10.999999999999993, 1โ€ฆ     โ”‚
โ”‚ outliers                     โ”† ortho-Phosphate (as P) -    โ”† 6229  โ”† outside                     โ”‚
โ”‚                              โ”† unspeโ€ฆ                      โ”†       โ”† [0.009500000000000005,โ€ฆ     โ”‚
โ”‚ negative concentration-like  โ”† ortho-Phosphate (as P) -    โ”† 2     โ”† negative value outside pH   โ”‚
โ”‚ meโ€ฆ                          โ”† unspeโ€ฆ                      โ”†       โ”† and โ€ฆ                       โ”‚
โ”‚ outliers                     โ”† pH                          โ”† 406   โ”† outside [6, 9.2] by IQR     โ”‚
โ”‚                              โ”†                             โ”†       โ”† rule                        โ”‚
โ”‚ outliers                     โ”† Temperature                 โ”† 134   โ”† outside                     โ”‚
โ”‚                              โ”†                             โ”†       โ”† [0.8499999999999988, 2โ€ฆ     โ”‚
โ”‚ outliers                     โ”† Total Hardness (as CaCO3)   โ”† 3     โ”† outside [-178, 486] by IQR  โ”‚
โ”‚                              โ”†                             โ”†       โ”† rulโ€ฆ                        โ”‚
โ”‚ outliers                     โ”† True Colour                 โ”† 1685  โ”† outside [-48.5, 147.5] by   โ”‚
โ”‚                              โ”†                             โ”†       โ”† IQR โ€ฆ                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

principle:
This is the rule that guides the cleaning decision in the next step.
shape: (1, 1)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ principle                       โ”‚
โ”‚ ---                             โ”‚
โ”‚ str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Bad input should not quietly bโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

4. Clean and normalize the data

  use data_pipeline::quality_flow::clean_and_normalize_the_data;
  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 4. Clean and normalize the data
      clean_and_normalize_the_data(df)?;

      Ok(())
  }
  cargo run --bin clean_and_normalize_the_data
============================================================
4. Clean and normalize the data
============================================================

cleaning summary:
This shows how many normalized rows were kept, rejected, or deduplicated.
shape: (3, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ metric                   โ”† value  โ”‚
โ”‚ ---                      โ”† ---    โ”‚
โ”‚ str                      โ”† i64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ cleaned_rows             โ”† 148372 โ”‚
โ”‚ invalid_rows             โ”† 2      โ”‚
โ”‚ exact_duplicates_removed โ”† 172375 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

cleaned sample:
This is the normalized long-form data that is easier to query and visualize.
shape: (5, 10)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location    โ”† sample_date โ”† year โ”† โ€ฆ โ”† parameter_ โ”† unit       โ”† value โ”† source_col โ”‚
โ”‚ ---        โ”† ---         โ”† ---         โ”† ---  โ”†   โ”† code       โ”† ---        โ”† ---   โ”† umn        โ”‚
โ”‚ i64        โ”† str         โ”† str         โ”† i32  โ”†   โ”† ---        โ”† str        โ”† f64   โ”† ---        โ”‚
โ”‚            โ”†             โ”†             โ”†      โ”†   โ”† str        โ”†            โ”†       โ”† str        โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 1          โ”† ABBEYTOWN_0 โ”† 2023-02-01  โ”† 2023 โ”† โ€ฆ โ”† ALKALINITY โ”† as CaCO3   โ”† 314.0 โ”† Alkalinity โ”‚
โ”‚            โ”† 10          โ”†             โ”†      โ”†   โ”† -TOTAL     โ”†            โ”†       โ”† -total (as โ”‚
โ”‚            โ”†             โ”†             โ”†      โ”†   โ”†            โ”†            โ”†       โ”† CaCO3)     โ”‚
โ”‚ 1          โ”† ABBEYTOWN_0 โ”† 2023-02-01  โ”† 2023 โ”† โ€ฆ โ”† AMMONIA-TO โ”† as N       โ”† 0.033 โ”† Ammonia-To โ”‚
โ”‚            โ”† 10          โ”†             โ”†      โ”†   โ”† TAL        โ”†            โ”†       โ”† tal (as N) โ”‚
โ”‚ 1          โ”† ABBEYTOWN_0 โ”† 2023-02-01  โ”† 2023 โ”† โ€ฆ โ”† BOD_-_5_DA โ”† Total      โ”† 1.2   โ”† BOD - 5    โ”‚
โ”‚            โ”† 10          โ”†             โ”†      โ”†   โ”† YS         โ”†            โ”†       โ”† days       โ”‚
โ”‚            โ”†             โ”†             โ”†      โ”†   โ”†            โ”†            โ”†       โ”† (Total)    โ”‚
โ”‚ 1          โ”† ABBEYTOWN_0 โ”† 2023-02-01  โ”† 2023 โ”† โ€ฆ โ”† CHLORIDE   โ”† not_encode โ”† 27.3  โ”† Chloride   โ”‚
โ”‚            โ”† 10          โ”†             โ”†      โ”†   โ”†            โ”† d          โ”†       โ”†            โ”‚
โ”‚ 1          โ”† ABBEYTOWN_0 โ”† 2023-02-01  โ”† 2023 โ”† โ€ฆ โ”† CONDUCTIVI โ”† @25ยฐC      โ”† 711.0 โ”† Conductivi โ”‚
โ”‚            โ”† 10          โ”†             โ”†      โ”†   โ”† TY         โ”†            โ”†       โ”† ty @25ยฐC   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

invalid rows sample:
These rows were separated so bad input does not become trusted data.
shape: (2, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location   โ”† raw_date โ”† source_column          โ”† raw_value โ”† invalid_reason         โ”‚
โ”‚ ---        โ”† ---        โ”† ---      โ”† ---                    โ”† ---       โ”† ---                    โ”‚
โ”‚ i64        โ”† str        โ”† str      โ”† str                    โ”† str       โ”† str                    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 111        โ”† ASKANAGAP  โ”† Jan      โ”† ortho-Phosphate (as P) โ”† -0.004    โ”† negative               โ”‚
โ”‚            โ”† STREAM_010 โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† concentration-like meโ€ฆ โ”‚
โ”‚ 15723      โ”† ASKANAGAP  โ”† Jan      โ”† ortho-Phosphate (as P) โ”† -0.004    โ”† negative               โ”‚
โ”‚            โ”† STREAM_010 โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† concentration-like meโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

5. Handle missing values carefully

  use data_pipeline::quality_flow::handle_missing_values_carefully;
  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 5. Handle missing values carefully
      handle_missing_values_carefully(df)?;

      Ok(())
  }
  cargo run --bin handle_missing_values_carefully
============================================================
5. Handle missing values carefully
============================================================

missing-value decision summary:
This separates invalid data, missing data, gap candidates, and flagged observed values.
shape: (7, 3)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ case                           โ”† count โ”† decision                        โ”‚
โ”‚ ---                            โ”† ---   โ”† ---                             โ”‚
โ”‚ str                            โ”† i64   โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ invalid or impossible rows     โ”† 2     โ”† quarantine                      โ”‚
โ”‚ missing critical fields        โ”† 0     โ”† reject row                      โ”‚
โ”‚ missing measurement values     โ”† 0     โ”† keep NULL unless safe to estimโ€ฆ โ”‚
โ”‚ small time-series gaps         โ”† 45165 โ”† candidate for interpolation afโ€ฆ โ”‚
โ”‚ large time-series gaps         โ”† 14179 โ”† keep missing                    โ”‚
โ”‚ suspicious but possible values โ”† 10677 โ”† keep observed value with qualiโ€ฆ โ”‚
โ”‚ values filled automatically    โ”† 0     โ”† none; filling is not automatic  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

quarantined row sample:
These rows are not filled because a critical field or measurement value is missing or invalid.
shape: (2, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location   โ”† raw_date โ”† source_column          โ”† raw_value โ”† decision               โ”‚
โ”‚ ---        โ”† ---        โ”† ---      โ”† ---                    โ”† ---       โ”† ---                    โ”‚
โ”‚ i64        โ”† str        โ”† str      โ”† str                    โ”† str       โ”† str                    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 111        โ”† ASKANAGAP  โ”† Jan      โ”† ortho-Phosphate (as P) โ”† -0.004    โ”† negative               โ”‚
โ”‚            โ”† STREAM_010 โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† concentration-like meโ€ฆ โ”‚
โ”‚ 15723      โ”† ASKANAGAP  โ”† Jan      โ”† ortho-Phosphate (as P) โ”† -0.004    โ”† negative               โ”‚
โ”‚            โ”† STREAM_010 โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† concentration-like meโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

time-series gap examples:
These are observed gaps; small gaps may be interpolated only after review.
shape: (20, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ location โ”† parameter_code   โ”† from_date  โ”† to_date    โ”† missing_months โ”† decision             โ”‚
โ”‚ ---      โ”† ---              โ”† ---        โ”† ---        โ”† ---            โ”† ---                  โ”‚
โ”‚ str      โ”† str              โ”† str        โ”† str        โ”† i64            โ”† str                  โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ALLUA    โ”† ALKALINITY-TOTAL โ”† 2007-09-01 โ”† 2008-01-01 โ”† 3              โ”† keep missing; gap is โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† too large            โ”‚
โ”‚ ALLUA    โ”† ALKALINITY-TOTAL โ”† 2008-12-01 โ”† 2009-04-01 โ”† 3              โ”† keep missing; gap is โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† too large            โ”‚
โ”‚ ALLUA    โ”† ALKALINITY-TOTAL โ”† 2009-04-01 โ”† 2009-06-01 โ”† 1              โ”† candidate for        โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† interpolation afโ€ฆ    โ”‚
โ”‚ ALLUA    โ”† ALKALINITY-TOTAL โ”† 2009-06-01 โ”† 2009-08-01 โ”† 1              โ”† candidate for        โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† interpolation afโ€ฆ    โ”‚
โ”‚ ALLUA    โ”† ALKALINITY-TOTAL โ”† 2009-08-01 โ”† 2009-10-01 โ”† 1              โ”† candidate for        โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† interpolation afโ€ฆ    โ”‚
โ”‚ โ€ฆ        โ”† โ€ฆ                โ”† โ€ฆ          โ”† โ€ฆ          โ”† โ€ฆ              โ”† โ€ฆ                    โ”‚
โ”‚ ALLUA    โ”† AMMONIA-TOTAL    โ”† 2009-06-01 โ”† 2009-08-01 โ”† 1              โ”† candidate for        โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† interpolation afโ€ฆ    โ”‚
โ”‚ ALLUA    โ”† AMMONIA-TOTAL    โ”† 2009-08-01 โ”† 2009-10-01 โ”† 1              โ”† candidate for        โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† interpolation afโ€ฆ    โ”‚
โ”‚ ALLUA    โ”† AMMONIA-TOTAL    โ”† 2009-10-01 โ”† 2010-03-01 โ”† 4              โ”† keep missing; gap is โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† too large            โ”‚
โ”‚ ALLUA    โ”† AMMONIA-TOTAL    โ”† 2010-03-01 โ”† 2010-07-01 โ”† 3              โ”† keep missing; gap is โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† too large            โ”‚
โ”‚ ALLUA    โ”† AMMONIA-TOTAL    โ”† 2010-08-01 โ”† 2010-10-01 โ”† 1              โ”† candidate for        โ”‚
โ”‚          โ”†                  โ”†            โ”†            โ”†                โ”† interpolation afโ€ฆ    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

quality-flagged sample:
These observed values are kept, but marked because they need caution.
shape: (10, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ location โ”† sample_date โ”† parameter_code  โ”† value โ”† quality_flag          โ”† missing_decision      โ”‚
โ”‚ ---      โ”† ---         โ”† ---             โ”† ---   โ”† ---                   โ”† ---                   โ”‚
โ”‚ str      โ”† str         โ”† str             โ”† f64   โ”† str                   โ”† str                   โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ALLUA    โ”† 2007-09-01  โ”† AMMONIA-TOTAL   โ”† 0.066 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-01-01  โ”† AMMONIA-TOTAL   โ”† 0.069 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-01-01  โ”† ORTHO-PHOSPHATE โ”† 0.005 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-01-01  โ”† AMMONIA-TOTAL   โ”† 0.068 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-01-01  โ”† AMMONIA-TOTAL   โ”† 0.067 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-02-01  โ”† AMMONIA-TOTAL   โ”† 0.133 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-02-01  โ”† AMMONIA-TOTAL   โ”† 0.111 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-02-01  โ”† AMMONIA-TOTAL   โ”† 0.113 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-03-01  โ”† AMMONIA-TOTAL   โ”† 0.04  โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ”‚ ALLUA    โ”† 2008-03-01  โ”† ORTHO-PHOSPHATE โ”† 0.005 โ”† suspicious_possible_o โ”† keep observed value   โ”‚
โ”‚          โ”†             โ”†                 โ”†       โ”† utlier                โ”† with qualiโ€ฆ           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

principle:
This is the rule for deciding whether a missing value should be filled.
shape: (1, 1)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ principle                       โ”‚
โ”‚ ---                             โ”‚
โ”‚ str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Filling data is a decision, noโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

handled data summary:
This confirms the row counts after applying the missing-value decisions.
shape: (3, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ metric             โ”† value  โ”‚
โ”‚ ---                โ”† ---    โ”‚
โ”‚ str                โ”† i64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ handled_rows       โ”† 148372 โ”‚
โ”‚ quarantined_rows   โ”† 2      โ”‚
โ”‚ duplicates_removed โ”† 172375 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

6. Validate before storing

  use data_pipeline::quality_flow::validate_before_storing;
  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 6. Validate before storing
      validate_before_storing(df)?;

      Ok(())
  }
  cargo run --bin validate_before_storing
============================================================
6. Validate before storing
============================================================

validation rule summary:
This shows the storage rules, how many records were checked, and what failed.
shape: (7, 4)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ rule                            โ”† checked โ”† failed โ”† action                          โ”‚
โ”‚ ---                             โ”† ---     โ”† ---    โ”† ---                             โ”‚
โ”‚ str                             โ”† i64     โ”† i64    โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ every measurement has a locatiโ€ฆ โ”† 320749  โ”† 0      โ”† reject missing locations        โ”‚
โ”‚ every measurement has a parameโ€ฆ โ”† 320749  โ”† 0      โ”† reject missing parameters       โ”‚
โ”‚ every measurement has a date    โ”† 320749  โ”† 0      โ”† reject missing or invalid dateโ€ฆ โ”‚
โ”‚ value is a valid number or expโ€ฆ โ”† 320749  โ”† 0      โ”† store numeric values; store miโ€ฆ โ”‚
โ”‚ known parameter respects range  โ”† 320749  โ”† 20     โ”† reject impossible values for kโ€ฆ โ”‚
โ”‚ exact duplicate records handleโ€ฆ โ”† 320729  โ”† 172369 โ”† remove exact duplicates         โ”‚
โ”‚ repeated measurements handled โ€ฆ โ”† 148360  โ”† 31854  โ”† keep with source_row so repeatโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

records rejected before storage:
These rows failed validation and should not be inserted into trusted tables.
shape: (10, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location     โ”† raw_date โ”† source_column         โ”† raw_value โ”† rule_failed           โ”‚
โ”‚ ---        โ”† ---          โ”† ---      โ”† ---                   โ”† ---       โ”† ---                   โ”‚
โ”‚ i64        โ”† str          โ”† str      โ”† str                   โ”† str       โ”† str                   โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 111        โ”† ASKANAGAP    โ”† Jan      โ”† ortho-Phosphate (as   โ”† -0.004    โ”† known parameter range โ”‚
โ”‚            โ”† STREAM_010   โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 2003       โ”† CAMCOR_020   โ”† Feb      โ”† Temperature           โ”† 58.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”†                       โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 4813       โ”† DARGLE_030   โ”† Jan      โ”† ortho-Phosphate (as   โ”† 42.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 4815       โ”† DARGLE_030   โ”† Feb      โ”† ortho-Phosphate (as   โ”† 22.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 4857       โ”† DARGLE_030   โ”† Jul      โ”† ortho-Phosphate (as   โ”† 70.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 4873       โ”† DARGLE_030   โ”† May      โ”† ortho-Phosphate (as   โ”† 29.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 4893       โ”† DARGLE_030   โ”† Mar      โ”† ortho-Phosphate (as   โ”† 26.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 4903       โ”† DARGLE_030   โ”† Sep      โ”† ortho-Phosphate (as   โ”† 25.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 6096       โ”† GLENCREE_010 โ”† Feb      โ”† ortho-Phosphate (as   โ”† 27.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ”‚ 6117       โ”† GLENCREE_010 โ”† Jul      โ”† ortho-Phosphate (as   โ”† 27.0      โ”† known parameter range โ”‚
โ”‚            โ”†              โ”†          โ”† P) - unspeโ€ฆ           โ”†           โ”† failed (โ€ฆ             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

duplicate handling sample:
These exact duplicates are handled deliberately before storage.
shape: (10, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location โ”† sample_date โ”† parameter_code   โ”† value โ”† action                          โ”‚
โ”‚ ---        โ”† ---      โ”† ---         โ”† ---              โ”† ---   โ”† ---                             โ”‚
โ”‚ i64        โ”† str      โ”† str         โ”† str              โ”† f64   โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 3          โ”† ALLUA    โ”† 2007-08-01  โ”† AMMONIA-TOTAL    โ”† 0.033 โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 3          โ”† ALLUA    โ”† 2007-08-01  โ”† BOD_-_5_DAYS     โ”† 1.2   โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 3          โ”† ALLUA    โ”† 2007-08-01  โ”† ORTHO-PHOSPHATE  โ”† 0.019 โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 4          โ”† ALLUA    โ”† 2007-08-01  โ”† AMMONIA-TOTAL    โ”† 0.033 โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 4          โ”† ALLUA    โ”† 2007-08-01  โ”† BOD_-_5_DAYS     โ”† 1.2   โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 4          โ”† ALLUA    โ”† 2007-08-01  โ”† ORTHO-PHOSPHATE  โ”† 0.019 โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 4          โ”† ALLUA    โ”† 2007-08-01  โ”† TEMPERATURE      โ”† 17.8  โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 6          โ”† ALLUA    โ”† 2007-09-01  โ”† ALKALINITY-TOTAL โ”† 19.0  โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 6          โ”† ALLUA    โ”† 2007-09-01  โ”† BOD_-_5_DAYS     โ”† 1.2   โ”† skip exact duplicate before stโ€ฆ โ”‚
โ”‚ 6          โ”† ALLUA    โ”† 2007-09-01  โ”† ORTHO-PHOSPHATE  โ”† 0.019 โ”† skip exact duplicate before stโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

trusted records sample:
These records passed validation and are shaped for database insertion.
shape: (5, 8)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location   โ”† sample_dat โ”† parameter  โ”† parameter_ โ”† unit       โ”† value โ”† source_col โ”‚
โ”‚ ---        โ”† ---        โ”† e          โ”† ---        โ”† code       โ”† ---        โ”† ---   โ”† umn        โ”‚
โ”‚ i64        โ”† str        โ”† ---        โ”† str        โ”† ---        โ”† str        โ”† f64   โ”† ---        โ”‚
โ”‚            โ”†            โ”† str        โ”†            โ”† str        โ”†            โ”†       โ”† str        โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 1          โ”† ABBEYTOWN_ โ”† 2023-02-01 โ”† Alkalinity โ”† ALKALINITY โ”† as CaCO3   โ”† 314.0 โ”† Alkalinity โ”‚
โ”‚            โ”† 010        โ”†            โ”† -total     โ”† -TOTAL     โ”†            โ”†       โ”† -total (as โ”‚
โ”‚            โ”†            โ”†            โ”†            โ”†            โ”†            โ”†       โ”† CaCO3)     โ”‚
โ”‚ 1          โ”† ABBEYTOWN_ โ”† 2023-02-01 โ”† Ammonia-To โ”† AMMONIA-TO โ”† as N       โ”† 0.033 โ”† Ammonia-To โ”‚
โ”‚            โ”† 010        โ”†            โ”† tal        โ”† TAL        โ”†            โ”†       โ”† tal (as N) โ”‚
โ”‚ 1          โ”† ABBEYTOWN_ โ”† 2023-02-01 โ”† BOD - 5    โ”† BOD_-_5_DA โ”† Total      โ”† 1.2   โ”† BOD - 5    โ”‚
โ”‚            โ”† 010        โ”†            โ”† days       โ”† YS         โ”†            โ”†       โ”† days       โ”‚
โ”‚            โ”†            โ”†            โ”†            โ”†            โ”†            โ”†       โ”† (Total)    โ”‚
โ”‚ 1          โ”† ABBEYTOWN_ โ”† 2023-02-01 โ”† Chloride   โ”† CHLORIDE   โ”† not_encode โ”† 27.3  โ”† Chloride   โ”‚
โ”‚            โ”† 010        โ”†            โ”†            โ”†            โ”† d          โ”†       โ”†            โ”‚
โ”‚ 1          โ”† ABBEYTOWN_ โ”† 2023-02-01 โ”† Conductivi โ”† CONDUCTIVI โ”† @25ยฐC      โ”† 711.0 โ”† Conductivi โ”‚
โ”‚            โ”† 010        โ”†            โ”† ty         โ”† TY         โ”†            โ”†       โ”† ty @25ยฐC   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

storage readiness summary:
This is the final count of clean records, rejected records, NULLs, and handled duplicates.
shape: (6, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ metric                          โ”† value  โ”‚
โ”‚ ---                             โ”† ---    โ”‚
โ”‚ str                             โ”† i64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ raw_measurement_rows            โ”† 320749 โ”‚
โ”‚ trusted_records_ready_to_store  โ”† 148360 โ”‚
โ”‚ records_rejected                โ”† 20     โ”‚
โ”‚ explicit_null_values            โ”† 0      โ”‚
โ”‚ exact_duplicates_removed        โ”† 172369 โ”‚
โ”‚ repeated_measurements_kept_witโ€ฆ โ”† 31854  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

principle:
This is the rule for deciding what is safe to store.
shape: (1, 1)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ principle                       โ”‚
โ”‚ ---                             โ”‚
โ”‚ str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ The database should store cleaโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

7. Store clean data with structure

  use data_pipeline::quality_flow::store_clean_data_with_structure;
  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 7. Store clean data with structure
      store_clean_data_with_structure(df)?;

      Ok(())
  }
  cargo run --bin store_clean_data_with_structure
============================================================
7. Store clean data with structure
============================================================

database file:
This is the SQLite file written for the API project.
shape: (1, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ item        โ”† value                           โ”‚
โ”‚ ---         โ”† ---                             โ”‚
โ”‚ str         โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ sqlite_file โ”† /Users/chiefkemist/Documents/nโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

structured schema:
The cleaned data is stored across small tables instead of one giant messy table.
shape: (5, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ table          โ”† purpose                         โ”‚
โ”‚ ---            โ”† ---                             โ”‚
โ”‚ str            โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ locations      โ”† one row per normalized locatioโ€ฆ โ”‚
โ”‚ parameters     โ”† one row per normalized parametโ€ฆ โ”‚
โ”‚ measurements   โ”† trusted observed measurements   โ”‚
โ”‚ ingestion_runs โ”† source file, import time, counโ€ฆ โ”‚
โ”‚ rejected_rows  โ”† rows that failed validation orโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ingestion run summary:
This records where the data came from and what happened during import.
shape: (6, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ metric                   โ”† value  โ”‚
โ”‚ ---                      โ”† ---    โ”‚
โ”‚ str                      โ”† i64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ingestion_run_id         โ”† 1      โ”‚
โ”‚ raw_rows                 โ”† 29159  โ”‚
โ”‚ raw_measurement_rows     โ”† 320749 โ”‚
โ”‚ accepted_measurements    โ”† 148360 โ”‚
โ”‚ rejected_rows            โ”† 20     โ”‚
โ”‚ exact_duplicates_removed โ”† 172369 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

database table counts:
These counts are read back from SQLite after the write finishes.
shape: (5, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ table          โ”† rows   โ”‚
โ”‚ ---            โ”† ---    โ”‚
โ”‚ str            โ”† i64    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ ingestion_runs โ”† 1      โ”‚
โ”‚ locations      โ”† 160    โ”‚
โ”‚ parameters     โ”† 11     โ”‚
โ”‚ measurements   โ”† 148360 โ”‚
โ”‚ rejected_rows  โ”† 20     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

stored measurement sample:
These accepted rows are stored in the measurements table with foreign keys.
shape: (5, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location      โ”† sample_date โ”† parameter_code   โ”† unit        โ”† value โ”‚
โ”‚ ---        โ”† ---           โ”† ---         โ”† ---              โ”† ---         โ”† ---   โ”‚
โ”‚ i64        โ”† str           โ”† str         โ”† str              โ”† str         โ”† f64   โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 1          โ”† ABBEYTOWN_010 โ”† 2023-02-01  โ”† ALKALINITY-TOTAL โ”† as CaCO3    โ”† 314.0 โ”‚
โ”‚ 1          โ”† ABBEYTOWN_010 โ”† 2023-02-01  โ”† AMMONIA-TOTAL    โ”† as N        โ”† 0.033 โ”‚
โ”‚ 1          โ”† ABBEYTOWN_010 โ”† 2023-02-01  โ”† BOD_-_5_DAYS     โ”† Total       โ”† 1.2   โ”‚
โ”‚ 1          โ”† ABBEYTOWN_010 โ”† 2023-02-01  โ”† CHLORIDE         โ”† not_encoded โ”† 27.3  โ”‚
โ”‚ 1          โ”† ABBEYTOWN_010 โ”† 2023-02-01  โ”† CONDUCTIVITY     โ”† @25ยฐC       โ”† 711.0 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

rejected row sample:
These failed rows are stored separately for traceability.
shape: (5, 6)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ source_row โ”† location   โ”† raw_date โ”† source_column          โ”† raw_value โ”† rejection_reason       โ”‚
โ”‚ ---        โ”† ---        โ”† ---      โ”† ---                    โ”† ---       โ”† ---                    โ”‚
โ”‚ i64        โ”† str        โ”† str      โ”† str                    โ”† str       โ”† str                    โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ 111        โ”† ASKANAGAP  โ”† Jan      โ”† ortho-Phosphate (as P) โ”† -0.004    โ”† known parameter range  โ”‚
โ”‚            โ”† STREAM_010 โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† failed (โ€ฆ              โ”‚
โ”‚ 2003       โ”† CAMCOR_020 โ”† Feb      โ”† Temperature            โ”† 58.0      โ”† known parameter range  โ”‚
โ”‚            โ”†            โ”†          โ”†                        โ”†           โ”† failed (โ€ฆ              โ”‚
โ”‚ 4813       โ”† DARGLE_030 โ”† Jan      โ”† ortho-Phosphate (as P) โ”† 42.0      โ”† known parameter range  โ”‚
โ”‚            โ”†            โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† failed (โ€ฆ              โ”‚
โ”‚ 4815       โ”† DARGLE_030 โ”† Feb      โ”† ortho-Phosphate (as P) โ”† 22.0      โ”† known parameter range  โ”‚
โ”‚            โ”†            โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† failed (โ€ฆ              โ”‚
โ”‚ 4857       โ”† DARGLE_030 โ”† Jul      โ”† ortho-Phosphate (as P) โ”† 70.0      โ”† known parameter range  โ”‚
โ”‚            โ”†            โ”†          โ”† - unspeโ€ฆ               โ”†           โ”† failed (โ€ฆ              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

principle:
This is the reason for storing accepted rows, rejected rows, and ingestion metadata.
shape: (1, 1)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ principle                       โ”‚
โ”‚ ---                             โ”‚
โ”‚ str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ The database is part of the daโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

8. Visualize after cleaning

  use data_pipeline::quality_flow::visualize_after_cleaning;
  use polars::{
      error::PolarsResult,
      io::{
          SerReader,
          csv::read::{CsvParseOptions, CsvReadOptions},
      },
  };

  fn main() -> PolarsResult<()> {
      let df = CsvReadOptions::default()
          .with_has_header(true)
          // Discovery step: scan the file because we do not know columns yet.
          .with_infer_schema_length(Some(10_000))
          .with_parse_options(CsvParseOptions::default().with_try_parse_dates(true))
          .try_into_reader_with_file_path(Some(
              "data/Water Quality Monitoring Dataset_ Ireland.csv".into(),
          ))?
          .finish()?;

      // 8. Visualize after cleaning
      visualize_after_cleaning(df)?;

      Ok(())
  }
  cargo run --bin visualize_after_cleaning
============================================================
8. Visualize after cleaning
============================================================

dashboard handoff:
The dashboard reads the cleaned SQLite database produced by the storage step.
shape: (5, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ item               โ”† value                           โ”‚
โ”‚ ---                โ”† ---                             โ”‚
โ”‚ str                โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ raw_rows_available โ”† 29159                           โ”‚
โ”‚ sqlite_file        โ”† /Users/chiefkemist/Documents/nโ€ฆ โ”‚
โ”‚ dashboard_page     โ”† http://localhost:3434/data_viz  โ”‚
โ”‚ summary_json       โ”† http://localhost:3434/api/dashโ€ฆ โ”‚
โ”‚ timeseries_json    โ”† http://localhost:3434/api/dashโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

dashboard views:
These views turn the cleaned records into visual checks for patterns, gaps, and problems.
shape: (9, 2)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ view                            โ”† source                          โ”‚
โ”‚ ---                             โ”† ---                             โ”‚
โ”‚ str                             โ”† str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ pH over time by location        โ”† measurements joined with locatโ€ฆ โ”‚
โ”‚ temperature over time           โ”† measurements joined with locatโ€ฆ โ”‚
โ”‚ dissolved oxygen over time      โ”† measurements joined with locatโ€ฆ โ”‚
โ”‚ ammonia spikes by location      โ”† measurements joined with locatโ€ฆ โ”‚
โ”‚ missing-data heatmap            โ”† measurement coverage by locatiโ€ฆ โ”‚
โ”‚ outlier count by parameter      โ”† rejected_rows grouped by sourcโ€ฆ โ”‚
โ”‚ data completeness by location   โ”† measurements grouped by locatiโ€ฆ โ”‚
โ”‚ before/after cleaning summary   โ”† ingestion_runs accepted and reโ€ฆ โ”‚
โ”‚ water-quality score by locatioโ€ฆ โ”† aggregated pH, dissolved oxygeโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

principle:
Visualization is the final check that the pipeline produced useful data.
shape: (1, 1)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ principle                       โ”‚
โ”‚ ---                             โ”‚
โ”‚ str                             โ”‚
โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก
โ”‚ Understand, clean, validate, sโ€ฆ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Footnotes