JavaScript (也称为JS)是Web的通用语言 ,因为所有主要的Web浏览器都支持它-在浏览器中运行的其他语言都已转换 (或翻译)为JavaScript。 有时JS 可能会造成混乱 ,但是我觉得使用起来很愉快,因为我会坚持好的部分 。 JavaScript的创建是为了在浏览器中运行,但它也可以在其他上下文中使用,例如嵌入式语言或用于服务器端应用程序 。
在本教程中,我将解释如何编写将在Node.js中运行的程序,Node.js是可以执行JavaScript应用程序的运行时环境。 我最喜欢Node.js的地方是它的事件驱动架构,用于异步编程 。 通过这种方法,可以将函数(又名回调)附加到某些事件。 发生附加事件时,将执行回调。 这样,开发人员不必编写主循环,因为运行时会负责。
JavaScript还具有使用不同语法的新异步函数 ,但是我认为它们很好地隐藏了事件驱动的体系结构,因此在操作方法文章中无法使用它们。 因此,在本教程中,我将使用传统的回调方法,即使这种情况下不必要。
了解程序任务
本教程中的程序任务是:
- 从包含Anscombe四重奏数据集的CSV文件中读取一些数据
- 用直线内插数据(即f(x)= m·x + q )
- 将结果绘制到图像文件
有关此任务的更多详细信息,您可以阅读本系列的前几篇文章,它们在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解析,用于解析CSV文件
- 用于计算数据相关因子的简单统计
- 用于确定拟合线的Regression-js
- 服务器端绘图的D3节点
运行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
存储在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 ( ) ) ;
读取数据
非常有趣的方法 ; 您可以选择同步或异步方法。 前者使用阻塞函数调用,而后者使用非阻塞函数调用。 在阻塞函数中,程序会在那里停止并等待,直到函数完成其任务为止,而非阻塞函数不会停止执行,而是以某种方式继续执行任务。 您可以在此处选择两个选项:可以定期检查函数是否结束,或者函数可以在结束时通知您。 本教程使用第二种方法:它使用EventEmitter
来生成与回调函数关联的事件 。 触发事件时执行回调。
首先,生成EventEmitter
:
const myEmitter = new EventEmitter ( ) ;
然后,将文件读取的结尾与名为myEmitter
的事件相关联。 尽管在这个简单的示例中不需要遵循此路径(可以使用简单的阻塞调用),但是它是一种非常强大的方法,在其他情况下非常有用。 在执行此操作之前,请在本部分中添加另一部分,以使用CSV Parse库进行数据读取。 该库提供了几种可供选择的方法 ,但是本示例将stream API与pipe一起使用。 该库需要一些配置,该配置在对象中定义:
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浏览器中运行,因此假定它具有要处理的网页。 在服务器端工作是一个非常不同的环境,您需要一个虚拟网页来进行工作。 幸运的是, 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生成的图像:
结论
JavaScript是当今的一项核心技术,非常适合使用正确的库进行数据探索。 通过对事件驱动的体系结构的介绍以及服务器端绘图在实际中的外观示例,我们可以开始考虑将Node.js和D3.js用作与数据科学相关的通用编程语言的替代方法。
翻译自: https://opensource.com/article/20/6/data-science-nodejs