Metadata-Version: 2.4
Name: pysuricata
Version: 0.1.3
Summary: Streaming EDA profiler: one pass over pandas or polars, bounded memory, a self-contained HTML report.
Author-email: Alvaro Diez <alvarodiez20@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://alvarodiez20.github.io/pysuricata/
Project-URL: Documentation, https://alvarodiez20.github.io/pysuricata/
Project-URL: Repository, https://github.com/alvarodiez20/pysuricata
Project-URL: Changelog, https://github.com/alvarodiez20/pysuricata/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/alvarodiez20/pysuricata/issues
Keywords: eda,exploratory-data-analysis,profiling,data-quality,pandas,polars,streaming
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas<4,>=2.0; python_version < "3.13"
Requires-Dist: pandas<4,>=2.2.3; python_version >= "3.13"
Requires-Dist: numpy>=2.1; python_version >= "3.13"
Requires-Dist: numpy>=2.3.3; python_version >= "3.14"
Requires-Dist: markdown>=3.4.0
Provides-Extra: polars
Requires-Dist: polars>=1.34.0; extra == "polars"
Provides-Extra: system
Requires-Dist: psutil>=7.1.0; extra == "system"
Dynamic: license-file

# PySuricata

[![Build Status](https://github.com/alvarodiez20/pysuricata/workflows/CI/badge.svg)](https://github.com/alvarodiez20/pysuricata/actions)
[![PyPI version](https://img.shields.io/pypi/v/pysuricata.svg)](https://pypi.org/project/pysuricata/)
[![Python versions](https://img.shields.io/pypi/pyversions/pysuricata.svg)](https://github.com/alvarodiez20/pysuricata)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![codecov](https://codecov.io/gh/alvarodiez20/pysuricata/branch/main/graph/badge.svg)](https://codecov.io/gh/alvarodiez20/pysuricata)
[![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg)](https://alvarodiez20.github.io/pysuricata/)
[![Downloads](https://static.pepy.tech/badge/pysuricata)](https://pepy.tech/project/pysuricata)
[![LinkedIn](https://img.shields.io/badge/LinkedIn-alvarodiez20-blue?logo=linkedin)](https://www.linkedin.com/in/alvarodiez20/)

<div align="center">
  <img src="https://raw.githubusercontent.com/alvarodiez20/pysuricata/main/pysuricata/static/images/logo_suricata_transparent.png" alt="PySuricata Logo" width="300">

  <h3>Exploratory Data Analysis for Python, Built on Streaming Algorithms</h3>

  <p>
    <a href="#quick-start">Quick Start</a> •
    <a href="https://alvarodiez20.github.io/pysuricata/">Documentation</a> •
    <a href="https://alvarodiez20.github.io/pysuricata/examples/">Examples</a>
  </p>
</div>

---

## What It Does

PySuricata generates **self-contained HTML reports** from pandas or polars DataFrames. Reports include per-column statistics, histograms, correlation chips, missing value analysis, and outlier detection.

Data is processed in chunks using streaming algorithms, so memory usage stays bounded **in the number of rows** — a million rows costs no more than twenty thousand. It is *not* bounded in the number of columns: each column keeps its own sketches for the whole run and gets its own card in the report, so both memory and report size grow linearly with the width of the frame. Measured at 20,000 rows: **~1.3 MB of RSS and ~59 KB of report per column**, so a 600-column frame needs roughly 850 MB. See [#207](https://github.com/alvarodiez20/pysuricata/issues/207).

It also does two things a profiler usually does not: `summarize()` returns the same numbers as a versioned JSON payload with no HTML in the way, and `pysuricata check` compares a dataset against a stored baseline and exits non-zero when a threshold is crossed — so the same single pass can run in a notebook and in CI.

## Quick Start

### Installation

```bash
# using uv (recommended)
uv add pysuricata

# or using pip
pip install pysuricata
```

With polars support (optional):

```bash
uv add pysuricata[polars]
# or: pip install pysuricata[polars]
```

### Generate a Report

```python
import pandas as pd
from pysuricata import profile

url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
df = pd.read_csv(url)

report = profile(df)
report.save_html("titanic_report.html")
```

**[▶ See a live example report →](https://alvarodiez20.github.io/pysuricata/assets/titanic_report.html)**

<div align="center">
  <img src="https://raw.githubusercontent.com/alvarodiez20/pysuricata/main/docs/assets/report-screenshot.png" alt="A PySuricata report: the dataset summary, the five columns that need a look, and a numeric column card with its histogram and bin controls" width="900">
</div>

The examples below assume a `df` in scope. The Quick Start frame works, or anything of your own:

<!-- docs-check:setup -->
```python
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
df = pd.DataFrame(
    {
        "age": rng.normal(30, 12, 800).round(1),
        "fare": rng.gamma(2, 20, 800).round(2),
        "sex": rng.choice(["male", "female"], 800),
        "booked": pd.date_range("2024-01-01", periods=800, freq="h"),
    }
)
```

## Features

- **Streaming architecture** — Data is processed in configurable chunks, keeping memory bounded in rows (not in columns — see above). Useful for datasets with more rows than fit in RAM.
- **Pandas and Polars** — Works natively with `pandas.DataFrame`, `polars.DataFrame` and `polars.LazyFrame`, plus Parquet files, Arrow IPC files (`.arrow`, `.feather`, `.ipc`), DuckDB relations and Arrow batches.
- **Arrow is the boundary, not pandas** — anything exporting the Arrow C stream interface (`__arrow_c_stream__`) is profiled without materialising it, whatever library produced it. Arrow IPC is what R, Julia and Rust write, so a file from another runtime is read directly.
- **Self-contained HTML** — Single file with inline CSS, JS, and SVG charts. No external assets needed.
- **Configurable** — Control chunk size, sample size, correlations and more with keyword options, a `preset=`, or a `ProfileConfig`.
- **Reproducible** — Seeded random sampling produces deterministic results across runs.
- **Typed** — Ships `py.typed`; `summarize()` returns a payload carrying a `schema_version`.
- **CLI tool** — `profile`, `summarize` and `check` from the command line.

## How It Works

PySuricata uses well-known streaming algorithms from the academic literature:

| Algorithm | Purpose | Time | Space |
|-----------|---------|------|-------|
| **Welford/Pébay** | Exact mean, variance, skewness, kurtosis | O(1) per value | O(1) |
| **KMV sketch** | Distinct count estimation (~2.2% error) | O(log k) per value | O(k) |
| **Misra-Gries** | Top-k frequent values | O(1) amortized | O(k) |
| **Reservoir sampling** | Uniform random sample for quantiles | O(1) per value | O(s) |

*k = sketch size (`max_uniques`, default 2048), s = sample size (`numeric_sample_size`, default 20 000)*

KMV's relative standard error is `1/sqrt(k - 2)`, which is where the ~2.2% comes from. Approximate values are labelled approximate in the report and carry their error bound rather than being printed as exact integers.

All statistics are computed in a **single pass** over the data.

## What's in a Report

Each column is analyzed based on its type:

- **Numeric** — Mean, variance, skewness, kurtosis, quantiles, histogram, outlier detection (IQR, MAD, z-score), correlations
- **Categorical** — Top values, distinct count, entropy, Gini impurity, string length statistics
- **DateTime** — Temporal range, hour/day/month distributions, monotonicity detection
- **Boolean** — True/false ratios, entropy, balance score

Plus dataset-level metrics: row/column counts, memory usage, missing value percentages, and duplicate row estimates.

## Streaming Large Datasets

Process datasets larger than RAM by passing a generator:

```python
import pandas as pd
from pysuricata import profile

def read_in_chunks():
    for i in range(100):
        yield pd.read_parquet(f"data/part-{i}.parquet")

report = profile(read_in_chunks())
report.save_html("large_report.html")
```

A Parquet path, an Arrow IPC file, a DuckDB relation or an Arrow source can be streamed directly, without loading the whole thing:

```python
import duckdb
from pysuricata import profile
from pysuricata.sources import stream_duckdb, stream_ipc, stream_parquet

report = profile(stream_parquet("data/events.parquet"))

# Written by arrow::write_ipc_file() in R, Arrow.write() in Julia, or the
# arrow crate in Rust. The framing is read from the file, not its extension.
report = profile(stream_ipc("data/events.arrow"))

relation = duckdb.connect("warehouse.db").sql("SELECT * FROM events")
report = profile(stream_duckdb(relation))
```

## Statistics Only (No HTML)

Use `summarize()` for CI/CD quality checks. The payload carries a `schema_version` and is treated as a contract:

```python
from pysuricata import summarize

stats = summarize(df)

assert stats["schema_version"] == 1
assert stats["dataset"]["missing_cells_pct"] < 5.0
assert stats["dataset"]["duplicate_rows_pct_est"] < 1.0

print(f"Mean age: {stats['columns']['age']['mean']:.1f}")
```

## Comparing Two Datasets

`compare()` runs both through the same single pass and reports what moved:

```python
from pysuricata import compare

last_week, this_week = df.iloc[:3], df.iloc[3:]
diff = compare(last_week, this_week).to_dict()
```

## Configuration

Pass keyword options for the common cases:

```python
from pysuricata import profile

report = profile(
    df,
    chunk_size=250_000,   # default 50_000
    sample=20_000,
    seed=42,
    correlations=True,
    title="My Analysis",
)
```

Or start from a preset — `"fast"` or `"thorough"`:

```python
from pysuricata import profile

report = profile(df, preset="fast")
```

For everything else, build a `ProfileConfig`. Keyword options and `config=` are mutually exclusive:

```python
from pysuricata import profile, ProfileConfig

config = ProfileConfig()
config.compute.chunk_size = 250_000
config.compute.random_seed = 42
config.compute.corr_threshold = 0.5
config.render.title = "My Analysis"

report = profile(df, config=config)
```

See the [Configuration Guide](https://alvarodiez20.github.io/pysuricata/configuration/) for all options.

## CLI

```bash
# Generate an HTML report
pysuricata profile data.csv --output report.html

# Get JSON statistics
pysuricata summarize data.csv

# Compare against a stored baseline; exit non-zero when a threshold is crossed
pysuricata check data.csv --write-baseline baseline.json
pysuricata check data.csv --baseline baseline.json --max-missing-pct 5
```

`check` exits `0` on pass, `1` when a threshold is crossed, and `2` when the check could not run — so it drops into CI without a wrapper.

## Documentation

- [Quick Start](https://alvarodiez20.github.io/pysuricata/quickstart/)
- [User Guide](https://alvarodiez20.github.io/pysuricata/usage/)
- [Configuration](https://alvarodiez20.github.io/pysuricata/configuration/)
- [API Reference](https://alvarodiez20.github.io/pysuricata/api/)
- [Statistical Methods](https://alvarodiez20.github.io/pysuricata/stats/overview/)
- [Examples](https://alvarodiez20.github.io/pysuricata/examples/)

## Contributing

Contributions are welcome. See the [Contributing Guide](https://alvarodiez20.github.io/pysuricata/contributing/).

```bash
git clone https://github.com/alvarodiez20/pysuricata.git
cd pysuricata
uv sync --dev
uv run pytest
```

## License

MIT License. See [LICENSE](LICENSE) for details.

## Acknowledgments

Built using algorithms from:

- Welford, B.P. (1962) — Streaming moments
- Pébay, P. (2008) — Parallel merging of moments
- Bar-Yossef, Z. et al. (2002) — KMV distinct count estimation
- Misra, J. & Gries, D. (1982) — Streaming heavy hitters

Named after **suricatas (meerkats)** — small, vigilant animals that work cooperatively and thrive in harsh environments with limited resources.
