Guides
Expressions
Phibble\Expr describes a computation over columns as a tree of objects. The in-memory DataFrame evaluates the tree, SqlTable compiles it to SQL, and PolarsTable maps it to Polars expressions. Each backend implements the same list of functions with R’s semantics, and runs a function in memory when it has no exact translation. Results can still differ where a database cannot represent R’s value, as the SQL tables page lists.
A PHP closure can only be run. An expression can also be inspected, translated, and serialized. phibble’s verbs accept either, and a backend that cannot run a closure itself loads the rows and runs it in memory.
Building expressions
An expression has four kinds of nodes:
Colreads a column:col('price').Litholds a literal value:lit(3),lit('north'), a date, or a list of values forin.Callapplies a registered function to arguments.Descmarks a sort key or ranking as descending.
Methods on any expression build operator calls. Arguments that are not expressions are wrapped in lit().
| Method | Operator |
|---|---|
add, sub, mul, div, mod, pow |
+, -, *, /, %%, ^ |
neg |
unary minus |
eq, ne, lt, le, gt, ge |
==, !=, <, <=, >, >= |
and, or, not |
&, |, ! |
isNa() |
is.na() |
isIn([...]) |
%in% |
between($low, $high) |
between() |
desc() |
desc() |
Functions in the Phibble\Expr namespace build the other calls. Options are named arguments, as in mean(col('x'), na_rm: true) or lag(col('x'), n: 2, default: 0). var_() stands for var(), which is a reserved word in PHP. call('name', ...) builds a call to any registered function by name.
columns() lists the columns an expression reads, and toArray() returns the tree as plain arrays:
$heavy = col('body_mass')->gt(5000)->and(col('species')->isIn(['Gentoo', 'Chinstrap']));
echo json_encode($heavy->toArray(), JSON_PRETTY_PRINT), "\n";
echo json_encode($heavy->columns()), "\n";{
"type": "call",
"fn": "&",
"args": [
{
"type": "call",
"fn": ">",
"args": [
{
"type": "col",
"name": "body_mass"
},
{
"type": "lit",
"value": 5000
}
]
},
{
"type": "call",
"fn": "in",
"args": [
{
"type": "col",
"name": "species"
},
{
"type": "lit",
"value": [
"Gentoo",
"Chinstrap"
]
}
]
}
]
}
["body_mass","species"]
Using expressions with verbs
Each verb evaluates expressions per group:
filter()keeps rows where the expression is true. NA counts as false.mutate()adds a column with one value per row. An aggregate such asmean()gives one value per group, which is repeated for each row of the group.summarize()needs one value per group. Anything else throws aLengthException.arrange()sorts by expressions over the whole table, ignoring groups. Wrap a key indesc()to sort it in descending order.
$penguins = DataFrame::fromCsv('penguins.csv');
$out = $penguins
->filter(is_na(col('body_mass'))->not())
->groupBy(['species'])
->mutate([
'z' => col('body_mass')->sub(mean(col('body_mass')))->div(sd(col('body_mass'))),
'rank' => min_rank(desc(col('body_mass'))),
'size' => case_when(
col('body_mass')->ge(5000), 'large',
col('body_mass')->ge(3500), 'medium',
default: 'small',
),
])
->filter(col('rank')->le(2))
->select(['species', 'body_mass', 'z', 'rank', 'size']);
echo $out;# A tibble: 6 × 5
# Groups: species [3]
species body_mass z rank size
<chr> <int> <dbl> <int> <chr>
1 Adelie 4725 2.23 2 medium
2 Adelie 4775 2.34 1 medium
3 Gentoo 6300 2.43 1 large
4 Gentoo 6050 1.93 2 large
5 Chinstrap 4550 2.13 2 medium
6 Chinstrap 4800 2.78 1 medium
$penguins = DataFrame::fromCsv('penguins.csv');
echo $penguins
->groupBy(['island'])
->summarize([
'n' => n(),
'mass' => mean(col('body_mass')),
'mass_rm' => mean(col('body_mass'), na_rm: true),
'q90' => quantile(col('body_mass'), 0.9, na_rm: true),
'species' => n_distinct(col('species')),
'any_heavy' => any(col('body_mass')->gt(6000), na_rm: true),
]);# A tibble: 3 × 7
island n mass mass_rm q90 species any_heavy
<chr> <int> <dbl> <dbl> <dbl> <int> <lgl>
1 Biscoe 168 NA 4716. 5670 2 TRUE
2 Dream 124 3713. 3713. 4300 2 FALSE
3 Torgersen 52 NA 3706. 4300 1 FALSE
Functions
The registry in Phibble\Expr\Functions::REGISTRY lists every function an expression may call, with its arguments and options. Calling anything else throws when the expression is built.
Scalar functions
Scalar functions return one value per row.
| Function | Notes |
|---|---|
is_na(x) |
True for NA and NaN. |
if_else(cond, true, false, missing:) |
missing is used where cond is NA. |
case_when(c1, v1, c2, v2, ..., default:) |
The first true condition wins. NA conditions count as false. |
coalesce(x, y, ...) |
The first non-NA value. |
x->isIn([...]) |
Never NA. NA is in the set only if NA is listed. |
between(x, low, high) |
Inclusive. NA if any argument is NA. |
na_if(x, y) |
NA where x == y. |
near(x, y, tol:) |
Equality within a tolerance. |
abs, sqrt, exp, log(x, base:), log10, floor, ceiling |
Math functions. |
round(x, digits:) |
Rounds as R 4 does. |
as_int, as_float, as_string, as_bool |
Casts with R’s as.*() rules. |
paste0(...), paste(..., sep:) |
NA becomes the text NA, as in R. |
Text functions (stringr)
As in stringr, a pattern given as text is a regular expression. phibble uses PCRE and matches by Unicode character; stringr uses ICU, and the two agree on common patterns. fixed('...') matches the text literally, and regex('...', ignore_case: true) sets options.
| Function | Notes |
|---|---|
str_detect, str_starts, str_ends (with negate:) |
str_starts and str_ends anchor the pattern. |
str_count(x, pattern) |
Non-overlapping matches. |
str_extract(x, pattern) |
The first match, or NA. |
str_extract_all(x, pattern), str_split(x, pattern) |
A list-column. NA gives a list holding NA. |
str_split_i(x, pattern, i) |
The i-th piece; negative i counts from the end; NA when out of range. |
str_replace, str_replace_all |
\1 in the replacement refers to a group. An NA replacement gives NA where the pattern matches. |
str_pad(x, width, side:, pad:) |
side is left (default), right, or both. |
str_trunc(x, width, side:, ellipsis:) |
side is right (default), left, or center. |
str_to_upper, str_to_lower, str_to_title, str_squish |
Title case capitalizes after a hyphen but not after an apostrophe, as ICU does. |
str_length, str_trim(x, side:), str_sub(x, start, end) |
str_sub is 1-based and inclusive; negative positions count from the end. |
str_lower, str_upper |
Aliases of str_to_lower and str_to_upper. |
Dates and times (lubridate)
Date functions accept dates and datetimes, and a date stays a date: results such as floor_date(d, 'month') or d + days(1) are dates when d is.
| Function | Notes |
|---|---|
ymd, dmy, mdy, ymd_hms, dmy_hms, mdy_hms (with tz:) |
Parse text. Separators are optional; invalid input gives NA. |
parse_date_time(x, orders, tz:) |
Orders made of Y, y, m, d, H, M, and S, such as 'Ymd HMS'; the first that fits wins. |
year, month, day, yday, quarter, hour, minute, second |
Parts as numbers. |
wday(x, week_start:) |
1 is Sunday by default, as in lubridate; week_start: 1 makes Monday 1. |
floor_date, ceiling_date, round_date (x, unit, week_start:) |
Units from second to year, with multiples such as '15 minutes'. ceiling_date moves a date that is already on a boundary to the next one, as lubridate does for dates. |
seconds, minutes, hours, days, weeks, months, years |
Periods, used as col('d')->add(months(1)). Month and year arithmetic that lands on a date that does not exist gives NA. |
difftime(x, y, units:) |
A number in secs, mins, hours, days (default), or weeks. date - date gives days and datetime - datetime seconds. |
with_tz(x, tz), force_tz(x, tz) |
The same instant in another zone; the same clock time in another zone (NA for a time the zone skips). |
today(tz:), now(tz:) |
Read Phibble\Expr\Clock, which tests can freeze. |
Aggregates
An aggregate returns one value per group. Each takes na_rm:, which is false by default, so an aggregate over a group containing NA returns NA unless na_rm: true.
| Function | Notes |
|---|---|
n() |
Rows in the group. |
sum, mean, median, min, max |
sum of no values is 0 and mean is NaN. |
sd, var_ |
Sample statistics, with n - 1. |
quantile(x, p), iqr(x) |
R’s default type 7. These throw on NA unless na_rm: true, as R does. |
n_distinct(x, ...) |
Counts NA as a value unless na_rm: true. Takes several columns. |
first, last, nth(x, n) |
Take default: and order_by:. Negative n counts from the end. |
any, all |
Three-valued, as in R. |
Window functions
A window function returns one value per row, computed over the rows of the group in row order.
| Function | Notes |
|---|---|
lag(x, n:, default:, order_by:), lead(...) |
Shift values within the group. |
row_number(x) |
Ranks with ties broken by position. Without x, the row’s position. |
min_rank, dense_rank, percent_rank, cume_dist |
NA values stay NA and are not counted. Pass desc(x) to rank in descending order. |
ntile(x, n:) |
Splits rows into n groups of nearly equal size, larger groups first. |
cumsum, cumprod, cummean, cummin, cummax |
Every value after the first NA is NA. |
cumall, cumany |
Follow dplyr’s NA rules. |
consecutive_id(...) |
Increments when any argument changes. |
$sales = tribble(
['store', 'week', 'units'],
['a', 1, 10], ['a', 2, 14], ['a', 3, 9],
['b', 1, 7], ['b', 2, 7], ['b', 3, 12],
);
echo $sales
->groupBy(['store'])
->mutate([
'prev' => lag(col('units')),
'change' => col('units')->sub(lag(col('units'))),
'running' => cumsum(col('units')),
'rank' => min_rank(desc(col('units'))),
'tile' => ntile(col('units'), n: 2),
]);# A tibble: 6 × 8
# Groups: store [2]
store week units prev change running rank tile
<chr> <int> <int> <int> <int> <int> <int> <int>
1 a 1 10 NA NA 10 2 1
2 a 2 14 10 4 24 1 2
3 a 3 9 14 -5 33 3 1
4 b 1 7 NA NA 7 2 1
5 b 2 7 7 0 14 2 1
6 b 3 12 7 5 26 1 2
NA and arithmetic
Expressions follow R’s rules for missing values and arithmetic:
- Arithmetic and comparisons with NA give NA.
&and|use three-valued logic.FALSE & NAis false andTRUE | NAis true./and^always return floats. Division by zero givesInf,-Inf, orNaN.%%takes the sign of the divisor, so-7 %% 3is 2.round()takes, of the two candidates, the one closer to the stored value, and an exact tie goes to the even digit.round(2.5)is 2, andround(2.675, digits: 2)is 2.67 because 2.675 is stored as slightly less than 2.675.- Text compares and sorts in byte order.
$df = tibble(
a: [true, false, null, null],
b: [null, null, true, false],
x: [2.5, 0.5, -1.5, 2.675],
y: [1, null, 3, 4],
);
echo $df->mutate([
'and' => col('a')->and(col('b')),
'or' => col('a')->or(col('b')),
'sum' => col('y')->add(1),
'round' => Phibble\Expr\round(col('x')),
'round2' => Phibble\Expr\round(col('x'), digits: 2),
'mod' => lit(-7)->mod(3),
'cumsum' => cumsum(col('y')),
]);# A tibble: 4 × 11
a b x y and or sum round round2 mod cumsum
<lgl> <lgl> <dbl> <int> <lgl> <lgl> <int> <dbl> <dbl> <int> <int>
1 TRUE NA 2.5 1 NA TRUE 2 2 2.5 2 1
2 FALSE NA 0.5 NA FALSE NA NA 0 0.5 2 NA
3 NA TRUE -1.5 3 NA TRUE 4 -2 -1.5 2 NA
4 NA FALSE 2.68 4 FALSE NA 5 3 2.67 2 NA
Serialization
toArray() returns a JSON-friendly array, and Expr::fromArray() rebuilds the expression. Each node has a type key: col, lit, call, or desc. A call lists its positional arguments under args and its options under named. A date literal carries dtype (date or datetime).
$expr = lag(col('score'), n: 2, default: 0, order_by: col('week'));
$json = json_encode($expr->toArray());
echo $json, "\n";
$back = Phibble\Expr\Expr::fromArray(json_decode($json, true));
var_dump($back == $expr);{"type":"call","fn":"lag","args":[{"type":"col","name":"score"}],"named":{"n":{"type":"lit","value":2},"default":{"type":"lit","value":0},"order_by":{"type":"col","name":"week"}}}
bool(true)
The test suite checks the in-memory evaluator against dplyr. The script tests/r/expr.R evaluates the same expressions in R and stores the results in tests/fixtures/expr/expected.json, which other backends can reuse.
From phpstats/phibble@36d0f3f.