phplyr

Reference

Single-table functions

Rows

filter() keeps rows where every condition is true. Conditions are row closures, vector helpers such as between() or if_any(), or the name of a logical column. NA counts as false. filter_out() drops the rows where every condition is true and keeps rows where a condition is NA, so filter() and filter_out() with the same conditions split a table without overlap.

$schools = DataFrame::fromCsv('schools.csv');

// Several conditions are combined with AND; NA counts as false.
echo $schools |>
    filter(fn ($r) => $r->prior_score > 75, between('study_hours', 2, 6)) |>
    head(4);

// filter_out() drops the rows where every condition is true.
echo "\n", $schools |> filter_out(fn ($r) => $r->prior_score < 70) |> count();
$schools = DataFrame::fromCsv('schools.csv');

// Several conditions are combined with AND; NA counts as false.
echo phplyr($schools)
    ->filter(fn ($r) => $r->prior_score > 75, between('study_hours', 2, 6))
    ->head(4);

// filter_out() drops the rows where every condition is true.
echo "\n", phplyr($schools)->filterOut(fn ($r) => $r->prior_score < 70)->count();
# A tibble: 0 × 4
# ℹ 4 variables: school <chr>, study_hours <dbl>, prior_score <dbl>,
#   score <dbl>
# A tibble: 1 × 1
      n
  <int>
1    59

In a grouped table, the second closure argument is the row’s group, and vector helpers are evaluated per group:

// Inside a group, the second closure argument is the group's table, and
// vector helpers such as max() are evaluated per group.
echo DataFrame::fromCsv('schools.csv') |>
    group_by('school') |>
    filter(fn ($r, $g) => $r->score === max('score')($g)) |>
    select('school', 'score') |>
    head(4);
// Inside a group, the second closure argument is the group's table, and
// vector helpers such as max() are evaluated per group.
echo phplyr(DataFrame::fromCsv('schools.csv'))
    ->groupBy('school')
    ->filter(fn ($r, $g) => $r->score === max('score')($g))
    ->select('school', 'score')
    ->head(4);
# A tibble: 4 × 2
# Groups:   school [4]
  school    score
  <chr>     <dbl>
1 school-01  91.0
2 school-02  92.1
3 school-03  84.0
4 school-04  83.9

arrange() sorts rows. It is stable and puts NA last. Keys are column names, desc(), or expressions, which are evaluated over the whole table. It ignores grouping unless you pass _byGroup: true.

echo DataFrame::fromCsv('housing.csv') |>
    arrange('bedrooms', desc(fn ($r) => $r->monthly_rent / $r->area_m2));
echo phplyr(DataFrame::fromCsv('housing.csv'))
    ->arrange('bedrooms', desc(fn ($r) => $r->monthly_rent / $r->area_m2));
# A tibble: 8 × 3
  area_m2 bedrooms monthly_rent
    <dbl>    <int>        <dbl>
1    45.7        1        1509.
2    37          1         960.
3    49.1        1        1268.
4    57.4        2        1805.
5    66.4        2        2022.
6    74.4        2        1879.
7    93.9        3        2387.
8    92.4        3        2240.

distinct() keeps unique rows over the given columns, or over all columns. Named arguments add computed columns first, and _keepAll: true keeps every column from the first row of each combination.

The slice family picks rows within each group:

Function Rows
slice(1, 3), slice(-1) By 1-based position; negative positions drop rows.
slice_head(n:), slice_tail(n:) The first or last rows.
slice_min(x, n:), slice_max(x, n:) The rows with the smallest or largest values, with ties kept unless withTies: false.
slice_sample(n:, replace:, weightBy:) Random rows. Seed with mt_srand().
head(n) The first rows of the whole table, ignoring groups.
top_n(n, wt), top_frac(prop, wt) Superseded by slice_max(). Rows ranked within n, in table order.
sample_n(size), sample_frac(size) Superseded by slice_sample().
add_row(x: 1, _before:, _after:) New rows of values; columns not given are NA.

Each takes prop: in place of n:, and _by:.

$schools = DataFrame::fromCsv('schools.csv') |> group_by('school');

// The two best students in each of the first three schools.
echo $schools |>
    slice_max('score', n: 2) |>
    filter(fn ($r) => $r->school <= 'school-03') |>
    select('school', 'score');
$schools = phplyr(DataFrame::fromCsv('schools.csv'))->groupBy('school')->get();

// The two best students in each of the first three schools.
echo phplyr($schools)
    ->sliceMax('score', n: 2)
    ->filter(fn ($r) => $r->school <= 'school-03')
    ->select('school', 'score');
# A tibble: 6 × 2
# Groups:   school [3]
  school    score
  <chr>     <dbl>
1 school-01  91.0
2 school-01  88.2
3 school-02  92.1
4 school-02  88.7
5 school-03  84.0
6 school-03  83.1

Columns

select() keeps columns by name or with tidyselect helpers. An associative array renames while selecting. rename() renames with new: 'old' arguments, rename_with() applies a function to selected names, and relocate() moves columns to the front or _before / _after another column.

$schools = DataFrame::fromCsv('schools.csv');

echo implode(', ', ($schools |> select('school', ends_with('score')))->columns()), "\n";
echo implode(', ', ($schools |> select(except('school')))->columns()), "\n";
echo implode(', ', ($schools |> select(where('numeric')))->columns()), "\n";
echo implode(', ', ($schools |> select(['hours' => 'study_hours'], everything()))->columns()), "\n";
echo implode(', ', ($schools |> relocate('score', _before: 'study_hours'))->columns()), "\n";
echo implode(', ', ($schools |> rename_with(strtoupper(...), contains('score')))->columns()), "\n";
$schools = DataFrame::fromCsv('schools.csv');

echo implode(', ', (phplyr($schools)->select('school', ends_with('score'))->get())->columns()), "\n";
echo implode(', ', (phplyr($schools)->select(except('school'))->get())->columns()), "\n";
echo implode(', ', (phplyr($schools)->select(where('numeric'))->get())->columns()), "\n";
echo implode(', ', (phplyr($schools)->select(['hours' => 'study_hours'], everything())->get())->columns()), "\n";
echo implode(', ', (phplyr($schools)->relocate('score', _before: 'study_hours')->get())->columns()), "\n";
echo implode(', ', (phplyr($schools)->renameWith(strtoupper(...), contains('score'))->get())->columns()), "\n";
school, prior_score, score
study_hours, prior_score, score
study_hours, prior_score, score
hours, school, prior_score, score
school, score, study_hours, prior_score
school, study_hours, PRIOR_SCORE, SCORE

mutate() adds or replaces columns. A value can be a row closure, a vector helper, a list with one value per row, a scalar, or null to drop the column. Later arguments see the columns made by earlier ones.

echo DataFrame::fromCsv('housing.csv') |>
    mutate(
        rent_per_m2: fn ($r) => round($r->monthly_rent / $r->area_m2, 2),
        size: if_else(fn ($r) => $r->area_m2 >= 60, 'large', 'small'),
        rank: min_rank(desc('monthly_rent')),
    ) |>
    arrange('rank');
echo phplyr(DataFrame::fromCsv('housing.csv'))
    ->mutate(
        rent_per_m2: fn ($r) => round($r->monthly_rent / $r->area_m2, 2),
        size: if_else(fn ($r) => $r->area_m2 >= 60, 'large', 'small'),
        rank: min_rank(desc('monthly_rent')),
    )
    ->arrange('rank');
# A tibble: 8 × 6
  area_m2 bedrooms monthly_rent rent_per_m2 size   rank
    <dbl>    <int>        <dbl>       <dbl> <chr> <int>
1    93.9        3        2387.        25.4 large     1
2    92.4        3        2240.        24.2 large     2
3    66.4        2        2022.        30.5 large     3
4    74.4        2        1879.        25.3 large     4
5    57.4        2        1805.        31.4 small     5
6    45.7        1        1509.        33.0 small     6
7    49.1        1        1268.        25.8 small     7
8    37          1         960.        25.9 small     8

mutate() takes dplyr’s options: _by groups for one call, _keep: 'none' keeps only the grouping and new columns, and _before or _after place the new columns. transmute() is mutate() that keeps only the new columns.

$housing = DataFrame::fromCsv('housing.csv');

// _keep: 'none' keeps only new columns (and grouping columns).
echo $housing |> mutate(rent_k: fn ($r) => $r->monthly_rent / 1000, _keep: 'none') |> head(3);

// _by groups for this call only; mean() is recycled within each group.
echo "\n", $housing |>
    mutate(vs_bedroom_avg: fn ($r, $g) => $r->monthly_rent - mean('monthly_rent')($g), _by: 'bedrooms', _after: 'bedrooms') |>
    head(4);
$housing = DataFrame::fromCsv('housing.csv');

// _keep: 'none' keeps only new columns (and grouping columns).
echo phplyr($housing)->mutate(rent_k: fn ($r) => $r->monthly_rent / 1000, _keep: 'none')->head(3);

// _by groups for this call only; mean() is recycled within each group.
echo "\n", phplyr($housing)
    ->mutate(vs_bedroom_avg: fn ($r, $g) => $r->monthly_rent - mean('monthly_rent')($g), _by: 'bedrooms', _after: 'bedrooms')
    ->head(4);
# A tibble: 3 × 1
  rent_k
   <dbl>
1   1.88
2   1.81
3   2.02
# A tibble: 4 × 4
  area_m2 bedrooms vs_bedroom_avg monthly_rent
    <dbl>    <int>          <dbl>        <dbl>
1    74.4        2          -22.9        1879.
2    57.4        2          -97.2        1805.
3    66.4        2          120.         2022.
4    49.1        1           22.3        1268.

pull() returns one column as a PHP list. It takes a name or a position (1-based, negative from the end, the last column by default) and an optional name: column whose values become the keys.

$housing = DataFrame::fromCsv('housing.csv');

echo $housing |> distinct('bedrooms') |> arrange('bedrooms');
echo "\n", json_encode($housing |> arrange('area_m2') |> pull('monthly_rent')), "\n";
echo json_encode($housing |> pull(-1)), "\n";
$housing = DataFrame::fromCsv('housing.csv');

echo phplyr($housing)->distinct('bedrooms')->arrange('bedrooms');
echo "\n", json_encode(phplyr($housing)->arrange('area_m2')->pull('monthly_rent')), "\n";
echo json_encode(phplyr($housing)->pull(-1)), "\n";
# A tibble: 3 × 1
  bedrooms
     <int>
1        1
2        2
3        3
[959.51,1509.24,1267.85,1805.03,2022.29,1879.25,2240.1,2386.54]
[1879.25,1805.03,2022.29,1267.85,1509.24,2240.1,959.51,2386.54]

Summaries

summarize() returns one row per group. summarise() is the same function. Each named argument is a summary helper or a closure that receives the group’s table and returns one value. With several grouping columns, the result drops the last one, as in dplyr. _groups accepts 'drop_last', 'drop', 'keep', and 'rowwise'.

echo DataFrame::fromCsv('schools.csv') |>
    summarize(
        students: n(),
        schools: n_distinct('school'),
        median_hours: median('study_hours'),
        q90_score: quantile('score', 0.9),
        iqr_score: iqr('score'),
        top_school: fn ($g) => ($g |> slice_max('score', n: 1) |> pull('school'))[0],
    );
echo phplyr(DataFrame::fromCsv('schools.csv'))
    ->summarize(
        students: n(),
        schools: n_distinct('school'),
        median_hours: median('study_hours'),
        q90_score: quantile('score', 0.9),
        iqr_score: iqr('score'),
        top_school: fn ($g) => (phplyr($g)->sliceMax('score', n: 1)->pull('school'))[0],
    );
# A tibble: 1 × 6
  students schools median_hours q90_score iqr_score top_school
     <int>   <int>        <dbl>     <dbl>     <dbl> <chr>     
1      250      12         6.66      86.1      15.3 school-06

reframe() is summarize() for summaries that return several values. Results are recycled to a common length within each group, and the output is ungrouped.

echo DataFrame::fromCsv('schools.csv') |>
    filter(fn ($r) => $r->school <= 'school-02') |>
    group_by('school') |>
    reframe(
        p: [0.25, 0.5, 0.75],
        score: fn ($g) => array_map(fn ($p) => Stats::quantile($g->column('score'), $p), [0.25, 0.5, 0.75]),
    );
echo phplyr(DataFrame::fromCsv('schools.csv'))
    ->filter(fn ($r) => $r->school <= 'school-02')
    ->groupBy('school')
    ->reframe(
        p: [0.25, 0.5, 0.75],
        score: fn ($g) => array_map(fn ($p) => Stats::quantile($g->column('score'), $p), [0.25, 0.5, 0.75]),
    );
# A tibble: 6 × 3
  school        p score
  <chr>     <dbl> <dbl>
1 school-01  0.25  67.6
2 school-01  0.5   72.5
3 school-01  0.75  76.9
4 school-02  0.25  65.3
5 school-02  0.5   70.3
6 school-02  0.75  81.0

count() counts rows per combination of columns within any existing groups and keeps the original grouping. wt: sums a column instead, sort: true orders by the count, and name: names the count column. tally() counts within the current groups, and add_count() and add_tally() add the count as a column.

$schools = DataFrame::fromCsv('schools.csv') |>
    mutate(band: case_when(
        [fn ($r) => $r->study_hours < 4, 'light'],
        [fn ($r) => $r->study_hours < 8, 'moderate'],
        default: 'heavy',
    ));

echo $schools |> count('band', sort: true);
echo "\n", $schools |> count('band', wt: 'study_hours', name: 'hours');
$schools = phplyr(DataFrame::fromCsv('schools.csv'))
    ->mutate(band: case_when(
        [fn ($r) => $r->study_hours < 4, 'light'],
        [fn ($r) => $r->study_hours < 8, 'moderate'],
        default: 'heavy',
    ))
    ->get();

echo phplyr($schools)->count('band', sort: true);
echo "\n", phplyr($schools)->count('band', wt: 'study_hours', name: 'hours');
# A tibble: 3 × 2
  band         n
  <chr>    <int>
1 heavy       95
2 moderate    90
3 light       65
# A tibble: 3 × 2
  band     hours
  <chr>    <dbl>
1 heavy     953.
2 light     159.
3 moderate  540.

glimpse() prints a compact summary of each column and returns the table unchanged.

From phpstats/phplyr@d319dc5.