Where: faircode/proxy.py's _labelize() (used by the proxy-hints chi-squared test) vs. faircode/profiler.py's _intersections(), which already special-cases this exact scenario.
The gap: _labelize() claims in its own docstring to be "the same value normalization the intersection crosstab uses (age → bands)" - but it isn't. For an age column containing free-text sentinels with no embedded digit (e.g. "unknown", "prefer not to say"), _age_to_numeric() returns None, then _age_band(None) also returns None, and pd.crosstab() silently drops every row carrying that value from the chi-squared test - exactly the bug profiler.py's _intersections() already had fixed for issue #524 (that fix's own commit message: "otherwise labelize() maps them to None and pd.crosstab silently drops those rows"). _labelize() in proxy.py never received the equivalent fix.
Repro:
import pandas as pd
from faircode.proxy import _labelize
df = pd.DataFrame({
'age': ['25','30','unknown','40','unknown','35','28','unknown','50','unknown',
'22','60','unknown','33','unknown','45','29','unknown','38','unknown'],
'gender': ['M','F']*10,
})
lab = _labelize(df, 'age', 'age')
ct = pd.crosstab(lab, df['gender'])
print(ct.to_numpy().sum(), 'of', len(df), 'rows survived')
# 14 of 20 - all 6 "unknown" rows silently vanished
Impact: the proxy-hints chi-squared test (faircode profile --proxy-hints) silently understates its sample size and can bias the reported p-value/Cramér's V for any dataset with digit-free age sentinels - with no warning that rows were dropped. Not currently triggered by any dataset shipped in this repo, but reachable by any real-world one.
Fix direction: either import/reuse profiler.py's categorical-age-sentinel handling directly, or port the same fix to _labelize().
Where:
faircode/proxy.py's_labelize()(used by the proxy-hints chi-squared test) vs.faircode/profiler.py's_intersections(), which already special-cases this exact scenario.The gap:
_labelize()claims in its own docstring to be "the same value normalization the intersection crosstab uses (age → bands)" - but it isn't. For an age column containing free-text sentinels with no embedded digit (e.g."unknown","prefer not to say"),_age_to_numeric()returnsNone, then_age_band(None)also returnsNone, andpd.crosstab()silently drops every row carrying that value from the chi-squared test - exactly the bugprofiler.py's_intersections()already had fixed for issue #524 (that fix's own commit message: "otherwise labelize() maps them to None and pd.crosstab silently drops those rows")._labelize()inproxy.pynever received the equivalent fix.Repro:
Impact: the proxy-hints chi-squared test (
faircode profile --proxy-hints) silently understates its sample size and can bias the reported p-value/Cramér's V for any dataset with digit-free age sentinels - with no warning that rows were dropped. Not currently triggered by any dataset shipped in this repo, but reachable by any real-world one.Fix direction: either import/reuse
profiler.py's categorical-age-sentinel handling directly, or port the same fix to_labelize().