From df553db08db130b88dba799214962900f6118410 Mon Sep 17 00:00:00 2001 From: Mohak Gupta Date: Sun, 6 Sep 2026 12:46:16 +0530 Subject: [PATCH 1/2] Support a list of column positions for read_csv index_col Closes #15127. index_col already accepted a single integer position or column label(s), but a list of integer positions (index_col=[0]) hit set_index() directly, which expects labels, and raised KeyError. Converts an all-integer list through the same get_labels_by_index() helper already used for the single-int case, then replicates that case's index name handling (int position for a headerless read, the real label or None for an 'Unnamed:' placeholder when a header exists) across each column. Verified against a real cudf install (26.08.01) on a real GPU: headerless and real-header CSVs, single and multi-column index_col lists, and the 'Unnamed:' placeholder case, all compared directly against real pandas output for both data and index names. Confirmed the existing single-int, single-label, and label-list index_col paths are unaffected. Added two cases to the existing test_csv_reader_index_col test; ran the full existing test_csv.py file before and after to confirm the change adds no new failures (128 pre-existing failures from missing test fixtures in this environment, unrelated to this change, drop to 127 - exactly the one test this fixes). Signed-off-by: Mohak Gupta --- python/cudf/cudf/io/csv.py | 16 +++++++++++++++- python/cudf/cudf/tests/input_output/test_csv.py | 10 ++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/python/cudf/cudf/io/csv.py b/python/cudf/cudf/io/csv.py index eccedc7d9d02..0d281e474295 100644 --- a/python/cudf/cudf/io/csv.py +++ b/python/cudf/cudf/io/csv.py @@ -5,7 +5,7 @@ import csv import itertools import os -from collections.abc import Collection, Mapping +from collections.abc import Collection, Mapping, Sequence from io import BytesIO, StringIO, TextIOBase from typing import TYPE_CHECKING, cast @@ -366,6 +366,20 @@ def read_csv( df.index.name = None elif names is None: df.index.name = index_col + elif isinstance(index_col, Sequence) and all( + isinstance(col, int) for col in index_col + ): + index_col_labels = list(df._data.get_labels_by_index(index_col)) + df = df.set_index(index_col_labels) + if names is None: + df.index.names = [ + (None if label.startswith("Unnamed:") else label) + if isinstance(label, str) and orig_header == "infer" + else position + for label, position in zip( + index_col_labels, index_col, strict=True + ) + ] else: df = df.set_index(index_col) diff --git a/python/cudf/cudf/tests/input_output/test_csv.py b/python/cudf/cudf/tests/input_output/test_csv.py index 3be019c1058c..760bbdee7a99 100644 --- a/python/cudf/cudf/tests/input_output/test_csv.py +++ b/python/cudf/cudf/tests/input_output/test_csv.py @@ -1263,6 +1263,16 @@ def test_csv_reader_index_col(): pd_df = pd.read_csv(StringIO(buffer), header=None, index_col=False) assert_eq(cu_df.index, pd_df.index) + # using a single column index wrapped in a list + cu_df = read_csv(StringIO(buffer), header=None, index_col=[0]) + pd_df = pd.read_csv(StringIO(buffer), header=None, index_col=[0]) + assert_eq(cu_df.index, pd_df.index) + + # using multiple column indices + cu_df = read_csv(StringIO(buffer), header=None, index_col=[0, 1]) + pd_df = pd.read_csv(StringIO(buffer), header=None, index_col=[0, 1]) + assert_eq(cu_df.index, pd_df.index) + @pytest.mark.parametrize("index_name", [None, "custom name", 124]) @pytest.mark.parametrize("index_col", [None, 0, "a"]) From 4c9d298b64c25b1ab064b68040ad05fb3bdf3223 Mon Sep 17 00:00:00 2001 From: Mohak Gupta Date: Tue, 8 Sep 2026 10:13:04 +0530 Subject: [PATCH 2/2] Fix index_col name resolution for an explicit header= argument CodeRabbit found that index_col with an explicit header=0 (not the default "infer") returned the column position as the index name instead of the real column label, same as the pre-existing single-int path already did. Both checked orig_header == "infer" instead of whether a header row actually exists; switched both to the header variable already used for that exact check elsewhere in this function. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4pNSy3B9U7iYE6jd3bqFs Signed-off-by: Mohak Gupta --- python/cudf/cudf/io/csv.py | 7 ++----- python/cudf/cudf/tests/input_output/test_csv.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/python/cudf/cudf/io/csv.py b/python/cudf/cudf/io/csv.py index 0d281e474295..e5b99b68fac1 100644 --- a/python/cudf/cudf/io/csv.py +++ b/python/cudf/cudf/io/csv.py @@ -177,9 +177,6 @@ def read_csv( if byte_range is None: byte_range = (0, 0) - # We need this later when setting index cols - orig_header = header - if names is not None: # explicitly mentioned name, so don't check header if header is None or header == "infer": @@ -358,7 +355,7 @@ def read_csv( if ( isinstance(index_col_name, str) and names is None - and orig_header == "infer" + and header != -1 ): if index_col_name.startswith("Unnamed:"): # TODO: Try to upstream it to libcudf @@ -374,7 +371,7 @@ def read_csv( if names is None: df.index.names = [ (None if label.startswith("Unnamed:") else label) - if isinstance(label, str) and orig_header == "infer" + if isinstance(label, str) and header != -1 else position for label, position in zip( index_col_labels, index_col, strict=True diff --git a/python/cudf/cudf/tests/input_output/test_csv.py b/python/cudf/cudf/tests/input_output/test_csv.py index 760bbdee7a99..d2fffc125278 100644 --- a/python/cudf/cudf/tests/input_output/test_csv.py +++ b/python/cudf/cudf/tests/input_output/test_csv.py @@ -1274,6 +1274,18 @@ def test_csv_reader_index_col(): assert_eq(cu_df.index, pd_df.index) +def test_csv_reader_index_col_position_with_explicit_header(): + buffer = "a,b,c\n3,4,5\n6,7,8" + + cu_df = read_csv(StringIO(buffer), header=0, index_col=[0]) + pd_df = pd.read_csv(StringIO(buffer), header=0, index_col=[0]) + assert_eq(cu_df.index, pd_df.index) + + cu_df = read_csv(StringIO(buffer), header=0, index_col=0) + pd_df = pd.read_csv(StringIO(buffer), header=0, index_col=0) + assert_eq(cu_df.index, pd_df.index) + + @pytest.mark.parametrize("index_name", [None, "custom name", 124]) @pytest.mark.parametrize("index_col", [None, 0, "a"]) def test_csv_reader_index_names(index_name, index_col):