Skip to content

fix(structured): anonymize columns whose name is not an identifier#2142

Open
uwezkhan wants to merge 3 commits into
data-privacy-stack:mainfrom
uwezkhan:structured-nonidentifier-column
Open

fix(structured): anonymize columns whose name is not an identifier#2142
uwezkhan wants to merge 3 commits into
data-privacy-stack:mainfrom
uwezkhan:structured-nonidentifier-column

Conversation

@uwezkhan

@uwezkhan uwezkhan commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Change Description

PandasDataProcessor._process reads each cell with getattr(row, key) while iterating DataFrame.itertuples(). itertuples renames any column whose name is not a valid Python identifier (a space, a hyphen, a leading digit, a keyword) into a positional field like _1, so the lookup raises AttributeError for the very columns that usually hold PII, such as "Full Name" or "e-mail". The mapping comes straight from df.columns via the analysis builder, so StructuredEngine.anonymize hits this on ordinary input.

Before: only columns named as Python identifiers were anonymized; a "Full Name" column aborted the run, and because the frame is mutated column by column, any columns processed earlier were redacted while the failing PII column was left untouched. After: the cell is read by label with data.at[index, key], the same way the result is already written back, so read and write agree and the column name no longer matters. The tradeoff is a per-cell .at lookup in place of a namedtuple field access, negligible next to the operator call already run on every cell.

Issue reference

No linked issue.

Checklist

  • I have reviewed the contribution guidelines
  • I agree to follow this project's Code of Conduct
  • I confirm that I have the right to submit this contribution and that it does not knowingly contain proprietary or confidential code.
  • My code includes unit tests
  • All unit tests and lint checks pass locally
  • My PR contains documentation updates / additions if required

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes structured anonymization for pandas DataFrames when column names are not valid Python identifiers (e.g., contain spaces or hyphens), ensuring those columns are correctly read and anonymized instead of failing or being skipped due to itertuples() field renaming.

Changes:

  • Update PandasDataProcessor._process to read/write cells by DataFrame label (data.at[index, key]) rather than getattr() on itertuples() rows.
  • Add a unit test covering anonymization of a column with a non-identifier name (e.g., "Full Name").

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
presidio-structured/presidio_structured/data/data_processors.py Switch per-cell access from itertuples()+getattr to label-based .at to support any column names.
presidio-structured/tests/data/test_data_transformers.py Add regression test validating anonymization for non-identifier column names.

Comment on lines +29 to +32
# Column names holding PII are often not valid Python identifiers
# ("Full Name", "e-mail", ...). itertuples renames those to positional
# fields, so a name-based getattr lookup skips them and the PII is left
# in place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded. The old path raised AttributeError and aborted mid-run, it didn't silently skip, so the comment now says that.

@SharonHart

SharonHart commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@uwezkhan
Thanks for fixing the non-identifier column case here.

I think we can preserve the correctness fix while avoiding a performance regression by iterating the column Series directly instead of doing a scalar .at read for every cell:

for key, operator_callable in key_to_operator_mapping.items():
    self.logger.debug(f"Operating on column {key}")
    for index, text_to_operate_on in data[key].items():
        operated_text = self._operate_on_text(text_to_operate_on, operator_callable)
        data.at[index, key] = operated_text

I tried this locally against this PR branch. The existing structured data tests still pass, including the new "Full Name" non-identifier column test.

On a synthetic no-op operator benchmark with identifier columns, this avoided the slowdown from the current implementation:

DataFrame legacy itertuples current PR .at read Series.items()
1,000 x 4 0.024383s 0.046735s 0.020948s
10,000 x 4 0.237030s 0.476665s 0.209146s
50,000 x 4 1.191399s 2.382683s 1.042770s

@uwezkhan

Copy link
Copy Markdown
Contributor Author

Makes sense, done. Switched to iterating data[key].items() as you suggested; the read stays label-based so non-identifier columns still work, and the per-cell scalar .at lookup is gone (only the write keeps .at, same as before). Only difference from your snippet is the wrapped _operate_on_text call for line length. Also reworded the test comment to describe the actual failure mode (AttributeError abort, not a silent skip). Full presidio-structured suite passes, 29 tests including the "Full Name" regression test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants