pyduck-janitor
DuckDB-backed pyjanitor for high-performance data cleaning on large datasets
The objective of this package is to perform data cleaning using an expressive grammar that coheres with the tidyverse design framework, but powered by DuckDB for high-performance execution on large datasets. The package is centered around data cleaning verbs, supplemented with many utilities for data transformation and manipulation.
Overview
pyduck-janitor provides a method-chaining API for data cleaning operations that mirrors pyjanitor, but uses DuckDB as the backend for:
- Speed: DuckDB's vectorized execution engine accelerates data cleaning operations
- Scalability: Process datasets larger than memory by working directly with Parquet, CSV, or other file formats
- Lazy evaluation: Build complex cleaning pipelines that execute efficiently
- Drop-in replacement: Use familiar pyjanitor syntax with automatic DuckDB optimization
The Main Verbs
The core functionality of pyduck-janitor is organized around several main groups of verbs:
clean_names()- Standardize column names to a consistent formatfilter_on(),filter_string()- Filter rows based on conditions or string patternsselect_columns(),select_rows()- Select specific columns or rowsadd_column(),remove_columns(),rename_column()- Modify columnsdropna(),remove_empty()- Handle missing datacoalesce(),fill(),fill_empty()- Impute missing valuesencode_categorical(),get_dummies()- Encode categorical variablestransform_column(),transform_columns()- Transform column valuescase_when(),find_replace()- Conditional transformationspivot_wider(),pivot_longer()- Reshape datagroupby_agg(),groupby_topk()- Grouped operations
Installation
Install the released package from PyPI:
pip install pyduck-janitor
For Excel and Parquet I/O helpers, install the optional engines with
pip install pyduck-janitor[io].
For optional graph analytics through Onager, install the graph extra. The extra pins a compatible DuckDB range; the native Onager extension is loaded only when requested from DuckDB's community repository:
pip install "pyduck-janitor[graph]"
For development from a checkout, use the editable install. The [dev] extra
pulls in the test and lint dependencies used by pytest, ruff, and the
API-doc generator:
git clone https://github.com/ezraair555/pyduck-janitor.git
cd pyduck-janitor
pip install -e ".[dev]"
Quick Start
import pandas as pd
from pyduck_janitor import DuckJanitor
# Load your data
df = DuckJanitor.from_pandas(pd.DataFrame({
'SalesMonth': ['Jan', 'Feb', 'Mar', 'April'],
'Company1': [150.0, 200.0, 300.0, 400.0],
'Company2': [180.0, 250.0, None, 500.0],
'Company3': [400.0, 500.0, 600.0, 675.0]
}))
# Build a cleaning pipeline
result = (
df
.clean_names()
.remove_columns(['company1'])
.dropna(subset=['company2', 'company3'])
.rename_column('company2', 'amazon')
.add_column('google', [450.0, 550.0, 800.0])
.collect()
)
print(result)
Select DSL (pyjanitor-compatible)
from pyduck_janitor import DuckJanitor, DropLabel
dj = DuckJanitor.from_pandas(pd.DataFrame({
'sales_month': ['Jan', 'Feb'],
'company2': [180.0, 250.0],
'company3': [400.0, 500.0],
'notes': ['x', 'y'],
}))
dj.select_columns('sales_month, company2') # comma-string
dj.select_columns('company*') # glob expansion
dj.select_columns('re:^company') # regex
dj.select_columns(['company*', DropLabel('company3')]) # exclude one column
Supported Functions
pyduck-janitor implements the complete pyjanitor documented API (94/94 functions — verified against the pyjanitor API reference) plus 18 DuckDB-specific extensions, exposed as 109 chainable methods on DuckJanitor.
📖 Full function-by-function reference with verified examples:
docs/api/functions.md— description, signature, parameters, returns, raises, and a runnable example for every function (modeled on pyjanitor's API functions page).
Core cleaning verbs (cleaning_ops.py)
clean_names()- Clean column namesremove_columns()- Remove columnsadd_column()/add_columns()- Add a new column (single or dict form)rename_column()/rename_columns()- Rename a columndropna()- Drop rows with NA valuesremove_empty()- Remove empty rows/columnsfilter_column()/filter_column_isin()- Filter by column condition / IS IN listfilter_on()- Filter with SQL-like criteriafilter_string()- Filter by substringcoalesce()- Merge columnsencode_categorical()- Encode as categoricalget_dummies()- One-hot encodeselect_columns()/select()- Select columns (supports comma-strings, globs,re:regex, andDropLabel)select_rows()- Select rowstransform_column()/transform_columns()- Transform one or many columns
Extended verbs (cleaning_ops_extended.py)
bin_numeric()- Bin numeric columnchange_type()- Change column typeconcatenate_columns()- Join columnsdeconcatenate_column()- Split columndrop_constant_columns()- Remove constant columnsfill()/fill_direction()- Fill missing values (forward/backward)fill_empty()- Fill empty stringsflag_nulls()- Flag null valueslimit_column_characters()- Truncate column namesmin_max_scale()- Scale to [0,1]groupby_agg()- Group and aggregategroupby_topk()- Top k per groupcase_when()- Conditional logiccurrency_column_to_numeric()- Parse currencyconvert_date()/convert_to_date()/convert_to_datetime()- Convert to date/datetimeconvert_unix_date(),convert_excel_date(),convert_matlab_date()- Numeric date conversionstruncate_datetime()/truncate_datetime_dataframe()- Truncate datetimepivot_wider()/pivot_longer()- Reshape wide/long
Hybrid verbs (cleaning_ops_final.py)
conditional_join()- Join with conditionget_dupes()- Find duplicate rowsdropnotnull()- Drop non-null valuesexpand_column()- Expand delimited columnimpute()- Impute missing valuesjitter()- Add noise to valueslabel_encode()- Encode as integersfind_replace()- Replace valuescount_cumulative_unique()- Count unique valuescomplete()- Complete missing combinationsalso()- Apply multiple operationsalias()- Create column aliasesmutate()/assign()/ungroup()- Add/modify columns, tidyverse-style verbsdrop_duplicate_columns()- Remove duplicate columnscompare_df_cols()/compare_df_cols_same()- Compare column contents/shapejoin_apply()- Apply function to joined dataprocess_text()- Text processing
pyjanitor parity methods (v0.2.0)
These were added in v0.2.0 to reach 100% coverage of pyjanitor's documented API. Every function in this table has a pyjanitor counterpart — the second column shows how the pyduck-janitor method relates to it (same-name implementation or alias of another pyduck verb). Functions with no pyjanitor counterpart are listed separately under DuckDB-only extensions — none of them appear in this table.
| pyjanitor function | pyduck-janitor | Notes |
|---|---|---|
rename_columns |
alias of rename_column |
plural form |
truncate_datetime_dataframe |
alias of truncate_datetime |
|
convert_to_date / convert_to_datetime |
aliases of convert_date |
|
convert_unix_date |
same-name port of pyjanitor convert_unix_date |
TO_TIMESTAMP, seconds/millis/micros |
convert_excel_date |
same-name port of pyjanitor convert_excel_date |
Excel serial dates |
convert_matlab_date |
same-name port of pyjanitor convert_matlab_date |
MATLAB datenums |
excel_time_to_numeric |
same-name port of pyjanitor excel_time_to_numeric |
Excel time fraction → seconds |
sas_numeric_to_date |
same-name port of pyjanitor sas_numeric_to_date |
SAS origin 1960-01-01 |
to_datetime |
same-name port of pyjanitor to_datetime |
DuckDB strptime cast |
fill_direction |
alias of fill |
|
filter_column_isin |
same-name port of pyjanitor filter_column_isin |
quoted-column IS IN filter |
filter_date |
same-name port of pyjanitor filter_date |
start/end date range filter |
add_columns |
same-name port of pyjanitor add_columns |
dict of {name: values} |
assign / ungroup |
aliases of mutate / no-op |
tidyverse naming |
get_columns / get_index_labels |
same-name ports of the pyjanitor select helpers | column introspection |
move / reorder_columns |
same-name ports of pyjanitor move / reorder_columns |
column placement verbs |
row_to_names |
same-name port of pyjanitor row_to_names |
promote a row to headers |
rle_id |
same-name port of pyjanitor rle_id |
run-length ids via hash + window |
factorize_columns |
same-name port of pyjanitor factorize_columns |
DENSE_RANK integer encoding |
sort_naturally |
same-name port of pyjanitor sort_naturally |
human (non-lexicographic) sort |
sort_column_value_order |
same-name port of pyjanitor sort_column_value_order |
explicit value ordering |
update_where |
same-name port of pyjanitor update_where |
conditional column update |
unionize_dataframe_categories |
same-name port of pyjanitor unionize_dataframe_categories |
cross-relation VARCHAR alignment |
shuffle |
same-name port of pyjanitor shuffle |
ORDER BY random() |
toset |
same-name port of pyjanitor toset |
distinct values as a list |
take_first |
same-name port of pyjanitor take_first |
first N rows |
round_to_fraction |
same-name port of pyjanitor round_to_fraction |
snap to 1/denominator |
scale_mad |
same-name port of pyjanitor scale_mad |
median-abs-deviation scaling |
cartesian_product |
same-name port of pyjanitor cartesian_product |
cross join helper |
then |
same-name port of pyjanitor then |
chain callables |
expand / expand_grid |
same-name ports of the pyjanitor expand family | distinct expansion / cross join grid |
change_index_dtype |
same-name port of pyjanitor change_index_dtype |
typed projection of a column |
collapse_levels |
same-name port of pyjanitor collapse_levels |
concat-join helper |
explode_index |
same-name port of pyjanitor explode_index |
regex-extract a parsed column |
summarise |
same-name port of pyjanitor summarise |
group-by aggregation helper |
pivot_longer_spec / pivot_wider_spec |
same-name ports of the pyjanitor _spec pivots |
UNPIVOT / PIVOT spec forms |
join_agg / get_join_indices |
same-name ports of the pyjanitor conditional-join helpers | aggregated/non-equi join |
select |
pyjanitor select folded into select_columns + select() alias |
comma-strings, globs, re: regex, DropLabel |
DropLabel |
same-name port of pyjanitor DropLabel |
select-DSL exclusion sentinel |
patterns |
same-name port of pyjanitor patterns |
regex helper with .compiled |
describe_class |
same-name port of pyjanitor describe_class |
column-type table (DESCRIBE-backed) |
DuckDB-only extensions (new — no pyjanitor equivalent)
These are the only truly new methods (no pyjanitor counterpart) — they exist because the backend is a live DuckDB connection rather than a pandas DataFrame:
from_pandas(),from_csv(),from_excel(),from_json(),from_parquet(),from_database(),from_sql()- Data source loadersasof_join()- Point-in-time temporal joins with backward, forward, nearest, key, and tolerance matchingwindow_mutate()- Composable partitioned, ordered, framed window expressionsrecursive_cte()- Recursive hierarchy, path, and reachability queries rooted in the current relationvalidate_keys()- Validate nulls, duplicates, and optional date bounds before temporal or graph operationsdeduplicate()- Deterministically keep one row per keyfilter_noise()- Remove configured IDs, regex matches, and low-frequency entitieshierarchy_edges()- Normalize and validate entity/parent tables into directed edgestime_slice()/event_window()- Reusable temporal filterschange_detection()- Detect field-level changes within ordered entity historiesnetwork_evolution()- Run graph metrics across time periods and calculate metric deltasmetrics()- Calculate named aggregates in one database-native queryprofile()- Profile every column with null, distinct, range, and type statisticsmetric_cube()- Calculate detail rows, rollups, cubes, and exact grouping setsrate_metrics()- Calculate safe numerator/denominator ratescohort_metrics()- Calculate cohort size, activity, and retentionfreshness()- Report row count, latest timestamp, age, and stalenessreconcile()- Compare key coverage between two relationsmetric_from_database()- Aggregate at the source database before transfersql()- Escape hatch: raw SQL against the current relation (useselfas the table name)explain()- EXPLAIN plan for the current pipelinecollect()/head()- Materialize to pandas / preview rowsget_shared_connection()- access to the underlying DuckDB connection
Optional Onager graph analytics
Onager is an optional DuckDB graph-algorithm extension. It adds centrality,
community detection, connected components, and path algorithms without making
graph binaries part of the base installation. The graph adapter targets
DuckDB 1.5.x because native extension binaries are version-specific.
edges = DuckJanitor.from_pandas(pd.DataFrame({
"employee_id": [1, 2, 3],
"manager_id": [2, 3, 1],
"weight": [1.0, 2.0, 1.0],
}))
# Set auto_install=True only when this process may download extensions.
metrics = edges.graph_analyze(
source="employee_id",
target="manager_id",
algorithms=["pagerank", "louvain", "components"],
weight="weight",
auto_install=True,
)
pagerank = metrics["pagerank"].collect()
For air-gapped or managed environments, install Onager separately and use
auto_install=False. The lower-level graph_algorithm() method accepts any
Onager table-function name, so new Onager algorithms can be used before a
convenience wrapper is added:
edges.load_extension("onager", auto_install=False)
result = edges.graph_algorithm(
"onager_pth_dijkstra",
source="employee_id",
target="manager_id",
parameters={"source": 1},
)
Onager is intentionally not imported at package import time and is not a hard dependency. DuckGQL remains a separate optional graph-querying adapter; Onager is for graph algorithms and network metrics.
Optional snapshot diffs with duck_diff
Install the diff extra when you need keyed, row-level, and column-level comparisons between snapshots:
pip install "pyduck-janitor[diff]"
changes = current.diff(
prior,
keys=["employee_id"],
columns=["department", "manager_id", "job_level", "salary"],
numeric_tolerance=0.01,
auto_install=True,
)
changed = changes.filter_on("diff_status = 'different'").collect()
summary = current.diff_summary(prior, keys=["employee_id"], auto_install=True).collect()
schema = current.schema_diff(prior, auto_install=True).collect()
diff() returns row status, JSON change details, and typed left/right values;
diff_summary() returns counts and percentages; schema_diff() compares
column names and types. The adapter uses INSTALL duck_diff FROM community
only when auto_install=True, and targets DuckDB 1.5.x because native
extension binaries are version-specific.
Plus the pandas-flavored bases that pyjanitor implements differently and pyduck implements natively (comparable intent, DuckDB-native implementation — documented in the module tables above): dropna, fill, filter_column, convert_date, truncate_datetime, get_dummies.
Supported Data Sources
pyduck-janitor can work with data from:
- In-memory pandas DataFrames - Via
from_pandas() - Parquet files - Local or remote (S3, HTTP)
- CSV files - Local or remote
- JSON files - Local or remote
- DuckDB databases - Existing
.duckdbfiles - SQL queries - Custom SQL as input
- External SQL databases - Any open DB-API 2.0 connection, including Vertica and Microsoft SQL Server
Point-in-time joins
Use asof_join() to attach the latest historical record at or before an
event, the next record after an event, or the closest record in time. It is
designed for effective-dated employee attributes, manager history,
compensation snapshots, market data, and point-in-time feature construction.
matched = events.asof_join(
manager_history,
left_on="event_time",
right_on="effective_time",
by=["employee_id", "department"],
direction="backward",
tolerance="90 days",
)
asof_join() preserves every left row, supports backward, forward, and
nearest matching, applies optional equality keys and tolerances, and keeps
duplicate timestamp selection deterministic.
Windows and recursive graph queries
Use window_mutate() for reusable analytical windows without writing the
surrounding registration and projection SQL yourself:
scored = events.window_mutate(
{
"previous_score": "LAG(score)",
"rolling_score": "AVG(score)",
"event_rank": "ROW_NUMBER()",
},
partition_by="employee_id",
order_by="event_time",
frame="ROWS BETWEEN 3 PRECEDING AND CURRENT ROW",
)
Use recursive_cte() for manager trees, ancestor/descendant paths, and
reachability. The current relation is available as self, and the recursive
query references the CTE by its supplied name:
tree = org.recursive_cte(
"org_tree",
"SELECT employee_id, manager_id, 0 AS depth "
"FROM self WHERE manager_id IS NULL",
"SELECT e.employee_id, e.manager_id, t.depth + 1 "
"FROM self e JOIN org_tree t ON e.manager_id = t.employee_id",
)
Together with asof_join(), these primitives support point-in-time
organizational hierarchy analysis while keeping the computation in DuckDB.
Database-native metrics
For database extracts, use metrics() for named aggregates and profile()
for a compact column-quality overview:
summary = employees.metrics(
{
"headcount": ("employee_id", "count_distinct"),
"average_salary": ("salary", "mean"),
"total_salary": ("salary", "sum"),
},
group_by=["department", "year"],
)
profile = employees.profile()
metric_cube() supports detail rows, hierarchical subtotals, all-dimension
cubes, and exact grouping sets. It adds grouping_level, grouping_id,
is_total, and is_grand_total when totals are enabled:
cube = employees.metric_cube(
["year", "department", "region"],
{"total_salary": "SUM(salary)", "headcount": "COUNT(DISTINCT employee_id)"},
totals="rollup",
grand_total=True,
total_label="ALL",
)
Use rate_metrics() for safe ratios, cohort_metrics() for retention-style
analysis, freshness() for source monitoring, and reconcile() for key
coverage checks between snapshots. For very large external sources,
metric_from_database() generates the aggregate query around the source SQL
so the source database performs the aggregation before pandas transfers it.
Generic ONA and temporal analysis
The ONA-oriented helpers are deliberately generic: they operate on entity, parent, edge, and event tables without assuming an HR schema. Validate and shape data before building a graph, then detect changes or run graph metrics over time:
events = DuckJanitor.from_pandas(history)
events.validate_keys("employee_id", date_col="event_date", unique=False)
events = events.deduplicate("employee_id", order_by="event_date", keep="last")
edges = events.hierarchy_edges("employee_id", "manager_id")
changes = events.change_detection(
"employee_id", "event_date", columns=["manager_id", "department"]
)
evolution = edges.network_evolution(
date_col="event_date",
source="source",
target="target",
algorithms=["pagerank", "components"],
auto_install=True,
)
network_evolution() returns one DuckJanitor relation per algorithm with a
period column and, when the graph result exposes node metrics, a
metric_delta column. It uses the optional Onager adapter and keeps extension
installation opt-in.
External database connections
from_database() accepts an already-open database connection and executes a
source-database query before bringing the result into DuckDB. Install the
driver for the system you use; drivers are intentionally not required by the
base package.
# Vertica: pip install vertica-python
import vertica_python
with vertica_python.connect(host="vertica.example.com", database="analytics",
user="analyst", password="...") as connection:
dj = DuckJanitor.from_database(
connection,
"SELECT employee_id, department, salary FROM employees "
"WHERE snapshot_date >= %s",
["2026-01-01"],
)
result = dj.clean_names().collect()
# Microsoft SQL Server: pip install pyodbc
import pyodbc
connection = pyodbc.connect("DRIVER={ODBC Driver 18 for SQL Server};SERVER=...;")
dj = DuckJanitor.from_database(
connection,
"SELECT employee_id, department, salary FROM dbo.employees WHERE department = ?",
["Finance"],
)
result = dj.collect()
The query runs in the source database using its SQL dialect and parameter
style. Returned rows are materialized into DuckDB, so existing DuckJanitor
transformations can be chained afterward. from_database() does not close
the connection; the caller owns its lifecycle.
Text & Similarity (v0.2.0)
Beyond structural cleaning, pyduck-janitor wraps three lazy-loaded DuckDB
extensions — icu, fts, and vss — for messy-text work at scale:
| Verb | Backend | What it does |
|---|---|---|
text_normalize() |
icu | Lowercase, accent strip, whitespace collapse |
search_text() |
fts | BM25-ranked full-text search |
keyword_filter() |
fts | Boolean contains (any/all phrases) |
build_fts_index() / drop_fts_index() |
fts | Index lifecycle |
embed_column() |
vss + sentence-transformers | Embed a text column |
build_vector_index() / vector_search() |
vss | HNSW kNN search |
fuzzy_dedupe() |
vss | Near-duplicate detection |
embed_install() / embed_list_installed() / embed_remove() |
— | Model cache management |
import pandas as pd
from pyduck_janitor import DuckJanitor, build_fts_index, search_text, text_normalize
df = pd.DataFrame({"text": ["The quick brown fox", "lazy dogs", "brown foxes"]})
dj = DuckJanitor.from_pandas(df)
dj = build_fts_index(dj, "text")
results = search_text(dj, "text", "fox quick", top_k=3)
# __pyduck_rowid text score
# 0 3 brown foxes 0.34
# 1 1 The quick brown fox 0.28
Embedding models are never downloaded silently — embed_install() is the
explicit opt-in, and supports the bundled companion wheel (offline), any
HuggingFace model (hf:org/model), or a local path. Full guide:
docs/text_ops.md.
Adding a HuggingFace model
Any sentence-transformers-compatible model from HuggingFace Hub works.
Pull it into the local cache by passing an hf:-prefixed identifier:
import pyduck_janitor as pj
# Public model — direct fetch
pj.embed_install("hf:sentence-transformers/all-MiniLM-L6-v2")
pj.embed_install("hf:BAAI/bge-small-en-v1.5") # better quality, 33M params
pj.embed_install("hf:BAAI/bge-base-en-v1.5") # bigger, ~110M params
pj.embed_install("hf:intfloat/multilingual-e5-small") # multilingual
# Pin a specific revision (sha or tag) for reproducibility
pj.embed_install("hf:org/model@sha256:abc123...")
pj.embed_install("hf:org/model@refs/pr/42")
# Gated / private models — set HF_TOKEN in your env, then call as usual
# export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxx
pj.embed_install("hf:meta-llama/Llama-Embed-8B")
# Or a local directory you've prepared (no HuggingFace needed)
pj.embed_install("/opt/models/my-finetuned-encoder")
Once installed, the model is available everywhere in your workflow:
dj = DuckJanitor.from_pandas(df)
dj = embed_column(dj, "text", model="hf:BAAI/bge-small-en-v1.5")
dj = build_vector_index(dj, metric="cosine")
hits = vector_search(dj, "lazy dog", model="hf:BAAI/bge-small-en-v1.5", top_k=5)
Manage the cache over time:
pj.embed_list_installed()
# model size_human has_config has_weights
# 0 sentence-transformers/all-MiniLM-L6-v2 90.2 MB True True
# 1 BAAI/bge-small-en-v1.5 33.4 MB True True
pj.embed_remove("hf:BAAI/bge-small-en-v1.5")
Override the cache location with PYDUCK_EMBED_CACHE=/some/path when you
need shared storage across venvs, CI runners, or read-only filesystems.
Key Features
Lazy Evaluation
Operations build a query plan without immediate execution. Use .collect() to execute:
result = df.clean_names().remove_empty().dropna().collect()
Out-of-Core Processing
Work with datasets larger than RAM:
df = DuckJanitor.from_parquet('large_dataset.parquet')
result = df.clean_names().remove_empty().collect()
Method Chaining
All methods return DuckJanitor objects, enabling fluent pipelines:
result = (
df
.clean_names()
.filter_on('age > 18')
.groupby_agg('gender', {'income': 'mean'})
.collect()
)
SQL Interoperability
Mix janitor methods with custom SQL:
result = df.sql('SELECT * FROM self WHERE age > 18').collect()
SQL Fragment Safety (0.1.3)
For methods that accept SQL fragments (for example filter_on, filter_column, select_rows(criteria=...), transform_column(func=...), case_when, and change_type), pyduck-janitor now rejects:
- Multi-statement fragments containing
; - SQL comments (
--,/* ... */) - Destructive DDL/DML keywords (
DROP,DELETE,UPDATE,INSERT, etc.)
This keeps expression-based APIs usable while reducing accidental or unsafe query fragments.
API Comparison
Traditional pandas + pyjanitor
import pandas as pd
import janitor
df = pd.read_csv('large_file.csv')
df = (
df
.clean_names()
.remove_empty()
.dropna(subset=['col1', 'col2'])
)
pyduck-janitor (faster, scalable)
from pyduck_janitor import DuckJanitor
df = DuckJanitor.from_csv('large_file.csv')
df = (
df
.clean_names()
.remove_empty()
.dropna(subset=['col1', 'col2'])
)
result = df.collect() # Explicit execution
Examples
See the examples/ directory for complete workflows:
examples/basic_cleaning.py- Basic data cleaning pipelineexamples/large_dataset.py- Out-of-core processing with Parquetexamples/sql_interop.py- Mixing janitor methods with SQLexamples/comparison.py- Performance comparison with pandas + pyjanitor
Architecture
pyduck-janitor works by:
- Wrapping DuckDB relations - Data is stored in DuckDB tables
- Translating janitor methods - Each method converts to DuckDB SQL
- Lazy evaluation - Operations build a query plan
- Optimized execution - DuckDB executes the entire pipeline efficiently
- Pandas compatibility - Results can be converted to pandas DataFrames
Hybrid Pattern
For operations that can't be pure SQL:
- Materialize - Convert DuckDB relation to pandas DataFrame
- Apply - Execute Python function
- Re-wrap - Create new DuckJanitor instance
Performance
pyduck-janitor provides significant speedups for:
- Large datasets (>1M rows)
- Complex cleaning pipelines
- Operations on disk-based data
- Column-wise transformations
Benchmark results vary by workload, but expect 2-10x speedups on typical data cleaning tasks.
Contributing
We welcome contributions! Please see our Contributing Guide for details.
Changelog
0.2.0 — 100% pyjanitor API parity
- Full pyjanitor surface: pyduck-janitor now covers 94/94 (100%) of the functions documented on the pyjanitor API reference, verified by a fresh scan of pyjanitor's live docs.
- ~35 newly added chainable methods on
DuckJanitor— all ports of pyjanitor functions (same names except where noted as aliases), not pyduck-only inventions; the pyduck-only extensions are listed separately:- Date conversions:
convert_unix_date,convert_excel_date,convert_matlab_date,excel_time_to_numeric,sas_numeric_to_date,to_datetime(all with float-safe numeric parsing viaTO_TIMESTAMP), plusconvert_to_date/convert_to_datetimealiases. - Structural verbs:
move,reorder_columns,get_columns,get_index_labels,row_to_names,collapse_levels,explode_index,change_index_dtype. - Reshape/aggregation:
expand,expand_grid,summarise,pivot_longer_spec(UNPIVOT),pivot_wider_spec(PIVOT with a literal value list),join_agg,get_join_indices. - Data quality / encoding:
rle_id,factorize_columns,update_where,unionize_dataframe_categories,scale_mad,round_to_fraction. - Row/column utilities:
shuffle,toset,take_first,sort_naturally,sort_column_value_order,filter_date,cartesian_product,then. - pyjanitor naming aliases:
rename_columns,truncate_datetime_dataframe,fill_direction,filter_column_isin,add_columns,assign,ungroup.
- Date conversions:
- Select DSL:
select_columnsnow accepts comma-separated strings ("a, b, c"), shell-globs ("value*"), and regex ("re:^v_"), matching pyjanitor'sselecthelper where it lives (underselect_columns). A thinselect()alias is exposed; non-column kwargs raiseNotImplementedError(pyjanitor itself deprecates them). - pyjanitor helper surface:
DropLabel(select-DSL exclusion sentinel, functional in mixed lists),patterns(regex helper), anddescribe_class()(DESCRIBE-backed column-type table). - Test suite: grew from 193 to 284 passing tests
(+91 alias/DSL/parity tests in
tests/test_pyjanitor_aliases.py). - Docs: fixed stale example references and expanded the supported
functions section; parity analysis in
REVIEW_PYJANITOR_PARITY.md.
0.1.3 — Validation hardening, audit documentation, and test expansion
- Added a full package audit report in
CODE_REVIEW.md. - Added stronger validation for SQL-fragment inputs and missing-column errors across cleaning modules.
- Added expanded edge-case and error-path tests in
tests/test_validation_and_edges.py. - Improved
join_applycross-connection handling andDuckJanitor.sql()identifier replacement behavior. - Bumped package version to
0.1.3.
0.1.2 — Production-ready stabilization, pure SQL rewrites, and test expansion
- Pure SQL Rewrites: Rewrote
alias,complete, anddrop_duplicate_columnsin 100% pure, out-of-core SQL to avoid in-memory materialization to Pandas. - API Expositions: Exposed previously hidden hybrid and final functions (
drop_duplicate_columns,compare_df_cols,join_apply,process_text, andget_dupes) directly as wrapper methods onDuckJanitor. - Bug Fixes:
- Fixed syntax parser errors in
fillby introducing physical row number index CTEs instead of nesting window functions. - Added safe string literal quoting fallback to
add_columnandfilter_columnwhen passing raw string scalars. - Resolved name-collision bugs in
clean_namesandcoalesce. - Implemented group-by partitioning support in
imputeusing SQL window functions. - Ensured operations like
fill_empty,currency_column_to_numeric, andconvert_dategracefully return NULLs instead of crashing.
- Fixed syntax parser errors in
- Metadata Update: Updated package version to
0.1.2and author information. - Unit Test Suite: Added 54 new test cases covering all edge cases, raising code coverage from 44% to 93% (with 100% coverage on
duck_janitor.py). - Logo Sticker: Added a custom package logo sticker of a duck dressed as a janitor.
0.1.1 — Connection handling and crash fixes
- Fixed cross-connection crashes across
cleaning_ops.py,cleaning_ops_extended.py, andcleaning_ops_final.pyby registering relations on the caller's DuckDB connection instead of creating new in-memory connections or relying onFROM relationreplacement scans. DuckJanitor.__init__now validates that the relation and connection belong to the same DuckDB connection.from_parquet,from_csv, andfrom_sqlnow return real DuckDB relations without round-tripping through pandas.- Fixed
remove_emptyto actually remove all-empty rows (in addition to all-empty columns). - Fixed
dropna(how='all')boolean condition. - Fixed
case_when,currency_column_to_numeric,convert_daterelation.databaseAttributeError crashes. - Fixed
impute()SELECT , COALESCE(...)syntax error. - Fixed
conditional_jointo use a single shared connection with an operator allow-list. - Replaced invalid
ROW() OVER ()inselect_rowswithROW_NUMBER() OVER (). - Added safer handling for identical-value columns in
min_max_scale. - Added 10 regression tests. Full suite: 40 passing.
0.1.0 — Initial release
- DuckDB-backed pyjanitor-style cleaning API with 51 functions.
- Lazy SQL evaluation for simple operations; hybrid SQL/Python for complex operations.
For a complete release history, see CHANGELOG.md.
License
MIT License - see LICENSE for details.
Acknowledgments
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyduck_janitor-0.2.3.tar.gz.
File metadata
- Download URL: pyduck_janitor-0.2.3.tar.gz
- Upload date:
- Size: 137.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
911e1f6674323212ff0051cd34b5a41453ff87e910173851a40baf22b9a1007b
|
|
| MD5 |
0c9f6c66222f7d33dca7b5befb41c7de
|
|
| BLAKE2b-256 |
cb3a523b18140da8dbe75acd95fe827a585ad64fcfb22b8b81a7ee983515640f
|
Provenance
The following attestation bundles were made for pyduck_janitor-0.2.3.tar.gz:
Publisher:
ci.yml on ezraair555/pyduck-janitor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyduck_janitor-0.2.3.tar.gz -
Subject digest:
911e1f6674323212ff0051cd34b5a41453ff87e910173851a40baf22b9a1007b - Sigstore transparency entry: 2719957702
- Sigstore integration time:
-
Permalink:
ezraair555/pyduck-janitor@46fa4abd2dd281a32b25a30d1b1826c2bf1ad06c -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/ezraair555
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@46fa4abd2dd281a32b25a30d1b1826c2bf1ad06c -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyduck_janitor-0.2.3-py3-none-any.whl.
File metadata
- Download URL: pyduck_janitor-0.2.3-py3-none-any.whl
- Upload date:
- Size: 90.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
45e3706e96828de9cbfdb32c6e2c4ef6ca27e5e82da2f449c5e6123af1794462
|
|
| MD5 |
ce4d23d8c4da4e0b826040762e284994
|
|
| BLAKE2b-256 |
64f5afea79e0fbf7fbef5ea7808db59cfc3b4cc80c830db42897cb14a21cde86
|
Provenance
The following attestation bundles were made for pyduck_janitor-0.2.3-py3-none-any.whl:
Publisher:
ci.yml on ezraair555/pyduck-janitor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyduck_janitor-0.2.3-py3-none-any.whl -
Subject digest:
45e3706e96828de9cbfdb32c6e2c4ef6ca27e5e82da2f449c5e6123af1794462 - Sigstore transparency entry: 2719957819
- Sigstore integration time:
-
Permalink:
ezraair555/pyduck-janitor@46fa4abd2dd281a32b25a30d1b1826c2bf1ad06c -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/ezraair555
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@46fa4abd2dd281a32b25a30d1b1826c2bf1ad06c -
Trigger Event:
push
-
Statement type: