Skip to main content

PySuricata

Build Status PyPI version Python versions License: MIT codecov Documentation Downloads LinkedIn

PySuricata Logo

Exploratory Data Analysis for Python, Built on Streaming Algorithms

One pass over your data. A self-contained HTML report, a versioned JSON payload, or a CI gate: all from the same pass.

Live DemoQuick StartDocumentationExamples


See it before you install it

A PySuricata report: the dataset summary, the columns flagged as needing a look with the threshold each one crossed, and a numeric column card with its histogram and bin controls
  • Run it in your browser →: drop a CSV, Parquet file or Excel workbook and get the real report back. The profiler is compiled to WebAssembly and runs in the page, so nothing is uploaded.
  • Open a finished report →: two years of hourly bike rentals, as PySuricata renders it — all four card kinds, and correlations worth ranking.

Quick Start

uv add pysuricata      # or: pip install pysuricata
import pandas as pd
from pysuricata import profile

df = pd.read_csv("bike_sharing.csv")
profile(df).save_html("report.html")

That is the whole API for the common case. Optional extras:

uv add "pysuricata[polars]"   # polars.DataFrame and LazyFrame
uv add "pysuricata[system]"   # psutil-backed memory reporting

Why PySuricata

It reads your data once. 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, which is a real limit rather than a footnote. See Where the bound stops holding.

It is not only a report. The same pass gives three outputs: profile() for the HTML, summarize() for a versioned JSON payload with no markup in the way, and pysuricata check for a CI gate that exits non-zero when a threshold is crossed. Most profilers give you the first and stop.

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.

Approximations say so. Quantiles, distinct counts and duplicate estimates come from sketches. The report labels them and carries their error bound rather than printing an estimate as an exact integer.

One file, no assets. A report is a single HTML file with inline CSS, JS and SVG. It opens from a mail attachment on a machine with no network.

Everything else

  • Streaming architecture: data is processed in configurable chunks, keeping memory bounded in rows, though 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.
  • 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.

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 counts and ratios, entropy

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


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

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"),
    }
)

Statistics Only (No HTML)

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

from pysuricata import summarize

stats = summarize(df)

assert stats["schema_version"] == 2
assert stats["dataset"]["missing_cells_pct"] < 5.0

# Gate on the upper bound, not the point estimate. Below the sketch's own
# resolution `duplicate_rows_est` is suppressed to 0 -- a frame with no
# duplicates and one whose duplicates are merely unresolved both read 0,
# and a gate reading either alone would pass the second case by accident.
# `duplicate_rows_hi` is the same bound the HTML report prints either way,
# so this fails closed instead.
rows = stats["dataset"]["rows_est"]
duplicate_pct_hi = stats["dataset"]["duplicate_rows_hi"] / rows * 100 if rows else 0.0
assert duplicate_pct_hi < 1.0

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

Streaming Large Datasets

Process datasets larger than RAM by passing a generator:

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 needs no generator at all. Hand it over and it is read a batch at a time, without ever existing as one frame:

import duckdb
from pysuricata import profile

report = profile("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("data/events.arrow")

# A relation is a query that has not run yet, so a filtered join across
# several files is profiled without any of it being landed.
relation = duckdb.connect("warehouse.db").sql("SELECT * FROM events")
report = profile(relation)

Measured on a 4,000,000 × 6 frame written as a 180 MB Parquet file, above a 118 MB bare-import floor: 307 MB for profile(path) against 581 MB for profile(pd.read_parquet(path)).

The readers behind that (stream_parquet, stream_ipc, stream_arrow and stream_duckdb) are exported from pysuricata.sources for when you want the batches rather than a profile.

Where the bound stops holding

Bounded memory is a claim about rows, not about 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 with python -m benchmarks.columns at 20,000 rows, taking the slope from 100 columns to 600: 1.2 MB of resident memory and 59 KB of report per column. A 20,000 x 600 frame costs 797 MB and emits a 35 MB report, while a 1,000,000 x 14 frame holds 1.2x the cells for 52 MB. Wide frames are the axis the streaming design does not yet cover, tracked in #207.

Comparing Two Datasets

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

from pysuricata import compare

last_week, this_week = df.iloc[:400], df.iloc[400:]
diff = compare(last_week, this_week)

diff.schema.added                       # columns that appeared
diff.columns["fare"].median_shift_sigma # in baseline standard deviations
diff.to_dict()                          # JSON-safe, three sections

It reports every delta, whether or not it crosses a threshold, because it is a description and not a verdict. pysuricata check is the same arithmetic with a threshold and an exit code.

Configuration

Pass keyword options for the common cases:

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":

from pysuricata import profile

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

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

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 for all options.

CLI

# 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.

GitHub Action

The same gate, as a step instead of a script:

- uses: alvarodiez20/pysuricata@v1
  with:
    file: data.csv
    baseline: baseline.json
    max-missing-pct: 5

Every pysuricata check flag is an input; see action.yml.

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 = top_k (default 50) for Misra-Gries; max_uniques (default 2048) sizes the KMV distinct-count sketch; 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.

Documentation

Contributing

Contributions are welcome. See the Contributing Guide.

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

License

MIT License. See 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pysuricata-0.2.0.tar.gz (1.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pysuricata-0.2.0-py3-none-any.whl (718.3 kB view details)

Uploaded Python 3

File details

Details for the file pysuricata-0.2.0.tar.gz.

File metadata

  • Download URL: pysuricata-0.2.0.tar.gz
  • Upload date:
  • Size: 1.0 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pysuricata-0.2.0.tar.gz
Algorithm Hash digest
SHA256 b6aaec58fd61f702abbd2b8f9f1266152e4eea7b83f94b06f652a9d9002c2b61
MD5 14e8745fd1f49bbf5d1802edd931b16b
BLAKE2b-256 84bd5559625bc39a64529c771454538f41917534b60e56f17c1613791eb58d7c

See more details on using hashes here.

Provenance

The following attestation bundles were made for pysuricata-0.2.0.tar.gz:

Publisher: cd.yml on alvarodiez20/pysuricata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pysuricata-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pysuricata-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 718.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pysuricata-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7166844a9ec3c37c5ee6cf114405efc711cfcd768c74b3159ecddda1b730e47a
MD5 8274ca64c2f85a3de9d8694a079bba96
BLAKE2b-256 a8d7b60ac2285eb589d777b7259cc80430d5f0046395565c74f44b4f8e0b9d67

See more details on using hashes here.

Provenance

The following attestation bundles were made for pysuricata-0.2.0-py3-none-any.whl:

Publisher: cd.yml on alvarodiez20/pysuricata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

0.0.73

2 files

0.0.72

2 files

0.0.71

2 files

0.0.70

2 files

0.0.68

2 files

0.0.66

2 files

0.0.65

2 files

0.0.64

2 files

0.0.63

2 files

0.0.62

2 files

0.0.61

2 files

0.0.60

2 files

0.0.59

2 files

0.0.58

2 files

0.0.57

2 files

0.0.56

2 files

0.0.55

2 files

0.0.54

2 files

0.0.53

2 files

0.0.52

2 files

0.0.51

2 files

0.0.50

2 files

0.0.49

2 files

0.0.48

2 files

0.0.47

2 files

0.0.46

2 files

0.0.45

2 files

0.0.44

2 files

0.0.43

2 files

0.0.42

2 files

0.0.41

2 files

0.0.40

2 files

0.0.39

2 files

0.0.38

2 files

0.0.37

2 files

0.0.36

2 files

0.0.35

2 files

0.0.34

2 files

0.0.33

2 files

0.0.32

2 files

0.0.31

2 files

0.0.30

2 files

0.0.29

2 files

0.0.28

2 files

0.0.27

2 files

0.0.26

2 files

0.0.25

2 files

0.0.24

2 files

0.0.23

2 files

0.0.22

2 files

0.0.21

2 files

0.0.20

2 files

0.0.19

2 files

0.0.18

2 files

0.0.17

2 files

0.0.16

2 files

0.0.15

2 files

0.0.14

2 files

0.0.13

2 files

0.0.12

2 files

0.0.11

2 files

0.0.10

2 files

0.0.9

2 files

0.0.8

2 files

0.0.7

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page