im-anishraj
arnio
Python✨ New

C++ accelerated data quality toolkit for Python: CSV parsing, cleaning, schema validation, profiling, and pandas integration.

Last updated Jul 5, 2026
97
Stars
404
Forks
1.0k
Issues
+1
Stars/day
Attention Score
66
Language breakdown
No language data available.
β–Έ Files click to expand
README


Arnio



Fast data preparation for the Python data stack.


Arnio is a compiled C++ data preparation engine for messy CSV and pandas workflows.
It parses, infers types, strips whitespace, deduplicates, validates, and profiles data β€”
then hands clean results back to the tools you already use.
Use Arnio before and alongside pandas, NumPy, scikit-learn, DuckDB, and Arrow.


PyPI  Python  CI  Coverage  MIT  GSSoC 2026  Join Discord PyPI Downloads



pip install arnio

Colab install smoke test: COLABSMOKE_TEST.md Interactive Notebooks: Basic Usage Pandas Interop Schema Validation


Quickstart Β· Integrations Β· Why Arnio Β· Architecture Β· Benchmarks Β· Community Β· Contribute




⚑ Quickstart

If you're new to Arnio, the example below demonstrates a simple first-run workflow for loading, cleaning, and preparing CSV data before converting it back into a pandas DataFrame. The workflow starts by loading a CSV dataset into an Arnio frame for preprocessing and cleaning.

import arnio as ar

Load CSV directly through C++ β€” no Python parsing overhead

frame = ar.readcsv("messysales_data.csv")

Strict mode (default) fails on inconsistent row widths

frame = ar.readcsv("messysales_data.csv", mode="strict")

Permissive mode fills missing trailing values with nulls

frame = ar.readcsv("messysales_data.csv", mode="permissive")

Each pipeline step applies a specific transformation such as trimming whitespace, normalizing text formatting, handling missing values, and removing duplicate rows.

# Declare what clean data looks like β€” arnio handles the rest
clean = ar.pipeline(frame, [
    ("strip_whitespace",),
    ("normalizecase", {"casetype": "lower"}),
    ("fill_nulls", {"value": 0.0, "subset": ["revenue"]}),
    ("drop_nulls",),
    ("drop_duplicates",),
])

After preprocessing is complete, the cleaned result can be converted back into a standard pandas DataFrame for further analysis or integration with existing workflows.

# Out comes a standard pandas DataFrame β€” use it like you always have
df = ar.to_pandas(clean)

Use copy=True when you need defensive pandas-owned buffers

safedf = ar.topandas(clean, copy=True)

Dry Run Validation

Use dry_run=True to validate pipeline configuration and step execution without returning transformed output.

ar.pipeline(
    frame,
    [
        ("drop_nulls",),
    ],
    dry_run=True,
)

Need step timings for debugging? Opt in without changing the default pipeline return type:

clean, metadata = ar.pipeline(
    frame,
    [("stripwhitespace",), ("dropduplicates",)],
    return_metadata=True,
)

print(metadata["step_timings"]) print(metadata["applied_steps"]) print(metadata["row_counts"]) print(metadata["execution_summary"])

Quick Example

import arnio

frame = arnio.read_csv("sample.csv")

Preview first 5 rows

frame.preview(5)

Generate and view scannable summary statistics

print(frame.describe())

Pipeline validation behavior

Pipeline step specifications are validated before execution begins.

Malformed step tuples, invalid kwargs structures, or unknown step names fail early before any pipeline steps execute.

ar.pipeline(
    frame,
    [
        ("strip_whitespace",),
        ("bad_step", "oops", "extra"),
    ],
)

This prevents partial pipeline execution when later pipeline steps are invalid.

from_dict support

This adds support for creating an ArFrame from a Python dictionary.

You can build an ArFrame directly from a dictionary of equal-length columns, which is useful for small inline datasets that you want to pass into a pipeline.

import arnio as ar

data = {"name": ["Alice", "Bob"], "age": [25, 30]}

frame = ar.from_dict(data)

or

frame = ar.ArFrame.from_dict(data)

Already working with a pandas DataFrame? Arnio can also be integrated directly into an existing pandas workflow without changing your current data-processing approach:

import pandas as pd
import arnio as ar

df = pd.readcsv("messysales_data.csv")

clean_df = df.arnio.clean([ ("strip_whitespace",), ("normalizecase", {"casetype": "lower"}), ("drop_duplicates",), ])

report = clean_df.arnio.profile()

trimmed_df = ( df.arnio.stripwhitespace(subset=["customername"]) .arnio.clipnumeric(upper=100, subset=["loyaltyscore"]) .arnio.dropnulls(subset=["customername"]) )

Cross-field validation rules

Pass a rules list to Schema for checks that span multiple columns. Each rule receives the full pandas DataFrame and must return a list[ValidationIssue] β€” an empty list means the rule passed.

import arnio as ar

def endafterstart(df): return [ ar.ValidationIssue( column="end_date", rule="cross_field", message="enddate must be >= startdate", row_index=int(i) + 1, ) for i, row in df.iterrows() if row["enddate"] < row["startdate"] ]

schema = ar.Schema( {"startdate": ar.String(), "enddate": ar.String()}, rules=[endafterstart], )

result = schema.validate(ar.read_csv("events.csv")) print(result.passed)

Row index convention: ValidationIssue.row_index values are 1-based and
count data rows only. The header row is excluded. row_index=1 is the first data
row in the file.

Schema diff reports

Use diff_schema() to compare expected and observed data contracts across datasets, releases, or generated schemas.

import arnio as ar

expected = ar.Schema({ "id": ar.Int64(nullable=False, unique=True), "email": ar.Email(nullable=False), })

observed = ar.Schema({ "id": ar.Int64(nullable=False), "created_at": ar.DateTime(format="%Y-%m-%d"), })

diff = ar.diff_schema(expected, observed) print(diff.summary()) print(diff.to_markdown())

CI data contracts (GitHub Actions)

If you want to block schema drift or invalid rows in pull requests, see DATACONTRACTCI.md for an inert copy-paste GitHub Actions workflow example.

Example contract files are included under examples/contracts/.

Select specific columns

Use select_columns() to create a new ArFrame with only the required columns before converting to pandas.

selected = ar.select_columns(frame, ["name", "revenue"])

print(selected.columns)

['name', 'revenue']

  • Preserves the requested column order.
  • Returns a new ArFrame.
  • Raises ValueError if any requested column does not exist.
  • Raises TypeError if columns is not a sequence of strings.

Handling missing values

Arnio supports configuring which strings are treated as null during CSV parsing using the nullvalues parameter in readcsv and scan_csv. By default, Arnio preserves its existing behavior and treats only empty cells as null. Custom matching is case-insensitive and applies to cell values only (not headers).

# Default behavior: empty cells are null
frame = ar.read_csv("data.csv")

Provide a custom list of sentinels (overrides the empty-cell default)

frame = ar.readcsv("data.csv", nullvalues=["", "MISSING", "UNKNOWN"])

Disable null sentinel handling completely

frame = ar.readcsv("data.csv", nullvalues=[])

Handling decimal separators

Use decimal_separator when numeric CSV data uses a separator other than the default dot. This is explicit by design: Arnio does not auto-detect decimal formats because a comma can also be the CSV delimiter.

# Semicolon-delimited CSV with unquoted European decimals
frame = ar.readcsv("prices.csv", delimiter=";", decimalseparator=",")

Comma-delimited CSV still needs quoted comma-decimal values

frame = ar.readcsv("prices.csv", decimalseparator=",")

The default remains decimal_separator=".", so existing dot-decimal files keep their current behavior. If you also use thousands_separator, it must differ from decimal_separator.

Handling invalid UTF-8 bytes

Use encoding_errors to control how invalid UTF-8 bytes are handled during CSV parsing.

# Raise an error on invalid UTF-8 bytes (default)
frame = ar.read_csv(
    "data.csv",
    encoding_errors="strict",
)

Replace invalid bytes with the Unicode replacement character (οΏ½)

frame = ar.read_csv( "data.csv", encoding_errors="replace", )

Ignore invalid bytes completely

frame = ar.read_csv( "data.csv", encoding_errors="ignore", )
Supported values:
  • "strict" (default)
  • "replace"
  • "ignore"
Every step above executes in C++. Your Python code is a configuration β€” not the execution engine.
Explore more in the examples/ folder β€” ready-to-run recipes for sales, customers, survey, logs, and finance datasets.


Security note: CSV formula injection

Arnio preserves cell values by default when writing CSV files. It does not rewrite strings that begin with spreadsheet formula prefixes such as =, +, -, or @ unless you opt in to formula escaping.

If you export Arnio-cleaned data back to CSV and expect users to open that file in Excel, Google Sheets, LibreOffice, or another spreadsheet application, treat untrusted text fields as potentially executable spreadsheet formulas. Use escape_formulas=True when exporting user-controlled text to prefix formula-like string cells with a single quote while leaving numeric columns unchanged:

ar.writecsv(cleanframe, "safeexport.csv", escapeformulas=True)

This is especially important for customer names, notes, comments, imported form fields, and any other free-text values that may come from outside your trust boundary. Arnio keeps the export policy explicit, so existing CSV output remains unchanged unless this option is enabled.


Error Handling

readcsv and scancsv

| Input | Raises | Message | |:---|:---|:---| | File not found | CsvReadError | Cannot open file: <path> | | Zero-byte file | CsvReadError | CSV file is empty: '<path>' | | Blank header line | CsvReadError | CSV header contains an empty column name | | Binary / NUL bytes | CsvReadError | CSV input contains NUL bytes and appears to be binary or corrupted |

Schema Validation

ar.validate() returns a ValidationResult; it does not raise for validation failures. Check result.passed and result.issues for dtype or required_column rule violations.

validate() currently operates on a single in-memory ArFrame. Chunked validation via readcsvchunked() iterators is not yet supported directly. Validate each chunk individually or materialize the data before validation when working with streamed/chunked inputs.

Pipeline Step Errors

Unknown step names raise UnknownStepError before execution begins.

πŸ“Έ Peek at a 100 GB file without loading it

scan_csv reads only the header + a sample to infer the schema. Zero data loaded.

# Pass sample_size to control how many rows are evaluated for type inference
schema = ar.scancsv("100GBfile.csv", sample_size=500)

{'id': 'int64', 'name': 'string', 'is_active': 'bool', 'revenue': 'float64'}

Useful for exploring datasets before committing memory.

πŸ“„ Read JSON Lines (JSONL / NDJSON) files

readjsonl parses one JSON object per line into an ArFrame. Blank lines are skipped, missing keys become nulls, and mixed-type columns are coerced to string β€” the same rules as frompandas.

# events.jsonl

{"user": "alice", "score": 9.5, "active": true}

{"user": "bob", "score": 8.1, "active": false}

frame = ar.read_jsonl("events.jsonl")

Limit rows

frame = ar.read_jsonl("large.jsonl", nrows=1000)

Stream large JSONL/NDJSON files in bounded chunks

for chunk in ar.readjsonlchunked("large.jsonl", chunksize=50_000): process(chunk)

Non-UTF-8 encoding

frame = ar.read_jsonl("data.ndjson", encoding="latin-1")

Plug straight into the cleaning pipeline

clean = ar.pipeline(frame, [("stripwhitespace",), ("dropnulls",)])

Raises ar.JsonlReadError with the 1-based line number if a line contains invalid JSON. The chunked reader uses the same validation and decoding rules as read_jsonl, but only materializes one chunk at a time.

πŸ“¦ Parquet import and export for columnar analytics pipelines

readparquet and writeparquet use pyarrow for Parquet I/O. Install the optional extra first:

pip install arnio[parquet]
# Import a Parquet file into an ArFrame (no pandas intermediate)
frame = ar.read_parquet("data.parquet")

Read only selected columns

frame = ar.read_parquet("data.parquet", usecols=["name", "age"])

Export an ArFrame to Parquet

ar.write_parquet(frame, "output.parquet")

Choose compression codec: "snappy" (default), "gzip", "zstd", "brotli", "none"

ar.write_parquet(frame, "output.parquet", compression="zstd")

Control row group size for large files

ar.writeparquet(frame, "output.parquet", rowgroupsize=50000)

.pq extension also accepted for both read and write

frame = ar.read_parquet("data.pq") ar.write_parquet(frame, "output.pq")

Both functions raise ImportError with an install hint if pyarrow is not available.

πŸ‘€ Preview rows without pandas conversion or full-column Python list materialization

preview() reads only the first n rows directly from the C++ frame β€” no pandas conversion triggered.

frame = ar.readcsv("hugefile.csv")

print(frame.preview()) # first 5 rows (default) print(frame.preview(n=10)) # first 10 rows

Raises ValueError for invalid n (zero, negative, or non-integer).

πŸ’° Financial Decimal Support

arnio provides support for converting Python decimal.Decimal objects.

  • Behavior: Python Decimal objects are automatically preserved as high-precision strings during serialization/binding to prevent floating-point precision loss.
  • Caveat: When reading back into Pandas, to_pandas() returns these as string (object dtype) columns. You will need to explicitly cast them back to Decimal objects on the resulting DataFrame if you want to resume exact math.
Example:
from decimal import Decimal

import pandas as pd

import arnio as ar

df = pd.DataFrame({ "price": [Decimal("19.99"), Decimal("29.95")] })

frame = ar.from_pandas(df) # Decimal values safely preserved as exact strings result = ar.to_pandas(frame)

result["price"] will be string objects ["19.99", "29.95"]

🧩 Add custom steps without touching C++

Register any Python function as a pipeline step. It receives a DataFrame, returns a DataFrame.

def removeoutliers(df, column="revenue", threshold=100000):
    return df[df[column] <= threshold]

ar.registerstep("removeoutliers", remove_outliers) ar.registerstep("team:dropnulls", remove_outliers) # namespaced custom step

Use builtin: for an explicit built-in step, and your own prefixes

like team: or plugin_name: to avoid name collisions.

Introspect built-in and custom step names without reaching into internals.

print(ar.list_steps())

Opt in to a context object only when you need execution metadata.

def capture_context(df, context=None): print(context.stepname, context.stepindex, context.total_steps) return df

Now use it in any pipeline alongside native C++ steps

clean = ar.pipeline(frame, [ ("builtin:strip_whitespace",), ("remove_outliers", {"column": "revenue", "threshold": 50000}), ("drop_duplicates",), ])

Need to inspect the built-in kwargs a step accepts before assembling a pipeline?

signatures = ar.getbuiltinstep_signatures()
print(list(signatures["drop_nulls"].parameters))  # ["subset"]
print(list(signatures["filter_rows"].parameters))  # ["column", "op", "value"]

Need to restore the registry back to built-in steps only during tests?

ar.reset_steps()

print(ar.list_steps())

Only built-in steps remain

Custom steps run through a pandas↔ArFrame conversion bridge. Prototype in Python, then optionally migrate hot paths to C++ for full speed.

πŸ”„ Custom Step Overwrite Policy

By default, trying to register a custom step with a name that is already taken by another custom Python step will raise a ValueError to prevent silent overwriting.

To intentionally replace an existing custom Python step, pass overwrite=True:

def custom_logging(df):
    print("Running step v1")
    return df

ar.registerstep("logdata", custom_logging)

This will succeed and safely overwrite the original logic

def customloggingv2(df): print("Running step v2") return df

ar.registerstep("logdata", customloggingv2, overwrite=True)

Note: Built-in C++ pipeline steps (like "drop_nulls") can never be overwritten, even if overwrite=True is explicitly supplied.

βœ‚οΈ Slice rows with head() and tail()

head() and tail() return the first or last n rows as a new ArFrame.

frame = ar.read_csv("data.csv")

frame.head() # first 5 rows (default) frame.head(10) # first 10 rows frame.tail(3) # last 3 rows

n larger than row count returns all rows safely

frame.head(1000)

n=0 returns an empty ArFrame

frame.head(0)

Raises ValueError for negative or boolean n.

Pipeline verbose diagnostics

Enable lightweight pipeline diagnostics with verbose=True:

result = ar.pipeline(
    frame,
    [
        ("strip_whitespace",),
        ("drop_nulls",),
    ],
    verbose=True,
)

This logs step execution order, execution path, elapsed time, and row-count changes through the arnio logger.




πŸ”— Integrations

Arnio is designed to make the rest of the Python data stack more productive, not to replace it.

| Workflow | How Arnio helps | |:---|:---| | pandas | Clean, validate, and profile messy DataFrames through df.arnio. | | NumPy | Prepare typed numeric data before array/modeling workflows. | | scikit-learn | Use Arnio cleaning as a preprocessing layer before model training. | | DuckDB / Arrow | Validate and prepare data before analytics and columnar exchange. Export ArFrame to pyarrow.Table via `ar.to_arrow(frame). | | notebooks | Inspect quality issues and cleaning suggestions before analysis. |

DuckDB registration

Use ar.registerduckdb(frame, conn, "tablename") to register an ArFrame directly as a DuckDB relation without writing pandas conversion glue yourself. DuckDB is an optional dependency β€” install it with pip install "arnio[duckdb]" when needed. You can also install DuckDB directly with pip install duckdb.

For development and CI, pip install -e ".[dev]" now includes DuckDB so the integration test module in tests/testintegrationsduckdb.py runs in the default test job.

<pre><code class="lang-python">import duckdb import arnio as ar

frame = ar.read_csv(&quot;data.csv&quot;) conn = duckdb.connect() ar.registerduckdb(frame, conn, &quot;mytable&quot;) result = conn.execute(&quot;SELECT * FROM my_table&quot;).fetchdf()</code></pre>

Row-dropping and schema-change behavior

ArnioCleaner enforces a strict transformer contract by default:

  • Row-count-changing steps (dropnulls, dropduplicates,
filterrows, keeprowswithnulls) are rejected by default with a clear ValueError. To allow row-count changes, pass allowrowcount_change=True.
  • Column schema-changing steps (renamecolumns, dropcolumns,
dropconstantcolumns, combine_columns) are rejected by default and allowed only when allowschemachanges=True is passed.

<pre><code class="lang-python"># Schema-preserving (default strict mode) cleaner = ArnioCleaner( steps=[ (&quot;strip_whitespace&quot;,), (&quot;fill_nulls&quot;, {&quot;value&quot;: 0}), ], )

Opt-in column schema changes

cleaner = ArnioCleaner( steps=[ (&quot;renamecolumns&quot;, {&quot;oldname&quot;: &quot;new_name&quot;}), (&quot;dropconstantcolumns&quot;,), ], allowschemachanges=True, )</code></pre>

Pandas accessor

Arnio integrates directly with pandas through a registered DataFrame accessor. After importing Arnio, every pandas DataFrame gains an .arnio accessor that provides cleaning, profiling, validation, and pipeline utilities without requiring you to leave your existing pandas workflow.

The accessor is registered through pandas' DataFrame extension API, allowing Arnio methods to be accessed directly as df.arnio.

Cleaning data with .clean()

Use .clean() to apply common data-cleaning operations and return a cleaned pandas DataFrame.

<pre><code class="lang-python">import pandas as pd import arnio as ar

df = pd.readcsv(&quot;rawcustomers.csv&quot;)

clean_df = df.arnio.clean( strip_whitespace=True, drop_duplicates=True, )</code></pre>

Supported options include:

  • strip_whitespace – remove leading and trailing whitespace from string values
  • drop_nulls – remove rows containing null values
  • drop_duplicates – remove duplicate rows
  • steps – optional Arnio pipeline steps. When provided, .clean() executes the supplied pipeline and returns the resulting pandas DataFrame.
.clean() returns a pandas DataFrame, allowing you to continue using your existing pandas analysis workflow.

Profiling data with .profile()

Use .profile() to generate a data quality summary.

<pre><code class="lang-python">quality = clean_df.arnio.profile()

print(quality)</code></pre>

.profile() returns a DataQualityReport containing dataset quality information and profiling insights that can help identify issues before downstream analysis.

Accessor vs ar.pipeline()

| Approach | Best for | |-----------|-----------| | df.arnio.clean() | Existing pandas workflows | | df.arnio.profile() | Quick quality assessment | | df.arnio.pipeline(...) | Custom pipeline execution from a DataFrame | | ar.pipeline(...) | Direct Arnio/ArFrame workflows |

End-to-end example

<pre><code class="lang-python">import pandas as pd import arnio as ar

df = pd.read_csv(&quot;customers.csv&quot;)

clean_df = df.arnio.clean( strip_whitespace=True, drop_duplicates=True, )

quality = clean_df.arnio.profile()

validation = clean_df.arnio.validate({ &quot;email&quot;: ar.Email(nullable=False), &quot;user_code&quot;: ar.Regex(r&quot;^USR-\d{4}$&quot;, nullable=False), &quot;age&quot;: ar.Int64(nullable=True, min=0), })

print(quality)</code></pre>

This approach keeps pandas as the analysis tool while Arnio handles data preparation, quality checks, and validation.

> Product direction: PROJECT_DIRECTION.md

πŸ“˜ Examples

These examples demonstrate how Arnio integrates with the Python data ecosystem.

They follow a simple workflow:

clean/validate data with Arnio β†’ analyze with other tools

πŸ”Ή Interoperability Examples

  • Arnio + pandas
Clean and normalize messy tabular data using Arnio, then analyze it using pandas. Run: <pre><code class="lang-bash">python examples/arniowithpandas.py</code></pre>
  • Arnio + NumPy
Prepare numeric data safely using Arnio, then perform computations using NumPy. Run: <pre><code class="lang-bash">python examples/arniowithnumpy.py</code></pre>
  • Arnio + scikit-learn
Prepare messy data with Arnio, then train a model with scikit-learn. Run: <pre><code class="lang-bash">python examples/arniowithsklearn.py</code></pre>
  • Arnio + DuckDB
Clean data with Arnio, then run SQL queries using DuckDB. Run: <pre><code class="lang-bash">python examples/arniowithduckdb.py</code></pre>
  • Arnio + Arrow
Export ArFrame to pyarrow.Table using
ar.to_arrow() for zero-copy interop with Arrow-native tools. Run: <pre><code class="lang-bash">python examples/arniowitharrow.py</code></pre>

<br>


<br>

πŸ” Why Arnio exists

Every data project starts the same way:

<pre><code class="lang-python">df = pd.read_csv(&quot;data.csv&quot;) # πŸ’₯ RAM spike β€” entire file as raw strings df.columns = df.columns.str.strip() # Why is this not automatic? df[&quot;name&quot;] = df[&quot;name&quot;].str.strip() # Python loop over every cell df[&quot;name&quot;] = df[&quot;name&quot;].str.lower() # Another Python loop df = df.dropna() # Another pass df = df.drop_duplicates() # Another pass</code></pre>

Six lines. Four full-data passes. All in interpreted Python. This is fine for a Jupyter demo β€” but it doesn't scale, it doesn't compose, and it definitely doesn't belong in production.

Arnio intercepts this entire pattern. It moves the preparation layer into a predictable pipeline, accelerates supported operations in C++, and gives you clean data for pandas, NumPy, scikit-learn, DuckDB, or notebooks.

<table> <tr> <td width="50%">

Without Arnio

<pre><code class="lang-python">df = pd.read_csv(path) df.columns = df.columns.str.strip() for col in str_cols: df[col] = df[col].str.strip() df[col] = df[col].str.lower() df = df.dropna(subset=[&quot;revenue&quot;]) df = df.drop_duplicates()

6+ lines, multiple passes, pure Python</code></pre>

</td> <td width="50%">

With Arnio

<pre><code class="lang-python">frame = ar.read_csv(path) df = ar.to_pandas(ar.pipeline(frame, [ (&quot;strip_whitespace&quot;,), (&quot;normalizecase&quot;, {&quot;casetype&quot;: &quot;lower&quot;}), (&quot;drop_nulls&quot;, {&quot;subset&quot;: [&quot;revenue&quot;]}), (&quot;drop_duplicates&quot;,), ]))

Declarative. Single pipeline. C++ execution.</code></pre>

</td> </tr> </table>

<br>


<br>

πŸ—οΈ Architecture

Arnio is not a pandas wrapper. It's a separate runtime with its own data model.

<pre><code class="lang-mermaid">flowchart LR subgraph python[&quot;Your Python Code&quot;] PY[&quot;frame = ar.readcsv(&#39;data.csv&#39;)\nclean = ar.pipeline(frame, [...])\ndf = ar.topandas(clean)&quot;] end

python --&gt;|&quot;pybind11 boundary&quot;| cpp

subgraph cpp[&quot;C++ Runtime (arniocpp)&quot;] direction TB CSV[&quot;CsvReader\nβ€’ RFC 4180\nβ€’ BOM strip\nβ€’ Type inference\nβ€’ Quoted fields&quot;] FRAME[&quot;Frame / Column\nβ€’ Columnar\nβ€’ std::variant\nβ€’ Bool null masks\nβ€’ O(1) column lookup&quot;] CLEAN[&quot;Cleaning Engine\nβ€’ dropnulls\nβ€’ fillnulls\nβ€’ dropdupes\nβ€’ stripws\nβ€’ normalize\nβ€’ rename/cast&quot;] CSV --&gt; FRAME --&gt; CLEAN end

cpp --&gt;|&quot;to_pandas() β†’ zero-copy NumPy buffer (numerics/bools)&quot;| OUT[&quot;pandas DataFrame&quot;]</code></pre>

Design decisions that matter

| Decision | What it means | |:---|:---| | Columnar storage | Data lives in typed std::vectors — vector, vector, vector — not rows of variants. Cache-friendly and SIMD-ready. | | Boolean null masks | Nulls are tracked in a separate vector, keeping data vectors dense. No sentinel values, no NaN tricks. | | Two-pass CSV read | Pass 1 infers types across all rows. Pass 2 parses values directly into the correct typed column. No string→object→cast overhead. | | Zero-copy bridge | to_pandas() exposes C++ memory directly via NumPy's buffer protocol where supported. Numeric columns preserve the fast zero-copy path by default, while copy=True requests defensive pandas-owned buffers. | | Step registry | Built-in and native steps use the C++ core via STEPREGISTRY; Python-backed built-ins dispatch through PYTHONSTEP_REGISTRY; custom user-defined steps follow the same Python registry path. Adding a new cleaning primitive is a single function + one registry entry. |

> Full architecture documentation: ARCHITECTURE.md > API reference guide: Arnio API Reference

<br>


<br>

🏎️ Benchmarks

> Reference environment: Ubuntu, Python 3.12, synthetic messy CSV inputs.<br> > Reproduce: make benchmark β€” generates deterministic tall and wide datasets and runs both engines.

To reproduce the published numbers from a fresh checkout:

<pre><code class="lang-bash">python -m venv .venv source .venv/bin/activate python -m pip install -U pip python -m pip install -e . python benchmarks/generate_data.py python benchmarks/benchmarkvspandas.py</code></pre>

benchmarks/generatedata.py uses deterministic NumPy seeds, so every run creates the same benchmarks/benchmark1m.csv tall input and benchmarks/benchmarkwide.csv wide input. The benchmark then executes three pandas runs and three arnio runs for each case, printing average wall-clock time from time.perfcounter() and peak Python allocation from tracemalloc. For cleaner comparisons, close other memory-heavy processes and run the script from the repository root after installing the same Python, pandas, NumPy, compiler, and arnio commit you want to compare.

Expected output format:

<pre><code class="lang-text">Tall CSV (1,000,000 rows x 12 columns) Metric pandas arnio ──────────────────────────────────────────── Exec Time (avg) 4.73s 5.75s Peak RAM 211MB 212MB Speed: 0.8x | RAM: -1% reduction

Wide CSV (5,000 rows x 256 columns) Metric pandas arnio ──────────────────────────────────────────── Exec Time (avg) ...s ...s Peak RAM ...MB ...MB Speed: ...x | RAM: ...% reduction</code></pre>

Small differences are expected across CPUs, operating systems, compilers, Python builds, and pandas/NumPy versions. If you share benchmark results in an issue or PR, include your OS, Python version, CPU model, pandas/NumPy versions, arnio commit, and the full command output so maintainers can compare like for like.

Arnio is near memory parity in the reference benchmark while replacing ad-hoc Python string loops with a compiled, declarative pipeline. Validate memory and speed on your own workload. The execution time gap is a known, active optimization target β€” the current dropduplicates and stripwhitespace implementations use unoptimized row-key serialization.

<table> <tr> <td>βœ… <b>What's already won</b></td> <td>🎯 <b>What's being optimized</b></td> </tr> <tr> <td>

  • Native C++ parsing eliminates Python memory spikes
  • Columnar storage matches pandas' internal efficiency
  • Declarative API eliminates .apply() spaghetti
  • Zero-copy bridge for numeric conversions
</td> <td>
  • drop_duplicates β€” replace string serialization with hash-based comparisons
  • strip_whitespace β€” in-place mutation instead of copy-on-write
  • Parallel column processing via std::thread
  • Help close the gap β†’
</td> </tr> </table>

<br>

🧠 Auto Clean Memory Benchmark

To measure the peak memory and execution time of the auto_clean pipeline using realistic dataset sizes:

<pre><code class="lang-bash">python benchmarks/benchmarkautoclean_memory.py --rows 100000</code></pre>

This script generates a reproducible synthetic dataset with mixed column types (strings, ints, floats, booleans, nulls, and duplicates) and measures:

  • ar.read_csv performance
  • ar.auto_clean(mode="safe") performance (low-risk cleanup like whitespace trimming)
  • ar.auto_clean(mode="strict") performance (includes type casting and deduplication)
The dataset is regenerated deterministically unless --reuse-file is provided. Each auto_clean benchmark run reloads the dataset to avoid mutation or caching effects between runs.

Options:

  • --repeat N runs each operation multiple times and reports average (and min/max range).
  • --seed N changes the deterministic dataset seed.
  • --reuse-file reuses an existing dataset file instead of regenerating it.
  • --keep-file keeps the generated CSV (otherwise it is removed at the end).
Expected output format:

<pre><code class="lang-text">Operation Time(s) Peak Py(MiB)


ar.read_csv 0.042 (0.041-0.044) 4.52 (4.50-4.60) ar.auto_clean(safe) 0.012 (0.011-0.013) 0.15 (0.14-0.16) ar.auto_clean(strict) 0.035 (0.034-0.036) 1.20 (1.18-1.22)
Total avg (Read+Strict) 0.077 4.52</code></pre> <br>


<br>

🧰 Cleaning primitives

Most operations below run natively in C++. Currently, filterrows, replacevalues and standardizemissingtokens run via the Python (pandas) backend and may be optimized in C++ later.

| Primitive | What it does | Example | |:---|:---|:---| | dropnulls | Remove rows with null/empty values | ar.dropnulls(frame, subset=["age"]) | | dropcolumns | Remove selected columns while preserving the remaining order | frame = ar.dropcolumns(frame, ["debug_col"]) | | dropemptycolumns | Remove columns whose values are all null/empty | frame = ar.dropemptycolumns(frame) | | keeprowswithnulls | Keep only rows that contain at least one null | ar.keeprowswithnulls(frame, subset=["age"]) | | validatecolumnsexist | Fail early when required columns are missing | ar.validatecolumnsexist(frame, ["age"]) | | filterrows | Filter rows using comparison operators | ar.filterrows(frame, column="age", op=">", value=18) | | fillnulls | Replace nulls with a scalar | ar.fillnulls(frame, 0, subset=["revenue"]) | | dropduplicates | Deduplicate rows (first/last/none) | ar.dropduplicates(frame, keep="first") | | dropconstantcolumns | Remove columns with only one unique value | ar.dropconstantcolumns(frame) | | clipnumeric | Clip numeric values to lower and/or upper bounds | ar.clipnumeric(frame, lower=0, upper=100) | | coalescecolumns | Select the first non-null value from a list of columns | ar.coalescecolumns(frame, subset=["phone", "mobile"], output_column="contact") | | combinecolumns | Combine multiple columns into a single output column | ar.combinecolumns(frame, subset=["first", "last"], separator=" ", output_column="name") | | stripwhitespace | Trim leading/trailing spaces from strings | ar.stripwhitespace(frame) | | standardizemissingtokens | Replace common missing-value strings with NaN | ar.standardizemissingtokens(frame) | | normalizecase | Force lower/upper/title case | ar.normalizecase(frame, case_type="title") | | renamecolumns | Rename columns via mapping | ar.renamecolumns(frame, {"old": "new"}) | | casttypes | Cast column types with errors="raise", "coerce", or "ignore" | ar.casttypes(frame, {"age": "int64"}, errors="raise") | | roundnumericcolumns | Round numeric columns (non-numeric columns in subset ignored safely) | ar.roundnumericcolumns(frame, decimals=2) | | replacevalues | Replace values using a mapping (column or whole-frame). Handles None/NaN. | ar.replacevalues(frame, {"active": "A", "inactive": "I"}, column="status") | | clean | Convenience shorthand supporting config dicts | ar.clean(frame, stripwhitespace={"subset": ["name"]}, dropnulls=True) | | safedividecolumns | Divide one column by another, handling zero/null denominators | ar.safedividecolumns(frame, numerator="revenue", denominator="cost", output_column="ratio") | | dropcolumnsmatching | Drop columns whose names match a regex pattern | ar.dropcolumnsmatching(frame, pattern="^temp_") | | trimcolumnnames | Strip leading/trailing whitespace from column names | ar.trimcolumnnames(frame) | | selectcolumns | Return a new frame containing only selected columns | ar.selectcolumns(frame, ["id", "name"]) | | slugifycolumnnames | Normalise column names to snakecase | ar.slugifycolumn_names(frame) |

ArFrame.select_dtypes β€” type-based column selection

Returns a new ArFrame containing only the columns whose dtype matches the filter. Raises ValueError if no columns match.

<pre><code class="lang-python">frame = ar.read_csv(&quot;data.csv&quot;)

Keep only numeric columns

numeric = frame.select_dtypes(include=[&quot;int64&quot;, &quot;float64&quot;])

Drop string columns

withoutstrings = frame.selectdtypes(exclude=&quot;string&quot;)</code></pre>

Valid dtype strings: "int64", "float64", "string", "bool", "null"

  • At least one of include or exclude must be given β€” raises ValueError otherwise.
  • include and exclude must not overlap β€” raises ValueError if they share a dtype.
  • Unknown dtype strings raise ValueError with a list of valid options.
  • Raises ValueError when no columns match (never returns an empty frame silently).
  • Column order in the result always matches the original frame.
Or compose them all into a pipeline:

<pre><code class="lang-python">clean = ar.pipeline(frame, [ (&quot;validatecolumnsexist&quot;, {&quot;columns&quot;: [&quot;name&quot;, &quot;city&quot;, &quot;revenue&quot;]}), (&quot;dropcolumns&quot;, {&quot;columns&quot;: [&quot;debugnotes&quot;]}), (&quot;strip_whitespace&quot;,), (&quot;standardizemissingtokens&quot;,), (&quot;normalizecase&quot;, {&quot;casetype&quot;: &quot;lower&quot;}), (&quot;fill_nulls&quot;, {&quot;value&quot;: &quot;unknown&quot;, &quot;subset&quot;: [&quot;city&quot;]}), (&quot;drop_duplicates&quot;, {&quot;keep&quot;: &quot;first&quot;}), ])</code></pre>

Winsorize outliers

winsorize_outliers() clips extreme numeric values using lower and upper quantiles. Non-numeric columns are ignored unless explicitly selected in subset.

<pre><code class="lang-python">frame = ar.read_csv(&quot;data.csv&quot;)

result = ar.winsorize_outliers( frame, lower=0.05, upper=0.95, )</code></pre>

It can also be used inside ar.pipeline() as ("winsorize_outliers", {"lower": 0.05, "upper": 0.95}).

πŸ” Replace values

Use replace_values to substitute values using a mapping. It works as a pipeline step (Python backend) and can operate on a single column or the whole frame when column is omitted. It also understands null semantics: using None (or np.nan) as a mapping key targets existing nulls, and mapping a value to None creates real nulls.

Column-specific example:

<pre><code class="lang-python">clean = ar.pipeline(frame, [ (&quot;replace_values&quot;, {&quot;mapping&quot;: {&quot;active&quot;: &quot;A&quot;, &quot;inactive&quot;: &quot;I&quot;}, &quot;column&quot;: &quot;status&quot;}), ])</code></pre>

Whole-frame example (no column):

<pre><code class="lang-python">clean = ar.pipeline(frame, [ (&quot;replace_values&quot;, {&quot;mapping&quot;: {None: &quot;MISSING&quot;, &quot;active&quot;: &quot;A&quot;, &quot;inactive&quot;: &quot;I&quot;}}), ])</code></pre>

Direct API:

<pre><code class="lang-python">frame2 = ar.replace_values(frame, {&quot;active&quot;: &quot;A&quot;, &quot;inactive&quot;: &quot;I&quot;})</code></pre>

πŸ”Ž Filter rows inside pipelines

Use filter_rows to keep only rows matching a condition.

<pre><code class="lang-python">clean = ar.pipeline(frame, [ (&quot;filter_rows&quot;, { &quot;column&quot;: &quot;revenue&quot;, &quot;op&quot;: &quot;&gt;=&quot;, &quot;value&quot;: 1000 }), ])</code></pre>

Supported operators:

  • >
  • <
  • >=
  • <=
  • ==
  • !=
Works with:
  • integers
  • floats
  • strings
  • booleans

πŸ”Ž Isolate rows with null values

Use keeprowswith_nulls to audit incomplete data β€” keep only rows that have at least one null.

<pre><code class="lang-python">frame = ar.read_csv(&quot;data.csv&quot;)

Keep all rows that have at least one null anywhere

nulls = ar.keeprowswith_nulls(frame)

Keep rows where specifically &#39;age&#39; or &#39;score&#39; is null

nulls = ar.keeprowswith_nulls(frame, subset=[&quot;age&quot;, &quot;score&quot;])

Works inside a pipeline too

result = ar.pipeline(frame, [ (&quot;keeprowswith_nulls&quot;, {&quot;subset&quot;: [&quot;age&quot;]}), ])</code></pre>

Useful for data auditing β€” inspect what's missing before deciding how to fill or drop.

Boolean string normalization

<pre><code class="lang-python">clean = ar.parseboolstrings(frame)</code></pre>

This normalizes values such as "yes", "no", "true", "false", "y", "n", "1", and "0" into boolean values while preserving unsupported values unchanged.

Columns containing both parsed boolean values and unsupported string values may round-trip as strings because of ArFrame column typing semantics.

<br>

πŸ”’ Safe column division

Divide one column by another while handling division by zero and null denominators explicitly:

<pre><code class="lang-python">result = ar.safedividecolumns( frame, numerator=&quot;revenue&quot;, denominator=&quot;cost&quot;, output_column=&quot;ratio&quot;, fill_value=0.0, # used when denominator is zero or null )</code></pre>

> When the denominator is zero or null, the result is replaced with fill_value (default 0.0) instead of raising an error or producing NaN/Inf.


<br>

πŸ“Š Pandas Dtype Support Matrix

This table helps users understand which pandas dtypes and workflows are fully supported, partially supported, unsupported, or planned.

If a dtype is partially supported, users may need conversion before processing. Unsupported dtypes should raise clear errors where applicable.

| Pandas Dtype | Support Status | Notes / Fix Hints | |---|---|---| | int64 / Int64 | βœ… Supported | Fully supported with native C++ columnar storage. Nulls mapped to pd.NA. | | float64 / Float64 | βœ… Supported | Fully supported with zero-copy conversion. Nulls mapped to np.nan or pd.NA. | | bool / boolean | βœ… Supported | Native booleans supported with C++ backing. Nulls mapped to pd.NA. | | string / string[python] | βœ… Supported | Native string extension type. Recommended for text. Nulls mapped to pd.NA. | | object (strings / scalars) | βœ… Supported | Handled as text or coerced to common type if mixed. | | object (nested / lists / dicts) | ❌ Unsupported | Nested structures not allowed in flat columnar storage. Raises TypeError. | | category | ❌ Unsupported | Raises TypeError with fix hint. Convert to string: df["col"].astype(str) | | datetime64[ns] / timezone-aware | ❌ Unsupported | Raises TypeError with fix hint. Use df["col"].astype(str) or string timestamps. | | timedelta64[ns] | ❌ Unsupported | Raises TypeError with fix hint. Use df["col"].dt.total_seconds(). | | complex64 / complex128 | ❌ Unsupported | Raises TypeError with fix hint. Split into real/imag columns or convert to strings. |

Notes

  • Zero-copy Optimization: Numeric columns (int64, float64) are optimized for fast zero-copy conversion between C++ and pandas where supported.
  • Defensive Buffers: Pass copy=True to to_pandas() when downstream pandas code needs defensive pandas-owned column buffers.
  • Boolean Buffers: Boolean conversion is copied because std::vector cannot be exposed as a zero-copy NumPy buffer.
  • Null Handling: Columns with null masks are automatically converted to pandas nullable Extension dtypes (Int64, BooleanDtype, StringDtype).
  • Index Drop: pandas DataFrame indexes are currently not preserved during frompandas() conversion; converted frames receive a default RangeIndex when converted back via topandas().
  • Validation: Attempting to convert any unsupported type will raise a clear, user-friendly TypeError detailing the column name and how to fix/preprocess it.
<br>

<br>

🧠 Data quality engine

Arnio now includes built-in dataset understanding before you analyze in pandas.

<pre><code class="lang-python">report = ar.profile(frame) print(report.summary())

suggestions = ar.suggest_cleaning(frame) clean = ar.pipeline(frame, suggestions)</code></pre>

For production data contracts:

<pre><code class="lang-python"># Register a custom validator once, then reference it by name in any schema ar.register_validator(&quot;positive&quot;, lambda v: v &gt; 0)

schema = ar.Schema({ &quot;id&quot;: ar.Int64(nullable=False, unique=True), &quot;email&quot;: ar.Email(nullable=False), &quot;phone&quot;: ar.PhoneNumber(nullable=False),

&quot;user_type&quot;: ar.String(nullable=False),

# country becomes required when user_type == &quot;international&quot; &quot;country&quot;: ar.String( nullable=True, requiredif=(&quot;usertype&quot;, &quot;international&quot;), ),

# CurrencyCode validates 3-letter uppercase formats (e.g., USD, EUR, INR). &quot;currency&quot;: ar.CurrencyCode(),

# LanguageCode validates lowercase ISO 639-1 language codes (e.g., en, hi, fr). &quot;language&quot;: ar.LanguageCode(),

# TimeZone validates IANA timezone identifiers (e.g., Asia/Kolkata). &quot;timezone&quot;: ar.TimeZone(),

&quot;username&quot;: ar.String(minlength=3, maxlength=20), &quot;user_code&quot;: ar.Regex(r&quot;^USR-\d{4}$&quot;, nullable=False), &quot;revenue&quot;: ar.Custom(&quot;positive&quot;, nullable=True, requiredif=(&quot;usertype&quot;, &quot;merchant&quot;)), &quot;signup_date&quot;: ar.Date(nullable=False), &quot;created_at&quot;: ar.DateTime(nullable=False, format=&quot;%Y-%m-%d&quot;),

})

result = ar.validate(frame, schema)

if not result.passed: summary = result.summary() print(summary[&quot;issuesbyrule&quot;]) print(summary[&quot;issuesbycolumn&quot;]) print(summary[&quot;issuesbycolumnandrule&quot;]) print(result.to_pandas()) print(result.tomarkdown(maxissues=10))</code></pre>

Numeric string compatibility hints

Validation messages indicate when string values appear safely convertible to numeric dtypes.

<pre><code class="lang-python">frame = ar.from_pandas( pd.DataFrame( { &quot;age&quot;: [&quot;1&quot;, &quot;2&quot;, &quot;3&quot;], } ) )

schema = ar.Schema( { &quot;age&quot;: ar.Int64(), } )

result = ar.validate(frame, schema)

print(result.issues[0].message)

Column &#39;age&#39; has dtype &#39;string&#39;; expected &#39;int64&#39;.

Values appear safely convertible to &#39;int64&#39;</code></pre>

In this example, country becomes required only when user_type == "international".

Date validates strict YYYY-MM-DD calendar dates.

Phone number validation

PhoneNumber() validates common international and formatted phone number strings.

<pre><code class="lang-python">schema = ar.Schema({ &quot;phone&quot;: ar.PhoneNumber(nullable=False), })

result = ar.validate(frame, schema) print(result.passed)</code></pre>

Accepted examples include:

  • +1-555-123-4567
  • +91 9876543210
  • 5551234567

Warning-only validation

<pre><code class="lang-python">schema = ar.Schema( { &quot;age&quot;: ar.Int64( min=18, severity=&quot;warning&quot;, ) } )

result = ar.validate(frame, schema)

print(result.passed) # True print(result.issue_count) # Warning issues are still reported</code></pre>

Warning-level issues remain visible in validation results without failing the overall validation status.

URL validation

URL() validates that values are well-formed URLs. By default, both http and https schemes are accepted.

<pre><code class="lang-python">schema = ar.Schema({ &quot;website&quot;: ar.URL(nullable=False), }) result = ar.validate(frame, schema) print(result.passed)</code></pre>

Use allowed_schemes to restrict which URL schemes are valid:

<pre><code class="lang-python"># https only schema = ar.Schema({ &quot;website&quot;: ar.URL(allowed_schemes=[&quot;https&quot;]), })

multiple schemes

schema = ar.Schema({ &quot;endpoint&quot;: ar.URL(allowed_schemes=[&quot;https&quot;, &quot;ftp&quot;]), })</code></pre>

Any URL with a scheme not in allowed_schemes will fail validation.

Schema JSON round-trips

<pre><code class="lang-python">schema = ar.Schema( { &quot;id&quot;: ar.String(nullable=False), &quot;created_at&quot;: ar.DateTime(format=&quot;%Y-%m-%dT%H:%M:%S&quot;), }, strict=True, unique=[&quot;id&quot;], )

payload = schema.to_json() restored = ar.Schema.from_json(payload)</code></pre>

See examples/schema_validation.py for a complete runnable tutorial covering Schema, field types, invalid-row reporting, and ValidationResult output.

ValidationResult.to_markdown() is useful in CI logs, GitHub comments, or data quality reports because it renders a compact validation summary plus a GitHub-friendly issue table.

For multi-column uniqueness (composite keys):

<pre><code class="lang-python">schema = ar.Schema({ &quot;user_id&quot;: ar.Int64(nullable=False), &quot;course_id&quot;: ar.Int64(nullable=False), }, unique=[&quot;userid&quot;, &quot;courseid&quot;])

result = ar.validate(frame, schema)</code></pre>

For automatic cleaning suggestions based on the profile:

<pre><code class="lang-python">suggestions = ar.suggest_cleaning(frame)

e.g. [(&quot;strip_whitespace&quot;, {&quot;subset&quot;: [&quot;name&quot;, &quot;city&quot;]}),

(&quot;drop_duplicates&quot;, {&quot;keep&quot;: &quot;first&quot;})]

clean = ar.pipeline(frame, suggestions)</code></pre>

For low-risk automatic cleanup in one call:

<pre><code class="lang-python">clean, report = ar.autoclean(frame, returnreport=True)</code></pre>

For strict automatic cleanup, inspect type casts before applying them:

<pre><code class="lang-python">report = ar.autoclean(frame, mode=&quot;strict&quot;, dryrun=True) castmapping = dict(report.suggestions).get(&quot;casttypes&quot;)

clean = ar.auto_clean( frame, mode=&quot;strict&quot;, allowlossycasts=True, confirmedcasts=castmapping, )</code></pre>

This is the layer pandas does not try to own: profiling, data contracts, row-level validation issues, and preview-gated cleaning suggestions for messy incoming datasets.

<br>

Beginner-friendly auto-clean tutorial

Use this workflow when you receive a small messy dataset and want to inspect what Arnio will change before applying it.

<pre><code class="lang-python">import arnio as ar import pandas as pd

raw = pd.DataFrame( { &quot;order_id&quot;: [1001, 1002, 1002, 1003, 1004], &quot;customer&quot;: [&quot; Ishan &quot;, &quot; Prasoon &quot;, &quot; Prasoon &quot;, &quot; Pranay &quot;, &quot; Dhruv &quot;], &quot;city&quot;: [&quot; Paris &quot;, &quot;London&quot;, &quot;London&quot;, &quot; New York &quot;, &quot; Tokyo &quot;], } )

frame = ar.from_pandas(raw)

report = ar.profile(frame) summary = report.summary() print(summary)

suggestions = ar.suggest_cleaning(frame) print(suggestions)

[(&#39;stripwhitespace&#39;, {&#39;subset&#39;: [&#39;customer&#39;, &#39;city&#39;]}), (&#39;dropduplicates&#39;, {&#39;keep&#39;: &#39;first&#39;})]

safe = ar.auto_clean(frame) strict = ar.auto_clean(frame, mode=&quot;strict&quot;)</code></pre>

Messy input:

| order_id | customer | city | |:--|:--|:--| | 1001 | Ishan | Paris | | 1002 | Prasoon | London | | 1002 | Prasoon | London | | 1003 | Pranay | New York | | 1004 | Dhruv | Tokyo |

Expected cleaned output with mode="strict":

| order_id | customer | city | |:--|:--|:--| | 1001 | Ishan | Paris | | 1002 | Prasoon | London | | 1003 | Pranay | New York | | 1004 | Dhruv | Tokyo |

mode="safe" only trims whitespace. Use mode="strict" when you also want deterministic built-in cleanup such as exact duplicate removal. If strict mode proposes type casts, run dryrun=True first and pass the exact proposed mapping as confirmedcasts with allowlossycasts=True.

See examples/autocleantutorial.py for a runnable version of this walkthrough, and examples/schemavalidation.py for a focused validation tutorial.

> For strict mode data-loss risks and safe workflow, see AUTOCLEAN_GUIDE.md.

<br>

Data Quality Reports

Arnio provides detailed profiling for datasets via ar.profile(). To generate the report shown in these examples, the following code was used:

<pre><code class="lang-python">import arnio as ar import pandas as pd

Sample dataset used for these examples

data = { &quot;user_id&quot;: [101, 102, 103, 104], &quot;email&quot;: [&quot;test@arnio.ai&quot;, &quot;invalid-email&quot;, None, &quot;test@arnio.ai&quot;], &quot;score&quot;: [85.5, 90.0, None, 88.2] } df = ar.from_pandas(pd.DataFrame(data))

Bounded profiling for large datasets (controls how many sample values are kept)

report = ar.profile(df, sample_size=5) safereport = report.todict(redactsamplevalues=True)</code></pre>

Profiling privacy and redaction

Profiling helps you understand data, but some report fields can still expose real emails, names, IDs, or other sensitive values. Before you paste output into GitHub issues, Slack, public notebooks, or shared logs, check whether you are sharing aggregate statistics only or raw/sample cell values.

What is aggregate-only vs may expose raw values

| Field or export | Aggregate-only? | May expose raw / sample data? | | --- | --- | --- | | rowcount, columncount, duplicaterows, duplicateratio, qualityscore, scorecomponents | Yes | No | | nullcount, nullratio, uniquecount, uniqueratio, whitespace / empty-string counts | Yes | No | | Numeric min / max / mean / std / q25–q95 | Yes | Statistics only; small tables can still be identifying | | Numeric iqr, outlierlowerbound, outlierupperbound, outliercount, outlierratio | Yes | Aggregate Tukey-fence summary (thresholds and counts, not which rows are outliers) | | semantictype, suggesteddtype, warnings | Metadata / hints | Can imply PII type (for example email-like), not redaction | | ColumnProfile.samplevalues (in-memory) | No | Yes β€” first N non-null values (samplesize on ar.profile()) | | ColumnProfile.top_values | Includes counts / ratios | Yes β€” frequent actual values (exact or approximate; see below) | | report.todict() | Mixed | Yes β€” includes samplevalues and top_values unless you redact samples | | report.todict(redactsamplevalues=True) | Mixed | samplevalues β†’ "[REDACTED]" (same list length); top_values[*].value β†’ "[REDACTED]" while counts and ratios remain | | report.to_markdown(), report.summary() | Yes | No raw cell values in output | | report.tohtml() / notebook display of report | Partial | Shows topvalues chips; does not list samplevalues. Use redacttopvalues=True or excludecolumns for safer sharing. | | report.topandas() | Partial | Includes topvalues, not sample_values | | ProfileComparison.todict() | Nested profiles | Yes β€” embeds leftprofile / rightprofile via default todict() |

Arnio does not auto-mask emails, phone numbers, or IDs by column type. Use the controls below for safer sharing.

Safe sharing practices

  • JSON logs and artifacts: report.todict(redactsample_values=True) before writing or uploading.
  • Collect fewer samples: ar.profile(frame, samplesize=0) skips samplevalues (defaults still apply to top_values counts on string columns).
  • Text summaries for CI or comments: prefer report.to_markdown() or report.summary() when you do not need per-value examples.
  • Notebooks and HTML exports: use report.tohtml(redacttopvalues=True) to replace every top-value chip label with [REDACTED] while preserving counts and ratios. To drop entire sensitive columns from the table, add excludecolumns=["ssn", "email"]. Avoid saving unredacted report.to_html() output for sensitive data.
  • GitHub bug reports and examples: use synthetic data (user@example.com, ID-001), a minimal CSV, and redacted to_dict() output β€” not production dumps.
  • Pandas export: ar.to_pandas(frame) returns full table data; redaction applies to quality reports, not the underlying frame.
  • Profile comparison: ProfileComparison.todict() nests full profiles; build shared artifacts with profile.todict(redactsamplevalues=True) if needed.
<pre><code class="lang-python">import arnio as ar import pandas as pd

df = ar.from_pandas(pd.DataFrame({ &quot;email&quot;: [&quot;user@example.com&quot;, &quot;bad-email&quot;, None], &quot;user_id&quot;: [101, 102, 103], })) report = ar.profile(df, sample_size=2)

Safer JSON for sharing (samplevalues and topvalues values redacted)

safejson = report.todict(redactsamplevalues=True)

Safer HTML export (top-value chip labels replaced with [REDACTED])

safehtml = report.tohtml(redacttopvalues=True)

or exclude an entire column from the HTML table:

safehtml = report.tohtml(redacttopvalues=True, exclude_columns=[&quot;email&quot;])

Safer text summary (no samplevalues or topvalues in output)

print(report.to_markdown())</code></pre>

When approxtopvalues=True, high-cardinality string columns estimate top_values from a deterministic sample. Each column may set topvaluesisapproximate, topvaluessamplecount, and topvaluessample_ratio. Counts and ratios are sample-based, but displayed values are still real strings from your data β€” treat them like top_values for privacy.

<pre><code class="lang-python"># Optional: approximate top values for high-cardinality string columns report = ar.profile( df, approxtopvalues=True, approxtopvaluesminunique=1000, approxtopvaluesminratio=0.2, approxtopvaluessamplesize=2000, )</code></pre>

Notebook dashboard (Jupyter / Colab)

DataQualityReport includes a notebook-friendly HTML dashboard. In a notebook, simply evaluate report in a cell to see a rich, static summary (quality score, duplicates, nulls, warnings, top values, and cleaning suggestions).

If you want to embed or save the HTML explicitly:

<pre><code class="lang-python">from IPython.display import HTML

HTML(report.to_html())

or: report.tohtml(filepath=&quot;dataqualityreport.html&quot;)</code></pre>

Sample output now includes quantiles and IQR outlier summary for numeric columns:

For numeric columns with at least four non-null values, Arnio reports iqr (q75 βˆ’ q25), Tukey fences outlierlowerbound (q25 βˆ’ 1.5Γ—IQR) and outlierupperbound (q75 + 1.5Γ—IQR), plus outliercount and outlierratio. A value is counted as an outlier only if it is strictly less than the lower bound or strictly greater than the upper bound. With fewer than four non-null values, quantiles may still appear but IQR/outlier fields are null in JSON.

Illustrative age column (not from the user_id / email / score sample below):

<pre><code class="lang-json">{ &quot;age&quot;: { &quot;dtype&quot;: &quot;float64&quot;, &quot;mean&quot;: 35.2, &quot;std&quot;: 10.1, &quot;min&quot;: 18.0, &quot;max&quot;: 60.0, &quot;q25&quot;: 27.5, &quot;q50&quot;: 35.0, &quot;q75&quot;: 44.0, &quot;q95&quot;: 57.0, &quot;iqr&quot;: 16.5, &quot;outlierlowerbound&quot;: 2.75, &quot;outlierupperbound&quot;: 68.75, &quot;outlier_count&quot;: 0, &quot;outlier_ratio&quot;: 0.0, &quot;null_count&quot;: 0 } }</code></pre>

Compare Profiles

Use
ar.compare_profiles() to compare two DataQualityReport profiles and flag per-column drift.

<pre><code class="lang-python">baseline = ar.profile(ar.read_csv(&quot;baseline.csv&quot;)) current = ar.profile(ar.read_csv(&quot;current.csv&quot;))

comparison = ar.compare_profiles(baseline, current) print(comparison.drift_report[&quot;score&quot;][&quot;status&quot;]) # &quot;ok&quot;, &quot;warning&quot;, or &quot;changed&quot; print(comparison.status_counts) # {&quot;ok&quot;: 2, &quot;warning&quot;: 1, &quot;changed&quot;: 0}</code></pre>

Use ar.checkqualitygates() when profile drift should become a pass/fail decision for CI, data releases, or monitoring.

<pre><code class="lang-python">result = ar.checkqualitygates( baseline, current, maxrowcountdeltaratio=0.10, maxnullratio_delta=0.05, maxnumericmeandeltaratio=0.10, )

if not result.passed: print(result.to_markdown()) result.raiseforfailures()</code></pre>

> Scoring Contract: The qualityscore starts at 100.0 and subtracts capped penalties for duplicates, nulls, and suggested dtype mismatches. The scorecomponents` field exposes these penalties as negative values. (Note: Semantic-validity pe


README truncated. View on GitHub

Β© 2026 GitRepoTrend Β· im-anishraj/arnio Β· Updated daily from GitHub