---
title: "Reading and writing data: CSV, Parquet, HDF5 and more"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Reading and writing data: CSV, Parquet, HDF5 and more}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
has <- function(p) requireNamespace(p, quietly = TRUE)
```

Real data come in many formats, and synthetic data often have to be delivered
in the format of the original. SynthesizerPlus offers one pair of functions,
`read_data()` and `write_data()`, that dispatch on the file extension.

```{r}
library(SynthesizerPlus)
```

## Supported formats

```{r}
supported_formats()
```

Only CSV, TSV, RDS and RData need nothing beyond base R. The other backends
are optional: install the package listed in the `read`/`write` columns for
the formats you need (for example `install.packages("arrow")` for Parquet and
Feather). The `available` column shows what is installed on this machine.
Text formats can be compressed by appending `.gz`, `.bz2` or `.xz`.

## Writing and reading

```{r}
dir <- tempfile("sp-formats-")
dir.create(dir)
syn <- synthesize(iris, n = 300, seed = 1)

write_data(syn, file.path(dir, "iris.csv"))
write_data(syn, file.path(dir, "iris.tsv.gz"))
write_data(syn, file.path(dir, "iris.rds"))
list.files(dir)
```

Text formats do not store R classes. Pass a `template` to restore them —
factor levels, `Date`, `POSIXct` (with time zone), integer and logical
columns:

```{r}
x <- read_data(file.path(dir, "iris.csv"))
class(x$Species)
x <- read_data(file.path(dir, "iris.csv"), template = iris)
levels(x$Species)
```

`match_types()` does the same for any data frame you read by other means.

### Parquet and Feather (Apache Arrow)

Columnar formats are the usual choice for large data in data science
pipelines (Spark, Python/pandas, DuckDB, Polars). Parquet is read and written
with **arrow** or, if that is not installed, the lightweight **nanoparquet**;
Feather / Arrow IPC needs **arrow**.

```{r, eval = has("arrow") || has("nanoparquet")}
f <- file.path(dir, "iris.parquet")
write_data(syn, f)
str(read_data(f))
```

### HDF5

HDF5 is common in scientific computing and is readable from Python (`h5py`,
`pandas`), MATLAB and Julia. `write_data()` stores one dataset per column in
a group (`dataset = "data"` by default), with attributes that record column
names, factor levels, dates and time zones; `read_data()` restores the data
frame exactly. A plain 1- or 2-dimensional dataset written by other software
can be read by giving its name in `dataset`.

```{r, eval = has("hdf5r")}
f <- file.path(dir, "iris.h5")
write_data(syn, f)
identical(read_data(f), syn)
```

### JSON, NDJSON, Excel, SPSS, Stata, SAS, fst

```{r, eval = has("jsonlite")}
write_data(head(syn, 2), file.path(dir, "iris.json"))
cat(readLines(file.path(dir, "iris.json")), sep = "\n")
```

```{r, eval = has("haven")}
write_data(syn, file.path(dir, "iris.sav"))   # SPSS
str(read_data(file.path(dir, "iris.sav")))
write_data(syn, file.path(dir, "iris.dta"))   # Stata: names are made valid
names(read_data(file.path(dir, "iris.dta")))
```

Value labels from SPSS/Stata files are converted to factors when reading.
Stata only allows letters, digits and underscores in variable names, so
`write_data()` renames columns such as `Sepal.Length` (with a warning).

## Synthesising straight from a file to a file

```{r}
real <- read_data(file.path(dir, "iris.rds"))
synthesize(real, n = 1000, seed = 1, file = file.path(dir, "synthetic.csv.gz"))
nrow(read_data(file.path(dir, "synthetic.csv.gz")))
```

## Very large synthetic data sets

`generate_to_file()` generates data in chunks, so the full data set never has
to fit in memory. Appendable formats (CSV, TSV, NDJSON) are written to a
single file; for other formats a directory of part files is created, which
`read_data()` — or `arrow::open_dataset()`, Spark, DuckDB — reads as one
table.

```{r}
fit <- fit_synthesizer(iris)
res <- generate_to_file(fit, n = 50000, path = file.path(dir, "big.csv"),
                        chunk_size = 10000, seed = 1)
nrow(read_data(file.path(dir, "big.csv")))

res <- generate_to_file(fit, n = 50000, path = file.path(dir, "big_rds"),
                        format = "rds", chunk_size = 20000, seed = 1)
basename(res$files)
nrow(read_data(file.path(dir, "big_rds")))
```

```{r, eval = FALSE}
# e.g. 100 million rows as a partitioned Parquet data set
generate_to_file(fit, n = 1e8, path = "synthetic_parquet", format = "parquet",
                 chunk_size = 1e6, seed = 1)
ds <- arrow::open_dataset("synthetic_parquet")
```

Output is reproducible for a given `seed` and `chunk_size`.

```{r, include = FALSE}
unlink(dir, recursive = TRUE)
```
