Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

R (4.6)  Module 7 – Importing and exporting data

Reading and writing CSV and text data

Read CSV and text files in R with read.csv(), readr and fread(): set column types, encodings, decimal commas and missing codes, list problems, write data out.

  • Beginner
  • 15 minutes
  • Examples run with R 4.6.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Read delimited files with read.csv(), readr::read_csv() and data.table::fread(), and set their column types
  • Handle encodings, byte-order marks, decimal commas and custom missing-value codes
  • Diagnose parsing problems with readr's problems() and spec()
  • Write data and R objects back out with write.csv(), write_csv(), saveRDS() and dput()

Before you start

What you’ll learn · Before you start

What you will learn

  • Read delimited files with read.csv(), readr::read_csv() and data.table::fread(), and set their column types
  • Handle encodings, byte-order marks, decimal commas and custom missing-value codes
  • Diagnose parsing problems with readr's problems() and spec()
  • Write data and R objects back out with write.csv(), write_csv(), saveRDS() and dput()

Before you start

On this page

A clinic’s billing system exports six visits as visits.csv, and R reads all five of its columns as text, the fees, the weights and the dates, whichever of R’s three common readers you use. Nothing stops with an error: three invisible bytes at the start, a fee written Rs. 3,450.50, a weight written - and a date that does not exist decide what you get back.

On runs of R 4.6.1 with readr 2.2.0 and data.table 1.18.6.1, you will read that file with read.csv(), readr::read_csv() and data.table::fread(), fix its encoding, decimal marks and missing-value codes, name the type of every column and read the list of values that failed, then write the clean table out in a form that keeps what you need.

Three readers return three kinds of table

visits.csv holds one visit per line: a patient id, the visit date, the ward, the fee and the weight, separated by commas. A file like this is a delimited file: A text file with one record per line whose fields are separated by one character: a comma in a CSV, a semicolon in many European exports, a tab in a TSV., and CSV, comma-separated values, is the commonest kind; RFC 4180 describes the format. Here is the file as a text editor shows it:

data/visits.csv CSV · data/visits.csv
id,visit_date,ward,fee,weight_kg
P-101,03/04/2026,General,"1,250",61.5
P-102,05/04/2026,ICU,"12,400",-
P-103,07/04/2026,,450,NA
P-104,31/04/2026,General,"1,25,000",72
P-105,09/04/2026,Day care,"Rs. 3,450.50",58
P-106,12/04/2026,ICU,"2,300",

You will meet three readers for it in other people’s code: read.csv() from base R, read_csv() from the readr package and fread() from data.table. The run reads the file with each one and lists the type each gave each column:

three_readers.R, typed at the R console Python · three_readers_session.py
from session import run, version

version()
# R reads three_readers.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f three_readers.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f three_readers.R
> # One clinic export, read by the three common readers.
> base_df <- read.csv("data/visits.csv")
> tidy_df <- readr::read_csv("data/visits.csv")
Rows: 6 Columns: 5
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (5): id, visit_date, ward, fee, weight_kg

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
> fast_df <- data.table::fread("data/visits.csv")
> class(base_df)
[1] "data.frame"
> class(tidy_df)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 
> class(fast_df)
[1] "data.table" "data.frame"
> # The type each reader gave each column:
> rbind(read.csv = sapply(base_df, class),
+       read_csv = sapply(tidy_df, class),
+       fread = sapply(fast_df, class))
         id          visit_date  ward        fee         weight_kg  
read.csv "character" "character" "character" "character" "character"
read_csv "character" "character" "character" "character" "character"
fread    "character" "character" "character" "character" "character"
> # P-103 has no ward. What did each reader make of the blank?
> base_df$ward[3]
[1] ""
> tidy_df$ward[3]
[1] NA
> fast_df$ward[3]
[1] ""

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_readers_session.py

Each reader returned its own kind of table: a plain data frame: R's table: a list of columns of equal length, each with its own type, with the class data.frame and row names. read.csv() and most modelling functions work with data frames. More in “Data frames in base R”, a tibble: A data frame from the tibble package, used by the tidyverse, with stricter rules: no partial matching with $, [ ] always returns a tibble, only values of length 1 are recycled, and there are no row names. More in “Data frames in base R” that also carries the specification readr printed, and a data.table. All three work wherever R expects a data frame. They agree on the types too, and that is the problem: every column is text, fees, weights and dates included. They disagree about the blank ward of P-103, which read.csv() and fread() keep as an empty string, "", and read_csv() makes NA.

Reader Returns Chooses types from An empty text field
read.csv() a data frame every value, all read as text first ""
readr::read_csv() a tibble 1,000 values, spread from first row to last (guess_max) NA
data.table::fread() a data.table 100 rows at each of 100 points ""

The help pages and readr’s guide to column types give the third column: read.table() reads every column as text and then converts it, readr inspects guess_max values spread evenly from the first row to the last, and fread() samples rows throughout the file and rereads any column that a row outside its sample shows to need a wider type. A reader does the same five things to every line, and you can make each of those decisions yourself instead of leaving it to the reader. Step through the line of P-104:

Interactive · stage by stage What a reader does with one line of visits.csv Step through the line of P-104, from the bytes in the file to the table R returns. The branch on the right is where readr keeps a value it could not convert.
What a reader does with one line of visits.csv, step 7 of 7. Showing Bytes in the file, Decode them to text, Split into fields (five fields, P-104 first), Mark missing values, Pick column types (given by col_types), Return a table, problems(). In focus: problems().failed valuesBytes in the fileef bb bf, then the headerDecode them to textUTF-8 unless told otherwiseSplit into fieldsfive fields, P-104 firstMark missing valuesonly the codes you listPick column typesgiven by col_typesReturn a tabledata.frame, tibble, data.tableproblems()values that failedthe visit date

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The bytes. The file starts with the three bytes ef bb bf, the UTF-8 byte-order mark, then the header id,visit_date,ward,fee,weight_kg. Every reader starts from these bytes.
  2. Decoding. The reader turns the bytes into text using an encoding, UTF-8 unless you name another one. In a UTF-8 locale read.csv() and readr drop the mark, so the first column is called id.
  3. Fields. The line of P-104 splits at its commas into five fields. The quotes keep 1,25,000 in one field, although it holds two commas.
  4. Missing values. A field that matches a missing-value code becomes NA. By default only "NA" counts, and for readr an empty field too, so the "-" that P-102 has for its weight stays text.
  5. Types. The reader picks one type for each column from the values it sees. "Rs. 3,450.50" is no plain number, and for read.csv() and fread() neither is any fee with a grouping comma, so the whole fee column stays text, 1,25,000 included.
  6. The table. All five columns of this file arrive as text from every reader. Name the types with col_types, and 1,25,000 becomes 125000 and every real visit date becomes a Date.
  7. What failed. With named types, readr converts what it can and lists the rest in problems(). For P-104 that is its visit date, day 31 of month 4, a day April does not have, which becomes NA.

The sections that follow take the stages in order: the encoding, the separator and decimal mark, the codes for missing values and the type of each column, then the way back out.

Checkpoint

Check: Three readers, three tables

  1. Question 1 of 2 P-103 has no ward in visits.csv. Which reader turned that empty field into NA?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: readr::read_csv()

    read.csv() and fread() returned "" for the blank ward, and read_csv() returned NA. The difference matters as soon as you count missing values with is.na().

  2. Question 2 of 2 A file of 5,000 rows has a column code with whole numbers in every row but one, which holds A7, and readr's sample of rows for guessing leaves that row out. What does read_csv() with its defaults return for code?

    Choose one answer.

    A hint

    Which values does readr look at before it chooses a type?

    Show the answer: Question 2 of 2

    Answer: A numeric column, with A7 turned into NA and listed in problems()

    readr's second edition inspects guess_max values, 1,000 by default, evenly spaced from the first row to the last. With A7 in data row 4,800, outside that sample, it chose numbers, then failed on A7 and listed it in problems() as file row 4,801 (the header is row 1). Name the type with col_types, or pass guess_max = Inf to look at every row, when values outside the sample can differ.

The first bytes decide how the text is decoded

A file is bytes, and a character encoding: The rule that turns a file's bytes into characters. UTF-8 is the usual one; a file saved as UTF-16LE or Latin-1 reads as garbled text or fails unless its encoding is named when it is read. is the rule that turns them into characters. The run prints the first eight bytes of two exports of the same visits, then reads each:

bytes.R, typed at the R console Python · bytes_session.py
from session import run, version

version()
# R reads bytes.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f bytes.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f bytes.R
> # The first bytes of two exports of the same visits.
> readBin("data/visits.csv", "raw", 8)
[1] ef bb bf 69 64 2c 76 69
> readBin("data/visits-unicode.txt", "raw", 8)
[1] ff fe 69 00 64 00 09 00
> # In a UTF-8 locale, read.csv() drops the UTF-8 mark ef bb bf.
> names(read.csv("data/visits.csv"))
[1] "id"         "visit_date" "ward"       "fee"        "weight_kg" 
> # The UTF-16 file, read as if it were UTF-8:
> try(read.delim("data/visits-unicode.txt"))
Error in make.names(col.names, unique = TRUE) : 
  invalid multibyte string at '<ff><fe>i'
In addition: Warning messages:
1: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 1 appears to contain embedded nulls
2: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 2 appears to contain embedded nulls
3: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 3 appears to contain embedded nulls
4: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 4 appears to contain embedded nulls
5: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 5 appears to contain embedded nulls
> # Read with the encoding it was written in:
> u <- read.delim("data/visits-unicode.txt", fileEncoding = "UTF-16LE")
> dim(u)
[1] 6 5
> names(u)
[1] "id"         "visit_date" "ward"       "fee"        "weight_kg" 
> # readr names the encoding in its locale; fread() refuses UTF-16:
> dim(readr::read_tsv("data/visits-unicode.txt", show_col_types = FALSE,
+                     locale = readr::locale(encoding = "UTF-16LE")))
[1] 6 5
> try(data.table::fread("data/visits-unicode.txt"))
Error in data.table::fread("data/visits-unicode.txt") : 
  File is encoded in UTF-16, this encoding is not supported by fread(). Please recode the file to UTF-8.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bytes_session.py

visits.csv starts with ef bb bf, then 69 64, the letters i and d. The three bytes are a byte-order mark: Bytes at the very start of a text file that announce its encoding: ef bb bf for UTF-8, ff fe for UTF-16 little-endian. R drops the UTF-8 one in a UTF-8 locale; other tools may keep it as part of the first name.: they announce UTF-8 and print as nothing. R discards them in a UTF-8 locale, the usual setting on macOS and Linux and, since R 4.2.0, on recent Windows, so the first column is called id. A tool that does not discard them makes them part of the first name, and the connection encoding "UTF-8-BOM", which its help page says removes a byte order mark if present, is how older scripts guard against that.

visits-unicode.txt is UTF-16 little-endian, the encoding that R’s help on connections names for Windows “Unicode” text files: two bytes for each ordinary character (69 00 is i) after the mark ff fe. Read as UTF-8 it stops with “invalid multibyte string”, and the five warnings about “embedded nulls” give it away, because every second byte is zero. With fileEncoding = "UTF-16LE" it reads into 6 rows and 5 columns. readr takes the same name as locale = locale(encoding = "UTF-16LE"), while fread() refuses UTF-16 and asks you to recode the file to UTF-8, as the last two lines of the run show.

Older tools also save one byte per character, in Latin-1:

latin1.R, typed at the R console Python · latin1_session.py
from session import run, version

version()
# R reads latin1.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f latin1.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f latin1.R
> # Names saved in Latin-1, one byte per character.
> p <- read.csv("data/names-latin1.csv")
> p$name
[1] "M\xfcller" "Jos\xe9"  
> validUTF8(p$name)
[1] FALSE FALSE
> try(nchar(p$name))
Error in nchar(p$name) : invalid multibyte string, element 1
> # fileEncoding = converts the file to UTF-8 as it is read.
> read.csv("data/names-latin1.csv", fileEncoding = "latin1")$name
[1] "Müller" "José"  
> readr::read_csv("data/names-latin1.csv", show_col_types = FALSE,
+                 locale = readr::locale(encoding = "latin1"))$name
[1] "Müller" "José"  
> # encoding = only labels strings; it cannot turn UTF-16 into text.
> try(read.delim("data/visits-unicode.txt", encoding = "UTF-16LE"),
+     silent = TRUE)[1]
[1] "Error in make.names(col.names, unique = TRUE) : \n  invalid multibyte string at '<ff><fe>i'\n"
Warning messages:
1: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 1 appears to contain embedded nulls
2: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 2 appears to contain embedded nulls
3: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 3 appears to contain embedded nulls
4: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 4 appears to contain embedded nulls
5: In read.table(file = file, header = header, sep = sep, quote = quote,  :
  line 5 appears to contain embedded nulls

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 latin1_session.py

Read as UTF-8, Müller comes back as "M\xfcller": the byte fc is not valid UTF-8, validUTF8() says so, and nchar() stops with an error. fileEncoding = "latin1" converts the text as it is read, and so does readr’s locale. The argument called encoding is another thing: its help page says it marks strings as Latin-1 or UTF-8 and is not used to re-encode the input, so it cannot rescue a UTF-16 file, as the last line of the run shows.

encoding = where fileEncoding = is needed

read.delim(f, encoding = "UTF-16LE") fails exactly as if you had passed nothing. Give read.csv() and read.delim() the argument fileEncoding = "UTF-16LE", give readr locale = locale(encoding = "UTF-16LE"), and recode the file to UTF-8 before fread().

Some of these defaults are younger than the scripts you will inherit, which is why old code passes arguments it no longer needs:

Timeline · 5 events in order The releases that changed what a reader returns Walk through the releases in order. Each one explains a habit you will meet in older scripts.
  1. R 3.0.0

    The UTF-8 mark is dropped reading

    readLines() and scan(), and so read.table() and read.csv(), discard a UTF-8 byte-order mark in a UTF-8 locale. The encoding name "UTF-8-BOM" drops it in any locale.

  2. R 4.0.0

    Text stays text new default

    read.table() and data.frame() default to stringsAsFactors = FALSE, so read.csv() returns character columns instead of factors. Older scripts pass the argument by hand.

  3. R 4.2.0

    UTF-8 on Windows Windows

    R uses UTF-8 as the native encoding on recent Windows systems, so a UTF-8 file, with or without the mark, reads there as it does on macOS and Linux.

  4. readr 2.0.0

    A new reading engine speed

    readr's second edition reads and writes through the vroom package, which uses several threads. It read lazily at first, column values only when they were used.

  5. readr 2.1.0

    Eager reading again new default

    All readr functions read the whole file at once again by default, because lazy reading kept files locked on Windows. options(readr.read_lazy = TRUE) brings lazy reading back.

The timeline has 5 events, from R 3.0.0 to readr 2.1.0.

Checkpoint

Check: Encodings and byte-order marks

  1. Question 1 of 2 A text file starts with the bytes ff fe 69 00 64 00. What is it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: UTF-16 little-endian text that starts with "id"

    The run printed ff fe 69 00 64 00 09 00 for visits-unicode.txt: the mark, then i, d and a tab, two bytes each.

  2. Question 2 of 2 Which call reads data/visits-unicode.txt into 6 rows and 5 columns?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: read.delim("data/visits-unicode.txt", fileEncoding = "UTF-16LE")

    For read.csv() and read.delim() the argument is fileEncoding; for readr it is locale = locale(encoding = "UTF-16LE").

Semicolon files need a reader that expects decimal commas

Where a comma is the decimal mark, a CSV cannot also use commas between fields, so many European exports separate them with semicolons; Microsoft’s guide to exporting text from Excel says its separator can be changed in the Windows region settings. fees-eu.csv writes P-301’s weight as 61,5 and its fee as 1.250,00:

decimal_comma.R, typed at the R console Python · decimal_comma_session.py
from session import run, version

version()
# R reads decimal_comma.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f decimal_comma.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f decimal_comma.R
> # Fees exported on a computer set to write decimal commas.
> readLines("data/fees-eu.csv")
[1] "id;weight_kg;fee"    "P-301;61,5;1.250,00" "P-302;70,25;980,50" 
[4] "P-303;58;12.400,00" 
> try(read.csv("data/fees-eu.csv"))
Error in read.table(file = file, header = header, sep = sep, quote = quote,  : 
  more columns than column names
> str(read.csv2("data/fees-eu.csv"))
'data.frame':	3 obs. of  3 variables:
 $ id       : chr  "P-301" "P-302" "P-303"
 $ weight_kg: num  61.5 70.2 58
 $ fee      : chr  "1.250,00" "980,50" "12.400,00"
> readr::read_csv2("data/fees-eu.csv")
ℹ Using "','" as decimal and "'.'" as grouping mark. Use `read_delim()` for more control.
Rows: 3 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ";"
chr (1): id
dbl (1): weight_kg
num (1): fee

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 3 × 3
  id    weight_kg    fee
  <chr>     <dbl>  <dbl>
1 P-301      61.5  1250 
2 P-302      70.2   980.
3 P-303      58   12400 
> f <- data.table::fread("data/fees-eu.csv")
> sapply(f, class)
         id   weight_kg         fee 
"character"   "numeric" "character" 

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 decimal_comma_session.py

read.csv() split the lines at their commas, found more fields than column names and stopped. read.csv2() expects semicolons and decimal commas, the variant the R Data Import/Export manual calls appropriate where the comma is the decimal point, so the weights are numbers, but 1.250,00 stays text: its dot is a grouping mark, and read.csv2() does not remove one. readr’s read_csv2() reads with a locale: The regional settings a program reads and writes text with: the character encoding, the decimal mark and the grouping mark of numbers, and the names of months. readr's locale() sets them for one read. whose decimal mark is , and grouping mark ., as its first message says, and returns both columns as numbers. fread() found the semicolon and the decimal comma by itself and, like read.csv2(), kept the grouped fee as text.

Reader Separator Decimal mark Grouping mark . in 1.250,00
read.csv() , . the read stops
read.csv2() ; , kept, so the column is text
readr::read_csv2() ; , removed: 1250
data.table::fread() found by itself found by itself kept, so the column is text

For any other mix, name the marks: readr::read_delim(f, delim = "\t", locale = locale(decimal_mark = ",")) reads tabs with decimal commas, and a locale with the decimal comma takes . as its grouping mark unless you set grouping_mark to something else.

Indian digit grouping

Indian exports group digits in lakhs and crores, so one lakh twenty-five thousand rupees is written 1,25,000. readr’s number parser drops the grouping mark wherever it stands, so parse_number("1,25,000") is 125000, the same as for 125,000, while base R’s as.numeric() gives NA for both. The fees section below reads them with col_number().

Checkpoint

Check: Separators and decimal marks

  1. Question 1 of 2 In fees-eu.csv, P-301's fee is written 1.250,00. Which reader returned it as the number 1250?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: readr::read_csv2()

    Only read_csv2() of the four removed the grouping mark. For other mixes, read_delim() with locale(decimal_mark, grouping_mark) names both marks.

  2. Question 2 of 2 What does readr::parse_number("12.400,00", locale = readr::locale(decimal_mark = ",", grouping_mark = ".")) return?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 12400

    With the dot as grouping mark and the comma as decimal mark, 12.400,00 is twelve thousand four hundred, the fee of P-303 in fees-eu.csv.

Only the codes you list become NA

A missing value has many spellings in exported files: NA, an empty field, a dash, n/a, or a code such as -999, a sentinel value: A stand-in that a file or a device writes for a missing measurement, such as -999. It must be turned into NA before any summary, or it is counted as a real value. More in “Indexing and subsetting vectors”. A reader turns into NA only the spellings it is told about. The clinic’s export writes a missing weight in three ways, and the run reads it with the defaults, then with every code listed:

missing_codes.R, typed at the R console Python · missing_codes_session.py
from session import run, version

version()
# R reads missing_codes.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f missing_codes.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f missing_codes.R
> # The export writes a missing weight as "NA", as "-" or as nothing.
> v <- read.csv("data/visits.csv")
> v$ward
[1] "General"  "ICU"      ""         "General"  "Day care" "ICU"     
> v$weight_kg
[1] "61.5" "-"    NA     "72"   "58"   ""    
> # List every code the file uses for "missing":
> v <- read.csv("data/visits.csv", na.strings = c("NA", "-", ""))
> v$ward
[1] "General"  "ICU"      NA         "General"  "Day care" "ICU"     
> v$weight_kg
[1] 61.5   NA   NA 72.0 58.0   NA
> r <- readr::read_csv("data/visits.csv", na = c("", "NA", "-"),
+                      show_col_types = FALSE)
> r$weight_kg
[1] 61.5   NA   NA 72.0 58.0   NA
> f <- data.table::fread("data/visits.csv", na.strings = c("", "NA", "-"))
> f$weight_kg
[1] 61.5   NA   NA 72.0 58.0   NA
> # A blank in a column of numbers is NA for every reader:
> kg <- "id,kg\nP-1,61.5\nP-2,\nP-3,70"
> read.csv(text = kg)$kg
[1] 61.5   NA 70.0
> readr::read_csv(I(kg), show_col_types = FALSE)$kg
[1] 61.5   NA 70.0
> data.table::fread(text = kg)$kg
[1] 61.5   NA 70.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_codes_session.py

With its defaults, read.csv() turned only the text NA into NA. The - stayed, and because one value of weight_kg is not a number, the whole column stayed text, with P-106’s blank weight as "". Listing the three spellings in na.strings made weight_kg numeric and the blank ward NA; readr’s na and fread’s na.strings gave the same weights. The last three lines show the one case where the readers agree without help: a blank field in a column of numbers is NA, which the help page of read.table() states for logical, integer, numeric and complex fields.

List the codes in the read

na = c("", "NA", "-") in the call records which codes the file uses, where a reader of the script can see them. Converting afterwards with as.numeric() also turns "-" into NA, but it turns every other unexpected value into NA too, with one warning for all of them.

The field holds read.csv() read_csv() fread()
NA NA NA NA
nothing, in a text column "" NA ""
nothing, in a column of numbers NA NA NA
- "-", and the column is text "-" "-"

Key idea

A reader knows only the missing-value codes you give it: any other code keeps its column as text, or, if it is a number such as -999, is counted as a value.

Checkpoint

Check: Missing-value codes

  1. Question 1 of 2 read.csv() with its defaults reads a column holding 61.5, -, NA and 72. What does it return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A character vector, with the NA turned into NA and the dash kept as "-"

    The run printed "61.5" "-" NA "72" "58" "" for weight_kg: text, until na.strings listed every code.

  2. Question 2 of 2 With its defaults, what does read.csv() make of an empty field in a text column, and in a column of numbers?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "" in the text column, NA in the column of numbers

    The ward of P-103 came back as "", and the kg value of P-2 as NA, from the same reader with the same defaults.

Name the column types, then read problems()

A guess is fine for a first look and wrong for a script that runs again next month on a new export. With the dash listed as missing, readr guessed weight_kg as numbers and left the other four columns as text. spec() prints that guess as code, and the run then names every type itself:

types_problems.R, typed at the R console Python · types_problems_session.py
from session import run, version

version()
# R reads types_problems.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f types_problems.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f types_problems.R
> library(readr)
> v <- read_csv("data/visits.csv", na = c("", "NA", "-"))
Rows: 6 Columns: 5
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (4): id, visit_date, ward, fee
dbl (1): weight_kg

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
> spec(v)
cols(
  id = col_character(),
  visit_date = col_character(),
  ward = col_character(),
  fee = col_character(),
  weight_kg = col_double()
)
> # Say what each column is, then ask what failed.
> v <- read_csv("data/visits.csv", na = c("", "NA", "-"),
+   col_types = cols(
+     id = col_character(),
+     visit_date = col_date(format = "%d/%m/%Y"),
+     ward = col_factor(levels = c("General", "ICU", "Day care")),
+     fee = col_number(),
+     weight_kg = col_double()
+   ))
Warning message:
One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
  dat <- vroom(...)
  problems(dat) 
> problems(v)[, 1:4]
# A tibble: 2 × 4
    row   col expected           actual      
  <int> <int> <chr>              <chr>       
1     5     2 date like %d/%m/%Y 31/04/2026  
2     6     4 a number           Rs. 3,450.50
> v
# A tibble: 6 × 5
  id    visit_date ward        fee weight_kg
  <chr> <date>     <fct>     <dbl>     <dbl>
1 P-101 2026-04-03 General    1250      61.5
2 P-102 2026-04-05 ICU       12400      NA  
3 P-103 2026-04-07 <NA>        450      NA  
4 P-104 NA         General  125000      72  
5 P-105 2026-04-09 Day care     NA      58  
6 P-106 2026-04-12 ICU        2300      NA  
> # A script that must not go on with failed values stops here:
> try(stop_for_problems(v))
Error : 2 parsing failures
> # read.csv() takes types too, but stops at the first value that fails:
> try(read.csv("data/visits.csv", colClasses = c(fee = "numeric")))
Error in scan(file = file, what = what, sep = sep, quote = quote, dec = dec,  : 
  scan() expected 'a real', got '"1'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 types_problems_session.py

The column specification: The type a reader gives each column of a file. readr prints the one it guessed and spec() returns it; col_types sets it yourself, so the read no longer depends on a guess. that spec() printed is the code col_types takes, so you can paste it and correct what the guess got wrong: col_date(format = "%d/%m/%Y") for the visit dates, col_number() for the fees, which drops grouping marks and text around the number, and col_factor() with the three wards in a fixed order. readr converted every field it could and warned about the rest. problems(v) lists each parsing failure: A value that cannot be converted to its column's type, such as a day the month does not have in a date column. readr turns it into NA and records its row, column, expected type and text in problems().: its row, counted from the top of the file with the header as row 1, its column, what readr expected and the text it found. Row 5 is P-104, whose date 31/04/2026 does not exist, and row 6 is P-105, whose fee Rs. 3,450.50 is not a plain number. Both became NA; every other NA in the table is a missing-value code that the read listed.

Interactive · step by step Five fields of visits.csv, before and after col_types Step through the fields. On the left is the text in the file, on the right the value readr returned when it was told each column's type.
Five fields of visits.csv, before and after col_types, step 6 of 6. Showing 1,25,000, Rs. 3,450.50, visit date of P-104, -, Day care, 125000, NA, NA, NA, Day care. In focus: NA, NA.1,25,000fee of P-104Rs. 3,450.50fee of P-105visit date of P-104day 31 of month 4-weight_kg of P-102Day careward of P-105125000col_number()NAcol_number(), in problems()NAcol_date(), in problems()NA"-" is listed in naDay carefactor level 3 of 3

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Grouping marks. col_number() drops grouping marks wherever they stand, so the Indian grouping 1,25,000 becomes 125000, the same as 125,000 would.
  2. A word with a dot. "Rs. 3,450.50" fails: the dot after Rs stops col_number() from reading a number. The value becomes NA, and its row and column go into problems().
  3. A date that does not exist. col_date(format = "%d/%m/%Y") reads day, month and year, so P-101's date is the third of April. P-104's date names day 31 of month 4, a day April does not have, so it becomes NA and is listed too.
  4. A missing-value code. Because "-" is in the na argument, P-102's weight becomes NA before any type is chosen. It is not a parsing failure, so problems() does not list it.
  5. A category with fixed levels. col_factor() with levels General, ICU and Day care keeps the categories in that order, and a ward outside the list would become NA and a problem.
  6. Two failures, both listed. The read converted every other field. problems() names exactly the two values that failed, so no value fails without a record; the only other NA values are the missing codes you listed.

stop_for_problems(v) turns the list into an error, the line a script should run when it must not go on with holes in its data. read.csv() also takes types, through colClasses, but it stops at the first fee it cannot read, without saying which rows would have failed.

Rs. before a number

parse_number() reads the first number it finds in a text. Written Rs. 3,450.50, the fee fails and becomes NA with a warning; written Rs.3,450.50, with no space, the dot after Rs starts a decimal and the result is a silent 0.345. Take the currency word off before you parse, as the next run does.

currency.R, typed at the R console Python · currency_session.py
from session import run, version

version()
# R reads currency.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f currency.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f currency.R
> library(readr)
> fees <- c("1,25,000", "Rs. 3,450.50", "Rs.3,450.50", "Rs 3,450.50")
> parse_number(fees)
Warning: 1 parsing failure.
row col expected       actual
  2  -- a number Rs. 3,450.50

[1] 125000.000         NA      0.345   3450.500
attr(,"problems")
# A tibble: 1 × 4
    row   col expected actual      
  <int> <int> <chr>    <chr>       
1     2    NA a number Rs. 3,450.50
> # Take the currency word off first:
> parse_number(sub("^Rs\\.?\\s*", "", fees))
[1] 125000.0   3450.5   3450.5   3450.5

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 currency_session.py

CSV & Excel Data Profiler (EDA Report) See each column's type, blanks and unusual values in a CSV in your browser, before you write its col_types.

Key idea

Name every column’s type in the read, then check problems(): a value that failed is NA, and the list says which row, which column and what text.

Checkpoint

Check: Column types and problems()

  1. Question 1 of 2 problems(v) reports a failure at row 5 of a file whose first line is the header. Which data row is that, counting the first visit as 1?

    Type a number.

    A hint

    The header is row 1.

    Show the answer: Question 1 of 2

    Answer: 4 data row

    problems() counts rows of the file, header included, so row 5 is the fourth visit, P-104, whose date names day 31 of month 4, a day April does not have.

  2. Question 2 of 2 Which entry of col_types turned the fee 1,25,000 into 125000?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: fee = col_number()

    In the run, col_number() gave 1250, 12400, 450, 125000 and 2300, and listed the one fee it could not read.

CSV keeps the text; RDS keeps the object

Writing is reading in reverse, and it loses what text cannot hold. The run writes three visits, with a factor and a Date, to CSV and reads them back, then does the same with an RDS file:

roundtrip.R, typed at the R console Python · roundtrip_session.py
from session import run, version

version()
# R reads roundtrip.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f roundtrip.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f roundtrip.R
> visits <- data.frame(
+   id = c("P-101", "P-102", "P-103"),
+   ward = factor(c("ICU", "General", "ICU"),
+                 levels = c("General", "ICU", "Day care")),
+   visit_date = as.Date(c("2026-04-03", "2026-04-05", "2026-04-07")),
+   fee = c(1250, 12400, 450)
+ )
> dir.create("out")
> write.csv(visits, "out/visits.csv")
> readLines("out/visits.csv")
[1] "\"\",\"id\",\"ward\",\"visit_date\",\"fee\"" 
[2] "\"1\",\"P-101\",\"ICU\",2026-04-03,1250"     
[3] "\"2\",\"P-102\",\"General\",2026-04-05,12400"
[4] "\"3\",\"P-103\",\"ICU\",2026-04-07,450"      
> sapply(read.csv("out/visits.csv"), class)
          X          id        ward  visit_date         fee 
  "integer" "character" "character" "character"   "integer" 
> write.csv(visits, "out/visits.csv", row.names = FALSE)
> readr::write_csv(visits, "out/visits-readr.csv")
> sapply(readr::read_csv("out/visits-readr.csv", show_col_types = FALSE), class)
         id        ward  visit_date         fee 
"character" "character"      "Date"   "numeric" 
> # RDS keeps the object itself:
> saveRDS(visits, "out/visits.rds")
> same <- readRDS("out/visits.rds")
> identical(same, visits)
[1] TRUE
> levels(same$ward)
[1] "General"  "ICU"      "Day care"
> # dput() writes the data as R code that rebuilds it:
> dput(visits[1:2, c("id", "ward")])
structure(list(id = c("P-101", "P-102"), ward = structure(2:1, levels = c("General", 
"ICU", "Day care"), class = "factor")), row.names = 1:2, class = "data.frame")

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 roundtrip_session.py

write.csv() wrote the row names as a first column with an empty header, which came back as a column called X, so pass row.names = FALSE. Back from text, the factor ward is character, the Date is character, and fee became an integer because 1250 was written without decimals. readr’s write_csv() writes no row names, and read_csv() recognised the dates, but the factor is text and its unused level, Day care, is gone. saveRDS() writes one R object to an RDS file: A file that holds one R object exactly as it was, written by saveRDS() and read back by readRDS(): types, factor levels and attributes all survive., and readRDS() returned an identical() copy, levels and all. dput() prints an object as R code, structure(list(...)), the way to paste a few rows into a question or a test.

save() keeps several objects in one .RData file, and load() brings them back under their saved names:

save_load.R, typed at the R console Python · save_load_session.py
from session import run, version

version()
# R reads save_load.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f save_load.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f save_load.R
> fee <- 1250
> save(fee, file = "work.RData")
> fee <- 999       # a newer value, worked out later
> load("work.RData")
> fee              # load() put the old value back, without a word
[1] 1250
> e <- new.env()
> load("work.RData", envir = e)
> ls(e)
[1] "fee"
> e$fee
[1] 1250

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 save_load_session.py

load() into your workspace

load("work.RData") put the old fee, 1250, over the newer 999 and printed nothing; its help page warns that it replaces all existing objects with the same names. Load into an environment of its own, load(f, envir = e <- new.env()), or write single objects with saveRDS() and read them into a name you choose.

Excel needs one thing more for text beyond English letters. write_excel_csv() starts the file with a UTF-8 byte-order mark, which, its help page says, tells Excel the file is UTF-8 encoded, and by default it puts every field in quotes, where write_csv() quotes only the fields that need it:

excel_csv.R, typed at the R console Python · excel_csv_session.py
from session import run, version

version()
# R reads excel_csv.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f excel_csv.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f excel_csv.R
> # Two Hindi names, written for R and for Excel.
> staff <- data.frame(name = c("अनीता", "रवि"), ward = c("ICU", "General"))
> readr::write_csv(staff, "staff.csv")
> readr::write_excel_csv(staff, "staff-excel.csv")
> readBin("staff.csv", "raw", 6)
[1] 6e 61 6d 65 2c 77
> readBin("staff-excel.csv", "raw", 6)
[1] ef bb bf 22 6e 61
> readr::read_csv("staff.csv", show_col_types = FALSE)$name
[1] "अनीता" "रवि"  
> readr::read_csv("staff-excel.csv", show_col_types = FALSE)$name
[1] "अनीता" "रवि"  

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 excel_csv_session.py

The first six bytes show both differences: the mark ef bb bf, then 22, the quote before name. The Hindi names read back the same in R from either file; the mark changes only how Excel decodes them. Which way out fits depends on who reads the file next. Answer three questions about yours:

Interactive · answer and compare Which way should this data leave R? Answer three questions about who reads the file next. The table marks the ways that fit and says why the others do not.
Will people or programs outside R open the file?
Must factors, their levels and dates come back exactly as they were?
Will someone open it in Excel with text beyond English letters, such as Hindi names?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Six ways to write data out of R, against the four things that decide between them
Function Types come back Opens outside R Non-English text in Excel Safe to read back
write.csv() Writes: a CSV; pass row.names = FALSE Poor ward and visit_date came back as text, fee as integer Good any program that reads CSV opens it Mixed no mark tells Excel the file is UTF-8 Good read.csv() builds a new data frame
readr::write_csv() Writes: a UTF-8 CSV with no row names Mixed the Date came back; the factor came back as text Good any program that reads CSV opens it Mixed no mark tells Excel the file is UTF-8 Good read_csv() builds a new tibble
readr::write_excel_csv() Writes: a UTF-8 mark, then every field in quotes Mixed as write_csv(): dates come back, factors do not Good a CSV, after three marker bytes Good ef bb bf tells Excel the text is UTF-8 Good read_csv() drops the mark again
saveRDS() Writes: one R object in a compressed file Good identical() was TRUE, Day care level and all Poor only R reads it Poor Excel cannot open it Good readRDS() returns it; you choose the name
save() Writes: several named objects in one file Good every object comes back exactly Poor only R reads it Poor Excel cannot open it Poor load() overwrote fee 999 with 1250, silently
dput() Writes: R code that rebuilds the object Good levels and classes are written out as code Poor R code, not a data file Poor Excel cannot use it Mixed it is code: read it before you run it
How the answers decide
  • “Will people or programs outside R open the file?” Yes, someone else opens it: rules out saveRDS(), save() and dput(). Only R reads RDS and RData files, and dput() writes R code. Other programs need a text file such as a CSV.
  • “Will people or programs outside R open the file?” No, only R reads it back and “Must factors, their levels and dates come back exactly as they were?” Yes, exactly: rules out write.csv(), readr::write_csv() and readr::write_excel_csv(). A CSV stores text. In the run the factor came back as text and its unused level, Day care, was lost.
  • “Will people or programs outside R open the file?” Yes, someone else opens it and “Must factors, their levels and dates come back exactly as they were?” Yes, exactly: adds a caveat to write.csv(), readr::write_csv() and readr::write_excel_csv(). A CSV cannot carry types, so send a short data dictionary with it that lists each column's type and levels.
  • “Will someone open it in Excel with text beyond English letters, such as Hindi names?” Yes: rules out write.csv() and readr::write_csv(). Only write_excel_csv() starts the file with the UTF-8 mark that tells Excel how to read it.
  • “Will people or programs outside R open the file?” No, only R reads it back: adds a caveat to save(). load() puts objects back under their saved names and replaces any of the same name. saveRDS() and readRDS() let you choose the name.

In short: a CSV for people and other programs, with a list of every column’s type and levels beside it; write_excel_csv() when the file will be opened in Excel; RDS between R sessions, to keep the object exactly; dput() for a few rows in a question; and load() only into an environment of its own.

Checkpoint

Check: Writing CSV, RDS and code

  1. Question 1 of 2 After write.csv(visits, f) and read.csv(f), the data frame has a new first column called X. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: write.csv() wrote the row names as a column with an empty header, and read.csv() named it X

    Pass row.names = FALSE to write.csv(), or use readr::write_csv(), which never writes row names.

  2. Question 2 of 2 A factor has the levels General, ICU and Day care, and no row is in Day care. Which round trip keeps all three levels?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: saveRDS() then readRDS()

    A CSV holds text values, so levels that no row uses cannot be in it. Send them in a data dictionary, or use RDS between R sessions.

Practise: predict, then check

Ten lines poke at what this lesson showed. Note what you expect from each, and only then look at the run.

Before you look

Predict what read.csv() and read_csv() make of an empty ward, what read.csv() makes of a weight column holding -, with and without na.strings = "-", what read.csv2() does with the fee 1.250,00, what parse_number("Rs 1,25,000") returns, what write.csv() writes for a one-row data frame, and which levels a factor has after saveRDS() and readRDS(). Then type the lines into your own R.

practice.R, typed at the R console Python · practice_session.py
from session import run, version

version()
# R reads practice.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f practice.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f practice.R
> # Predict each result before you look.
> read.csv(text = "id,ward\n1,\n2,ICU")$ward
[1] ""    "ICU"
> readr::read_csv(I("id,ward\n1,\n2,ICU"), show_col_types = FALSE)$ward
[1] NA    "ICU"
> read.csv(text = "kg\n61.5\n-\n70")$kg
[1] "61.5" "-"    "70"  
> read.csv(text = "kg\n61.5\n-\n70", na.strings = "-")$kg
[1] 61.5   NA 70.0
> read.csv2(text = "kg;fee\n61,5;1.250,00")$fee
[1] "1.250,00"
> readr::parse_number("Rs 1,25,000")
[1] 125000
> write.csv(data.frame(kg = 61.5), stdout())
"","kg"
"1",61.5
> f <- tempfile(fileext = ".rds")
> saveRDS(factor("ICU", levels = c("General", "ICU")), f)
> levels(readRDS(f))
[1] "General" "ICU"    

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

read.csv() kept the empty ward as "" where read_csv() gave NA; the dash kept the weights as text until it was listed; read.csv2() kept the grouped fee as text; parse_number() dropped Rs and both grouping commas; the row-name column came first in the written CSV; and the RDS file kept both levels of the factor, the unused one included.

Then work through the questions below. They start with the idea you have shown least so far, and each one explains its answer the moment you check it:

Practice that adapts to you

Practise this lesson

15 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 Which readers return an object that R also treats as a data frame? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 15

    Answer:

    • readr::read_csv()
    • read.csv()
    • data.table::fread()

    All three tables work wherever R expects a data frame; they differ in printing and in their extra features.

  2. Question 2 of 15 A script reads a 3 GB CSV of sensor readings every night, and the read takes most of the run. Which change does the most for its speed?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: Read it with data.table::fread(), which uses several threads

    readr's second edition reads with several threads too; the point is to move off the base reader for large files, and to name column types so nothing is guessed twice.

  3. Question 3 of 15 names-latin1.csv holds Müller in Latin-1. What does read.csv() without fileEncoding print for it in a UTF-8 session?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: "M\xfcller"

    validUTF8() returned FALSE for both names, and fileEncoding = "latin1" turned them into Müller and José.

  4. Question 4 of 15 What does the encoding argument of read.csv() do, according to its help page?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: It marks the strings as Latin-1 or UTF-8, without re-encoding the input

    Two arguments with similar names do different jobs: fileEncoding converts, encoding only labels.

  5. Question 5 of 15 Another program shows the first column of your CSV as id. What does that tell you?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: The file is UTF-8 with a byte-order mark, and that program read the three mark bytes as Latin-1 letters

    R drops the mark in a UTF-8 locale; other tools may not. Write without a mark (write_csv()) for those tools, and with one (write_excel_csv()) for Excel.

  6. Question 6 of 15 Which field separator and decimal mark does read.csv2() expect?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: A semicolon between fields and a comma as decimal mark

    read.csv2() read 61,5 as 61.5 in the run, but left the grouped 1.250,00 as text.

  7. Question 7 of 15 A file separates fields with tabs and writes decimal commas, such as 61,5. Which calls read its numbers correctly? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 7 of 15

    Answer:

    • readr::read_delim(f, delim = "\t", locale = readr::locale(decimal_mark = ","))
    • read.delim2(f)

    Name both things a file decides: the separator and the decimal mark, and the grouping mark when numbers have one.

  8. Question 8 of 15 Which argument of read.csv() lists the codes that mean "missing"?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: na.strings

    read.csv() uses na.strings, readr uses na, and fread() uses na.strings, each a character vector.

  9. Question 9 of 15 A weight column writes a missing weight as -999, and you read it without listing that code. The real weights average about 65 kg. What happens to mean(weight_kg, na.rm = TRUE)?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: It is far too low, because each -999 counts as a weight

    A sentinel value hides in plain sight. List it when you read (na.strings = c("NA", "-999")), or replace it before any summary.

  10. Question 10 of 15 What does spec(v) return for a tibble read by readr?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: The column types readr used, written as a cols() call you can pass to col_types

    Paste the printed specification into col_types, correct the guesses, and the read no longer depends on them.

  11. Question 11 of 15 What does readr::parse_number("Rs.3,450.50") return, as the lesson ran it?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: 0.345, with no warning

    A silent wrong number is worse than NA. Strip currency words with sub("^Rs\\.?\\s*", "", x) before parse_number().

  12. Question 12 of 15 A nightly report must stop, not continue, when any fee fails to parse. What do you add after the read?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: stop_for_problems(v)

    In the run, stop_for_problems(v) on the clinic file stopped with "2 parsing failures", so a report never sums fees with a hole in them.

  13. Question 13 of 15 Which call writes visits to CSV without a row-name column?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: write.csv(visits, "visits.csv", row.names = FALSE)

    readr::write_csv() never writes row names, which is one reason scripts prefer it.

  14. Question 14 of 15 fee <- 1250; save(fee, file = "work.RData"); fee <- 999; load("work.RData"). What is fee now?

    Type a number.

    Show the answer: Question 14 of 15

    Answer: 1250

    load() replaced fee with the saved value, without a message. Load into new.env() to keep both.

  15. Question 15 of 15 You write a table with Hindi names using readr::write_csv(). It reads back fine in R, but a colleague sees garbled names in Excel. What do you change?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Write it with readr::write_excel_csv()

    The run showed the difference in the first bytes: 6e 61 6d 65 for write_csv() and ef bb bf for write_excel_csv().

Interview questions

Warm-up: a column of fees comes back from read.csv() as text. What usually causes it, and how do you find the values responsible?

One value that is not a plain number keeps the whole column as text, because read.csv() reads every column as text and converts it only if every value converts. The usual culprits are a currency word (Rs. 3,450.50), grouping commas (1,250) and a missing-value code the reader was not told about (-, n/a). To find them, convert a copy and look at what failed: x[is.na(suppressWarnings(as.numeric(x))) & !is.na(x)] lists the values that are present but not numbers. With readr, read with col_types = cols(fee = col_number()) and problems() gives each failed value with its row. Then fix the cause in the read: list the missing codes in na.strings, and remove currency words and grouping marks before converting, rather than letting them turn into silent NA values.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A nightly R script reads a supplier's CSV with read_csv() and sums the fee column. It has run for months; one morning the total is 40 % lower, and nothing failed. How do you find what changed, and how do you make the script stop next time instead of reporting a wrong total?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    You must send a cleaned table of 200,000 survey rows to two colleagues: one opens files in Excel, the other works in R. Some names are in Devanagari, and two columns are factors whose level order matters. What do you send each of them, and how do you check that it arrived intact?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • read.csv(), readr::read_csv() and data.table::fread() return a data frame, a tibble and a data.table, choose column types in different ways, and disagree about empty text fields.
  • The encoding decides how bytes become text: R drops a UTF-8 byte-order mark in a UTF-8 locale, a UTF-16LE file needs fileEncoding = "UTF-16LE", and encoding = only labels strings.
  • Semicolon files with decimal commas need read.csv2(), read_csv2() or a locale(), and only readr removes the grouping mark in a value such as 1.250,00.
  • A reader makes NA only of the codes you list, so pass every code the file uses, such as "-" or "-999", in na.strings or na.
  • Name each column’s type with col_types, read problems() for the values that failed, and stop a script with stop_for_problems() when it must not continue.
  • A CSV keeps text only: factors, levels and dates come back changed, so use saveRDS() between R sessions, write_excel_csv() for Excel, and load() only into an environment of its own.

Terms in this lesson

byte-order mark
Bytes at the very start of a text file that announce its encoding: ef bb bf for UTF-8, ff fe for UTF-16 little-endian. R drops the UTF-8 one in a UTF-8 locale; other tools may keep it as part of the first name.
character encoding
The rule that turns a file's bytes into characters. UTF-8 is the usual one; a file saved as UTF-16LE or Latin-1 reads as garbled text or fails unless its encoding is named when it is read.
column specification
The type a reader gives each column of a file. readr prints the one it guessed and spec() returns it; col_types sets it yourself, so the read no longer depends on a guess.
delimited file
A text file with one record per line whose fields are separated by one character: a comma in a CSV, a semicolon in many European exports, a tab in a TSV.
locale
The regional settings a program reads and writes text with: the character encoding, the decimal mark and the grouping mark of numbers, and the names of months. readr's locale() sets them for one read.
parsing failure
A value that cannot be converted to its column's type, such as a day the month does not have in a date column. readr turns it into NA and records its row, column, expected type and text in problems().
RDS file
A file that holds one R object exactly as it was, written by saveRDS() and read back by readRDS(): types, factor levels and attributes all survive.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.