Skip to main content

untanglepy

Transform irregular Pandas tables with explicit rules and positional lineage. Move embedded subheaders into columns, combine header rows, join split records, and propagate known groups without silently discarding conflicting values.

import pandas as pd
from untanglepy import untangle

raw = pd.DataFrame(
    {"item": ["REGION: NORTH", "Tea", "REGION: SOUTH", "Cocoa"], "value": [None, 10, None, 20]}
)
result = untangle(raw, group_column="item", group_pattern=r"^REGION:", group_name="region")
print(result.frame)  # NORTH / Tea / 10; SOUTH / Cocoa / 20
assert result.source_rows == ((0, 1), (2, 3))

Install

Python 3.11+ and Pandas 2.2+ are required. Until the initial PyPI publication is verified, install from a checkout:

python -m pip install .
# After the first verified PyPI release:
python -m pip install untanglepy

The package has no runtime network calls, telemetry or dependency on sibling projects.

Composable API

All functions accept a DataFrame or an earlier TransformResult, and return TransformResult. Pass the wrapper through a pipeline to preserve lineage; passing only .frame starts a new positional source. Input frames are never changed. Results use a fresh RangeIndex; original index labels can be arbitrary or duplicated. Keep the original input if you need to look up source values.

Function Explicit rule
extract_subheaders(data, column=..., pattern=..., group_name=...) Regex-match context rows; move their group into a new column
untangle(data, group_column=..., group_pattern=..., group_name=...) Convenience form of subheader extraction
merge_headers(data, rows=[0, 1], separator="_", fill_horizontal=False) Combine selected current row positions and remove those rows
join_broken_rows(data, continuation=[False, True, ...], text_columns=[...], separator=" ") Attach explicitly marked rows to the previous anchor
propagate_groups(data, columns=[...]) Carry selected group fields downward into structural blanks

result.frame contains the output table. result.source_rows contains the original zero-based input row positions contributing to each output row, including context subheaders and group donors. result.header_sources records selected original heading-row positions for each output column, separately from record lineage. result.lineage() returns a table of output positions and source tuples. Source tuples are unique and sorted in original source order. Treat the exposed frame and provenance as snapshots; external frame mutation can invalidate their correspondence.

Rules and ambiguity

Structural blanks are scalar missing values or empty/whitespace-only strings. Zero and False are values. Subheaders match from the start of a string using Python re.match. A pattern with one capture extracts exactly that capture. Without a capture, the matched prefix is removed and the remainder stripped. More than one capture or an empty extracted group raises StructureError. Rows matching a subheader must have blank other fields, preventing accidental payload loss. Group column collisions also raise.

extract_subheaders drops fully blank rows by default; drop_empty=False retains them with lineage. Data preceding the first subheader gets group None; require_group=True rejects it. Matching context rows are removed intentionally. propagate_groups preserves leading blanks and all nonblank original values.

merge_headers accepts current positional rows in the order supplied. It omits blank pieces; horizontal propagation across each heading row requires explicit fill_horizontal=True. Every output column needs a nonblank heading. Generated duplicate names raise unless duplicate_names="suffix" is chosen. Suffixes are globally unique and reserve natural base names: A, A, A_2 becomes A, A_3, A_2. Input column labels must already be unique and flat; duplicate or MultiIndex labels raise with instructions to rename them explicitly.

join_broken_rows requires one actual boolean per current row. A leading continuation is rejected. Only the named text columns concatenate two occupied strings, with the exact separator supplied. Elsewhere, a value can fill a blank or repeat an equal scalar; incompatible occupied values raise before returning a result. Outputs use object dtype for joined/propagated columns to avoid rounding large integers or coercing original objects when a neighbor is missing. No original value is converted to text except explicitly merged heading pieces and selected string concatenation.

Five worked irregular tables

examples/irregular_tables.py contains executable input tables, transformation pipelines and expected outputs:

Example Problems Pipeline and result
Regional catalog Embedded region rows, blank separators, split product text Extract region, then join continuation: Green tea retains source rows (0,1,2)
Quarterly sales Two-level horizontal headings, absent repeated countries Merge heading rows with span fill, then propagate country; headings become Sales_Q1, Sales_Q2
Department register Header row mixed with rows, embedded department metadata Promote selected header, then capture department from explicit bracket pattern
PDF invoice Split descriptions and amount on second row Propagate invoice ID, then join explicit continuations; numeric conflicts are rejected
Survey export Repeated heading text and blank subgroup cells Merge with explicit suffix policy, then propagate group
python examples/quickstart.py
python examples/irregular_tables.py

Existing tools and limitations

Pandas and pyjanitor already supply header promotion, filling, reshaping and cleaning primitives. untanglepy adds composable context provenance and explicit occupied-value conflict handling for irregular tables. It is inspired by unheadr, whose broader functionality includes spreadsheet annotation. See RESEARCH.md for the capability comparison and licenses.

Version 0.1.0 accepts in-memory Pandas tables. CSV, Excel and PDF readers remain the caller's responsibility. There is no automatic layout detection, OCR, rich cell formatting support or Polars backend. Rules assume the caller knows which rows are headers or continuations; sparse data is not guessed into a structure. Lineage records row contribution, not a complete cell-level edit history. Blank rows intentionally dropped and unused trailing subheaders do not create output records. Dtypes can become object while original scalar values are retained.

Validation and benchmark

python -m pip install -e ".[dev]"
python -m pytest --cov=untanglepy --cov-report=term-missing
python -m ruff check .
python -m mypy src
python benchmarks/benchmark.py --rows 20000 --repeat 5

Tests include five worked tables, duplicate indices, compound provenance, extension missing values, large integers, ambiguous conflicts, and generated order/value invariants. The benchmark checks output equivalence to an explicit Pandas subheader recipe, then measures five repetitions after warmup and a separate traced Python memory peak. Raw measurements contain environment and dispersion; methodology explains the additional validation/lineage cost. No performance advantage is assumed.

See CONTRIBUTING.md for development and release checks. MIT licensed original code. Proposed 0.2.0 work: explicit per-column conflict resolvers, per-cell lineage and bounded provenance for very large imports.

Metadata

Release files for untanglepy 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for untanglepy 0.1.0
File Size Uploaded Provenance
untanglepy-0.1.0.tar.gz 21.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for untanglepy 0.1.0
File Interpreter ABI Platform
untanglepy-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 31.4 kB

Release files / untanglepy-0.1.0.tar.gz

Download URL untanglepy-0.1.0.tar.gz
Size 21.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1dbd5292d94956b2aae0e486b0180158c5c7fba201da779333cf3106777890df
BLAKE2b-256 checksum
How to use checksums
cffee0592c020f45fbf415841bdc3a36a7475068091686ede016691e02cd95b8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / untanglepy-0.1.0-py3-none-any.whl

Download URL untanglepy-0.1.0-py3-none-any.whl
Size 9.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8db5d0bcddde17c99216dc65f79f21abe1892496657746ad17e21be19b34278b
BLAKE2b-256 checksum
How to use checksums
19e8e0cc43d9592f95d15d0f3c7a5e8ee398b94c10d7b8cf6e167d85b6425734
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for untanglepy 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for untanglepy-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

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