JavaScript(也称为 JS)是网络的通用语言,因为它受到所有主流网络浏览器的支持——在浏览器中运行的其他语言会被转译(或翻译)为 JavaScript。有时 JS 可能会让人困惑,但我发现使用起来很愉快,因为我尽量坚持使用好的部分。JavaScript 最初是为了在浏览器中运行而创建的,但它也可以用于其他环境,例如嵌入式语言或用于服务器端应用程序。
在本教程中,我将解释如何编写一个将在 Node.js 中运行的程序,Node.js 是一个可以执行 JavaScript 应用程序的运行时环境。我最喜欢 Node.js 的是其用于异步编程的事件驱动架构。通过这种方法,可以将函数(又名回调)附加到某些事件;当附加的事件发生时,回调就会执行。这样,开发人员不必编写主循环,因为运行时会处理这个问题。
JavaScript 还具有新的async 函数,它们使用不同的语法,但我认为它们隐藏事件驱动架构的效果太好了,不适合在操作指南文章中使用。因此,在本教程中,我将使用传统的回调方法,即使对于这种情况来说这不是必要的。
理解程序任务
本教程中的程序任务是
有关此任务的更多详细信息,您可以阅读本系列之前的文章,这些文章在 Python 和 GNU Octave 以及 C 和 C++ 中完成了相同的任务。所有示例的完整源代码都可以在我的 GitLab 上的 polyglot_fit 存储库中找到。
安装
在运行此示例之前,您必须安装 Node.js 及其包管理器 npm。要在 Fedora 上安装它们,请运行
$ sudo dnf install nodejs npm
在 Ubuntu 上
$ sudo apt install nodejs npm
接下来,使用 npm
安装所需的软件包。软件包安装在本地 node_modules
子目录中,因此 Node.js 可以在该文件夹中搜索软件包。所需的软件包是
- CSV Parse 用于解析 CSV 文件
- Simple Statistics 用于计算数据相关系数
- Regression-js 用于确定拟合线
- D3-Node 用于服务器端绘图
运行 npm 以安装软件包
$ npm install csv-parse simple-statistics regression d3-node
代码注释
就像在 C 语言中一样,在 JavaScript 中,您可以通过在注释前放置 //
来插入注释,解释器将丢弃该行的其余部分。另一种选择:JavaScript 将丢弃 /*
和 */
之间的任何内容
// This is a comment ignored by the interpreter.
/* Also this is ignored */
加载模块
您可以使用 require()
函数加载模块。该函数返回一个对象,其中包含模块的函数
const EventEmitter = require('events');
const fs = require('fs');
const csv = require('csv-parser');
const regression = require('regression');
const ss = require('simple-statistics');
const D3Node = require('d3-node');
其中一些模块是 Node.js 标准库的一部分,因此您无需使用 npm 安装它们。
定义变量
变量不必在使用前声明,但如果在使用时未声明,它们将被定义为全局变量。一般来说,全局变量被认为是不良习惯,因为如果使用不当,它们可能会导致错误。要声明变量,您可以使用 var、let 和 const 语句。变量可以包含任何类型的数据(甚至函数!)。您可以通过将 new
运算符应用于构造函数来创建一些对象
const inputFileName = "anscombe.csv";
const delimiter = "\t";
const skipHeader = 3;
const columnX = String(0);
const columnY = String(1);
const d3n = new D3Node();
const d3 = d3n.d3;
var data = [];
从 CSV 文件读取的数据存储在 data
数组中。数组是动态的,因此您不必预先确定其大小。
定义函数
在 JavaScript 中有几种定义函数的方法。例如,函数声明允许您直接定义函数
function triplify(x) {
return 3 * x;
}
// The function call is:
triplify(3);
您还可以使用表达式声明函数并将其存储在变量中
var triplify = function (x) {
return 3 * x;
}
// The function call is still:
triplify(3);
最后,您可以使用 箭头函数表达式,它是函数表达式的语法简短版本,但它有一些限制。它通常用于对其参数进行简单计算的简洁函数
var triplify = (x) => 3 * x;
// The function call is still:
triplify(3);
打印输出
为了在终端上打印,您可以使用 Node.js 标准库中的内置 console
对象。log()
方法在终端上打印(在字符串末尾添加换行符)
console.log("#### Anscombe's first set with JavaScript in Node.js ####");
console
对象是一个比仅仅打印输出更强大的工具;例如,它还可以打印警告和错误。如果您想打印变量的值,您可以将其转换为字符串并使用 console.log()
console.log("Slope: " + slope.toString());
读取数据
Node.js 中的输入/输出使用了一种非常有趣的方法;您可以选择同步或异步方法。前者使用阻塞函数调用,后者使用非阻塞函数调用。在阻塞函数中,程序停止在那里并等待函数完成其任务,而非阻塞函数不会停止执行,而是在其他地方继续执行其任务。
您在这里有几个选择:您可以定期检查函数是否结束,或者函数可以在结束时通知您。本教程使用第二种方法:它使用一个 EventEmitter
,它生成一个与回调函数关联的事件。当事件被触发时,回调就会执行。
首先,生成 EventEmitter
const myEmitter = new EventEmitter();
然后将文件读取的结束与名为 myEmitter
的事件关联起来。虽然对于这个简单的示例,您不需要遵循这条路径——您可以使用简单的阻塞调用——但这是一种非常强大的方法,在其他情况下可能非常有用。在执行此操作之前,请在本节中添加另一部分,以使用 CSV Parse 库进行数据读取。该库提供了您可以从中选择的几种方法,但本示例使用带有管道的流 API。该库需要一些配置,这些配置在一个对象中定义
const csvOptions = {'separator': delimiter,
'skipLines': skipHeader,
'headers': false};
由于您已经定义了选项,您可以读取文件
fs.createReadStream(inputFileName)
.pipe(csv(csvOptions))
.on('data', (datum) => data.push({'x': Number(datum[columnX]), 'y': Number(datum[columnY])}))
.on('end', () => myEmitter.emit('reading-end'));
我将逐步讲解这个简短而密集的代码片段的每一行
fs.createReadStream(inputFileName)
打开一个从文件中读取的数据流。流会逐步分块读取文件。.pipe(csv(csvOptions))
将流转发到 CSV Parse 库,该库处理读取文件和解析文件的困难任务。.on('data', (datum) => data.push({'x': Number(datum[columnX]), 'y': Number(datum[columnY])}))
相当密集,所以我将分解它(datum) => ...
定义一个函数,CSV 文件的每一行都将传递给该函数。data.push(...
将新读取的数据添加到data
数组。{'x': ..., 'y': ...}
构造一个带有x
和y
成员的新数据点。Number(datum[columnX])
将columnX
中的元素转换为数字。
.on('end', () => myEmitter.emit('reading-end'));
使用您创建的发射器在文件读取完成时通知您。
当发射器发出 reading-end
事件时,您就知道文件已完全解析,其内容位于 data
数组中。
拟合数据
现在您已经填充了 data
数组,您可以分析其中的数据。执行分析的函数与您定义的发射器的 reading-end
事件相关联,因此您可以确保数据已准备就绪。发射器将回调函数与该事件关联起来,并在事件被触发时执行该函数。
myEmitter.on('reading-end', function () {
const fit_data = data.map((datum) => [datum.x, datum.y]);
const result = regression.linear(fit_data);
const slope = result.equation[0];
const intercept = result.equation[1];
console.log("Slope: " + slope.toString());
console.log("Intercept: " + intercept.toString());
const x = data.map((datum) => datum.x);
const y = data.map((datum) => datum.y);
const r_value = ss.sampleCorrelation(x, y);
console.log("Correlation coefficient: " + r_value.toString());
myEmitter.emit('analysis-end', data, slope, intercept);
});
统计库期望数据采用不同的格式,因此使用 data
数组的 map()
方法。map()
从现有数组创建一个新数组,并将函数应用于每个数组元素。箭头函数在这种情况下非常实用,因为它们简洁。当分析完成时,您可以触发一个新事件以在新的回调中继续。您也可以直接在此函数中绘制数据,但我选择在新函数中继续,因为分析可能是一个非常耗时的过程。通过发出 analysis-end
事件,您还可以将此函数中的相关数据传递到下一个回调。
绘图
D3.js 是一个用于绘制数据的非常强大的库。学习曲线相当陡峭,可能是因为它是一个被误解的库,但它是我找到的用于服务器端绘图的最佳开源选项。我最喜欢的 D3.js 功能可能是它适用于 SVG 图像。D3.js 旨在在 Web 浏览器中运行,因此它假定它有一个 Web 页面要处理。服务器端工作是一个非常不同的环境,您需要一个虚拟网页才能工作。幸运的是,D3-Node 使这个过程非常简单。
首先定义一些稍后需要的有用测量值
const figDPI = 100;
const figWidth = 7 * figDPI;
const figHeight = figWidth / 16 * 9;
const margins = {top: 20, right: 20, bottom: 50, left: 50};
let plotWidth = figWidth - margins.left - margins.right;
let plotHeight = figHeight - margins.top - margins.bottom;
let minX = d3.min(data, (datum) => datum.x);
let maxX = d3.max(data, (datum) => datum.x);
let minY = d3.min(data, (datum) => datum.y);
let maxY = d3.max(data, (datum) => datum.y);
您必须在数据坐标和绘图(图像)坐标之间进行转换。您可以使用比例尺进行此转换:比例尺的域是您选择数据点的数据空间,比例尺的范围是您放置点的图像空间
let scaleX = d3.scaleLinear()
.range([0, plotWidth])
.domain([minX - 1, maxX + 1]);
let scaleY = d3.scaleLinear()
.range([plotHeight, 0])
.domain([minY - 1, maxY + 1]);
const axisX = d3.axisBottom(scaleX).ticks(10);
const axisY = d3.axisLeft(scaleY).ticks(10);
请注意,y
比例尺具有反向范围,因为在 SVG 标准中,y
比例尺的原点位于顶部。定义比例尺后,开始在新创建的 SVG 图像上绘制绘图
let svg = d3n.createSVG(figWidth, figHeight)
svg.attr('background-color', 'white');
svg.append("rect")
.attr("width", figWidth)
.attr("height", figHeight)
.attr("fill", 'white');
首先,绘制插值线,将 line
元素附加到 SVG 图像
svg.append("g")
.attr('transform', `translate(${margins.left}, ${margins.top})`)
.append("line")
.attr("x1", scaleX(minX - 1))
.attr("y1", scaleY((minX - 1) * slope + intercept))
.attr("x2", scaleX(maxX + 1))
.attr("y2", scaleY((maxX + 1) * slope + intercept))
.attr("stroke", "#1f77b4");
然后为每个数据点添加一个 circle
到正确的位置。D3.js 的关键点在于它将数据与 SVG 元素相关联。因此,您可以使用 data()
方法将数据点与您创建的圆圈相关联。enter()
方法告诉库如何处理新关联的数据
svg.append("g")
.attr('transform', `translate(${margins.left}, ${margins.top})`)
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.classed("circle", true)
.attr("cx", (d) => scaleX(d.x))
.attr("cy", (d) => scaleY(d.y))
.attr("r", 3)
.attr("fill", "#ff7f0e");
您绘制的最后一个元素是轴及其标签;这样做是为了确保它们与绘图线和圆圈重叠
svg.append("g")
.attr('transform', `translate(${margins.left}, ${margins.top + plotHeight})`)
.call(axisX);
svg.append("g")
.append("text")
.attr("transform", `translate(${margins.left + 0.5 * plotWidth}, ${margins.top + plotHeight + 0.7 * margins.bottom})`)
.style("text-anchor", "middle")
.text("X");
svg.append("g")
.attr('transform', `translate(${margins.left}, ${margins.top})`)
.call(axisY);
svg.append("g")
.attr("transform", `translate(${0.5 * margins.left}, ${margins.top + 0.5 * plotHeight})`)
.append("text")
.attr("transform", "rotate(-90)")
.style("text-anchor", "middle")
.text("Y");
最后,将绘图保存到 SVG 文件。我选择了同步写入文件,所以我可以展示这种第二种方法
fs.writeFileSync("fit_node.svg", d3n.svgString());
结果
运行脚本非常简单,只需
$ node fitting_node.js
命令行输出为
#### Anscombe's first set with JavaScript in Node.js ####
Slope: 0.5
Intercept: 3
Correlation coefficient: 0.8164205163448399
这是我使用 D3.js 和 Node.js 生成的图像

(Cristiano Fontana,CC BY-SA 4.0)
结论
JavaScript 是当今的核心技术,并且非常适合使用正确的库进行数据探索。通过对事件驱动架构的介绍以及服务器端绘图在实践中的样子的示例,我们可以开始考虑将 Node.js 和 D3.js 作为与数据科学相关的常用编程语言的替代方案。
2 条评论