Guides
Porting an R script
This page takes a typical R analysis of the schools data and translates it to PHP with phplyr and phit. The analysis reads a CSV file, cleans and reshapes it, summarizes it, and fits a linear, a logistic, and a random-intercept model. Both scripts ran when the documentation was built. The output under each script is what it printed.
The data is schools.csv. The R script needs dplyr, tidyr, and lme4. The PHP script needs phplyr and the phit extension for the models.
R
suppressPackageStartupMessages({
library(dplyr)
library(tidyr)
library(lme4)
})
schools <- as_tibble(read.csv("schools.csv"))
# Clean: drop incomplete rows, band study time, compute the gain.
clean <- schools |>
filter(!is.na(score), study_hours > 0) |>
mutate(
hours_band = case_when(study_hours < 4 ~ "low", study_hours < 8 ~ "mid", .default = "high"),
gain = score - prior_score,
passed = as.integer(score >= 75)
)
# Reshape: one row per student and test.
long <- clean |>
mutate(student = row_number()) |>
select(student, school, prior = prior_score, final = score) |>
pivot_longer(c(prior, final), names_to = "test", values_to = "points")
long |> group_by(test) |> summarise(mean = mean(points), sd = sd(points))
# Summarise: students per school and band, then the schools with the largest gains.
clean |>
count(school, hours_band) |>
pivot_wider(names_from = hours_band, values_from = n, values_fill = 0) |>
head(3)
clean |>
group_by(school) |>
summarise(n = n(), mean_gain = mean(gain)) |>
arrange(desc(mean_gain)) |>
head(3)
# Model: linear, logistic, and random-intercept fits.
round(coef(lm(score ~ study_hours + prior_score, data = clean)), 4)
round(coef(glm(passed ~ study_hours + prior_score, family = binomial, data = clean)), 4)
round(fixef(lmer(score ~ study_hours + prior_score + (1 | school), data = clean)), 4)# A tibble: 2 × 3
test mean sd
<chr> <dbl> <dbl>
1 final 72.0 10.3
2 prior 59.8 12.4
# A tibble: 3 × 4
school high low mid
<chr> <int> <int> <int>
1 school-01 7 4 7
2 school-02 8 8 8
3 school-03 5 3 7
# A tibble: 3 × 3
school n mean_gain
<chr> <int> <dbl>
1 school-07 19 24.3
2 school-10 25 14.1
3 school-02 24 13.6
(Intercept) study_hours prior_score
45.3014 1.5691 0.2730
(Intercept) study_hours prior_score
-6.9865 0.5688 0.0395
(Intercept) study_hours prior_score
46.6627 1.6141 0.2453
PHP
$schools = DataFrame::fromCsv('schools.csv');
// Clean: drop incomplete rows, band study time, compute the gain.
$clean = $schools |>
filter(col('score')->isNa()->not(), col('study_hours')->gt(0)) |>
mutate(
hours_band: case_when([col('study_hours')->lt(4), 'low'], [col('study_hours')->lt(8), 'mid'], default: 'high'),
gain: col('score')->sub(col('prior_score')),
passed: if_else(col('score')->ge(75), 1, 0),
);
// Reshape: one row per student and test.
$long = $clean |>
mutate(student: row_number()) |>
select('student', 'school', ['prior' => 'prior_score', 'final' => 'score']) |>
pivot_longer(['prior', 'final'], namesTo: 'test', valuesTo: 'points');
echo $long |> group_by('test') |> summarize(mean: mean('points'), sd: sd('points')), "\n";
// Summarise: students per school and band, then the schools with the largest gains.
echo $clean |>
count('school', 'hours_band') |>
pivot_wider(namesFrom: 'hours_band', valuesFrom: 'n', valuesFill: 0) |>
head(3), "\n";
echo $clean |>
group_by('school') |>
summarize(n: n(), mean_gain: mean('gain')) |>
arrange(desc('mean_gain')) |>
head(3), "\n";
// Model: linear, logistic, and random-intercept fits.
$coef = fn (array $fit) => implode(' ', array_map(
fn ($name, $b) => $name.' '.\round($b, 4),
$fit['columns'],
$fit['coefficients'],
));
echo $coef($clean |> lm('score ~ study_hours + prior_score')), "\n";
echo $coef($clean |> glm('passed ~ study_hours + prior_score', family: 'binomial')), "\n";
echo $coef($clean |> lmer('score ~ study_hours + prior_score + (1 | school)')), "\n";$schools = DataFrame::fromCsv('schools.csv');
// Clean: drop incomplete rows, band study time, compute the gain.
$clean = phplyr($schools)
->filter(col('score')->isNa()->not(), col('study_hours')->gt(0))
->mutate(
hours_band: case_when([col('study_hours')->lt(4), 'low'], [col('study_hours')->lt(8), 'mid'], default: 'high'),
gain: col('score')->sub(col('prior_score')),
passed: if_else(col('score')->ge(75), 1, 0),
)
->get();
// Reshape: one row per student and test.
$long = phplyr($clean)
->mutate(student: row_number())
->select('student', 'school', ['prior' => 'prior_score', 'final' => 'score'])
->pivotLonger(['prior', 'final'], namesTo: 'test', valuesTo: 'points')
->get();
echo phplyr($long)->groupBy('test')->summarize(mean: mean('points'), sd: sd('points')), "\n";
// Summarise: students per school and band, then the schools with the largest gains.
echo phplyr($clean)
->count('school', 'hours_band')
->pivotWider(namesFrom: 'hours_band', valuesFrom: 'n', valuesFill: 0)
->head(3), "\n";
echo phplyr($clean)
->groupBy('school')
->summarize(n: n(), mean_gain: mean('gain'))
->arrange(desc('mean_gain'))
->head(3), "\n";
// Model: linear, logistic, and random-intercept fits.
$coef = fn (array $fit) => implode(' ', array_map(
fn ($name, $b) => $name.' '.\round($b, 4),
$fit['columns'],
$fit['coefficients'],
));
echo $coef(phplyr($clean)->lm('score ~ study_hours + prior_score')), "\n";
echo $coef(phplyr($clean)->glm('passed ~ study_hours + prior_score', family: 'binomial')), "\n";
echo $coef(phplyr($clean)->lmer('score ~ study_hours + prior_score + (1 | school)')), "\n";# A tibble: 2 × 3
test mean sd
<chr> <dbl> <dbl>
1 final 72.0 10.3
2 prior 59.8 12.4
# A tibble: 3 × 4
school high low mid
<chr> <int> <int> <int>
1 school-01 7 4 7
2 school-02 8 8 8
3 school-03 5 3 7
# A tibble: 3 × 3
school n mean_gain
<chr> <int> <dbl>
1 school-07 19 24.3
2 school-10 25 14.1
3 school-02 24 13.6
(Intercept) 45.3014 study_hours 1.5691 prior_score 0.273
(Intercept) -6.9865 study_hours 0.5688 prior_score 0.0395
(Intercept) 46.6627 study_hours 1.6141 prior_score 0.2453
What changed in translation
- Loading.
read.csv()becomesDataFrame::fromCsv(). phibble infers column types like readr. - Pipes. On PHP 8.5, R’s
|>stays|>. Each function returns a closure that the pipe applies. On PHP 8.3 and 8.4, the pipeline becomes a method chain:phplyr($t)->filter(...), ending with->get(). - Column references. Bare names become strings, or
col('x')inside expressions.score - prior_scorebecomescol('score')->sub(col('prior_score')). A row closure,fn ($r) => $r->score - $r->prior_score, also works. - Conditions.
case_when(x < 4 ~ "low", .default = "high")becomescase_when([col('x')->lt(4), 'low'], default: 'high'). - Renaming in select.
select(prior = prior_score)becomesselect(['prior' => 'prior_score']). - Column sets.
c(prior, final)becomes['prior', 'final']. - Models.
lm(),glm(), andlmer()take the same formulas. They return arrays withcolumns(R’s coefficient names),coefficients, standard errors, and fit statistics instead of model objects, socoef()andfixef()become array reads. - Printing. DataFrames print in tibble’s format. R rounds printed numbers to significant digits; phibble does the same.
Because the PHP pipeline uses col() expressions instead of closures, it also runs unchanged on a database table from Phibble\tbl(). See Databases and expressions.
From phpstats/phplyr@d319dc5.