Where: faircode/profiler.py's _age_to_numeric/_age_band (age dimension banding).
The gap: _age_band checks edges[i] <= num < edges[i+1] for AGE_BANDS = [0,18,30,45,60,75]; any number below 0 falls through every branch and returns the "75+" fallback instead of being treated as unbandable. Real datasets commonly use -1/-999 as an "unknown age" sentinel.
Repro:
>>> import pandas as pd
>>> from faircode import profile
>>> df = pd.DataFrame({'age': [25,30,45,22,60,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]})
>>> profile(df)['dimensions'][0]['groups'][0]
{'label': '75+', 'count': 10, 'share': 0.6666666666666666, ...}
10 of 15 rows (66.7%) are sentinel values fabricated into a "75+" majority; missing_pct stays 0.0 and no flag mentions the sentinel at all.
There's also a related inconsistency in the same function: a string "-5" (the numeric-extraction regex \d+ strips the sign) bands as "0-18", while a numeric -5.0 bands as "75+" - the same logical value produces two different wrong answers depending on dtype.
Why it matters: silently mislabels a fairness dimension - a data-quality sentinel becomes a phantom elderly majority with zero flag, for a tool whose whole design philosophy is "loud failure over silent wrong result" (see the JSON-orientation and manifest-validation fixes elsewhere in this repo).
Suggested fix: in _age_to_numeric, reject negative numbers (return None) before the string/numeric branches diverge; in _age_band, treat num < edges[0] as unbandable (None) rather than falling through to the last band.
Where:
faircode/profiler.py's_age_to_numeric/_age_band(age dimension banding).The gap:
_age_bandchecksedges[i] <= num < edges[i+1]forAGE_BANDS = [0,18,30,45,60,75]; any number below 0 falls through every branch and returns the"75+"fallback instead of being treated as unbandable. Real datasets commonly use-1/-999as an "unknown age" sentinel.Repro:
10 of 15 rows (66.7%) are sentinel values fabricated into a "75+" majority;
missing_pctstays0.0and no flag mentions the sentinel at all.There's also a related inconsistency in the same function: a string
"-5"(the numeric-extraction regex\d+strips the sign) bands as"0-18", while a numeric-5.0bands as"75+"- the same logical value produces two different wrong answers depending on dtype.Why it matters: silently mislabels a fairness dimension - a data-quality sentinel becomes a phantom elderly majority with zero flag, for a tool whose whole design philosophy is "loud failure over silent wrong result" (see the JSON-orientation and manifest-validation fixes elsewhere in this repo).
Suggested fix: in
_age_to_numeric, reject negative numbers (returnNone) before the string/numeric branches diverge; in_age_band, treatnum < edges[0]as unbandable (None) rather than falling through to the last band.