Skip to main content

Declarative data cleaning contracts for pandas.

Project description

pycleanframe

Declarative data cleaning contracts for pandas. Annotation-based. Zero API calls. Offline-first.

Stop writing repetitive pandas cleaning code. Define your rules once in a Schema, decorate your function, and get a clean DataFrame automatically — with a full report of every change made.


Install

pip install pycleanframe

Quick Start

from cleanframe import clean, Col, Schema

# 1. Define your cleaning rules
schema = Schema(
    age    = Col(null="median",  clip=(0, 120)),
    salary = Col(null="median",  clip=(0, 999_999)),
    dept   = Col(null="Unknown", dtype="category"),
)

# 2. Decorate your function
@clean(schema)
def train_model(df):
    model.fit(df)

# 3. Call it with dirty data — it cleans automatically
result = train_model(dirty_df)

# 4. See what changed
print(result.explain())

# 5. Get the equivalent pandas code
print(result.to_code())

How It Works

When you call a decorated function:

  1. cleanframe scans the DataFrame for issues (nulls, out-of-range values, wrong dtypes)
  2. It fixes them according to your Schema rules
  3. Your function receives the clean DataFrame
  4. You get back a CleanResult with the cleaned data + a full change log

Your original DataFrame is never mutated — cleanframe always works on a copy.


Col() — Column Rules

Col() defines the cleaning rules for a single column.

Col(
    null="median",   # How to fill missing values (see strategies below)
    clip=(0, 120),   # Clip numeric values to [min, max] range
    dtype="float",   # Coerce column to this dtype after cleaning
    rename="age_yr", # Rename the column after all other operations
    required=True,   # Raise KeyError if this column is missing from the DataFrame
)

Null Strategies

Strategy What it does
"mean" Fill nulls with the column mean
"median" Fill nulls with the column median (robust to outliers)
"mode" Fill nulls with the most frequent value
"drop" Drop rows that have a null in this column
"ffill" Forward fill — copy the last valid value downward
"bfill" Backward fill — copy the next valid value upward
"auto" Automatically pick the best strategy based on dtype and column name
"ignore" Do nothing (default)
any scalar Fill with that exact value, e.g. null=0 or null="Unknown"

Dtype Options

dtype What it does
"int" Convert to nullable integer (Int64)
"float" Convert to float64
"str" Convert to string
"bool" Convert to boolean
"category" Convert to pandas Categorical (saves memory)
"datetime" Parse as datetime using pd.to_datetime

Decorators

@clean(schema) — Clean silently

Cleans the DataFrame before your function runs. Returns a CleanResult.

from cleanframe import clean, Col, Schema

schema = Schema(
    age  = Col(null="median", clip=(0, 120)),
    dept = Col(null="Unknown", dtype="category"),
)

@clean(schema)
def train(df):
    model.fit(df)
    return "done"

result = train(dirty_df)
print(result.df)           # cleaned DataFrame
print(result.return_value) # "done"
print(result.explain())    # full change report

@validate(schema) — Validate without modifying

Checks the DataFrame and raises DataQualityError if any issues are found. Never modifies the data. Use this in production APIs where you want to reject bad data instead of fixing it.

from cleanframe import validate, Col, Schema
from cleanframe.exceptions import DataQualityError

schema = Schema(
    age    = Col(null="median", clip=(0, 120)),
    salary = Col(null="median", clip=(0, 999_999)),
)

@validate(schema)
def predict(df):
    return model.predict(df)

try:
    result = predict(dirty_df)
except DataQualityError as e:
    print(e.issues)  # list of exactly what's wrong

@audit(schema) — Clean and print a change log

Same as @clean but automatically prints a detailed report of every change. Great for debugging and development.

from cleanframe import audit, Col, Schema

schema = Schema(
    age  = Col(null="median", clip=(0, 120)),
    dept = Col(null="mode"),
)

@audit(schema)
def process(df):
    pass

process(dirty_df)
# Automatically prints:
# cleanframe → process() — Cleaning report
# =======================================================
#   ✓ [age] Filled 3 null(s) using median → value: 34.0
#   ✓ [age] Clipped 1 value(s) to range [0, 120]
#   ✓ [dept] Filled 2 null(s) using mode → value: 'Eng'

@profile — Inspect without any schema

Prints a data quality report before your function runs. No schema needed — just attach it to any function to see what's in the DataFrame.

from cleanframe import profile

@profile
def explore(df):
    pass

explore(df)
# cleanframe @profile  →  explore()
# Shape       : 1000 rows × 5 columns
# Memory      : 42.3 KB
#
#    Column               dtype        nulls   null%   note
#    --------------------------------------------------------
#  ⚠ age                 float64          23    2.3%   skewed (+2.1)
#  ⚠ dept                object           10    1.0%   4 unique
#    salary              float64           0    0.0%

@pipeline(schema1, schema2, ...) — Chain multiple schemas

Applies multiple schemas in sequence. Each schema's output feeds into the next. Use this to separate raw cleaning from feature engineering.

from cleanframe import pipeline, Col, Schema

# Step 1: fix raw data quality issues
raw_schema = Schema(
    age  = Col(null="median"),
    dept = Col(null="mode"),
)

# Step 2: engineer features
feature_schema = Schema(
    age  = Col(clip=(0, 120), dtype="float"),
    dept = Col(dtype="category"),
)

@pipeline(raw_schema, feature_schema)
def train(df):
    model.fit(df)

result = train(dirty_df)
print(result.explain())  # shows changes from ALL schemas

CleanResult

Every decorator (except @profile and @validate) returns a CleanResult object.

result = train(dirty_df)

result.df            # the cleaned DataFrame
result.return_value  # whatever your function returned
result.steps         # raw list of every change (dicts)
result.explain()     # human-readable report (string)
result.to_code()     # equivalent pandas code you can copy-paste
result.summary()     # one-line summary string

result.explain() example output

cleanframe → train() — Cleaning report
=======================================================
  ✓ [age] Filled 3 null(s) using median → value: 34.0
  ✓ [age] Clipped 1 value(s) to range [0, 120]
  ✓ [salary] Filled 1 null(s) using median → value: 65000.0
  ✓ [dept] Filled 2 null(s) using scalar('Unknown')
  ✓ [dept] Dtype coerced: object → category

  DataFrame shape after cleaning: (1000, 3)

result.to_code() example output

import pandas as pd
import numpy as np

# Generated by cleanframe — equivalent pandas code
df = df.copy()

# Fill nulls in 'age' (median)
df['age'] = df['age'].fillna(34.0)

# Clip outliers in 'age'
df['age'] = df['age'].clip(lower=0, upper=120)

# Fill nulls in 'dept' (scalar('Unknown'))
df['dept'] = df['dept'].fillna('Unknown')

# Coerce dtype of 'dept'
df['dept'] = df['dept'].astype('category')

Modes

All cleaning decorators support a mode parameter:

@clean(schema, mode="fix")    # default — clean silently, no output
@clean(schema, mode="warn")   # clean and emit a UserWarning for each change
@clean(schema, mode="raise")  # raise DataQualityError instead of cleaning

Schema Features

Case-insensitive column matching

# Works even if your DataFrame has 'Age', 'AGE', or ' age '
schema = Schema(age=Col(null="median"))

Optional vs required columns

schema = Schema(
    age    = Col(null="median"),                     # optional — silently skipped if missing
    salary = Col(null="median", required=True),      # required — raises KeyError if missing
)

Rename columns

schema = Schema(
    dept = Col(null="mode", rename="department"),    # renamed after cleaning
)

Full Example

import pandas as pd
from cleanframe import clean, Col, Schema

# Dirty data
df = pd.DataFrame({
    "age":    [25, None, 34, None, 200],
    "salary": [50000, 200000, None, 80000, -1000],
    "dept":   ["Eng", None, "HR", "Eng", None],
})

# Define rules
schema = Schema(
    age    = Col(null="median",  clip=(0, 120)),
    salary = Col(null="median",  clip=(0, 999_999)),
    dept   = Col(null="Unknown", dtype="category"),
)

# Decorate and call
@clean(schema)
def process(df):
    return df.shape

result = process(df)

print(result.explain())
# cleanframe → process() — Cleaning report
# =======================================================
#   ✓ [age] Filled 2 null(s) using median → value: 29.5
#   ✓ [age] Clipped 1 value(s) to range [0, 120]
#   ✓ [salary] Filled 1 null(s) using median → value: 65000.0
#   ✓ [salary] Clipped 1 value(s) to range [0, 999999]
#   ✓ [dept] Filled 2 null(s) using scalar('Unknown')
#   ✓ [dept] Dtype coerced: object → category

print(result.to_code())     # copy-paste pandas equivalent
print(result.summary())     # one-line summary
print(result.return_value)  # (5, 3)

Requirements

  • Python >= 3.8
  • pandas >= 1.3
  • numpy >= 1.21

License

MIT

Project details


Download files

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

Source Distribution

pycleanframe-1.0.1.tar.gz (20.8 kB view details)

Uploaded Source

Built Distribution

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

pycleanframe-1.0.1-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

Details for the file pycleanframe-1.0.1.tar.gz.

File metadata

  • Download URL: pycleanframe-1.0.1.tar.gz
  • Upload date:
  • Size: 20.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for pycleanframe-1.0.1.tar.gz
Algorithm Hash digest
SHA256 9c716e0358b7ca39e05f91c704667bc3749b129b0d835618c7a9cc980a5828af
MD5 c2812f0d459b43a122324d092b1a1f0a
BLAKE2b-256 29fb80f5b9e63904e95a8dfd07ec96b087e27c6c6b34f7152f7707eb91796f38

See more details on using hashes here.

File details

Details for the file pycleanframe-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: pycleanframe-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 16.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for pycleanframe-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7e78129fff98eeffd193e27903e35ea1a0881b10d3225e39ac2af56fe27d7779
MD5 ea3a075005a1dd77439e5d5de3f96da7
BLAKE2b-256 5df69eef7146033adc36521e9b3e27cd273392706b27b016525852c9cb992581

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page