This page is the repository's own document docs/r-twin.md, shown as it is, so that it never drifts from the product.
A statistician or a programmer who knows the SAS macros of sas/macros/ must be able to read the R program of the same table, and the other way round. The R library r/sapling/ has one R function for each SAS macro. A program of the R library reads like the SAS program of the same table: the same blocks, in the same order, with the same labels.
Three slices are made and proven:
describe,freq,tlf_stack,columns,ard,tlf_layout,tlf_print,tlf_tableandtable_summary(andstudywithadam(), the two small parts every program needs).The tests and the models, and the pairs:
ttest,wilcoxon,chisq,fisher,cmh,tlf_anova,ancova,lsmeans,adjust.worst,termflags,proportion,risk,rate,rateratio, and the one-call tablestable_ae,table_visit,table_shift,table_response. They share one program writer (r/sapling/_spl_program.R), as the SAS macros share%_spl_program.The time to the event and the models:
km,logrank,cox,table_tte,logistic(andtable_response(covars=)),mmrm,table_model, the fourinteraction_*; and the listings of records:csvandtlf_printof records.The RTF of
tlf_print()and the nine figures (base graphics).The ADaM steps and
import_xpt.
All 94 macros are accounted for in the table below: done, or not needed with the reason (the inner parts that SAS needs and R does not).
All data of the proof are the made-up data of the study SYNHTN01. No number here is a result of a study.
The idea#
- The library is plain R: one file for one function,
r/sapling/<name>.R, with a header box that mirrors the header of the macro (what it does, each argument, the result, an example).r/sapling/sapling.Rloads them; it is the twin ofsas/macros/sapling.sas. - The slices use base R and
statsonly, exceptrateratioandinteraction_genmodwithdist = "negbin"(MASS::glm.nb),km,logrank,cox,interaction_phreg(survival) andmmrm(nlme): all three ship with R, and each function checks for its package with a clear message. Noemmeans: the LS means are written out fromlm. A later function that needs a package names it in its header and checks for it with a clear message. - A function has the name of its macro and the names of its arguments, in the same order.
out=is the return value.debug=has no twin (R has no work data sets). - A result has the same shape as the SAS result: the same columns (lower case), the same rows in the same order, the same statistic names, the same text of the numbers.
- An error is the message of the macro, with its
fix:line:[describe] var=nope is not a variable of adsl, then[describe] fix: give var= a variable of data=.. R stops withstop(); SAS writes the lines to the log and returns.
The naming rule#
The R function has the name of the macro. Where base R (base, stats, utils, graphics, methods) already has a function of that name, the R name is tlf_<name>. A test (test/r_twin.test.ts) checks every macro name with exists() in a vanilla R session. These seven are the exceptions:
| Macro | R function | Base R already has |
|---|---|---|
%stack | tlf_stack | utils::stack |
%layout | tlf_layout | graphics::layout |
%print | tlf_print | base::print |
%table | tlf_table | base::table |
%anova | tlf_anova | stats::anova |
%barplot | tlf_barplot | graphics::barplot |
%boxplot | tlf_boxplot | graphics::boxplot |
The inner helpers of the R library start with .spl_ and sit in the files that start with _spl_, as the inner macros do.
How an argument is given#
- A variable name is given bare or as a string, as reads best:
describe(data = adsl, var = age, by = trt01a),by = c(trt01a, paramcd),by = "trt01a paramcd". Names are lower case. whereis an R condition on the variables of the data:where = saffl == "Y". A missing result is "no", as in SAS.statsis a named vector, as SAS writes it:stats = c(n = "XX", mean = "XX.X", sd = "XX.XX"). The defaults are those of the macro. A statistic without a pattern has the default pattern of the statistic:stats = c("n", "pct").levelsis a named list:levels = list(agegr1 = c("<65", "65-<75", ">=75")).totalis"yes","no"or the name of the total group.formatoftlf_layoutis a named list of codelists (thePROC FORMATof the SAS program):format = list(sex = c(F = "Female", M = "Male")).setupoftlf_layoutandtlf_tableis the name of a variable (a string) that keeps the print call;tlf_print(t14_2_1_print)is%print(&t14_2_1_print.). A display data frame also carries its print call:tlf_print(display, file = ...).- The
TITLEandFOOTNOTEstatements of a SAS program are the argumentstitleandfootnote, the last two oftlf_print,tlf_tableand each of the nine figure functions (kmplot,forestplot,lineplot,meanplot,tlf_boxplot,tlf_barplot,aeplot,profileplot,scatterplot): SAS draws the TITLE and FOOTNOTE of a figure, so the R function does (titles centred and bold above, footnotes left below). The macros of the figures have no such parameters; this is the only place where a function has more arguments than its macro. study()setsspl_study,spl_out,spl_programin the global environment (the%GLOBALmacro variables) and the options thatard()andtlf_table()read.adam("adsl")isadam.adsl.
One table program, side by side#
Table 14.2.1 of SYNHTN01 (the ITT population). On the left the SAS program out/sas/programs/t14_2_1.sas, on the right r/sapling/examples/t14_2_1.R.
SAS:
%describe(data=adsl, var=age, by=trt01p, total=yes, label="Age (years)", out=t14_2_1_age_summ)
%freq(
data = adsl
, var = agegr1
, by = trt01p
, keep = agegr1n
, levels = agegr1 '<65' '65-<75' '>=75'
, total = yes
, label = "Age group (years)"
, out = t14_2_1_agegr_summ
)
%freq(data=adsl, var=sex, by=trt01p, total=yes, label="Sex", out=t14_2_1_sex_summ)
%stack(t14_2_1_age_summ t14_2_1_agegr_summ t14_2_1_sex_summ, out = t14_2_1)
%columns(data=t14_2_1, n=adsl, var=trt01p, order=trt01pn, total=yes, out=t14_2_1_cols)
%ard(data=t14_2_1, columns=t14_2_1_cols)
%layout(
data = t14_2_1
, columns = t14_2_1_cols
, var = trt01p
, format = sex $sexf.
, file = &spl_out./tlf/t14_2_1
, setup = t14_2_1_print
, out = t14_2_1_display
)
TITLE1 "Table 14.2.1 Demographic and Baseline Characteristics";
FOOTNOTE1 J=l "Program: t14_2_1.sas Output: T14_2_1";
%print(&t14_2_1_print.)
R:
t14_2_1_age_summ <- describe(data = adsl, var = age, by = trt01p, total = "yes", label = "Age (years)")
t14_2_1_agegr_summ <- freq(
data = adsl
, var = agegr1
, by = trt01p
, keep = agegr1n
, levels = list(agegr1 = c("<65", "65-<75", ">=75"))
, total = "yes"
, label = "Age group (years)"
)
t14_2_1_sex_summ <- freq(data = adsl, var = sex, by = trt01p, total = "yes", label = "Sex")
t14_2_1 <- tlf_stack(t14_2_1_age_summ, t14_2_1_agegr_summ, t14_2_1_sex_summ)
t14_2_1_cols <- columns(data = t14_2_1, n = adsl, var = trt01p, order = trt01pn, total = "yes")
ard(data = t14_2_1, columns = t14_2_1_cols)
t14_2_1_display <- tlf_layout(
data = t14_2_1
, columns = t14_2_1_cols
, var = trt01p
, format = list(sex = c(F = "Female", M = "Male", U = "Unknown"))
, file = file.path(spl_out, "tlf", "t14_2_1")
, setup = "t14_2_1_print"
)
tlf_print(
t14_2_1_print
, title = "Table 14.2.1 Demographic and Baseline Characteristics"
, footnote = "Program: t14_2_1.sas Output: T14_2_1"
)
The same table in one call (R; the SAS call is %table_summary(...) with the same parameters):
table_summary(
data = adsl
, where = ittfl == "Y"
, vars = "age agegr1#agegr1n sex# race# ethnic#"
, by = trt01p
, order = trt01pn
, total = "yes"
, levels = list(agegr1 = c("<65", "65-<75", ">=75"))
, file = file.path(spl_out, "tlf", "t14_2_1")
)
Like the macro, table_summary() writes the long R program (one call of describe() or freq() for each variable, tlf_stack(), tlf_table()) and runs it. show = "yes" prints the program; show = "<file>" writes it to a file.
The correspondence: the 94 macros#
"1: done" to "6: done" are made and proven (the slice). "Planned" is not made yet. The inner _spl_ macros of SAS are listed with their R helper, or as not needed.
Setting up a program#
| SAS macro | R function | Slice |
|---|---|---|
%import_xpt | import_xpt | 6: done |
%study | study | 1: done |
The statistics macros, the tables and the steps of a table#
| SAS macro | R function | Slice |
|---|---|---|
%adjust | adjust | 2: done |
%ancova | ancova | 2: done |
%anova | tlf_anova | 2: done |
%ard | ard | 1: done |
%chisq | chisq | 2: done |
%cmh | cmh | 2: done |
%columns | columns | 1: done |
%cox | cox | 4: done |
%csv | csv | 4: done |
%describe | describe | 1: done |
%fisher | fisher | 2: done |
%freq | freq | 1: done |
%interaction_genmod | interaction_genmod | 4: done |
%interaction_glm | interaction_glm | 4: done |
%interaction_logistic | interaction_logistic | 4: done |
%interaction_phreg | interaction_phreg | 4: done |
%km | km | 4: done |
%layout | tlf_layout | 1: done |
%logistic | logistic | 4: done |
%logrank | logrank | 4: done |
%lsmeans | lsmeans | 2: done |
%mmrm | mmrm | 4: done |
%proportion | proportion | 3: done |
%rate | rate | 3: done |
%rateratio | rateratio | 3: done |
%risk | risk | 3: done |
%table | tlf_table | 1: done |
%table_ae | table_ae | 3: done |
%table_model | table_model | 4: done |
%table_response | table_response | 3: done |
%table_shift | table_shift | 3: done |
%table_summary | table_summary | 1: done |
%table_tte | table_tte | 4: done |
%table_visit | table_visit | 3: done |
%termflags | termflags | 3: done |
%ttest | ttest | 2: done |
%wilcoxon | wilcoxon | 2: done |
%worst | worst | 3: done |
Printing#
| SAS macro | R function | Slice |
|---|---|---|
%print | tlf_print | 1: done |
%stack | tlf_stack | 1: done |
Figures#
| SAS macro | R function | Slice |
|---|---|---|
%aeplot | aeplot | 5: done |
%barplot | tlf_barplot | 5: done |
%boxplot | tlf_boxplot | 5: done |
%forestplot | forestplot | 5: done |
%kmplot | kmplot | 5: done |
%lineplot | lineplot | 5: done |
%meanplot | meanplot | 5: done |
%profileplot | profileplot | 5: done |
%scatterplot | scatterplot | 5: done |
The steps of an ADaM program#
| SAS macro | R function | Slice |
|---|---|---|
%baseline | baseline | 6: done |
%flag_first | flag_first | 6: done |
%flag_if_any | flag_if_any | 6: done |
%locf | locf | 6: done |
%merge_subject | merge_subject | 6: done |
%param_wide | param_wide | 6: done |
%pick | pick | 6: done |
%pool | pool | 6: done |
%relchange | relchange | 6: done |
%summarize | summarize | 6: done |
%summarize_groups | summarize_groups | 6: done |
%tte | tte | 6: done |
%visits | visits | 6: done |
%windows | windows | 6: done |
The inner parts (a person does not call them)#
| SAS macro | R function | Slice |
|---|---|---|
%_spl_arg | not needed: the program writer builds the calls itself (.spl_prog_call) | not needed |
%_spl_begin | not needed: no begin line (R messages carry the NOTE lines) | not needed |
%_spl_call | done: .spl_prog_call | done |
%_spl_chars | not needed: is.character() | not needed |
%_spl_csv | done: .spl_csv | done |
%_spl_empty | done: .spl_empty | done |
%_spl_end | not needed: no end line | not needed |
%_spl_figure_attrs | done: .spl_fig_* (_spl_figure.R) | done |
%_spl_figure_close | done: .spl_fig_* (_spl_figure.R) | done |
%_spl_figure_dodge | done: .spl_fig_* (_spl_figure.R) | done |
%_spl_figure_label | done: .spl_fig_label | done |
%_spl_figure_nodata | done: .spl_fig_nodata | done |
%_spl_figure_open | done: .spl_fig_* (_spl_figure.R) | done |
%_spl_figure_outpar | done: .spl_fig_outpar | done |
%_spl_figure_par | done: .spl_fig_par (draw again from par=) | done |
%_spl_levels | done: .spl_levels | done |
%_spl_need | done: .spl_need | done |
%_spl_nobs | not needed: nrow() | not needed |
%_spl_oneof | done: .spl_oneof | done |
%_spl_pairlist | done: .spl_pairlist | done |
%_spl_pairs | done: .spl_pairs | done |
%_spl_program | done: .spl_prog_* | done |
%_spl_quote | not needed: an R string is its own quote | not needed |
%_spl_rows | done: .spl_rows | done |
%_spl_stackpairs | done: .spl_stackpairs | done |
%_spl_text | done: .spl_text | done |
%_spl_total | done: .spl_total | done |
%_spl_vars | done: .spl_vars | done |
%_spl_windows | done: .spl_windows | done |
What the library does the way SAS does#
- Rounding. SAS rounds half away from zero (with a small fuzz: 0.285 gives 0.29). R's
round()does not always..spl_round()does what SAS does, and every text of a number uses it. - Quantiles.
PROC MEANShasQNTLDEF=5as its default.describe()usesquantile(type = 2)for it, and the types 4, 3, 1, 6 forqntldef1 to 4. The median is the quantile 0.5 of the same definition. - Order.
PROC SORTandCLASSorder text by its bytes, a missing value first. The R library sorts withmethod = "radix"(the C locale), never by the locale of the computer. - Blank is missing. A blank text is a missing value in SAS. The R library reads the records so (
.spl_data). - The text of a number is the pattern of the statistic (
XX.X,(XX.X%)): the X group is the width and the decimals, the text round it stays, a p value is never 0 or 1.
The proof#
node tools/r_twin.ts has these parts. No SAS is started.
1. Twelve tables and three listings of SYNHTN01 against the recorded SAS run#
The R program of each table (r/sapling/examples/<table>.R, line by line against out/sas/programs/<table>.sas) is run with Rscript --vanilla and compared with the recorded SAS run (.scratch/sas_finish/synhtn01). The R programs read the ADaM data of that run (out/adam/data/*.rds).
| Table | Functions it uses | Rows of the numbers file | Listing lines |
|---|---|---|---|
| T14.1.1 Analysis populations | freq with levels | 15 | 14 |
| T14.1.2 Subject disposition | freq | 33 | 17 |
| T14.2.1 Demographics | describe, freq (levels, keep, total) | 81 | 32 |
| T14.3.1 SBP change | describe, ancova (the p value in the column "All groups") | 39 | 27 |
| T14.4.1 Exposure | describe | 38 | 26 |
| T14.5.1 Overview of TEAEs | freq with denom= and where | 22 | 14 |
| T14.5.2 TEAEs by SOC and PT | freq with two levels and events | 62 | 28 |
| T14.5.3 Serious TEAEs | the same | 26 | 18 |
| T14.7.1 Vital signs by visit | describe with three groups and keep = c(param, paramn, avisitn), format of paramcd | 254 | 153 |
| T90.1 Worst severity, risk difference, rate | worst, freq (threshold, order="frequency", orderof, levels), termflags, risk (newcombe), rate; three pages | 146 | 103 |
| T90.2 Time to the first TEAE | describe, cox (efron, both directions of a pair) | 22 | 22 |
| SG90_T Cox model by sex | freq, cox with within, interaction_phreg, the p value in "All groups" | 21 | 28 |
The listings of records L14.5.2, L14.5.3 and L90.1 (csv() and tlf_print() of records) are compared with their .lst and their .csv line by line: 189, 35 and 189 lines. They have the labels of the variables as headers, dates as DATE9., the columns that do not fit in 160 characters in a second part with the BY columns repeated, and one blank line and the titles between the parts.
A table passes when:
- its numbers file has the same rows in the same order (the result, the statistic, the values of the groups),
- every formatted text is the same,
- every number is the same to 12 significant digits (SAS writes 15 digits and cuts the last one; R rounds it; the largest relative difference found is 5.4e-13, in T90.2), except the likelihood-ratio p value of SG90_T:
%interaction_phregleaves the convergence of PROC PHREG at its default, SAS stops 4e-9 (relative) from the converged value that R reaches, so that table is compared to 1e-7, and - its
.lstis the same as the.lstof SAS, line by line (the blanks at the end of a line left out).
Together: 12 tables, 759 numbers, 910 listing lines, 3 listings of records.
Each of the 15 outputs also has its RTF file (.rtf, written by tlf_print() next to the .lst, see "The RTF" below). The text of its cells in order (titles, header cells, body cells, footnotes) is the same as that of the .rtf of SAS: 15 of 15 files.
2. The one-call tables against their long programs#
r/sapling/examples/t14_2_1_summary.R and r/sapling/examples/tables_oneshot.R. Each of table_summary, table_ae, table_visit, table_shift, table_response (with and without covars), table_tte and table_model (ANCOVA and MMRM) writes its long program (show = "<file>" keeps it); the program is run on its own; the display, the stacked blocks and the columns are identical data frames. table_summary is also identical to the display of t14_2_1.R and table_ae to that of t14_5_2.R (both proven against SAS). The population and the categories of the shift table and the responder flag of the response table are made up in that program (ADSL has no response; ADVS has no reference range).
3. The methods: the known answers#
r/sapling/examples/methods.R runs the R functions on the made-up fixture study of the method registry (FIXTURE_STUDY of src/methods.ts, 12 subjects), with the calls of each method's SAS fixture written in R. tools/r_twin.ts compares every number with the known answer of the registry, the answer the SAS macros were proven on (tools/methods_sas.ts; tolerance 1e-6, 1e-4 for the iterative methods): all 31 methods of the registry, 425 numbers.
| Method (registry id) | R function |
|---|---|
count, contsumm, catsumm (with worst) | freq, describe, freq + worst |
| anova, lsmeans, ancova | tlf_anova, lsmeans, ancova |
| mmrm (Kenward-Roger and between-within) | mmrm |
| chisq, fisher (two ways), cmh (two ways) | chisq, fisher, cmh |
| ttest (three ways of giving the pairs), wilcoxon | ttest, wilcoxon |
| km, kmrate (log-log and linear), logrank (stratified or not) | km, logrank |
| cox (efron, breslow, two strata variables, log-rank p value), logistic (covariate, class factor) | cox, logistic |
| propci | proportion (Clopper-Pearson) |
riskdiff (newcombe; the four adjust methods and the weighted Bonferroni; the Mantel-Haenszel difference in strata), riskratio, oddsratio, perterm | risk, adjust, termflags |
| poisson, negbin, eair, eventrate | rateratio, rate |
| interaction-lm, -logistic, -poisson, -negbin, -cox | interaction_glm, interaction_logistic, interaction_genmod (both families), interaction_phreg |
npm test has test/r_twin.test.ts: the name exceptions, the header box of each file, the argument names of each function against sas/macros/index.json, a few made-up subjects (the text of the numbers, the display rows, the messages of errors, the pairs, a Kaplan-Meier median that is NE, the refusal of a covariance structure or a df method the R library cannot make), the comparing tool itself, and the known answers (part 3).
4. The figures#
r/sapling/examples/figures.R runs the nine figure functions on the made-up data of tools/figures_sas.ts (the cases of FIGURES SAS), with the calls of the SAS cases in R. tools/r_twin.ts compares the numbers each function returns (the out= of the macro) with the out= of the recorded SAS run (.scratch/figures_sas/sas/out/<case>.csv) on the columns of that file, and with the known answers written by hand in tools/figures_sas.ts: 22 cases, 144 rows (km, forest, mean, median, line, line_band, box, bar, bar_stack, ae, profile, scatter, scatter_loess, and the empty cases, which have no row and the figure "No data qualified"). The PNG of each case is made (base graphics, png(), 300 dpi, 6.5in by 4.5in).
I looked at the pictures of kmplot, forestplot, meanplot, boxplot (beside the SAS picture, .scratch/figures_sas/sas/out/*.png), aeplot and barplot (the SAS picture of those two was not put beside), and made them drawn without error for lineplot, profileplot and scatterplot, which I did not look at after the last change of layout. I changed what was clearly worse: the median of kmplot is text in the panel as in SAS (not lines), the legend of kmplot has the entry "Censored", the names of the groups stand before the rows of the number at risk, the panels have the light grid and a grey box, the panels of a figure with panel= are stacked with a common axis label (the first drawing wasted space and clipped the label), the number table of meanplot is inside the panel, the forest plot has the end caps, the numbers at the right with the header "HR (CI)" and a legend of markers. What still differs in looks: the light grid of tlf_barplot is drawn over the bars; SAS draws a title (a TITLE statement, which the R functions do not take), a strip over each panel, filled markers of another size and its own fonts; the log axis of the forest plot has the ticks R chooses (0.5, 1, 2, 5), SAS 0.2, 0.5, 1, 2, 5; the colours, the markers and the line patterns of the groups are the same, in the order of columns=.
5. The ADaM steps#
r/sapling/examples/adam_steps.R runs the twin of each step macro on the fixtures of the step registry (src/adam_steps.ts, the fixtures that tools/adam_steps_sas.ts runs on SAS): the DATA steps around a call are written in R (reading a date, a flag), the call is the twin. tools/r_twin.ts writes the SDTM records and the expected records of each fixture as CSV and compares the values: 527 values, all the same. The fixtures: fx2, fx4, fx7, fx13 (merge_subject), fx9, fx10 (pick, first and last, with keys), fx11 (summarize: sum, mean, max, min), fx12, fx14 (flag_if_any, else N and blank), fx15, fx23 (flag_first, first and last, with awtdiff and ady as keys), fx17 (pool), fx19, fx23 (windows), fx20, fx25 (visits), fx21, fx22 (baseline, with chg and pchg), fx23 (locf), fx26 (relchange), fx27 (summarize_groups), fx28 (tte with censoring dates and censor_cnsr), fx35 (param_wide).
The RTF#
tlf_print() writes <file>.rtf next to <file>.lst: plain RTF text, the page and the styles of ODS RTF style=journal as the recorded files of SAS have them (landscape, 15840 by 12240 twips, margins 360; titles and footnotes in the header and the footer of each page, bold italic Arial 9.5 pt; the header row of the columns italic and centred with a rule above and below, a header split at | with its pieces one under the other; the cells in Arial 9.5 pt, left, numbers right in 9 characters (BEST9. of the report); a rule under the last row of a page; a new section for each page of together=; the columns that do not fit the page in a second part with the BY columns repeated). The look differs in these ways: SAS sets the width of a column from the width of its text in Arial, here a width is the number of characters times 100 twips plus the padding, so the cell widths differ a little and the break between two parts of a wide listing is set so that it falls where the three recorded listings have it (13000 twips; another listing may break one column earlier or later); the keepn (keep with next) of a row is set on a heading row only; the shading of the journal style (none in the recorded files) is not written. The text of the cells is the same.
The generated R programs#
Sapling writes the programs of this page for a study: sapling r programs makes out/r/programs/<id>.R for each table, with its listings and figures, from the same analysis contract and the same model as the SAS programs. The SAS generator (src/sas.ts) records every step it writes (the header, the population and record steps, each macro call with its parameters in the order of the macro, the titles); src/r_twin.ts writes the same steps as R. The text of the .sas files is never parsed, so the two programs cannot drift: a block of the SAS program is the block of the R program, in the same order, with the same arguments.
| Which is which | Folder | Made by | Used by |
|---|---|---|---|
the engine's programs (r-agentic: sma_setup, ars_load, ars_output) | out/tlf/programs/ | sapling tlf programs | the chain, the dashboard, the pilot digest: unchanged |
| the SAS programs and the macros | out/sas/ | sapling sas programs | DataStep Studio |
| the readable R programs and the library copy | out/r/programs/, out/r/sapling/ | sapling r programs | sapling r run, a person who reads or runs them |
The readable programs are an additional product, as out/sas/ is. Nothing of out/tlf/, out/ard/ or the chain reads out/r/.
sapling r programs [--no-companions] out/r/programs/*.R, out/r/sapling/ (copy of r/sapling/), out/r/programs.json
sapling r run [OUTPUT] [--jobs N] [--timeout SECONDS]
Rscript on out/adam/data: out/r/tlf, ard, listings, figures, logs;
N programs at once (default: half the cores, at most 3), each killed after
SECONDS (default 1800); a program that fails or times out is named with its log
sapling r check every number of out/ard (the engine) against out/r/ard: out/r/check.md
How a SAS step becomes R: the population and the records are subset(), order() and cbind() where SAS has PROC SORT and a DATA step merge; a condition is an R condition, and a blank is not equal to a value as in SAS (ablfl ne 'Y' is ablfl != "Y" | is.na(ablfl)); %macro(a=1, b=2, out=x) is x <- macro(a = 1, b = 2) (tlf_ for the seven names of the table above), on one line when it is 100 characters or less, else one argument per line with the commas in front and the = aligned; levels, pairs, stats, format are the lists and vectors of "How an argument is given"; TITLE and FOOTNOTE are the title and footnote of tlf_print. out/r/ is the study folder of the R programs (SPL_ROOT, SPL_OUT, SPL_ADAM are set by sapling r run; r.root: of sapling.yml names the folder on another computer).
A macro without an R function is a # NOT MADE line in the program and a NOT MADE line of the command, never a wrong number. The title and footnote of a figure are the title and footnote arguments of the figure functions, drawn as SAS draws TITLE and FOOTNOTE (the titles centred and bold above, the footnotes left below).
sapling run --readable (or sapling tlf run --readable, or tlf.programs: readable in sapling.yml; --jobs and --timeout as above) makes the TLF step of the chain use them: the numbers come from out/r/programs, and the engine lays the tables out from them (ars_output(run = FALSE) on those numbers), so the files the later steps read are the engine's (out/ard/<id>.csv with the place of each result, out/tlf/listings/<id>.lst, ars_results.json). src/r_chain.ts turns a number of a readable program into the engine's result (analysis, operation, the five group keys: all 18392 results of the pilot are rebuilt exactly). The default is not changed.
To make the readable programs the main R programs of the chain later: the chain would call sapling r programs and sapling r run in place of sma_setup/ars_output, and the steps that read out/ard (the dashboard, package, the pilot digest) would read out/r/ard (the same shape, proved equal by sapling r check); the programs would need the R libraries survival and nlme on the machine (as the engine does), the figure titles, and a faster listing writer. Until then sapling r check keeps the two equal.
The conventions of SAS that the new functions follow#
- Kaplan-Meier. The interval of the survival is
survfit(conf.type = "plain")forconftype = "linear"(SAS default), "log" and "log-log" for the others; the limits of the median are those of Brookmeyer and Crowley by the same transform (quantile()of the fit); a median or a limit the curve does not reach prints as NE. - Log-rank.
survdiff(rho 0), the chi-square p value on the groups minus 1 degrees of freedom, stratified withstrata(); no test with one group. - Cox.
coxph, the ties of the macro (efron, breslow;exactis SAS DISCRETE, the conditional logistic form), the convergence tightened (1e-12) as the macro does, model-based standard errors; the hazard ratio of a pair is the difference of the group coefficients with the Wald interval and the Wald normal p value. - Logistic.
glmbinomial; the odds ratio of a pair as for the hazard ratio. - MMRM.
nlme::gls(REML or ML) with the unstructured covariance of the visits (corSymmandvarIdent) and its fallbacks; the LS means at the covariate means and the average of the other class factors; the between-within degrees of freedom of PROC MIXED for an effect that varies within the subject; Kenward-Roger written out for the unstructured covariance. No installed package does these (mmrm,lme4,pbkrtest,emmeansare not installed here): the function stops with a message for a covariance structure (toep, ante(1), ...) or a df method (satterthwaite, containment) it cannot make, never another method in its place. - Interactions. The F test of the model with the terms against the model without; the likelihood-ratio tests from the log likelihoods (the full log likelihood and the number of estimated coefficients for the counts, the log partial likelihood for Cox).
What is not the same as SAS, and what is not proven#
- The look of the RTF differs as said above; the proof compares the text of the cells, not the pixels or the widths.
- The figures are proven by their numbers, not by their pixels (the look differs as said above); the RTF file of a figure macro (the PNG in an RTF) is not written;
plot=andpar=draw again as the macro does (par=is a data frame of parameter and value, theoutparattribute of an earlier result) but only the numbers of the figure are compared with SAS, not the draw-again cases oftools/figures_sas.ts(a changed row, a changed label). - The ADaM steps are proven on the fixtures that call a macro of the library (and whose surrounding DATA steps are simple to write in R): not the fixtures of
ttewith a list of events and censorings of several rows (fx29, fx30, fx31: the call is written in the same way, but the DATA steps that make its events and censors are not), fx34 and fx36 (their results are made by DATA steps, not by the call), andimport_xpt, which needs the package haven (not installed here: the function stops with that message; its twin of a transport file was not run). - What the
.rdsfiles lack for a listing. The.rdsfiles of the recording are not the SAS data sets. They have the label of each variable (kept byadam(); a subset of the rows drops the attribute, soadam()keeps them aside), the dates as classDate(printed as DATE9., the format the SAS data has; another SAS date format would not be known), and character values already cut at the length of the data (40). They do not have the SAS format or the informat of a variable, the length of a character variable of 1 to 40 characters apart from thewidthattribute, or the order of the variables in the data set. A listing of a data set whose variables have another SAS format than DATE9. or BEST12. would be printed the wrong way. - Page width. The line size is 160 as
%studysets it. A column wider than a line is not split. - A missing value in a condition. SAS compares a blank text with
NE 'Y'as true; R'sNA != "Y"isNA, which is "no". The R programs writeablfl != "Y" | is.na(ablfl)where SAS writesablfl NE 'Y'(T14.7.1, and the default oftable_visit(postwhere = )). A missing number in a condition is "no" in R and the smallest number in SAS. - Options that no table of the recorded run and no known answer uses are written from the macro and the textbook and were run only to see that they work; the numbers are not compared with SAS:
risk(ci = "wald" / "mn" / "ac")and the limits of a risk difference of "wald" (kept within -1 and 1 here),proportion(ci = "wilson" / "wald" / "agresti-coull"),ttest(variances = "unequal")(the registry has its Welch numbers; the macro call of it is not in a fixture),wilcoxon(correction = "no"),cmh(statistic = "rowmean")(checked against the correlation form for two groups) and the general association with one stratum (written out, checked againstmantelhaen.teston two strata),chisq(within = ),fisher(within = ),lsmeans(within = , class = )and the LS means of a model with class factors and covariates,rateratio(within = , covars = , class = ),termflagswithout terms,freq(total = "yes", levels = ),describe(qntldef = 1 to 4),logistic(within = ),cox(ties = "exact"),cox(covars = , class = ),km(level = ),mmrmwithmethod = "ml", with another type thanun, withddfm = "residual"and with incomplete data (the Kenward-Roger adjustment is proven only where it is none: complete balanced data, 9 df),table_modelandtable_tteon data other than the made-up ones. - Wilcoxon limits. SAS gives asymptotic (Moses) limits through
PROC NPAR1WAY HL. The R function uses the formula of the known answer (the differences of order C and n1 n2 + 1 - C, C rounded). That is proven on the one fixture (4 against 4); other sample sizes, and a C below 1 (no limit), were not compared with SAS. No exact test is made. format=oftlf_layout()is a named list of codelists, not the name of a SAS format.- The last digit of
raw. SAS cuts at 15 digits; R rounds at 15 digits. - Column order of a result. The R result has the columns in the order of the manual: the group variables,
groups,block,stat,value,text. The proof compares the numbers file and the listing, not the order of the columns.
Faults seen in the SAS macros (reported, not changed)#
%freqwithvar=and no record that qualifies gives a result with no row and no NOTE, while%describesays "No data qualified".%adjustover a result that has awithinvariable (one p value per pair and term) adjusts all those p values as one family: the family is the pairs, not the pairs by term. That may be meant; the header does not say it.%riskand%ttestneedorder=only forpairs=all; with a list of pairs a missingorder=variable is no error (the recorded T90.1 relies on it).%_spl_stackpairsshould say so in its header.%printdrops nothing at the top of a page itself: the blank row of the display data set of%layoutthat begins a page is shown as the one blank line of HEADSKIP (T90.1, page 3). A reader of the display data set may count one blank row too many.%interaction_phregand%interaction_logisticleave the convergence at the default (the other macros tighten it:%coxxconv 1E-12,%logisticgconv 1E-10). The likelihood-ratio p value of SG90_T is 4e-9 (relative) from the converged value. A tightergconvandxconvwould make it the same on any computer.%mmrmreadsConvergenceStatusonly; withddfm=krand a structure other thanunPROC MIXED gives the Kenward-Roger df, the macro header does not say that the R twin cannot.
What is left#
Nothing of the 94 macros is unmade. What is left is proof: the SAS record of the cases marked unproven above (the options of a method that no table or known answer uses), the tte fixtures with events and censors from several data sets, import_xpt on a transport file (with haven installed), the look of the RTF and the figures against the SAS files with a person's eye, and a SAS run of the draw-again cases for the R plot= and par=.