Learn how rows, columns, missing values, and text encodings affect a CSV file in R.
Before a community teamβs weekly meeting, a coordinator receives one CSV file containing feedback from email, web forms, and live chat. The group plans to examine low ratings. First, the coordinator must find what the five records contain and whether loading changed their structure.
A file can open without an error even when a column is missing, an accent is damaged, or an identifier changes format. A reader can also mistake a blank rating for a score unless they know its meaning. We will examine the fileβs raw lines, then find the one low rating.
We will run the checks again on a second export. That export contains a repeated identifier, an impossible rating, and a row with too many fields.
Note
The author created these fictional feedback records for teaching. They do not describe real people or customers.
TipWhat you will learn
By the end of this lesson, you will be able to:
explain how a structured data file organizes information.
load a CSV file into R.
examine rows, columns, and data types.
find missing values.
examine the parse problems, duplicate keys, and out-of-range values in a flawed file.
select records that meet a stated condition.
What makes a file structured?
What does each part of the file represent? A structured data file follows a consistent arrangement. In a CSV file:
each row usually represents one record.
each column represents one kind of information.
commas separate one column from the next.
CSV stands for comma-separated values. The first line often contains the column names. Quotation marks enclose text containing a comma so the comma remains part of the text. A quoted field can also contain a line break, so one physical line does not always equal one record. A quotation mark inside a quoted field usually appears twice.
Before asking R to make a table, the coordinator reads the file as plain lines. read_lines() from readr returns one text value for each physical line. The output shows the first three stored lines.
The file has six lines: one header and five records. Quotation marks keep the first commentβs comma inside one field rather than separating columns. This line count works because the teaching file has no quoted line breaks.
Load the CSV into R
The line count and header match expectations. The next step organizes the records. The readr function read_csv() converts the file into a tibble, a table that prints its size and each columnβs type. By default, readr interprets text as UTF-8, an encoding for characters from many languages. It therefore uses UTF-8 to read the accented text.
The teaching fileβs data dictionary defines a blank rating as missing. na = "" applies that rule while loading. A real project should use the missing-value rules supplied by its data provider. Do not treat the literal text "NA", zero, and a blank field as equivalent without such evidence.
Those checks show that loading preserved the visible layout. We still need to examine how R interpreted each column.
Why choose the column types?
The col_types = cols(...) setting tells readr which data type to use for each column. A data type records whether values should behave as text, whole numbers, dates, or another kind of information.
identification numbers are text because they identify records.
dates begin as text so we can convert them deliberately.
ratings are whole numbers.
channels and comments are text.
Treating an ID as text protects leading zeros. For example, the label "0012" should remain different from "12" when that difference exists in the source.
We can ask R to report the type it used for every column. map_chr() from purrr applies the same check to each column and returns a character vector. The shorthand \(column) means βfor each column, run the small calculation that follows.β
R reports character for text and integer for whole numbers. Here, character is Rβs name for text. A value may contain an entire comment. Choosing these types deliberately prevents R from converting record labels to quantities.
Convert the date column
The submission dates are text. The dplyr function mutate() changes a column while keeping the rest of the table. Here as.Date() converts the text to Rβs date type. The coordinator can then sort and compare calendar dates.
The format %Y-%m-%d means four-digit year, two-digit month, and two-digit day. Writing it explicitly records the order expected in this file rather than requiring the reader to guess.
Find missing information
After examining rows, columns, and types, the coordinator finds absent values. R uses NA to represent a missing value. The CSV contains one blank rating, which read_csv() changed to NA. map_int() returns one whole-number count for each column.
The rating column has one missing value. A blank means only that this file records no rating. It differs from a rating of zero. Do not replace the blank without evidence about its meaning.
Select a record that needs attention
Only now does the coordinator select feedback with a rating of two or lower. filter() from dplyr keeps rows that meet a condition, and select() keeps the columns needed for the meeting. The expression !is.na(rating) excludes the unknown rating from the comparison rather than treating it as high or low.
One record meets the condition: feedback 1004, which says that the parcel arrived late. The missing rating remains separate because the file does not tell us how that person would have scored the experience.
When the next file is not clean
The first file was well formed. The next example represents another export from the same web form. The lesson defines the text directly rather than reading a saved file, so each line remains visible.
The six records contain six features to examine. These include a repeated feedback_id, a rating typed as a word, and a row with an extra field. Another rating exceeds the documented range of one to five. The file also contains a blank rating and a quoted comment across two physical lines.
messy_text <-paste("feedback_id,submitted_at,channel,rating,comment","1006,2026-08-26,email,4,\"Arrived on time\"","1006,2026-08-26,email,4,\"Second row with the same ID\"","1007,2026-08-27,chat,five,\"Rating typed as a word\"","1008,2026-08-27,web,3,\"Extra field\",unexpected","1009,2026-08-28,web,9,\"Rating outside the documented range\"","1010,2026-08-28,email,,\"Comment that runs\non to a second line\"",sep ="\n")messy_lines <-read_lines(I(messy_text))messy_feedback <-suppressWarnings(read_csv(I(messy_text),na ="",col_types =cols(feedback_id =col_character(),submitted_at =col_character(),channel =col_character(),rating =col_integer(),comment =col_character() ) ))messy_raw <-suppressWarnings(read_csv(I(messy_text),na =character(),col_types =cols(feedback_id =col_character(),submitted_at =col_character(),channel =col_character(),rating =col_character(),comment =col_character() ) ))tibble(measure =c("Physical lines", "Records read"),count =c(length(messy_lines), nrow(messy_feedback)))
# A tibble: 2 Γ 2
measure count
<chr> <int>
1 Physical lines 8
2 Records read 6
Eight physical lines produced six records because one quoted comment contains a line break. The earlier line-count check worked only because that file had no quoted line breaks.
suppressWarnings() appears because this page treats warnings as errors. Suppressing the warning does not discard the evidence. readr keeps a structured report of everything it could not parse.
readr counts the header line. Reported row 4 is therefore the third record, whose rating is five. The parser changed that value to NA. For the extra-field row, the parser added the unexpected text to the comment.
A parse problem does not always produce a missing value. A missing value does not always indicate a parse problem.
The same checks apply in other languages. In pandas, dtype and na_values declare column types and missing-value rules. Arrow can use an explicit schema. Keep the parserβs problem report with those declarations. An unreported parser guess creates a risk in any language.
Examine what a parser cannot establish
Successful parsing confirms the expected structure. It does not establish that identifiers are unique or ratings are possible. The data dictionary supplies those rules. Each rule can become a check that reports a failure.
All records with reasons for inclusion or exclusion
Record
Feedback ID
Raw rating
Parsed rating
Decision
1
1006
4
4
quarantine: repeated feedback_id
2
1006
4
4
quarantine: repeated feedback_id
3
1007
five
NA
quarantine: rating failed integer parsing
4
1008
3
3
quarantine: unexpected sixth field
5
1009
9
9
quarantine: rating outside 1 to 5
6
1010
NA
keep: rating recorded as missing
The report excludes five of six records for different reasons. Two share an ID. The source must explain the duplicate before either can identify record 1006. The raw value five failed integer parsing and differs from an ordinary missing rating. One row has an unexpected sixth field. Another has a rating of nine, which exceeds the dictionaryβs maximum.
The remaining record has a blank rating. A blank that the dictionary defines as missing is not a parse error.
The report classifies all six input records and omits none without explanation. Filtering to usable rows alone would report one record. That output would omit the evidence of a duplicate ID in the source.
Questions to ask about any structured file
The five-row check is complete, but structural checks cannot answer every question about the feedback. Before an analysis of any structured file, ask:
What does one row represent?
What does each column mean?
Which encoding does the file use?
How are missing values recorded?
Are dates and identification numbers stored correctly?
Who created the file, and what information did they exclude?
A tidy table cannot tell you whether the collection process was fair or complete. These rows represent only the feedback that reached the recorded channels and was included in this file. The fileβs origin, exclusions, and limits belong alongside the technical checks.
What to remember
A structured file uses a consistent arrangement of rows and columns.
Quotation marks protect text that contains a separator such as a comma.
Quoted line breaks make physical lines and records different counts.
Choose important column types rather than relying on guesses.
Read the parserβs problem report instead of only its output table.
Uniqueness and value ranges come from the data dictionary, not the parser.
Examine missing values before filtering or summarizing.
Opening a file successfully does not complete the required checks.
At the meeting, the coordinator can report five records, one missing rating, and one recorded rating of two. The second file contains six records. One is usable, and five remain excluded for stated reasons. The blank remains missing, and the fileβs coverage remains uncertain.
Report the recorded values and missing values. State which questions the file cannot answer.