我们经常会遇到以下情况:冗长的(数据分析)计算吐出一堆结果值。想象一下 Python 或脚本或 R 脚本进行一些资源密集型计算。然后我们撰写一篇文章或报告,我们想要描述结果并在文本或表格中列出结果值。复制和粘贴值很容易出错,而且工作量很大,所以最好自动包含值。
例子:这里的所有数值都是脚本的结果,需要以某种方式包含在文章中:
实现这一目标的最佳方法是什么?
笔记:计算可能会持续数小时 - 甚至可能在单独的计算集群上进行。因此,计算结果应该与将结果包含在 LaTeX 文档中并对其进行编译是分开的步骤。
奖金要求:看到 Latex 编辑器(例如 Overleaf)中显示的值而不仅仅是 include 命令可能会很好。当使用 Overleaf 上的所见即所得编辑器时,这可能非常有用,但我怀疑如果不复制和粘贴值或以某种方式预处理 Latex 文件,是否可以做到这一点。
也可以看看:reddit 上的这个问题。
答案1
将结果写入单独的文本文件并使用\input
最直接、最简单的解决方案是在计算过程中将每个值写入文本文件。例如,计算可以在最后收集 LaTeX 文档中所需的所有值并将时间写入文本文件:
def write_include_value(name, value):
"""Write a value to a text file for later use in a LaTeX document.
Parameters
----------
name : str
name of the file, also used in the LaTeX document as name
value : str
the value to be stored, passed as string so formatting (e.g. number of
digits) needs to be done before calling the function
"""
with open(OUTPUT_DIR / "include-values" / name, "w") as f:
f.write(value + "\n")
调用函数时可以配置显示的位数:
write_include_value("average_temperature", f"{average_temperature.values:.1f}")
将以下代码片段放在 LaTeX 文档的序言中,可以轻松地将值包含在文本中:
\newcommand*{\includevalue}[1]{\input{../../data/output/include-values/#1}\unskip}
然后可以使用新命令使用该值\includevalue
,例如:
\includevalue{average_temperature}
陷阱和缺点
- siunitx 包不适用于高级命令
\input
,因此\includevalue
不能在命令内部使用\qty
。因此我添加了一个额外的命令来处理带单位的数量:
% The package siunitx does not work with the highlevel command \input, therefore \includevalue
% cannot be used inside of a \qty command. Instead use: \qtyincludevalue{filename}{m/s^2}
% Copied and adapted from here: https://tex.stackexchange.com/a/108093/8964
\def\inputval{0}
\newread\inputFile
\newcommand*{\qtyincludevalue}[3][]{%
\IfFileExists{../../data/output/data-values/#2}{
\openin\inputFile=../../data/output/data-values/#2
\read\inputFile to \inputval
\closein\inputFile
\qty[#1]{\inputval}{#3}%
}{\qty[#1]{#2}{#3}}%
}
- 有些期刊在提交过程中会限制文件数量。使用这种通过单独文件包含值的方法意味着您很容易就会有 100 个文件,而提交门户将不允许您提交文章。我使用这个 Python 脚本作为解决方法,将所有包含的内容替换为实际值。这并不好,因为它为 LaTeX 文档的编译增加了一个额外的步骤,这会使事情更容易出错,但它确实有效。
import os
import re
import sys
def replace_placeholders(filename):
with open(filename, "r") as f:
contents = f.read()
pattern = r"\\(qty)?includevalue\{([\w-]+)\}"
matches = re.findall(pattern, contents)
for match in matches:
replace_string = ""
file_path = os.path.join("data", "output", "data-values", match[1])
with open(file_path, "r") as f:
replace_string = f.read().strip()
if match[0] == "qty":
replace_string = "\\qty{" + replace_string + "}"
contents = contents.replace(
"\\{}includevalue{{{}}}".format(match[0], match[1]), replace_string
)
return contents
if __name__ == "__main__":
print(replace_placeholders(f"{sys.argv[1]}.noreplace"))
- 包含值文件的文件夹路径必须指定两次 - 一次在 Python 代码中,然后再次在 LaTeX 标头中。
答案2
如果你使用 R,请使用 Knitr
针织品允许您在 LaTeX 文档中执行 R 代码片段。如果计算结果存储在文件中(也可以是二进制文件,如 NetCDF 或 CSV 文件),则可以使用 Knitr 和 R 代码加载所需的值并将其包含在 LaTeX 文件中:
<<results="asis",echo=FALSE>>=
cat(read.csv("a_csv_file.csv", sep=";")[1,2])
@
或者表格:
<<xtable, results="asis">>=
n <- 100
x <- rnorm(n)
y <- 2*x + rnorm(n)
out <- lm(y ~ x)
library(xtable)
xtable(summary(out)$coef, digits=c(0, 2, 2, 1, 2))
@
(示例取自卡尔·布罗曼)
理论上,Knitr 也支持 Python,但使用 R 在 LaTeX 中执行 Python 代码片段感觉很奇怪。所以如果你不使用 R,我建议不要使用 Knitr。
最小示例
\documentclass{article}
\begin{document}
The meaning is:
<<results="asis",echo=FALSE>>=
cat(read.csv("a_csv_file.csv", sep=";")[1,2])
@
\section*{A table}
<<xtable, results="asis",echo=FALSE>>=
n <- 100
x <- rnorm(n)
y <- 2*x + rnorm(n)
out <- lm(y ~ x)
library(xtable)
xtable(summary(out)$coef, digits=c(0, 2, 2, 1, 2))
@
\end{document}
上述代码存储为knitr_test.Rnw
并使用以下命令创建一个 tex 文件:
R -e 'library(knitr);knit("knitr_test.Rnw")'
该 tex 文件如下所示:
\documentclass{article}
% knitr inserts a lot of stuff in the header, omitted here for simplicity
\begin{document}
The meaning is:
42
\section*{A table}
% latex table generated in R 4.3.2 by xtable 1.8-4 package
% Wed Jan 17 17:53:32 2024
\begin{table}[ht]
\centering
\begin{tabular}{rrrrr}
\hline
& Estimate & Std. Error & t value & Pr($>$$|$t$|$) \\
\hline
(Intercept) & 0.24 & 0.08 & 2.8 & 0.01 \\
x & 2.02 & 0.09 & 21.3 & 0.00 \\
\hline
\end{tabular}
\end{table}
\end{document}
这是渲染的结果:
背页
在线 LaTeX背页支持开箱即用的 Knitr。只需将您的 LaTeX 文件重命名为*.Rtex
。
缺点
- 编译 LaTeX 文件时需要执行一个额外步骤。
- 包含单个值的代码相当长。
更多资源
这是一个简短的教程关于如何将 Knitr 与 LaTeX 结合使用。
在背面文件。
这问答讨论了如何避免使用cat()
隐藏输出前缀所必需的[1]
。
答案3
您正在搜索的概念文学编程,这实际上不仅限于通过 knitr 使用 R 的 LaTeX,而且可能是近年来最成功的概念证明。
但对于一般的文学编程,不限于 R,也不限于 Python,也不限于 LateX,我建议四开本。Quarto 可以使用 Python、R、Julia 和 Observable 创建动态内容,以通过 LaTeX 或 ConTeXt(以及其他几种格式,但这与此无关...)生成 PDF。
如果有任何 R 块,Quarto 将默认使用 Knitr 引擎,但 Jupyter 的可执行代码是另一种语言(python、julia、bash 等)。请参阅这里 了解引擎选择的详细信息。
最后,Knitr 支持 Python 和许多其他语言,而不仅仅是“理论上”。通过 R 执行 Python 并不是什么大罪,只要它有效。此外,这还可以带来一些优势,因为它允许在同一文档中运行两种语言的代码片段,甚至可以将变量从一种语言传递到另一种语言,例如:
---
title : A minimal working example
format: pdf
classoption: twocolumn
header-includes: \columnsep1.5cm
---
```{r}
#| echo: false
library(reticulate)
```
##### This is Python in \LaTeX:
```{python}
#| echo: false
#| results: asis
import matplotlib.pyplot
import pylab
lst = [11.21,22.84,33.25,44.67,55.35]
lst2 = [5,6,7,12,24]
print("This is a plot of list")
print(lst)
print("and")
print(lst2)
print(":")
```
```{python}
#| echo: false
#| fig-cap: Pyhon plot
matplotlib.pyplot.scatter(lst,lst2)
```
\newpage
##### And this R using Python code
The values of python list "`lst`" are
`r knitr::combine_words(py$lst)` with a mean
of rougly `r round(mean(py$lst),1)`.
```{r}
#| echo: false
#| results: asis
#| fig.cap: R plot of Python lists
#| fig-height: 4
plot(py$lst,py$lst2,ylab="",
xlab="",col="blue",pch=19)
```
您可以添加engine: jupyter
到标题以避免使用 knitr(R 代码将仅显示但不执行)或者简单地删除最后一部分(从到\newpage
末尾)以自动切换到 Jupyter,它将运行没有 R 或 knitr 的 python3。
但如果出于某种原因,这种工作流程让你感到困扰,那么还有运行 Python 的 LaTeX 包直接地。
答案4
为 Latex 中的结果生成新的 Latex 命令
您可以创建自己的 Latex 命令:每个结果一个命令。计算管道可以运行一个脚本,该脚本创建一个包含命令定义和结果的 Latex 头文件。然后可以将该文件包含在 Latex 文档中。
这有点类似于通过文本文件解决\input
。感谢@Teepeemm 在评论中提出此解决方案的变体。
代码片段 1
在你的计算管道中添加类似这样的内容,并将所有值添加到你的 Latex 文档中所需的字典中:
LATEX_FILE_NAME = 'result_values.tex'
result_values = {}
meaning_of_life = 42
result_values["meaningoflife"] = f"{meaning_of_life:d}"
# - A unit can be passed as string as second parameter: the LaTeX package siunitx will be used to display the quantity.
# - Use .2f for rounding to two decimals.
gravity_ms2 = 9.80665
result_values["gravity"] = f"{gravity_ms2:.2f}", "m/s^2"
write_result_values(result_values, LATEX_FILE_NAME)
代码片段 2
此函数写入 Latex 标头:
import re
def format_latex_command(key, value, unit=None):
# test if key contains invalid characters for a latex command:
if not re.match(r"^[a-zA-Z]+$", key):
raise ValueError(f"Invalid key '{key}': not a valid latex command name")
if unit is not None:
value = f"\\qty{{{value}}}{{{unit}}}"
return f"\\newcommand{{\\{key}}}{{{value}}}"
def write_result_values(result_values, filename):
"""Write the result values to a latex header file creating new Latex command for each value.
Parameters
----------
result_values : dict
Results to be written to the Latex header file: keys of the dictionary are the names of the
latex commands the values are either a single value or a tuple containing the value and the
unit.
filename : str
The name of the Latex header file to write the result values to.
"""
result_values_flat = [
(key, *value) if isinstance(value, tuple) else (key, value)
for key, value in result_values.items()
]
latex_commands = [format_latex_command(*params) for params in result_values_flat]
with open(filename, "w") as f:
f.write("\n".join(latex_commands) + "\n")
如何在Latex文档中使用
result_values.tex
运行上述代码片段 1 后,将创建一个文件:
\newcommand{\meaningoflife}{42}
\newcommand{\gravity}{\qty{9.81}{m/s^2}}
...然后可以使用新的 Latex 命令将值添加到文本中:
\documentclass{article}
\usepackage{siunitx}
\include{result_values}
\begin{document}
\section*{Example of including result values}
The gravity constant is \gravity. And the meaning of life is \meaningoflife.
\end{document}
渲染结果如下: