pd.read_csv reads the file in blocks and infers each column's dtype from the block, because low_memory defaults to True. When two blocks disagree, pandas emits DtypeWarning: Columns (3) have mixed types. Specify dtype option on import or set low_memory=False and the column ends up as object holding a mix of ints and strings.
The trap is that the warning depends on file size and block boundaries, so the same code succeeds on a sample and warns on the full extract. Downstream comparisons then behave inconsistently, and a groupby on that column produces separate groups for 1 and "1".
There are two correct fixes. Pass dtype={"id": "string"} for the affected columns so the whole file is read consistently, which is also faster. Or pass low_memory=False to make pandas read the whole column before deciding, at the cost of holding it in memory twice. Reading with dtype_backend="pyarrow", available since pandas 2.0, avoids the object fallback entirely and gives proper nullable types.