Lisp-Stat 简介
虽然我在一年多前就在我的 Gateway 33Mhz PC 上安装了 Linux,占用了一个 80MB 的分区,但我并没有认真地将 Linux 用于我的科学工作,主要是因为我缺乏磁盘空间。最近,我买了一个新的 1GB 硬盘,这个借口就不存在了。所以我决定安装 Lisp-Stat,这是一个我主要用于统计计算的程序。
Lisp-Stat 由明尼苏达大学的 Luke Tierney 编写,是一个强大、交互式、面向对象的统计计算环境,它基于 Lisp 的 Xlisp 方言。它几乎统一地运行在 Microsoft Windows、Mac 和基于 Unix 的 X11 系统下。它具有良好的图形功能,可用于静态和动态图形,以及用于常见统计计算的函数。
此外,使用外部函数接口,可以从 Lisp-Stat 中调用 C 和 Fortran 程序。字节码编译器可用于加速您调试后的程序。当然,Lisp-Stat 不仅仅用于统计计算;我经常将其用于各种用途:作为计算器,用于计算学生的成绩,作为超文本插图以及矩阵运算的引擎。
在本文中,我将向您介绍 Lisp-Stat 的一些功能。虽然我在本文中使用了 Lisp,但我不会深入探讨 Lisp 编程的细节,除非它影响我们的讨论。如果您是 Lisp 的新手,您可能想阅读 Robert Sanders 在 1995 年 3 月的Linux Journal上发表的关于 Scheme 的文章,因为 Lisp 和 Scheme 密切相关。我所做的一些评论实际上适用于 Lisp,但区分什么是 Lisp 部分,什么是 Stat 部分没有实际意义。您无需了解 Lisp 即可使用任何示例或阅读本文。如果您对 Lisp-Stat 产生了浓厚的兴趣,您应该购买 Tierney 撰写的名为 Lisp-Stat 的书,ISBN 0-471-50916-7,由 John Wiley 出版。除了是 Lisp-Stat 的权威参考书外,它还提供了 Lisp 的快速实用的入门介绍。
假设您已成功安装 Lisp-Stat,只需键入 xlispstat 即可调用该程序。要退出程序,只需键入 (exit)。图 1 显示了一个简单的会话。大小写无关紧要,您在图中看到的 > 是 Lisp-Stat 的提示符。我使用的数据是 WWW 服务器在一天中的 24 小时内处理的请求数。def 宏将变量名 requests 绑定到值列表。
如示例所示,计算平均值和标准差等汇总统计信息非常简单。由于 Lisp-Stat 基于 Lisp,因此您拥有 Lisp 的所有数据操作能力。可以使用丰富的数据类型,包括向量、序列、字符串、矩阵。在图 1 中,变量 A 被定义为一个 3x3 矩阵,B 是一个包含三个数字的列表。该示例通过计算 A<+>-1<+>b 求解 Ax=b,得到解 [2, -6, 1]。请注意,b 是一个列表,而 A<+>-1<+> 是一个矩阵,但 Lisp 解释器会处理类型,并实际上计算矩阵和向量的乘积。
Lisp-Stat 的许多函数都对序列进行操作,序列可以是列表或向量,并且它们是向量化的,这意味着这些函数可以应用于列表参数,结果是应用该函数到列表的每个元素的结果列表。另一些函数是向量归约,这意味着它们可以应用于参数列表,但它们返回单个数字。在图 1 中,函数 mean 是向量归约函数的示例;它将列表的列表视为一个长列表,并返回长列表的平均值。另一方面,函数 normal-cdf 是一个向量化函数,在包含三个数字的列表上调用它会生成包含三个答案的列表。当然,如果我们希望 mean 以向量化的方式运行,语句 (mapcar #'mean (list (normal-rand 10) (normal-rand 20))) 就可以实现。
一张图片胜过千言万语,尤其是在统计学中。Lisp-Stat 拥有出色的图形工具。图形系统基于面向对象的范例。创建图形窗口或绘图的函数返回一个对象作为结果。返回的对象只是另一种数据类型,很像数字或列表,可以在适当的计算中使用。
常用的图形函数有 histogram(用于构建直方图)、plot-points(用于绘制 (x,y) 对)、plot-lines(用于通过线条连接 (x,y) 对)、plot-function(用于绘制单变量函数)、spin-function(用于绘制双变量函数)和 spin-plot(用于 3D 绘图)。
所有 spin 函数都提供控件,用于在它们创建的图形中进行偏航、俯仰和滚动。图 2 显示了请求数与 24 小时中每个小时的绘图。这些图是使用以下代码行生成的。
(histogram requests) (def time (iseq 24)) (plot-lines time requests) (send * :add-points time requests) (send ** :point-symbol (iseq 24) 'diamond)
仅绘制线条是不够令人满意的,因为点的确切位置会丢失。因此,在构建绘图后,我们使用 send 函数向绘图发送“消息”,要求对象向图形添加点,从而生成如图所示的图形。* 在 send 函数中指的是上一个命令的结果,即绘图对象。** 指的是上一个命令之前的命令的结果。在示例中,我要求绘图符号是菱形而不是默认的圆形。用户可以选择相当多的绘图符号。
在每个绘图中,都有一个菜单按钮,其中包含更多有用的选项。可以使用鼠标选择或取消选择点,突出显示某些点,将绘图另存为 postscript 文件等。我将仅讨论一个功能,即链接。链接绘图是一种在绘图之间共享信息的方式。例如,考虑图 2,其中我们有一个 requests 与 time 的绘图以及一个 requests 的直方图。
如果在每个绘图的菜单中选择 Link View 项来启用链接,则通过在按下按钮的情况下拖动鼠标来选择直方图中的垂直条会导致折线图中的相应点突出显示。您可能需要仔细查看该图才能看到最高峰值出现的点被突出显示,因为它对应于突出显示的直方图条。链接在查看多维数据时非常有用,因为它可以更好地了解同一组点如何在不同视图中投影。
Lisp-Stat 的在线文档可通过函数 help、help* 和 apropos 获得。要获得有关 mean 函数的帮助,请键入 (help 'mean)。使用引号至关重要,否则解释器会假设 mean 是一个变量并尝试对其求值。但是,在许多情况下,人们不知道函数的名称。
例如,将两个矩阵相乘的函数是 mat-mult 还是 matrix-multiply?键入 (apropos 'mult) 将打印所有包含单词“mult”的符号列表。这可能有助于您缩小搜索范围。另一方面,如果您知道要查找的函数包含单词 matrix,则 (help* 'matrix) 将返回所有包含单词 matrix 的符号的帮助。现在的帮助工具不如最佳,一些人正在开发更完善的帮助系统。
我通常阅读新闻组 sci.stat.math,几乎总会有人想知道如何计算 F 概率或如何生成正态随机变量。Lisp-Stat 具有所有常用分布的分布函数和生成器。例如 (normal-cdf 1.645) 将给出 1.645 左侧的概率,约为 0.95。语句 (def x (normal-rand 100)) 将定义 x 为 100 个标准正态变量的列表。Students-T、Gamma、Beta、卡方和 F 分布也存在类似的函数,如 (help* 'cdf) 或 (help* 'rand) 将显示。
Lisp-Stat 有许多用于输入和输出的函数。对于处理文件,我很少需要超出使用两个函数 read-data-file 和 read-data-columns。语句 (read-data-file "foo.dat") 将整个文件内容作为一个长列表返回,而 (read-data-columns "foo.dat") 返回文件列的列表。可以将数据文件中的列数指定为 read-data-columns 的第二个参数。否则,它会根据第一行猜测列数。函数 format 类似于 C 的 sprintf(),是一个用于格式化打印的多功能函数。
Lisp-Stat 的图形系统和回归模型是使用基于原型的对象系统实现的。这与 C++ 等语言使用的基于类的对象系统或 Common Lisp Object System (CLOS) 使用的方法不同。简而言之,有一个根原型对象,从中创建所有其他对象的实例。对象可以具有槽来保存信息,并且它们响应使用 send 函数分派给对象的消息。消息通常是关键字,即以冒号开头的单词——图 2 中的 :add-points 就是一个例子。
实际实现操作的代码称为消息的方法。宏 defproto 和 defmeth 使构造对象和编写方法的过程更容易。如果 Lisp-Stat 仅提供用于构建统计模型的对象,那将不会那么有趣。窗口系统提供了用于构建用户界面(如菜单、对话框、滑块控件等)的对象。因此,可以构建漂亮的对话框来配合计算。
图 3 显示了使用滑块对话框的动态动画示例。绘制了函数 sin2pi x/n。滑块允许用户在 n 更改时查看绘图变化。执行此操作的代码如下。
(setf n 1) (defun f (x) (sin (/ (* 2 pi x) n))) (def sine-plot (plot-function #'f -5 5)) (defun change-n (x) (setf n x) (send sine-plot :clear :draw nil) (send sine-plot :add-function #'f -5 5)) (sequence-slider-dialog (iseq 1 20) :action #'change-n)
函数 sequence-slider-dialog 创建一个滑块。最初,全局变量 n 为 1。每次用户使用鼠标移动滑块停止符时,都会调用函数 change-n,其中 n 的值对应于滑块停止符。在我们的示例中,n 可以是从 1 到 20 的任何整数。函数 change-n 设置 n 的值并重绘绘图。
为了使讨论可以容忍,我选择了一个可能不太有用的简单示例。对于严肃的编程,需要了解 Tierney 的书中讨论的 Lisp-Stat 的内置原型和函数。我将在进行过程中介绍我需要的内容。
我们将创建一个对象,该对象接受 (x,y) 值列表,并绘制一个叠加了最小二乘线的绘图。我们还将要求在绘图中显示最小二乘线的方程。我们首先定义一个新的原型。我们的原型是内置原型 scatterplot-proto 的后代是很自然的,scatterplot-proto“知道”关于绘制 2D 绘图的所有信息。
(defproto least-squares-plot-proto '(intercept slope) () scatterplot-proto)
请注意,我们的原型有两个槽,用于保存最小二乘线的截距和斜率。我们稍后需要访问这些槽中的值,因此最好使用 defmeth 宏定义两个简单的方法,这些方法返回槽值。
(defmeth least-squares-plot-proto :slope () "Returns the slope of the least squares line." (slot-value 'slope)) (defmeth least-squares-plot-proto :intercept () "Returns the intercept of the least squares line." (slot-value 'intercept))
我们为这些方法提供了文档字符串;可以通过诸如 (send least-squares-plot-proto :help :slope) 之类的命令来检索文档。
为了使用我们的原型,我们必须定义一个 :isnew 方法,该方法初始化原型的实例。我们的 :isnew 方法必须计算最小二乘线并存储斜率和截距。它应该利用其作为 scatterplot-proto 后代的血统,通过调用继承的方法来执行绘图任务。必须在页边距中创建一些空间来显示最小二乘线的方程。
最后,必须绘制 x,y 点,标记轴,并重绘窗口以反映更改。这是该方法。
(defmeth least-squares-plot-proto :isnew (x y &key (title "LS Plot")) (let* ((m (regression-model x y :print nil)) (beta (send m :coef-estimates))) (setf (slot-value 'intercept) (select beta 0)) (setf (slot-value 'slope) (select beta 1))) (call-next-method 2 :title title) (send self :margin 0 (+ (send self :text-ascent) (send self :text-descent)) 0 0) (send self :add-points x y) (send self :variable-label 0 "X") (send self :variable-label 1 "Y") (send self :redraw))
我们使用了 regression-model 函数来计算最小二乘线。call-next-method 函数调用 scatterplot-proto 的 :isnew 继承方法——这实际上创建了一个绘图窗口。参数 2 只是指将要绘制的变量的数量。此时,绘图窗口实际上是空白的。使用有关正在使用的字体的信息,创建了一个页边距区域。然后绘制这些点。在一个方法的主体中,变量 self 绑定到接收消息的对象。该方法通过为变量提供一些有意义的名称并重绘窗口来结束。
以上所有代码将执行的操作只是绘制点。我们如何确保最小二乘线及其方程也显示出来?我们使用任何窗口实际上都是使用 :redraw 消息绘制的事实。通过编写新的 :redraw 消息,我们可以确保获得我们想要的结果。实际上,:redraw 消息本身是通过其他三个消息 :redraw-background、:redraw-content 和 :redraw-overlays 执行的。我们实际上只需要编写一个 :redraw-content 方法,因为只有绘图的内容受到影响。所以我们开始了。
(defmeth least-squares-plot-proto :redraw-content () (call-next-method) ; Let the scatterplot do its things. (send self :adjust-to-data :draw nil) ; make sure scale is ok. (let* ((limits (send self :range 0)) (intercept (send self :intercept)) (slope (send self :slope)) (info-str (format nil "y = ~5,3f + ~5,3f x" intercept slope))) (send self :draw-string info-str 10 (+ (send self :text-ascent) (send self :text-descent))) ; Display the equation in the margin. (send self :add-function ; Draw the LS line. #'(lambda (x) (+ intercept (* slope x))) (car limits) (cadr limits) :draw nil)))
请注意,关键字参数 :draw 为 nil,以避免重绘过程中的无限循环。如果 :draw 不为 nil,则会再次调用 :redraw 方法。实际上,该线是使用 scatterplot-proto 的 :add-function 方法绘制的。我们无需担心绘制点,因为一旦我们在 :isnew 方法中添加了点,这就是 scatterplot-proto 的责任。
图 4 显示了使用以下程序运行此代码的结果。
(def x (normal-rand 20)) (def y (+ 5 (* 2 x) (normal-rand 20))) (def m (send least-squares-plot-proto :new x y))
编译 Lisp-Stat 程序非常简单。Lisp-Stat 中的语句 (compile-file "foo") 会将文件 foo 编译为 foo.fsl。当您稍后加载文件 foo 时,如果编译后的文件存在且比未编译的文件新,则会加载编译后的文件。调试可以通过 debug、baktrace 和 trace 函数完成。步进器也可用于单步执行代码行。
可以在 Lisp-Stat 中构建许多有趣的动态动画。本文仅触及皮毛。Lisp-Stat 继续发展,并且由于许多人(尤其是 Tom Almy 和 Luke Tierney)的努力,Xlisp 本身也继续越来越接近 Common Lisp。Lisp-Stat 的可用应用程序和软件库也在不断增长;有关更多信息,请参见侧边栏“获取 Lisp-Stat”。
Balasubramanian Narasimhan 在宾夕法尼亚州立大学伊利分校比伦德学院教授统计学。他的兴趣包括古典西方音乐、塞米诺尔橄榄球和印度历史。可以通过 naras@euler.bd.psu.edu 与他联系
Lisp-Stat 在网上免费提供。主要分发站点是 ftp.stat.umn.edu。在 pub/xlispstat 下查找 xlispstat-3-44.tar.gz。该文件约为 1.2 兆字节,这意味着它可以很好地放入 3.5 英寸磁盘中。它可以在 Linux 上开箱即用进行编译,但是要使用外部函数接口,您必须首先安装 GNU dld 库,该库可从 tsx-11.mit.edu 的 pub/linux/binaries/libs 下以 dld-3.2.5.bin.tar.gz 获得。对于那些不想从头开始构建冒险的人,您可以从 euler.bd.psu.edu 的 pub/lj/xlispstat 下获取二进制文件。按照 README 文件中的说明进行操作。文件 xlispstat-3.44-bin.tar.gz 是整个二进制文件。
有一个 Xlisp-Stat 用户邮件列表。要加入邮件列表,请发送一封包含您的电子邮件地址的消息,说明您要订阅 stat-lisp-news-request@stat.umn.edu。
Usenet 新闻组 comp.lang.lisp.x 专门讨论 XLisp,但这是一个低流量新闻组,平均每天 2-3 篇文章。
站点 ftp.stat.ucla.edu 有大量与 Lisp 和 Lisp-Stat 相关的内容。要查看一些超文本应用程序,请查看站点 euler.bd.psu.edu 和 www.stat.ucla.edu。