Skip to content

A .csv file with one messy row picks a different delimiter in the CLI than in the web profiler, silently collapsing every demographic column into one #729

Description

@yakew7

Where: faircode/loaders.py's _sniff_delimiter() (used by the CLI/MCP's actual file-reading path via loaders_extra.read_table()) vs assets/profiler-engine.js's sniffDelimiter()/logicalRowDelimiterCounts() (the browser's only reader).

The gap: Issue #439 fixed the CLI's old "sniff only for an unrecognized extension" behavior by making _read_delimited() always sniff via Python's csv.Sniffer, over the full 8192-byte sample. But logicalRowDelimiterCounts() in the JS engine deliberately samples only the first 5 logical (quote-aware) rows before deciding. For a file that is consistently delimited in its first 5 rows but has one messy row later in the sample (e.g. a free-text field with a stray, unquoted delimiter character - a common real-world data-entry artifact), the two engines now disagree: JS confidently locks onto the real delimiter from the first 5 rows; Python's csv.Sniffer scans further into the sample, hits the inconsistency, raises csv.Error, and silently falls back to the extension-implied default (, for .csv). This is a regression the fix for #439 introduced, and tests/test_js_parity.py's one sniffing test only exercises a case where both algorithms already agree.

Repro:

sex;race;age
M;White;25
F;Black;30
M;White;45
F;Asian;22
M;White;50
F;Black; recorded twice;29
M;Asian;33
>>> from faircode.loaders_extra import read_table
>>> df = read_table('minimal_messy.csv')
>>> list(df.columns), df.shape
(['sex;race;age;'], (7, 1))   # collapsed into one bogus column
> const E = require('./assets/profiler-engine.js');
> E.parseCSV(fs.readFileSync('minimal_messy.csv','utf8')).columns
[ 'sex', 'race', 'age' ]      // correctly split

Running it end-to-end: faircode profile minimal_messy.csv reports "Rows: 7 Columns: 1 ... Representation score: 100/100 (Grade A)" with a single bogus column, while the JS engine correctly detects Sex/Race/Age as separate demographic dimensions.

Fix direction: Make logicalRowDelimiterCounts() sample more rows (or the same byte window Python uses) so both engines see the same evidence, or make _sniff_delimiter() tolerant of one inconsistent row within a larger sample (a majority/consistency-across-most-rows rule instead of csv.Sniffer's all-or-nothing approach).

Activity

  1. pinned this issue on Sep 26, 2026
  2. yakew7 commented on Sep 30, 2026

    @yakew7
    OwnerAuthor

    Fixed in f7de6ad: rewrote faircode/loaders.py's _sniff_delimiter to a quote-aware, first-5-logical-rows algorithm (_logical_row_delimiter_counts) that mirrors assets/profiler-engine.js's sniffDelimiter() exactly, replacing the csv.Sniffer()-based approach that disagreed with the web engine on files with one messy row past the first few. Added a regression test reproducing the exact repro from this issue.

  3. added a commit that references this issue on Sep 30, 2026
    f7de6ad
  4. unpinned this issue on Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions