Guides
Reading and writing data
CSV files
DataFrame::fromCsv() reads delimited text the way readr’s read_csv() and read_delim() do. It takes a path (files ending in .gz are decompressed) or an open stream. DataFrame::fromCsvString() reads from a string.
readr’s options are named arguments in camelCase:
| Option | Default | Meaning |
|---|---|---|
types |
guess | Column types, as a compact string or by name. |
delimiter |
',' |
The field separator. |
na |
['', 'NA'] |
Text that is read as NA. |
colNames |
true |
true reads a header row, false names columns X1, X2, and so on, and a list gives the names. |
select |
all | Columns to keep, in this order. |
skip |
0 |
Lines to skip before the header. |
nMax |
all | The most rows to read. |
comment |
'' |
Lines starting with this text are skipped. |
quote |
'"' |
The quoting character. |
trimWs |
true |
Trim spaces around unquoted values. |
skipEmptyRows |
true |
Skip blank lines. |
guessMax |
1000 |
Rows used to guess types. |
nameRepair |
'unique' |
How to repair empty or duplicated names. |
decimalMark, groupingMark |
'.', none |
Number formatting, as in readr’s locale(). |
$text = <<<'CSV'
# exported 2024-06-01
id;zip;score;visited
1;02134;3,5;2024-05-01
2;10001;-;2024-05-03
3;94110;4,25;2024-05-04
CSV;
$df = DataFrame::fromCsvString(
$text,
types: ['zip' => 'c'],
delimiter: ';',
na: ['-'],
comment: '#',
decimalMark: ',',
);
echo $df;# A tibble: 3 × 4
id zip score visited
<int> <chr> <dbl> <date>
1 1 02134 3.5 2024-05-01
2 2 10001 NA 2024-05-03
3 3 94110 4.25 2024-05-04
Column types
types accepts readr’s compact string, with one letter per column:
| Letter | Type |
|---|---|
c |
string |
i |
int |
d |
float |
n |
float, reading numbers with grouping marks and other characters |
l |
bool |
f |
factor |
D |
date |
T |
datetime |
t |
string (phibble has no time-of-day type) |
? |
guess |
_ or - |
skip the column |
types also accepts an array by column name. A value is a Type, a type name such as 'factor', or a letter. The key .default sets the type of every column not named.
$penguins = DataFrame::fromCsv('penguins.csv', types: 'ffddii_i');
echo $penguins->glimpse(), "\n";
$some = DataFrame::fromCsv('penguins.csv', types: ['species' => 'factor', '.default' => '_'], select: ['species']);
echo $some->glimpse();Rows: 344
Columns: 7
$ species <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Ad…
$ island <fct> Torgersen, Torgersen, Torgersen, Torgersen, Torgersen, Tor…
$ bill_len <dbl> 39.1, 39.5, 40.3, NA, 36.7, 39.3, 38.9, 39.2, 34.1, 42.0, …
$ bill_dep <dbl> 18.7, 17.4, 18.0, NA, 19.3, 20.6, 17.8, 19.6, 18.1, 20.2, …
$ flipper_len <int> 181, 186, 195, NA, 193, 190, 181, 195, 193, 190, 186, 180,…
$ body_mass <int> 3750, 3800, 3250, NA, 3450, 3650, 3625, 4675, 3475, 4250, …
$ year <int> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…
Rows: 344
Columns: 1
$ species <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adelie…
Guessing
Without explicit types, phibble guesses each column from up to guessMax rows, sampled the way readr 2 samples them. A column is:
- bool if every value is
T,F,TRUE,FALSE,True,False,true, orfalse, - int if every value is a whole number (readr guesses double here),
- float if every value is a number, including
InfandNaN, - date if every value is an ISO 8601 date,
- datetime if every value is an ISO 8601 date and time,
- string otherwise.
Numbers with leading zeros such as 007 stay strings, as in readr.
When a value cannot be parsed as its column’s type, a row has the wrong number of fields, or a quoted field is never closed, phibble still returns the data. The value becomes NA, and the read raises one E_USER_WARNING that lists the problems. Rows are numbered as in readr::problems(): the first data row is row 2, and skipped, blank, and comment lines are not counted.
Quotes
Quoted fields follow readr 2’s rules. A field is quoted when its first character is the quote character. Inside a quoted field, a doubled quote is a literal quote. Any other quote turns quoting off or back on and is dropped, so "x "y" z" reads as x y z, and "x "y",z" reads as x y,z. A quote inside a field that does not start with one is kept as it is. With escapeDouble: false, the quotes inside a quoted field are kept, and only the opening and closing quotes are dropped.
Files written with PHP’s fputcsv() and its default backslash escape contain \" instead of "". readr does not treat the backslash as an escape, and neither does phibble unless escapeBackslash: true is passed.
set_error_handler(function (int $level, string $message): bool {
echo "Warning: {$message}\n";
return true;
});
$df = DataFrame::fromCsvString("id,score\n1,2.5\n2,oops\n3\n", types: ['score' => 'double']);
echo $df;Warning: One or more parsing issues: row 4: expected 2 columns, got 1; column 'score': 1 value could not be parsed as float.
# A tibble: 3 × 2
id score
<int> <dbl>
1 1 2.5
2 2 NA
3 3 NA
Reading in chunks
DataFrame::fromCsvChunked() reads a file a chunk of rows at a time, as readr’s read_delim_chunked() does. It passes each chunk to a callback with the 1-based index of the chunk’s first row, and stops if the callback returns false. DataFrame::csvChunks() returns the same chunks as a generator keyed by that index. Memory depends on the chunk size, so a file larger than the memory limit can be filtered down as it is read:
$kept = [];
foreach (DataFrame::csvChunks('feed.tsv', chunkSize: 10_000, delimiter: "\t", types: ['.default' => 'c']) as $chunk) {
$kept[] = $chunk->filter(col('category')->eq('Running'));
}
$running = DataFrame::concat($kept);Types not given are guessed from the first chunk, and every later chunk is parsed as those types. As in readr, guessMax defaults to the chunk size. Give every type, as above, to skip guessing. A value in a later chunk that does not fit its column’s type becomes NA. Factors without levels take their levels from each chunk.
Every other option works as it does in fromCsv(). Each chunk’s problems raise their own warning, with rows numbered from the start of the file.
When the phit extension is loaded (0.2.0 or later), the chunks are read natively with Phit\CsvReader, about three times faster on a wide tab-separated file. The results are the same as the PHP reader’s, which is used otherwise.
Writing CSV
writeCsv($path) writes a file and toCsv() returns the text. The format follows readr’s write_csv(). Values are quoted only when needed, NA is written as NA, logicals as TRUE and FALSE, dates in ISO 8601, and datetimes in UTC with a Z. Factors are written as their labels, and list-columns throw.
$df = tribble(
['name', 'note', 'score', 'seen'],
['Ana', 'likes "quotes"', 1e5, true],
['Ben', 'a, b', null, false],
);
echo $df->toCsv();name,note,score,seen
Ana,"likes ""quotes""",100000,TRUE
Ben,"a, b",NA,FALSE
The options are delimiter, na, append, colNames, quote ('needed', 'all', or 'none'), escape, and eol.
JSON
toJson() writes a list of records, and toJson('columns') writes an object of columns. NA is written as null and kept in every record. DataFrame::fromJson() reads either shape and detects which one it has.
$df = tibble(id: [1, 2], score: [2.5, null]);
echo $df->toJson(), "\n";
echo $df->toJson('columns'), "\n";
echo DataFrame::fromJson('{"id": [1, 2], "ok": [true, null]}');[{"id":1,"score":2.5},{"id":2,"score":null}]
{"id":[1,2],"score":[2.5,null]}
# A tibble: 2 × 2
id ok
<int> <lgl>
1 1 TRUE
2 2 NA
Databases
DataFrame::fromPdo() runs a query with positional or named bindings and loads the result. PostgreSQL and MySQL report each column’s declared type, and phibble uses it:
| Declared type | Column type |
|---|---|
Text types, JSON, UUID, TIME, intervals, arrays, and enums |
string |
BOOLEAN (PostgreSQL) |
bool |
Integer types and MySQL’s YEAR |
int |
REAL, DOUBLE PRECISION, FLOAT, and DOUBLE |
float |
NUMERIC and DECIMAL |
float, or int when every value is a whole number |
DATE |
date |
TIMESTAMP, TIMESTAMPTZ, and DATETIME |
datetime |
A text column stays text, so 'TRUE' and '2024-01-02' in a TEXT column are strings. Only values without a declared type are guessed with the CSV rules. SQLite stores values without a column type, so its results are always guessed, and so are results from other drivers.
NUMERIC and DECIMAL values become doubles, as RPostgres and RMariaDB read them. A double holds about 15 significant digits, so 12345678901234567.89 loses its last digits. Prices such as NUMERIC(10,2) fit. Pass a string type to keep a decimal exact:
DataFrame::fromPdo($pdo, 'SELECT id, balance FROM accounts', types: ['balance' => 'string']);Explicit types always take precedence over declared ones. fromQuery(), the Laravel ->toDataFrame() macro, and SqlTable::collect() follow the same rules.
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE visits (id INTEGER, clinic TEXT, bp REAL, seen TEXT)');
$pdo->exec("INSERT INTO visits VALUES (1, 'north', 121.5, '2024-05-01'), (2, 'south', NULL, '2024-05-02'), (3, 'north', 133.0, '2024-05-02')");
$df = DataFrame::fromPdo($pdo, 'SELECT * FROM visits WHERE clinic = ?', ['north']);
echo $df;# A tibble: 2 × 4
id clinic bp seen
<int> <chr> <dbl> <date>
1 1 north 122. 2024-05-01
2 3 north 133 2024-05-02
DataFrame::fromQuery() loads a query builder without depending on its library:
- A Laravel query builder, Eloquent builder, or relation is read through
toSql(),getBindings(), and its connection’s PDO object. - A Doctrine DBAL
QueryBuilderis read throughgetSQL()andgetParameters(). Pass a PDO or DBAL connection as the second argument to run the SQL through PDO. Without one, the builder runs the query itself and phibble guesses every column, since DBAL results carry no column types.
To keep a query in the database and run verbs there, use a SQL table instead.
Laravel collections
DataFrame::fromCollection() reads any object with an all() or toArray() method, such as a Laravel Collection. The items may be arrays, objects, or Eloquent models. toCollection() returns an Illuminate\Support\Collection of row arrays and throws when Laravel is not installed.
// Any object with all() or toArray() works, such as a Laravel Collection
// of arrays or Eloquent models. This stand-in avoids a Laravel dependency.
$collection = new class
{
public function all(): array
{
return [
['id' => 1, 'name' => 'Ana'],
(object) ['id' => 2, 'name' => 'Ben'],
];
}
};
echo DataFrame::fromCollection($collection);# A tibble: 2 × 2
id name
<int> <chr>
1 1 Ana
2 2 Ben
From phpstats/phibble@36d0f3f.