Repository navigation
XLSX/XLS: keep repeated header names instead of Total.1, Item.1 - #2709
Cindy (cindykrafft) wants to merge 5 commits into
Conversation
pandas renames duplicate column labels. When a label looks renamed, re-read the sheet's raw first row and use the original labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVn4SeAXPsUajXT2rtHQfo
pd.read_excel parsed cells stored as text into numbers, so 02134 became 2134, +15551234 became 15551234 and 007 became 7.0. Read with dtype=object and infer_objects() so text stays text and numeric columns render as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVn4SeAXPsUajXT2rtHQfo
Bicheng (Kenneth) (BichengWang)
left a comment
There was a problem hiding this comment.
Reviewed 20b92625a3bcd4252a4b4a4430dc74cd9e5dd1ca (both commits), with one nonblocking coverage gap:
Repeated NA-like text headers still receive suffixes. The raw reread at _xlsx_converter.py:60–63 uses pandas' default NA parsing, so _restore_header sees missing values for literal NA, N/A, and NULL headers and retains their mangled names. For example, this workbook still produces | NA | NA.1 |:
import io
import openpyxl
from markitdown import MarkItDown, StreamInfo
workbook = openpyxl.Workbook()
workbook.active.append(["NA", "NA"])
workbook.active.append([1, 2])
stream = io.BytesIO()
workbook.save(stream)
stream.seek(0)
print(MarkItDown().convert_stream(
stream, stream_info=StreamInfo(extension=".xlsx")
).markdown)The same reproduction yields | N/A | N/A.1 | and | NULL | NULL.1 |. The helper docstring acknowledges this fallback, but the PR's stated behavior has this exception. Consider disabling NA parsing for the header reread while retaining a separate fallback for genuinely blank header cells, with parametrized coverage for these labels. Reading only the first row would also avoid parsing the entire sheet again.
Validation on Python 3.12.15 / pandas 3.0.6: core suite 1053 passed, 57 skipped with GITHUB_ACTIONS=1; OCR suite 108 passed; pre-commit run --all-files passed. These used direct pytest runs in an offline environment. The three additional header assertions above failed as described; header-only, numeric, date, and mixed text/numeric header probes converted successfully. I did not generate new .xls fixtures.
The header re-read used pandas' default NA parsing, so repeated headers such as "NA", "N/A" or "NULL" were read as missing and kept their renamed NA.1 / N/A.1 / NULL.1 names. Re-read only the first row with NA parsing off; blank header cells still keep pandas' "Unnamed: n" names, including when the first row is narrower than the sheet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVn4SeAXPsUajXT2rtHQfo
|
Thanks for checking this so thoroughly. You're right; that was a gap. 2cee2ab addresses it:
Validation (Python 3.12, pandas 3.0.6): core suite 1059 passed, 57 skipped with Disclosure: this fix and reply were produced by an AI agent (Claude Code), which ran the reproduction and tests quoted above. |
Keeping numeric-looking text as text does not change how pandas reads "NA", "N/A", "NULL" or "nan" data cells: they were missing before and still are. Say so in the helper docstring and add a test with a kept text cell next to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVn4SeAXPsUajXT2rtHQfo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVn4SeAXPsUajXT2rtHQfo
PRABHU KIRAN VANDRANKI (VANDRANKI)
left a comment
There was a problem hiding this comment.
I ran this on Windows with Python 3.13.3, pandas 3.0.6, openpyxl 3.1.5 and xlrd 2.0.2, against upstream main (4cc9fa1) and the PR head. This branch also contains the text-cells commit from #2708, which I reviewed earlier, so this covers the header change.
tests/test_xlsx.pyon the PR head: 35 passed. The same test file against main's source: 8 failed, 27 passed. Those 8 are the new text-cell, NA-marker, repeated-header and narrow-first-row cases.- Full
packages/markitdown/testson the PR head: 1113 passed, 3 skipped, 1 failed. The failure istest_speech_transcription, a FileNotFoundError from the audio converter on this machine. I did not run that test on main. - I compared main and the PR on 13 workbooks: dates and times, mixed columns, int and float columns, booleans, empty columns, big ints, numeric and date headers, text that looks like numbers, and an all-empty sheet. The output differs only for repeated headers (
1 | 1.1becomes1 | 1; two equal date headers no longer get.1) and for text cells (1.50and2stay1.50and2; main gave1.5and2.0). - Three sheets in one workbook (repeated
x x x, a plain sheet with01, and a sheet withk k): all three come out right on the PR. Main givesx x.1 x.2andk k.1. A stream that starts at offset 4 still gives the restoredx x xheader, so the re-read of the first row seeks back correctly. - Names that already contain dots (
a.b a.b,v1.2 v1.2, and a realw.1next tow) come back as written. test.xlsconverts to a correct table through the xlrd path. I could not write an.xlswith repeated headers here, so the xlrd side of the header fix is read-only. Themarkitdown-ocrtest change (the addeddtype: objectkwarg) I also only read.git merge-treeof the #2708 head and this head merges clean.
The stacking is stated in the description. Approving.
pandas renames duplicate column labels, and pandas 2 removed the option to turn that off, so a header row
Item | Total | Total | Itemis rendered asItem | Total | Total.1 | Item.1. The Markdown table no longer matches the spreadsheet.When a label looks renamed, this re-reads that sheet's raw first row and uses the original labels. Eleven other sheet shapes compared give identical output.
This builds on the XLSX text-cells PR; the branch contains both commits, so please merge that one first or review just the last commit.
Tests
test_xlsx_repeated_header_names_are_keptfails on main (| Item | Total | Total.2 | Item.1 | Total.1 | NA | Unnamed: 6 |) and passes with the change.packages/markitdown1098 passed, 12 skipped;packages/markitdown-ocr108 passed. black clean.Disclosure: this was found, reproduced and patched by an AI agent (Claude Code), which ran the reproduction and tests quoted above. It has not been separately reviewed by a human.