Skip to content

CRMCleaner.transform silently corrupts complex amounts into wrong finite floats #129

Description

@slegarraga

Found while writing the coverage tests for #118 (this is the observation I flagged in that PR body). Reproducible on current main; filing as its own issue per the request in #126 for genuine external-user signal — this is a real friction point I hit, not manufactured.

What happens

A DataFrame whose amount column mixes regular values with one Python complex slips through every guard in CRMCleaner.transform and produces a plausible-looking but wrong finite number, instead of raising or NaN-ing:

import numpy as np
import pandas as pd
from philanthropy.preprocessing import CRMCleaner

X_fit = pd.DataFrame({
    "gift_date": ["2023-01-01", "2023-02-01"],
    "gift_amount": [5.0, 10.0],
})
t = CRMCleaner().fit(X_fit)

X_bad = pd.DataFrame({
    "gift_date": ["2023-03-01"],
    "gift_amount": pd.Series([np.complex128(3 + 4j)], dtype=object),
})
print(t.transform(X_bad))
# gift_date              2023-03-01 00:00:00
# gift_amount                          -34.0      <- silently corrupted

3+4j becomes -34.0. For context on how easy this is to hit: np.complex128 values show up in real CRM exports when a formula column round-trips through Excel/openpyxl.

Root cause (three guards miss it in sequence)

  1. The mixed str/complex frame raises DTypePromotionError in the first validate_data pass; the object-cast retry introduced for exactly this kind of input lets it through.
  2. np.iscomplexobj(X_arr) returns False because after the retry the array has object dtype — iscomplexobj inspects dtype, not elements.
  3. _coerce_currency_to_float takes the string path: str(3+4j) is "(3+4j)", which matches the parenthesised-negative rule (-\1), then character-stripping deletes the parens and j, leaving "-34"-34.0. Sign flipped, imaginary part dropped, digits concatenated.

The same frame passed directly to validate_data raises Complex data not supported cleanly at fit time — the corruption is specific to the transform-time retry path.

Suggested direction

In _coerce_currency_to_float's string path, reject parsed-out-of-nothing values that were originally complex before the regex cleanup, or check element-wise isinstance(v, complex) on object columns before casting to str. A regression test would assert either a raise or NaN for the complex cell — anything but a finite number. Happy to send the PR if you tell me which behavior you want (raise vs NaN); my instinct is NaN-plus-warning, consistent with "values that still don't parse become NaN".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions