Reading tables in lab report PDFs reliably starts with the layout: decide whether the table is ruled, unruled or split into two columns, find each column from where text sits on the page, rejoin rows that wrap onto a second line, and attach every comment or footnote to the result it describes before any value is saved.
Lab report table layouts: ruled, unruled and two-column
Lab report tables come in three common layouts. A ruled table draws lines between rows and columns. An unruled table has no lines and relies on spacing alone. A two-column layout prints two narrow result tables side by side on one page. Each layout needs a different way of finding where one cell ends and the next begins.
Ruled tables
A ruled table is the easiest to read when the PDF has a text layer, which is the text a computer can select, rather than only an image of the page. The lines are drawn objects in the PDF, so software can use them as cell borders. The pdfplumber project documentation describes this as its default: table extraction "uses the page's vertical and horizontal lines (or rectangle edges) as cell-separators".
Ruled tables still fail in small ways. Some labs draw only horizontal lines, so rows are clear but columns are not. Others shade alternate rows instead of drawing lines, and the shading may or may not exist as rectangles in the file. On a scanned page, the lines are part of the image and have to be detected before they help.
Unruled tables
An unruled table prints columns of text separated by white space and nothing else. It is the most common lab layout and the hardest to read, because the only evidence of a column is that values line up vertically. A long test name or a value with a flag can break that alignment.
Two-column layouts
A two-column layout fits more results on a page by printing two tables next to each other, often chemistry on the left and hematology on the right. Read in plain left-to-right order, a line of text mixes a row from each table. The first job is to split the page down the middle and read each half as its own table.
Labs also mix these layouts on one report. A header block may be ruled, the results unruled, and a comment section printed as running text. Treat each block of the page separately rather than forcing one rule onto the whole page.
How do you find column boundaries in an unruled lab table?
You find column boundaries in an unruled lab table by measuring where each word sits on the page and looking for vertical bands of empty space that run down every row. Each band is a gap between columns. Words to the left of a gap belong to one column, and words to the right belong to the next.
A PDF with a text layer stores the position of every character. The pdfplumber project documentation lists x0 as the "distance of left side of character from left side of page" and x1 as the distance of the right side, and it can group characters into words with their bounding boxes. The same documentation describes a "text" strategy that deduces imaginary lines connecting the left, right or center of words and uses them as cell borders.
An unruled table with x-coordinates, worked
This fictional example shows four rows from a made-up report. The numbers in brackets are example x0 and x1 positions in points, measured from the left edge of the page. They are invented to show the method.
| Row | Test name | Result | Flag | Unit | Reference range |
|---|---|---|---|---|---|
| 1 | Hemoglobin [40-112] | 13.8 [210-232] | g/dL [300-326] | 12.0-15.5 [380-428] | |
| 2 | Platelet Count [40-121] | 412 [210-228] | H [252-258] | x10E3/uL [300-344] | 150-400 [380-420] |
| 3 | WBC [40-68] | 6.1 [210-226] | x10E3/uL [300-344] | 3.4-10.8 [380-422] | |
| 4 | Mean Corpuscular Volume [40-172] | 91 [210-222] | fL [300-310] | 79-97 [380-412] |
Read the positions down each column rather than across each row:
- Every test name starts at 40 and ends no later than 172.
- Every result starts at 210 and ends by 232.
- The only flag sits between 252 and 258.
- Every unit starts at 300 and ends by 344.
- Every range starts at 380.
The gaps that no word crosses in any row are 172 to 210, 232 to 252, 258 to 300 and 344 to 380. Those four gaps give five columns. The flag column is narrow and mostly empty, which is normal: a column can exist even when most rows have nothing in it. A boundary taken from row 1 alone would miss the flag column entirely, which is why the gaps must be found across all rows.
Left edges are more stable than right edges for text, and right edges are more stable for numbers when a lab aligns them on the decimal point or the right margin. Check both, and prefer the edge that lines up more tightly down the column.
Writing this as code is its own topic. The guide to lab report parsing in Python with pdfplumber and regular expressions walks through parsing one row.
Wrapped test names and merged cells in lab report tables
A wrapped test name is too long for its column, so it continues on a second printed line that has text in the name column only. A merged cell is one value that spans several rows or columns, such as a single range printed once for a group of tests. Both split one logical row across lines.
Rejoining wrapped rows
A wrapped row has a clear signature. The continuation line has text only in the name column, its vertical position is closer to the line above than a normal row gap, and the line above has values in the result columns. When all three are true, join the continuation text to the name above with a space.
Watch the opposite case too. A line with text only in the name column can also be a section heading, such as "Lipid Panel" or "Hematology". Section headings usually sit above the rows they introduce, are often bold or in capitals, and are followed by a normal row gap. Use the font and the spacing, not the empty cells alone, to tell a heading from a wrap.
Values can wrap as well. A long reference range, such as the made-up example "Male: 13.2-16.6 Female: 11.6-15.0", may print on two lines inside the range column. The same rule applies: a line with text only in the range column, close beneath a full row, continues that row's range.
Merged cells
A merged cell prints one value for several rows. Labs do this for a shared unit, a shared reference range, or a shared comment such as "Specimen hemolyzed" printed once beside a group of results. When a cell spans rows, copy its value to every row it covers, and record that it was printed once for the group, so a reviewer knows it was shared on the page.
Merged header cells are also common, such as one heading "Current" over two columns for result and flag. Read the lowest header row as the column name and keep the merged heading as a group label.
Where do lab result comments and footnotes belong in the data?
Lab result comments and footnotes belong with the result they describe, in a comment field on that result record, not in a separate free-text blob for the whole report. A comment that applies to the whole report belongs on the report record. The extraction step decides which by following the marker or position that ties each note to its row.
Lab result comments and footnotes extraction starts with finding the link between a note and its row. Labs use a few patterns:
- Inline comments. A line of text directly under a result row, often indented, that applies only to that row.
- Footnote markers. A symbol or number beside a value, such as an asterisk or a superscript 1, with the matching note at the bottom of the table or page.
- Group comments. A note printed under a panel that applies to every result in it.
- Report comments. Notes about the whole specimen or report, such as a delayed delivery or a method change, printed in a block at the end.
Each pattern maps to a different place in the data. Inline comments and footnotes go on the result record they point to. Group comments go on every result in the group, marked as shared. Report comments go on the report record.
Keep the comment text exactly as printed, including the marker. Do not shorten it or reword it. Comments often carry facts about the specimen or the method that a person needs when reading the result. The CLIA test report standard at 42 CFR 493.1291 on the eCFR requires a report to include information about the condition and disposition of specimens that do not meet the laboratory's criteria for acceptability, and notes like that usually print as comments.
A footnote marker can be easy to lose. OCR may read a superscript 1 as part of the value, turning 13.8 with a footnote into 13.81. Compare the marker size and vertical position with the digits beside it, and send any value with a doubtful trailing character to review. The data fields that comments fill, and how they differ from results, are part of the tour of the parts of a lab report for data entry.
Lab PDF table extraction errors: shifted columns and split rows
Lab PDF table extraction errors usually take two forms. A shifted column puts a value under the wrong heading, so a flag lands in the unit field or a range lands in the result field. A split row breaks one result into two records, often by treating a wrapped name as a new test with no value.
What a shifted column looks like
A shifted column is easiest to spot by the type of text in each field. A result field holding "mg/dL", a unit field holding "H", or a range field holding a single number with no dash are all signs that a boundary was placed in the wrong spot. Most shifts start at one row, often where a value is unusually wide, and carry on for every row below it.
Simple type checks catch most of them:
- the result field holds a number, a less-than or greater-than value, or a known text result such as "Negative",
- the flag field holds only the short codes the lab prints,
- the unit field holds a unit string, not a number,
- the range field holds a range form, such as low and high joined by a dash, a less-than value or an expected text.
A broader set of these rules, including whether the unit fits the test and whether the flag agrees with the value and range, is covered in the guide to checking extracted lab values against units and ranges.
What a split row looks like
A split row produces a record with a test name and no value, followed by a record with a fragment of a name and a full set of values. "Mean Corpuscular" with no result, then "Hemoglobin Concentration" with a result, is a typical pair. Look for any record with an empty result where the page shows no blank cell, and check whether joining it to the next record gives a known test name.
The reverse error also happens: two short rows merged into one, with two values crammed into the result field. A result field holding two numbers separated by a space is almost always a merge.
Counting as a check
Counting is a cheap guard. Count the result rows you extracted and compare with the number of value lines on the page. A difference of one usually means a split or a merge. A large difference usually means the table boundary was wrong and part of a comment block or a second table was read as rows.
Structured results that do not fit a table at all, such as culture reports with organisms and drug lists, need a different shape; the guide to microbiology report data extraction covers that nested layout. Schema Care reads test names, results, units, reference ranges and abnormal flags from lab reports and gives each field a link to where it sits on the page; the list of supported documents shows the fields for each type. The medical OCR basics category covers how scanned pages become text before any table can be read.