<- function (object, type, ...) {
include_report UseMethod("include_report")
}
Tl;dr
Holding package files in inst/reports
we can combine system.file(...)
with knitr::knit_child(...)
to create a include_report
generic to allow subreports to be added based on fit objects.
Why?
Most of the packages I have been working on lately are mainly used for one purpose: fit some model to the data and show the output.
If you are new to creating packages that this specific purpose, I highly recommend Conventions for R Modeling Packages.
Package Components
Here are the main components any modelling package needs, in my opinion,
a
fit
function that returns a classed object;a
predict
method (see?stats::predict
);a
print
method;a
tidy
method;some form of example, either in
data/
or a full simulation study that is callable by some function.
By adding a class to the model output we can define methods for already existing generics such as print
, summary
and tidy
.
I hope to expand on this more in the coming weeks but let’s suppose we have a package called foo
that can fit to some data via foofit(...)
Reporting
We create a report in the inst/reports
directory, I have found that creating a snippet / template that only expected a single object named fit
allows most of the model to be shown but this is clearly situational.
It may be beneficial to supply more objects, for example, a test data set to broadcast predictive power.
Placing any file in inst/
means that when the package is built or installed by another user, it will be put in the package directory which we can access programmatically.
To get the path to the report, we simply use
system.file(
"reports", "foo-report.qmd",
package = "foo",
mustWork = TRUE
)
Using this pattern allows for two main possibilities
render the report to some user-specified directory;
include the report as a knitr child.
In my workflow, since the only requisite is some object named fit
, I opt for the second and use the report as the text
argument to the knitr::knit_child
method.
Moreover, since the object has a class we can use the S3 generic / method system.
Generic
First, define a generic that can be placed in any report and used by any package.
I choose to define a method for a simple character to reduce code repetition but any new methods can avoid using / needing this.
This is because the method assumes that we want to pass the child document as text only and creates a new environment to ensure no side effects are kept.
If we wanted side-effects, we would simply pass knitr::knit_global()
to the envir
argument.
Further, note the ...
arguments are passed to the environment to ensure the report has access to what it needs to work.
<- function (object, ...) {
include_report.character <- rlang::child_env(.parent = knitr::knit_global(), ...)
envir
<- readLines(object)
input_text
<- knitr::knit_child(
out text = input_text,
quiet = TRUE,
envir = envir
)return(structure(out, class = "knit_asis", knit_cacheable = NA))
}
Method
Then, we can define a method for our hypothetical object and even dispatch based on type.
<- function(object, type, ...) {
include_report.foofit <- match.arg(type, choices = c("analysis", "prediction"))
report_type
<-
input_filename switch(
report_type,"analysis" = "foo-analysis.qmd",
"prediction" = "cross-validation.qmd"
)
include_report(
system.file("reports", input_filename, package = "foo", mustWork = TRUE),
fit = object
) }
Implementation
We can then fit the data in a main report in the usual way,
<- foo::foofit(...) fit
And include any analysis with a single line,
include_report(fit)
Image Credit
Josh Cowley. December 1st, 2022. “Reflections on a placid Tyne”