phibble

Guides

Types

Column types

Every column has one type from the Phibble\Type enum. The print header shows tibble’s abbreviation for each type.

Type Values Printed as
Type::Float float <dbl>
Type::Int int <int>
Type::Bool bool <lgl>
Type::String string <chr>
Type::Factor level labels, stored as codes <fct>, or <ord> when ordered
Type::Date DateTimeImmutable at midnight UTC <date>
Type::DateTime DateTimeImmutable <dttm>
Type::List any PHP value, including data frames <list>

null is NA in every type. $df->types() returns the types by column name and $df->type('x') returns one.

Inference

phibble infers a column’s type from its PHP values the way R’s c() combines values:

  • Bools and ints combine to int, and ints and floats combine to float.
  • Any string makes the column a string column, and the other values are converted to text.
  • DateTimeInterface values make a datetime column.
  • Arrays and other objects make a list-column.
  • A column that is entirely NA is bool, R’s logical type.
$df = DataFrame::fromColumns([
    'logical' => [true, false, null],
    'whole' => [1, 2, null],
    'mixed' => [1, 2.5, null],
    'with_bool' => [true, 2, 3],
    'text' => ['a', 1, null],
    'when' => [new DateTimeImmutable('2024-03-01 09:30'), null, null],
    'nested' => [[1, 2], [3], null],
    'empty' => [null, null, null],
]);

echo $df->glimpse();
Rows: 3
Columns: 8
$ logical   <lgl> TRUE, FALSE, NA
$ whole     <int> 1, 2, NA
$ mixed     <dbl> 1.0, 2.5, NA
$ with_bool <int> 1, 2, 3
$ text      <chr> "a", "1", NA
$ when      <dttm> 2024-03-01 09:30:00, NA, NA
$ nested    <list> <1, 2>, 3, <NULL>
$ empty     <lgl> NA, NA, NA

Text read from CSV files and databases is guessed differently. The input and output guide describes that.

Casts

cast() converts columns to other types. Conversions follow R’s as.*() functions. A value that cannot be converted becomes NA, and phibble raises one E_USER_WARNING per column, as R does with “NAs introduced by coercion”.

  • Floats convert to int by truncating toward zero.
  • "TRUE", "true", "T", and "True" convert to true. "yes" and "1" do not.
  • Floats convert to text with up to 15 significant digits, in fixed or scientific notation, whichever is shorter.
set_error_handler(function (int $level, string $message): bool {
    echo "Warning: {$message}\n";

    return true;
});

$df = DataFrame::fromColumns([
    'x' => [2.7, -2.7, '3.9', 'abc'],
    'flag' => ['TRUE', 'T', 'yes', '0'],
    'n' => [1.0, 0.1 + 0.2, 1e5, 1 / 3],
]);

$cast = $df->cast(['x' => 'int', 'flag' => 'bool', 'n' => 'string']);

echo $cast;
Warning: NAs introduced by coercion to int in column 'x' (1 value).
Warning: NAs introduced by coercion to bool in column 'flag' (2 values).
# A tibble: 4 × 3
      x flag  n                
  <int> <lgl> <chr>            
1     2 TRUE  1                
2    -2 TRUE  0.3              
3     3 NA    1e+05            
4    NA NA    0.333333333333333

A factor converts to int or float as its 1-based level codes, as as.integer(factor) does in R. Converting it to text gives its labels. Dates convert to numbers as days since 1970-01-01, and datetimes as seconds since the epoch.

Factors

A factor column stores an ordered list of level labels and one code per row. asFactor() sorts the unique values to make the levels unless levels are given. Numbers sort numerically and text sorts in byte order. Values that are not among explicit levels become NA.

levels() returns the labels. storage('x')->codes() returns the codes, which are 0-based indexes into the levels. An ordered factor sorts by level position.

$penguins = DataFrame::fromCsv('penguins.csv')->asFactor('species');

echo json_encode($penguins->levels('species')), "\n";
echo json_encode(array_slice($penguins->storage('species')->codes(), 150, 5)), "\n";

$sizes = tibble(size: ['small', 'large', 'medium', 'small'])
    ->asOrdered('size', ['small', 'medium', 'large']);

echo $sizes->arrange(['size']), "\n";
var_dump($sizes->isOrdered('size'));
["Adelie","Chinstrap","Gentoo"]
[0,0,2,2,2]
# A tibble: 4 × 1
  size  
  <ord> 
1 small 
2 small 
3 medium
4 large 
bool(true)

The forcats functions are fct* methods. Each returns a new data frame with the column’s levels changed.

Method Effect
fctRelevel($col, $levels, after: 0) Moves the given levels to the front, or after a position.
fctLevels($col, $levels) Sets the full level order.
fctRev($col) Reverses the level order.
fctInorder($col) Orders levels by first appearance.
fctInfreq($col) Orders levels by frequency, most common first.
fctReorder($col, $by, $summary) Orders levels by a summary of another column, the median by default.
fctRecode($col, ['new' => 'old']) Renames levels.
fctCollapse($col, ['new' => ['a', 'b']]) Merges levels.
fctDrop($col) Removes levels that no row uses.
$penguins = DataFrame::fromCsv('penguins.csv')->asFactor('species');

$show = fn (string $label, DataFrame $f) => print(\str_pad($label, 26).json_encode($f->levels('species'))."\n");

$show('sorted (default)', $penguins);
$show('fctRelevel Gentoo', $penguins->fctRelevel('species', ['Gentoo']));
$show('fctInfreq', $penguins->fctInfreq('species'));
$show('fctReorder by body_mass', $penguins->fctReorder('species', 'body_mass', fn ($xs) => array_sum(array_filter($xs)) / count(array_filter($xs))));
$show('fctRev', $penguins->fctRev('species'));
$show('fctRecode', $penguins->fctRecode('species', ['Chinstrap penguin' => 'Chinstrap']));
$show('fctCollapse', $penguins->fctCollapse('species', ['brush-tailed' => ['Adelie', 'Gentoo']]));
sorted (default)          ["Adelie","Chinstrap","Gentoo"]
fctRelevel Gentoo         ["Gentoo","Adelie","Chinstrap"]
fctInfreq                 ["Adelie","Gentoo","Chinstrap"]
fctReorder by body_mass   ["Adelie","Chinstrap","Gentoo"]
fctRev                    ["Gentoo","Chinstrap","Adelie"]
fctRecode                 ["Adelie","Chinstrap penguin","Gentoo"]
fctCollapse               ["brush-tailed","Chinstrap"]

Dates and datetimes

A date column holds DateTimeImmutable values at midnight UTC. A datetime column holds DateTimeImmutable values with their time and time zone. CSV text in ISO 8601 form is parsed as a date when it has no time, and as a datetime otherwise. Text without an offset is read as UTC.

$visits = DataFrame::fromCsvString(<<<'CSV'
    day,checked_in
    2024-01-02,2024-01-02T09:15:00Z
    2024-01-03,2024-01-03 14:40:00
    CSV);

echo $visits, "\n";
echo $visits->cast(['day' => 'float', 'checked_in' => 'float']);
# A tibble: 2 × 2
  day        checked_in         
  <date>     <dttm>             
1 2024-01-02 2024-01-02 09:15:00
2 2024-01-03 2024-01-03 14:40:00
# A tibble: 2 × 2
    day checked_in
  <dbl>      <dbl>
1 19724 1704186900
2 19725 1704292800

List-columns

A list-column holds any PHP value in each row, such as arrays or other data frames. nest() collapses columns into a list-column of data frames, one row per unique combination of the remaining columns. unnest() expands it again.

$penguins = DataFrame::fromCsv('penguins.csv')->select(['species', 'island', 'body_mass']);

$nested = $penguins->nest(['island', 'body_mass'], into: 'data');
echo $nested, "\n";

$sizes = $nested->mutate(['n' => fn ($r) => $r->data->nrow()]);
echo $sizes->select(['species', 'n']), "\n";

echo $nested->unnest('data')->head(3);
# A tibble: 3 × 2
  species   data              
  <chr>     <list>            
1 Adelie    <tibble [152 × 2]>
2 Gentoo    <tibble [124 × 2]>
3 Chinstrap <tibble [68 × 2]> 
# A tibble: 3 × 2
  species       n
  <chr>     <int>
1 Adelie      152
2 Gentoo      124
3 Chinstrap    68
# A tibble: 3 × 3
  species island    body_mass
  <chr>   <chr>         <int>
1 Adelie  Torgersen      3750
2 Adelie  Torgersen      3800
3 Adelie  Torgersen      3250

Model data

toModelData() returns columns in the form phit’s model functions read. Numbers, bools, and strings are plain lists. Dates and datetimes become days and seconds since the epoch. A factor becomes an array with its levels, its 0-based codes, and whether it is ordered, so a model keeps the data frame’s level order, including unused levels. A list-column throws.

$df = tibble(
    mass: [3750.0, 3800.0, null],
    male: [true, false, true],
    species: ['Adelie', 'Gentoo', 'Adelie'],
)->asFactor('species', ['Adelie', 'Chinstrap', 'Gentoo']);

echo json_encode($df->toModelData(), JSON_PRETTY_PRINT), "\n";
{
    "mass": [
        3750,
        3800,
        null
    ],
    "male": [
        true,
        false,
        true
    ],
    "species": {
        "levels": [
            "Adelie",
            "Chinstrap",
            "Gentoo"
        ],
        "codes": [
            0,
            2,
            0
        ],
        "ordered": false
    }
}

From phpstats/phibble@36d0f3f.