write.csv() экспортирует датафрейм
Вытащить датафрейм из R в файл, который смогут открыть другие программы, — это один вызов:
write.csv(df, "results.csv", row.names = FALSE)
Вот строка, которую стоит запомнить, включая row.names = FALSE, который мы обоснуем чуть ниже. Первый аргумент — датафрейм, второй — имя файла, а функция ничего не возвращает: вся её работа — побочный эффект создания файла.
Увидеть, что именно производит write.csv(), можно, не трогая диск, потому что она принимает любое соединение там, где ждёт имя файла, — включая stdout(), который просто печатает:
Вот ваш CSV: строка заголовка, по строке на запись, текстовые значения в кавычках, числа голыми. (Кавычки — это quote = TRUE по умолчанию; любой распространённый ридер CSV с этим справляется, так что не трогайте.) Запись в настоящий файл — тот же вызов с "results.csv" вместо stdout().
Почему row.names = FALSE должен быть вашим умолчанием
У каждого датафрейма есть имена строк — обычно просто числа 1, 2, 3... — и по умолчанию write.csv() записывает их в файл дополнительным первым столбцом с пустым заголовком. Посмотрите, как выглядит вывод по умолчанию и что происходит, когда этот файл совершает обратный путь в R:
Экспорт обзавёлся безымянным первым столбцом со значениями "1", "2", а при обратном импорте read.csv() пришлось как-то его назвать, и она изобрела X. Это и есть загадочный столбец X, озадачивающий каждого новичка: это не данные, это нумерация строк R, просачивающаяся в ваш файл. Проделайте так несколько циклов экспорта и импорта — и накопите X, X.1, X.2...
Имена строк почти никогда не несут настоящей информации, а когда несут, эта информация всё равно принадлежит нормальному столбцу. Так что доведите row.names = FALSE до рефлекса. Пожалуй, это и должно было быть значением по умолчанию; но не стало, из соображений обратной совместимости, поэтому привычка на вас.
Куда попадает файл
write.csv(df, "results.csv") пишет в рабочий каталог R, и если это не то место, где вы ожидали, файл будто исчезает. Он не потерялся; он просто лежит в той папке, которую печатает getwd(). Это та же ловушка, что и при чтении, только в зеркальном отражении, и разрешают её те же два инструмента:
getwd() # the folder your file went to
write.csv(df, "C:/Users/ada/results/out.csv", row.names = FALSE) # or: remove all doubt
Полные пути (прямые слеши работают на всех платформах, включая Windows) делают скрипты однозначными. Как задаётся рабочий каталог и как им управлять, разобрано в статье про рабочий каталог.
write.table() для других разделителей — и вопрос о дозаписи
write.csv() на самом деле тонкая обёртка над универсальной write.table(), преднастроенной под запятые. Вызывайте write.table() напрямую, когда нужна другая форма, — чаще всего разделение табуляцией:
Разделение табуляцией, без кавычек — формат, который предпочитают многие инструменты дальше по цепочке. Обратите внимание, что в write.table() вы задаёте всё явно: она не наследует запятую и кавычки из умолчаний write.csv.
Одного write.csv() делать отказывается — дозаписи: передача append = TRUE даст вам предупреждение о том, что настройка проигнорирована, потому что дозапись переписала бы строку заголовка в середину файла. Если вам действительно нужно добавлять строки в существующий CSV — скажем, вести журнал результатов по запускам, — спуститесь к write.table() и подавите заголовок сами:
write.table(new_rows, "log.csv", sep = ",",
append = TRUE, col.names = FALSE, row.names = FALSE)
А если вы в tidyverse, readr::write_csv(df, "out.csv") — двойник read_csv(): тот же вывод, быстрее на больших фреймах, и она вообще никогда не пишет имена строк — одним аргументом меньше держать в голове.
saveRDS() и readRDS(): точный полный цикл
CSV — формат с потерями. Он хранит текст, и точка: столбец Date становится строкой "2026-08-07", фактор становится своими метками с потерянным порядком уровней, атрибуты исчезают, а каждый тип приходится заново угадывать при импорте (обычно верно, иногда нет — ведущие нули, это про вас). И CSV может держать только одну прямоугольную таблицу: у подогнанной модели, списка или матрицы с dimnames нет никакого представления в CSV.
Когда файл предназначен для R — сохранить сегодняшние очищенные данные для завтрашней сессии, — пропускайте текстовые форматы целиком:
saveRDS(df, "clean_data.rds") # today
df <- readRDS("clean_data.rds") # tomorrow: identical object, types intact
saveRDS() сериализует один объект R, любой объект, в точности. readRDS() возвращает его байт в байт: факторы сохраняют уровни, даты остаются датами, ничего не угадывается заново. Она работает на чём угодно — на датафрейме, на списке датафреймов, на подогнанной регрессионной модели. Файл .rds — сжатый двоичный, поэтому обычно он ещё и меньше CSV. Единственная плата в том, что открыть его не может ничто, кроме R, — и именно поэтому правило большого пальца таково: CSV — чтобы делиться с людьми и другими инструментами, RDS — чтобы сохранить для себя.
save() и load() — и почему RDS обычно выигрывает
Вам встретится и более старая пара. save() записывает несколько именованных объектов в один файл .RData, а load() восстанавливает их в ваше рабочее пространство под исходными именами:
save(df, model, params, file = "session.RData")
load("session.RData") # df, model, params silently appear
Это удобство и является проблемой. Имена переменных выбирает load(), а не вы: он вслепую подсаживает объекты в ваше окружение, молча затирая всё, что уже носило эти имена. Через полгода никто не вспомнит, что вообще содержится в session.RData, не загрузив его, чтобы это выяснить.
У readRDS() честный интерфейс: имя выбираете вы (df <- readRDS(...)), ничто не затирается за вашей спиной, и один файл означает один объект. Если только вам специально не нужно упаковать несколько объектов в один файл, предпочитайте RDS. (А если пакет всё же нужен, положите объекты в именованный список и сохраните список через saveRDS() — получите тот же эффект без всяких сюрпризов.)
Что вы уносите с собой
write.csv(df, "out.csv", row.names = FALSE)— весь экспорт, и да,row.names = FALSEвсегда.- Загадочный столбец
Xпри обратном импорте — это имена строк, просочившиеся в файл; умолчание их записало, а вам не следует. - Файлы попадают в рабочий каталог —
getwd()находит «потерянные» экспорты, полные пути их предотвращают. write.table()умеет другие разделители и (сcol.names = FALSE) настоящую дозапись;write.csv()намеренно дозаписывать не может.saveRDS()/readRDS()точно сохраняют и восстанавливают любой объект R — типы, факторы, атрибуты целы. CSV — чтобы делиться, RDS — для себя.- Предпочитайте RDS вместо
save()/load(): явные имена, никаких молчаливых перезаписей.
Дальше: тип данных, который заслуживает отдельной страницы, прежде чем укусит вас при импорте, — даты.
Часто задаваемые вопросы
Как экспортировать датафрейм в CSV в R?
write.csv(df, "output.csv", row.names = FALSE). Часть row.names = FALSE важна: без неё R запишет свои внутренние номера строк дополнительным первым столбцом, который всплывёт как загадочный столбец с именем X, когда кто-нибудь заново импортирует файл.
Почему в моём экспортированном CSV появляется столбец X при обратном чтении?
Потому что по умолчанию у write.csv стоит row.names = TRUE — она записала номера строк безымянным первым столбцом, а read.csv на обратном пути назвала этот столбец X. Экспортируйте с row.names = FALSE, и фантомный столбец исчезнет.
В чём разница между write.csv и saveRDS в R?
write.csv создаёт текстовую таблицу, которую может открыть кто угодно, но хранит она только текст: уровни факторов, тип Date, атрибуты и нетабличные объекты уплощаются или теряются, а типы приходится заново угадывать при импорте. saveRDS сохраняет один объект R байт в байт; readRDS возвращает его ровно таким, каким он был. Используйте CSV, чтобы делиться с другими, и RDS, чтобы сохранять для собственных сессий R.
Куда write.csv сохраняет файл?
В текущий рабочий каталог R, если вы не указали полный путь. Выполните getwd(), чтобы увидеть, где это, — если ваш файл будто испарился, он лежит в той папке, которую печатает getwd(). Передача полного пути вроде "C:/Users/ada/results/out.csv" снимает неоднозначность.