flowchart LR
A["analysis code"] --> B["results_macros.tex"]
B --> C["\input{results_macros.tex} in preamble"]
C --> D["\MacroName{} everywhere in the paper"]
In this post I want to introduce a quick and simple way to deal with all your in-text numbers in a coherent and safe way.1 What do I mean with in-text numbers? Here is an example:
In our experiment, welfare increases by 2.3% under policy A, while it increases by 1.1% under policy B.
2.3 and 1.1 are in-text numbers, and odds are that over the course of your paper’s journey through the review process, they might change - ever so slightly, but still.
1 In-Text Numbers are often Wrong
This all too common pattern occurs frequently in our reproducibility checks. In general, there are two sources of the problem. Either the numbers cannot be verified from the replication package in a straightforward way (because they were computed by hand, which is pretty bad), or if there is a way to recompute the numbers via the package, they might not correspond (they might be outdated). Good news is that there is a simple solution for this: never type a numeric result. Read it from disk.
2 The pattern
Instead of directly typing numbers into your \(\LaTeX\) source, your analysis script writes them to a small auxiliary file as \newcommand macros. Your paper \inputs that file once in the preamble, and every in-text number becomes a macro call instead of a literal.
Two properties fall out of this for free:
One source of truth. The abstract, the intro, a footnote, and any of your tables all read the same macro. If the estimate changes, you re-run one function and recompile - every occurrence updates together, or none do.
Your results become traceable. A replicator (or your future self) can check which defined macros actually get used in the paper - and, just as usefully, which don’t (dead results, or a number quietly still hard-coded somewhere instead of using its macro):
# every macro name defined in results_macros.tex grep -oP '\\newcommand\{\\\K[A-Za-z]+' results_macros.tex | sort -u > defined.txt # which of those names actually appear (as a \Macro{} call) in the paper grep -f defined.txt paper.tex
3 A worked example
Let’s pretend we actually “run” policy A and policy B from the introductory example, and write out exactly the numbers quoted in that opening sentence. We use R here and show the same for STATA in Section 6.
# --- Policy A: a toy "run" of counterfactual A ---
# In a real project this would be your full analysis,
# as complex and lengthy as you prefer. This is a trivial
# example that just outputs the numeric results.
run_policy_A <- function() {
baseline_welfare <- 100 # welfare under the status quo
new_welfare <- 102.3 # welfare after applying policy A
pct_change <- (new_welfare / baseline_welfare - 1) * 100 # % change
list(pct = pct_change, n_units_reassigned = 64) # a % AND a count, bundled
}
# --- Policy B: a second, independent counterfactual ---
run_policy_B <- function() {
baseline_welfare <- 100
new_welfare <- 101.1
pct_change <- (new_welfare / baseline_welfare - 1) * 100
list(pct = pct_change, n_units_reassigned = 12)
}
resA <- run_policy_A() # named list: % welfare change + a reassignment count
resB <- run_policy_B()
resA$pct
[1] 2.3
$n_units_reassigned
[1] 64
resB$pct
[1] 1.1
$n_units_reassigned
[1] 12
Now write a tiny helper that turns named R values into \newcommand lines, and point it at a file your paper will \input. Notice this one file mixes three genuinely different kinds of value - a percentage, a plain integer count, and a text label - because \newcommand{name}{...} doesn’t care what’s inside the braces:
# a latex macro creator function
# it needs a name, and a value:
mac <- function(name, val) sprintf("\\newcommand{\\%s}{%s}", name, val)
# you can see, this just wraps both parts in a latex command, that will have the `name` you gave above.
# Which policy "wins" is a STRING, not a number, and it belongs in the same
# file for the same reason the percentages do: it should never be typed by
# hand into the paper ("policy A is preferred...").
winner <- if (resA$pct > resB$pct) "A" else "B"
# this creates a vector of strings
lines <- c(
"% Auto-generated -- do not edit by hand.",
sprintf("%% Generated: %s", format(Sys.time(), "%Y-%m-%d %H:%M")),
"",
"% ---- percentages (one decimal, LaTeX percent sign baked in) ----",
mac("WelfareA", sprintf("%.1f\\%%", resA$pct)),
mac("WelfareB", sprintf("%.1f\\%%", resB$pct)),
"",
"% ---- integer counts (no decimals, no percent sign) ----",
mac("NReassA", resA$n_units_reassigned),
mac("NReassB", resB$n_units_reassigned),
"",
"% ---- a text label -- not a number at all ----",
mac("PreferredPolicy", winner)
)
# we write this vector to a file
writeLines(lines, "results_macros.tex")
# let's print it to screen
cat(lines, sep = "\n")% Auto-generated -- do not edit by hand.
% Generated: 2026-09-11 14:42
% ---- percentages (one decimal, LaTeX percent sign baked in) ----
\newcommand{\WelfareA}{2.3\%}
\newcommand{\WelfareB}{1.1\%}
% ---- integer counts (no decimals, no percent sign) ----
\newcommand{\NReassA}{64}
\newcommand{\NReassB}{12}
% ---- a text label -- not a number at all ----
\newcommand{\PreferredPolicy}{A}
That file is now a build artifact, exactly like a figure or a table - check it into your replication package under outputs/, regenerate it every run, never hand-edit it. The header comment matters more than it looks: it’s the thing that stops a coauthor “quickly fixing” a number by typing over a macro definition instead of re-running the code that produced it.
In the paper’s preamble:
\input{outputs/results_macros.tex}And in the text, instead of typing 2.3, 1.1, 64, 12 and A by hand, you will henceforth write:
In our experiment, welfare increases by \WelfareA{} under policy A
(reassigning \NReassA{} units), while it increases by \WelfareB{} under
policy B (reassigning \NReassB{} units). Policy \PreferredPolicy{} is
therefore preferred.Change either policy function, rerun the two chunks above, recompile the paper, and every quoted number - and the text label naming the winner - moves together. There is no step where a human retypes a digit.
4 Relationship to Table Builders
If you already use a table-exporting package - esttab/estout or outreg2 in Stata, stargazer/texreg/modelsummary in R - you might wonder why you’d need any of this. Those packages solve a different problem: they take a fitted model object and emit a complete, ready-made \(\LaTeX\) table, formatting and all. That’s exactly the right tool for producing a full regression table, and you should keep using them for that.
What they don’t do is help with any numbers you want to quote in the text, a table, or on a slide - a single scalar pulled out of that same result and stated in prose. Typically, any kind of non-standard output (i.e. everything except a regression table) needs to be presented somehow, in a table of some sort or in text, and this is where those macros are useful.
5 The same idea works with JSON
Everything above is \(\LaTeX\)-specific, because that’s what the overwhelming majority of economists use. But the underlying idea - analysis code writes small, structured facts to disk; a downstream document reads them - has nothing to do with \(\LaTeX\) itself. If your output is Markdown, a Word document, or a website, you could for example write a JSON instead of \newcommand lines:
library(jsonlite) # provides write_json() for structured, human-readable output
results <- list(
welfare_A = round(resA$pct, 1), # numeric
welfare_B = round(resB$pct, 1), # numeric
n_reassigned_A = resA$n_units_reassigned, # integer
n_reassigned_B = resB$n_units_reassigned, # integer
preferred_policy = winner # string
)
write_json(results, "results.json", auto_unbox = TRUE, pretty = TRUE)
cat(readLines("results.json"), sep = "\n"){
"welfare_A": 2.3,
"welfare_B": 1.1,
"n_reassigned_A": 64,
"n_reassigned_B": 12,
"preferred_policy": "A"
}
A Markdown or Quarto document then pulls values with inline code, e.g. 2.3 (this is a Quarto document, and that number appeared by including r results$welfare_A in backticks, not because I typed 2.3). The mechanism differs - file format, retrieval syntax - but the idea is identical: never type a number, read it from disk.
Once numbers live in JSON, an entire family of general-purpose templating engines is built to consume exactly this: a template file with placeholders, a data file with values, and a render step that stitches them together - tools like Mustache and others.2 Those are not \(\LaTeX\), but that’s the point: your results.json becomes a single data source that a paper’s macro file, a slide deck, a templated HTML report, and an auto-generated email can all render from independently, without any of them agreeing on a common document format.
6 Stata Example
Nothing about this requires R. Here is the direct Stata analogue of the two chunks above: same two toy policies, same three kinds of output (percentage, count, string), written to the same kind of file.
// policy_macros.do
// -----------------------------------------------------------------
// Runs two toy policy counterfactuals and writes the results out as
// LaTeX \newcommand macros -- the Stata analogue of the R example above.
// -----------------------------------------------------------------
clear all // start from a clean Stata session
set more off // don't pause output with --more--
// ---- Policy A: a toy "run" of counterfactual A ----
scalar baseline_welfare = 100 // welfare level under the status quo
scalar new_welfare_A = 102.3 // welfare level after applying policy A
scalar pct_change_A = (new_welfare_A / baseline_welfare - 1) * 100
scalar n_reass_A = 64 // a second, unrelated output: a count
// ---- Policy B: a second, independent counterfactual ----
scalar new_welfare_B = 101.1 // welfare level after applying policy B
scalar pct_change_B = (new_welfare_B / baseline_welfare - 1) * 100
scalar n_reass_B = 12
// ---- decide the "winner" -- a STRING output, not a number at all ----
local winner "B" // default guess
if pct_change_A > pct_change_B local winner "A" // overwrite if A actually wins
// ---- format every number to a plain string BEFORE writing it ----
// string(x, "%4.1f") pads to a field width of 4, so strtrim() removes the
// resulting leading space -- Stata's numeric-to-string formatting is
// field-width based, unlike R's sprintf(), so this trim step is needed.
local welfareA = strtrim(string(pct_change_A, "%4.1f"))
local welfareB = strtrim(string(pct_change_B, "%4.1f"))
local nreassA = strtrim(string(n_reass_A, "%2.0f"))
local nreassB = strtrim(string(n_reass_B, "%2.0f"))
// ---- open the macro file for writing (overwrites any existing file) ----
tempname fh // a handle Stata uses to refer to the open file
file open `fh' using "results_macros.tex", write replace text
// header comment: same "do not edit by hand" convention as the R version
file write `fh' "% Auto-generated by policy_macros.do -- do not edit by hand." _n
file write `fh' "% Generated: `c(current_date)' `c(current_time)'" _n _n
// percentages: "\%" is the literal, escaped LaTeX percent sign
file write `fh' "\newcommand{\WelfareA}{`welfareA'\%}" _n
file write `fh' "\newcommand{\WelfareB}{`welfareB'\%}" _n _n
// integer counts: no decimals, no percent sign
file write `fh' "\newcommand{\NReassA}{`nreassA'}" _n
file write `fh' "\newcommand{\NReassB}{`nreassB'}" _n _n
// a text label -- Stata locals interpolate directly into the string
file write `fh' "\newcommand{\PreferredPolicy}{`winner'}" _n
file close `fh' // flush the buffer and close the file
// echo the file back into the log, so the log itself documents what was written
type "results_macros.tex"Run it from the command line the way you’d run any batch job:
stata-mp -b policy_macros.do
This produces policy_macros.log alongside the usual output. Here is the actual results_macros.tex this script writes:
% Auto-generated by policy_macros.do -- do not edit by hand.
% Generated: 10 Sep 2026 17:42:46
\newcommand{\WelfareA}{2.3\%}
\newcommand{\WelfareB}{1.1\%}
\newcommand{\NReassA}{64}
\newcommand{\NReassB}{12}
\newcommand{\PreferredPolicy}{A}
Same numbers as the R version. Use whatever you already use for your analysis; you don’t need to add a second language just for this.
7 Also useful for your slides
The macro file isn’t paper-specific. Nothing stops a talk from \input-ing the same results_macros.tex and building an entirely different presentation of the same numbers - a Beamer table with a progressive reveal, say, instead of the paper’s static table. Same macros, different skeleton, one source of truth for both documents:
\begin{frame}{Main Result}
\begin{itemize}
\item<1-> Policy A raises welfare by \WelfareA{} (\NReassA{} units reassigned)
\item<2-> Policy B raises welfare by \WelfareB{} (\NReassB{} units reassigned)
\item<3-> Policy \PreferredPolicy{} is preferred
\end{itemize}
\end{frame}8 Checklist
Footnotes
Notice that none of this is new, rather it follows in the footsteps of literate programming and it’s various incarnations (Sweave, knitr, and othersR Markdown, Quarto itself, Jupyter notebooks, Org-mode Babel for Emacs, Pweave for Python, Stata’s own
dyndoc, and, closer to the root of the family tree, Norman Ramsey’snoweb- itself a simplification of Knuth’s original WEB.↩︎“logic-less” templates, ports in dozens of languages, Jinja (Python, also what powers Ansible), Handlebars (Mustache’s JS-flavoured superset), and Liquid (Ruby, used by Jekyll).↩︎
Citation
@misc{oswald2026,
author = {Oswald, Florian},
title = {Are {Your} in-Text {Numbers} Correct? {Do} {They} {Match}
{Your} {Table?}},
date = {2026-09-10},
url = {https://jpedataeditor.github.io/posts/20260910-latex-macros/},
langid = {en}
}